Radish alpha
r
rad:z39mP9rQAaGmERfUMPULfPUi473tY
Radicle terminal user interface
Radicle
Git
radicle-tui src terminal.rs
use std::io;
use std::time::Duration;

use tokio::sync::broadcast;
use tokio::sync::mpsc::UnboundedSender;

use tokio_util::sync::CancellationToken;

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

use ratatui::crossterm::execute;
use ratatui::crossterm::terminal::{disable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen};

use super::event::Event;
use super::{Interrupted, Share};

pub type Backend<S> = CrosstermBackend<S>;
pub type InlineTerminal = ratatui::Terminal<Backend<io::Stdout>>;
pub type FullscreenTerminal = ratatui::Terminal<Backend<io::Stdout>>;

const STDIN_TICK_RATE: Duration = Duration::from_millis(20);

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

impl Terminal {
    pub fn restore(&mut self) -> io::Result<()> {
        match self {
            Terminal::Fullscreen(inner) => {
                disable_raw_mode()?;
                execute!(io::stdout(), LeaveAlternateScreen)?;
                inner.clear()?;
            }
            Terminal::Inline(inner) => {
                disable_raw_mode()?;
                inner.clear()?;
            }
        }
        Ok(())
    }

    pub fn clear(&mut self) -> io::Result<()> {
        match self {
            Terminal::Fullscreen(inner) | Terminal::Inline(inner) => {
                inner.clear()?;
            }
        }
        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 => {
                execute!(io::stdout(), EnterAlternateScreen)?;
                let options = TerminalOptions { viewport };
                let mut terminal = ratatui::init_with_options(options);

                terminal.clear()?;

                Ok(Terminal::Fullscreen(terminal))
            }
            _ => {
                let options = TerminalOptions { viewport };
                let terminal = ratatui::init_with_options(options);

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

#[derive(Default)]
pub struct StdinReader {}

impl StdinReader {
    pub async fn run<P: Share>(
        self,
        event_tx: UnboundedSender<Event>,
        mut interrupt_rx: broadcast::Receiver<Interrupted<P>>,
    ) -> anyhow::Result<Interrupted<P>> {
        use ratatui::crossterm::event;

        let token = CancellationToken::new();
        let token_clone = token.clone();

        let key_event_tx = event_tx.clone();
        let key_listener = tokio::spawn(async move {
            loop {
                match event::poll(STDIN_TICK_RATE) {
                    Ok(true) => match event::read() {
                        Ok(event) => {
                            if key_event_tx.send(event.into()).is_err() {
                                return;
                            }
                        }
                        Err(err) => {
                            log::error!("Could not read from stdin: {err}");
                        }
                    },
                    _ => {
                        if token_clone.is_cancelled() {
                            break;
                        }
                    }
                }
            }
        });

        let result: anyhow::Result<Interrupted<P>> = tokio::select! {
            Ok(interrupted) = interrupt_rx.recv() => {
                token.cancel();
                Ok(interrupted)
            }
        };
        key_listener.await?;

        result
    }
}