verco 6.3.3

A simple Git/Mercurial/PlasticSCM tui client based on keyboard shortcuts
use std::{
    io,
    sync::{mpsc, Arc},
    thread,
    time::Duration,
};

use crate::{
    backend::Backend,
    mode::{self, ModeContext, ModeKind, ModeResponse},
    platform::{Key, Platform, PlatformEventReader},
    ui::Drawer,
};

enum Event {
    Key(Key),
    Resize(u16, u16),
    Response(ModeResponse),
    ModeChange(ModeKind),
    ModeRefresh(ModeKind),
}

#[derive(Clone)]
pub struct EventSender(mpsc::SyncSender<Event>);
impl EventSender {
    pub fn send_response(&self, result: ModeResponse) {
        self.0.send(Event::Response(result)).unwrap();
    }

    pub fn send_mode_change(&self, mode: ModeKind) {
        self.0.send(Event::ModeChange(mode)).unwrap();
    }

    pub fn send_mode_refresh(&self, mode: ModeKind) {
        self.0.send(Event::ModeRefresh(mode)).unwrap();
    }
}

#[derive(Default)]
struct Application {
    current_mode: ModeKind,

    status_mode: mode::status::Mode,
    log_mode: mode::log::Mode,
    revision_details_mode: mode::revision_details::Mode,
    branches_mode: mode::branches::Mode,
    tags_mode: mode::tags::Mode,

    spinner_state: u8,
}
impl Application {
    pub fn enter_mode(&mut self, ctx: &ModeContext, mode: ModeKind) {
        self.current_mode = mode;
        match &self.current_mode {
            ModeKind::Status => self.status_mode.on_enter(ctx),
            ModeKind::Log => self.log_mode.on_enter(ctx),
            ModeKind::RevisionDetails(revision) => {
                self.revision_details_mode.on_enter(ctx, revision);
            }
            ModeKind::Branches => self.branches_mode.on_enter(ctx),
            ModeKind::Tags => self.tags_mode.on_enter(ctx),
        }
    }

    pub fn refresh_mode(&mut self, ctx: &ModeContext, mode: ModeKind) {
        if std::mem::discriminant(&self.current_mode) == std::mem::discriminant(&mode) {
            self.enter_mode(ctx, mode);
        }
    }

    pub fn on_key(&mut self, ctx: &ModeContext, key: Key) -> bool {
        let status = match &self.current_mode {
            ModeKind::Status => self.status_mode.on_key(ctx, key),
            ModeKind::Log => self.log_mode.on_key(ctx, key),
            ModeKind::RevisionDetails(revision) => {
                self.revision_details_mode.on_key(ctx, revision, key)
            }
            ModeKind::Branches => self.branches_mode.on_key(ctx, key),
            ModeKind::Tags => self.tags_mode.on_key(ctx, key),
        };

        if !status.pending_input {
            if key.is_cancel() {
                return false;
            }

            match key {
                Key::Char('s') => self.enter_mode(ctx, ModeKind::Status),
                Key::Char('l') => self.enter_mode(ctx, ModeKind::Log),
                Key::Char('b') => self.enter_mode(ctx, ModeKind::Branches),
                Key::Char('t') => self.enter_mode(ctx, ModeKind::Tags),
                _ => (),
            }
        }

        true
    }

    pub fn on_response(&mut self, response: ModeResponse) {
        match response {
            ModeResponse::Status(response) => {
                self.status_mode.on_response(response);
            }
            ModeResponse::Log(response) => self.log_mode.on_response(response),
            ModeResponse::RevisionDetails(response) => {
                self.revision_details_mode.on_response(response);
            }
            ModeResponse::Branches(response) => {
                self.branches_mode.on_response(response);
            }
            ModeResponse::Tags(response) => {
                self.tags_mode.on_response(response);
            }
        }
    }

    pub fn is_waiting_response(&self) -> bool {
        match &self.current_mode {
            ModeKind::Status => self.status_mode.is_waiting_response(),
            ModeKind::Log => self.log_mode.is_waiting_response(),
            ModeKind::RevisionDetails(_) => self.revision_details_mode.is_waiting_response(),
            ModeKind::Branches => self.branches_mode.is_waiting_response(),
            ModeKind::Tags => self.tags_mode.is_waiting_response(),
        }
    }

