Radish alpha
r
rad:z39mP9rQAaGmERfUMPULfPUi473tY
Radicle terminal user interface
Radicle
Git
Various terminal improvements
Merged did:key:z6MkswQE...2C1V opened 1 year ago

This includes:

  • add event logging to imUI
  • process key events only 200ms after startup
  • introduce typed inline and fullscreen terminal
3 files changed +94 -37 ea9fc686 066e7502
modified src/terminal.rs
@@ -1,17 +1,86 @@
use std::io::{self, Write};
use std::thread;
+
use std::time::Instant;

+
use ratatui::termion::screen::{AlternateScreen, IntoAlternateScreen};
use termion::input::TermRead;
use termion::raw::{IntoRawMode, RawTerminal};

-
use ratatui::prelude::*;
+
use ratatui::{prelude::*, CompletedFrame};
use ratatui::{TerminalOptions, Viewport};

use tokio::sync::mpsc::{self};

use super::event::Event;

-
pub type Backend = TermionBackendExt<RawTerminal<io::Stdout>>;
+
pub type Backend<S> = TermionBackendExt<S>;
+

+
pub type InlineTerminal = ratatui::terminal::Terminal<Backend<RawTerminal<io::Stdout>>>;
+
pub type FullscreenTerminal =
+
    ratatui::terminal::Terminal<Backend<AlternateScreen<RawTerminal<io::Stdout>>>>;
+

+
pub enum Terminal {
+
    Inline(InlineTerminal),
+
    Fullscreen(FullscreenTerminal),
+
}
+

+
impl Terminal {
+
    pub fn restore(&mut self) -> io::Result<()> {
+
        match self {
+
            Terminal::Fullscreen(inner) => {
+
                inner.clear()?;
+
            }
+
            Terminal::Inline(inner) => {
+
                // TODO(erikli): Check if still needed.
+
                let size = inner.get_frame().size();
+
                inner.set_cursor(size.x, size.y)?;
+
            }
+
        }
+

+
        Ok(())
+
    }
+

+
    pub fn draw<F>(&mut self, f: F) -> io::Result<CompletedFrame>
+
    where
+
        F: FnOnce(&mut Frame),
+
    {
+
        match self {
+
            Terminal::Inline(inner) => inner.draw(f),
+
            Terminal::Fullscreen(inner) => inner.draw(f),
+
        }
+
    }
+
}
+

+
impl TryFrom<Viewport> for Terminal {
+
    type Error = anyhow::Error;
+

+
    fn try_from(viewport: Viewport) -> Result<Self, Self::Error> {
+
        match viewport {
+
            Viewport::Fullscreen => {
+
                let stdout = io::stdout().into_raw_mode()?.into_alternate_screen()?;
+
                let options = TerminalOptions { viewport };
+
                let mut terminal = ratatui::terminal::Terminal::with_options(
+
                    TermionBackendExt::new(stdout),
+
                    options,
+
                )?;
+

+
                terminal.clear()?;
+

+
                Ok(Terminal::Fullscreen(terminal))
+
            }
+
            _ => {
+
                let stdout = io::stdout().into_raw_mode()?;
+
                let options = TerminalOptions { viewport };
+
                let terminal = ratatui::terminal::Terminal::with_options(
+
                    TermionBackendExt::new(stdout),
+
                    options,
+
                )?;
+

+
                Ok(Terminal::Inline(terminal))
+
            }
+
        }
+
    }
+
}

/// FIXME Remove workaround after a new `ratatui` version with
/// https://github.com/ratatui-org/ratatui/pull/981/ included was released.
@@ -92,26 +161,6 @@ impl<W: Write> ratatui::backend::Backend for TermionBackendExt<W> {
    }
}

-
/// Setup a `Terminal` with inline viewport using the `termion` backend.
-
pub fn setup(viewport: Viewport) -> anyhow::Result<Terminal<Backend>> {
-
    let is_fullscreen = viewport == Viewport::Fullscreen;
-
    let stdout = io::stdout().into_raw_mode()?;
-
    let options = TerminalOptions { viewport };
-
    let mut terminal = Terminal::with_options(TermionBackendExt::new(stdout), options)?;
-

-
    if is_fullscreen {
-
        terminal.clear()?;
-
    }
-

-
    Ok(terminal)
-
}
-

-
/// Restore the `Terminal` on quit.
-
pub fn restore(terminal: &mut Terminal<Backend>) -> anyhow::Result<()> {
-
    terminal.clear()?;
-
    Ok(())
-
}
-

/// Spawn one thread that polls `stdin` for new user input and another thread
/// that polls UNIX signals, e.g. `SIGWINCH` when the terminal window size is
/// being changed.
@@ -119,10 +168,14 @@ pub fn events() -> mpsc::UnboundedReceiver<Event> {
    let (tx, rx) = mpsc::unbounded_channel();
    let events_tx = tx.clone();
    thread::spawn(move || {
+
        let start = Instant::now();
        let stdin = io::stdin();
        for key in stdin.keys().flatten() {
-
            if events_tx.send(Event::Key(key)).is_err() {
-
                return;
+
            // TODO(erikli): Remove this hack! Perhaps use `tokio::CancellationToken`?
+
            if start.elapsed().as_millis() > 200 {
+
                if events_tx.send(Event::Key(key)).is_err() {
+
                    return;
+
                }
            }
        }
    });
modified src/ui/im.rs
@@ -21,6 +21,7 @@ use crate::event::Event;
use crate::store::Update;
use crate::task::Interrupted;
use crate::terminal;
+
use crate::terminal::Terminal;
use crate::ui::theme::Theme;
use crate::ui::{Column, ToRow};

@@ -53,7 +54,7 @@ impl Frontend {
    {
        let mut ticker = tokio::time::interval(RENDERING_TICK_RATE);

-
        let mut terminal = terminal::setup(viewport)?;
+
        let mut terminal = Terminal::try_from(viewport)?;
        let mut events_rx = terminal::events();

        let mut state = state_rx.recv().await.unwrap();
@@ -63,9 +64,13 @@ impl Frontend {
            tokio::select! {
                // Tick to terminate the select every N milliseconds
                _ = ticker.tick() => (),
-
                Some(event) = events_rx.recv() => match event {
-
                    Event::Key(key) => ctx.store_input(key),
-
                    Event::Resize => (),
+
                // Handle input events
+
                Some(event) = events_rx.recv() => {
+
                    log::info!("Received event: {:?}", event);
+
                    match event {
+
                        Event::Key(key) => ctx.store_input(key),
+
                        Event::Resize => (),
+
                    }
                },
                // Handle state updates
                Some(s) = state_rx.recv() => {
@@ -73,8 +78,8 @@ impl Frontend {
                },
                // Catch and handle interrupt signal to gracefully shutdown
                Ok(interrupted) = interrupt_rx.recv() => {
-
                    let size = terminal.get_frame().size();
-
                    let _ = terminal.set_cursor(size.x, size.y);
+
                    log::info!("Received interrupt: {:?}", interrupted);
+
                    terminal.restore()?;

                    break Ok(interrupted);
                }
@@ -89,8 +94,7 @@ impl Frontend {

            ctx.clear_inputs();
        };
-

-
        terminal::restore(&mut terminal)?;
+
        terminal.restore()?;

        result
    }
modified src/ui/rm.rs
@@ -11,6 +11,7 @@ use crate::event::Event;
use crate::store::Update;
use crate::task::Interrupted;
use crate::terminal;
+
use crate::terminal::Terminal;
use crate::ui::rm::widget::RenderProps;
use crate::ui::rm::widget::Widget;

@@ -53,7 +54,7 @@ impl Frontend {
    {
        let mut ticker = tokio::time::interval(RENDERING_TICK_RATE);

-
        let mut terminal = terminal::setup(viewport)?;
+
        let mut terminal = Terminal::try_from(viewport)?;
        let mut events_rx = terminal::events();

        let mut root = {
@@ -67,6 +68,7 @@ impl Frontend {
            tokio::select! {
                // Tick to terminate the select every N milliseconds
                _ = ticker.tick() => (),
+
                // Handle input events
                Some(event) = events_rx.recv() => match event {
                    Event::Key(key) => root.handle_event(key),
                    Event::Resize => (),
@@ -77,16 +79,14 @@ impl Frontend {
                },
                // Catch and handle interrupt signal to gracefully shutdown
                Ok(interrupted) = interrupt_rx.recv() => {
-
                    let size = terminal.get_frame().size();
-
                    let _ = terminal.set_cursor(size.x, size.y);
+
                    terminal.restore()?;

                    break Ok(interrupted);
                }
            }
            terminal.draw(|frame| root.render(RenderProps::from(frame.size()), frame))?;
        };
-

-
        terminal::restore(&mut terminal)?;
+
        terminal.restore()?;

        result
    }