    pub fn draw_header(&mut self, drawer: &mut Drawer) {
        let spinner = [b'-', b'\\', b'|', b'/'];
        self.spinner_state = (self.spinner_state + 1) % spinner.len() as u8;
        let spinner = match self.is_waiting_response() {
            true => spinner[self.spinner_state as usize],
            false => b' ',
        };

        let (mode_name, left_help, right_help) = match &self.current_mode {
            ModeKind::Status => self.status_mode.header(),
            ModeKind::Log => self.log_mode.header(),
            ModeKind::RevisionDetails(_) => self.revision_details_mode.header(),
            ModeKind::Branches => self.branches_mode.header(),
            ModeKind::Tags => self.tags_mode.header(),
        };
        drawer.header(mode_name, left_help, right_help, spinner);
    }

    pub fn draw_body(&self, drawer: &mut Drawer) {
        match &self.current_mode {
            ModeKind::Status => self.status_mode.draw(drawer),
            ModeKind::Log => self.log_mode.draw(drawer),
            ModeKind::RevisionDetails(_) => {
                self.revision_details_mode.draw(drawer);
            }
            ModeKind::Branches => self.branches_mode.draw(drawer),
            ModeKind::Tags => self.tags_mode.draw(drawer),
        }
        drawer.clear_to_bottom();
    }
}

fn terminal_event_loop(mut event_reader: PlatformEventReader, sender: mpsc::SyncSender<Event>) {
    event_reader.init();

    let mut keys = Vec::new();
    loop {
        keys.clear();
        let mut resize = None;

        event_reader.read_terminal_events(&mut keys, &mut resize);

        for &key in &keys {
            if sender.send(Event::Key(key)).is_err() {
                break;
            }
        }
        if let Some(resize) = resize {
            if sender.send(Event::Resize(resize.0, resize.1)).is_err() {
                break;
            }
        }
    }
}

pub fn run(platform_event_reader: PlatformEventReader, backend: Arc<dyn Backend>) {
    let (event_sender, event_receiver) = mpsc::sync_channel(1);

    let mut ctx = ModeContext {
        backend,
        event_sender: EventSender(event_sender.clone()),
        viewport_size: Platform::terminal_size(),
    };

    let _ = thread::spawn(move || {
        terminal_event_loop(platform_event_reader, event_sender);
    });

    let mut application = Application::default();
    application.enter_mode(&ctx, ModeKind::default());

    let stdout = io::stdout();
    let mut stdout = stdout.lock();
    let mut stdout_buf = Vec::new();

    const TIMEOUT: Duration = Duration::from_millis(100);

    loop {
        let event = if application.is_waiting_response() {
            event_receiver.recv_timeout(TIMEOUT)
        } else {
            event_receiver
                .recv()
                .map_err(|_| mpsc::RecvTimeoutError::Disconnected)
        };

        let mut draw_body = true;

        match event {
            Ok(Event::Key(key)) => {
                if !application.on_key(&ctx, key) {
                    break;
                }
            }
            Ok(Event::Resize(width, height)) => {
                ctx.viewport_size = (width, height);
            }
            Ok(Event::Response(response)) => application.on_response(response),
            Ok(Event::ModeChange(mode)) => application.enter_mode(&ctx, mode),
            Ok(Event::ModeRefresh(mode)) => application.refresh_mode(&ctx, mode),
            Err(mpsc::RecvTimeoutError::Timeout) => draw_body = false,
            Err(mpsc::RecvTimeoutError::Disconnected) => break,
        }

        let mut drawer = Drawer::new(stdout_buf, ctx.viewport_size);
        application.draw_header(&mut drawer);
        if draw_body {
            application.draw_body(&mut drawer);
        }
        stdout_buf = drawer.take_buf();

        use io::Write;
        stdout.write_all(&stdout_buf).unwrap();
        stdout.flush().unwrap();
    }
}