Radish alpha
r
Radicle terminal user interface
Radicle
Git (anonymous pull)
Log in to clone via SSH
tui: Implement basic application
Erik Kundt committed 3 years ago
commit a4a43cdbe6c2b0ae00c6b4b45ae295a49e94b7c7
parent 89ad69e46c11f4c1b76a5183bb15cf99a9fe3c93
5 files changed +192 -1
modified radicle-tui/Cargo.toml
@@ -16,6 +16,14 @@ lexopt = { version = "0.2" }
tuirealm = { version = "1.8.0", default-features = false, features = [ "with-termion" ] }
tui-realm-stdlib = { version = "1.2.0", default-features = false, features = [ "with-termion" ] }

+
[dependencies.radicle]
+
version = "0"
+
path = "../radicle"
+

+
[dependencies.radicle-cli]
+
version = "0"
+
path = "../radicle-cli"
+

[dependencies.radicle-term]
version = "0"
path = "../radicle-term"
added radicle-tui/src/app.rs
@@ -0,0 +1,154 @@
+
use std::time::Duration;
+

+
use anyhow::Result;
+

+
use tuirealm::application::PollStrategy;
+
use tuirealm::event::{Event, Key, KeyEvent, KeyModifiers};
+
use tuirealm::props::{AttrValue, Attribute};
+
use tuirealm::tui::layout::{Constraint, Direction, Layout};
+
use tuirealm::{Application, Component, Frame, NoUserEvent, Sub, SubClause, SubEventClause};
+

+
use radicle_tui::ui;
+
use radicle_tui::ui::components::{GlobalListener, ShortcutBar};
+
use radicle_tui::ui::theme;
+
use radicle_tui::ui::widget::Widget;
+

+
use radicle_tui::Tui;
+

+
use radicle::identity::{Id, Project};
+

+
#[allow(dead_code)]
+
pub struct App {
+
    id: Id,
+
    project: Project,
+
    quit: bool,
+
}
+

+
/// Messages handled by this application.
+
#[derive(Debug, Eq, PartialEq)]
+
pub enum Message {
+
    Quit,
+
}
+

+
/// All components known to this application.
+
#[derive(Debug, Eq, PartialEq, Clone, Hash)]
+
pub enum ComponentId {
+
    ShortcutBar,
+
    GlobalListener,
+
}
+

+
/// Creates a new application using a tui-realm-application, mounts all
+
/// components and sets focus to a default one.
+
impl App {
+
    pub fn new(id: Id, project: Project) -> Self {
+
        Self {
+
            id,
+
            project,
+
            quit: false,
+
        }
+
    }
+
}
+

+
impl Tui<ComponentId, Message> for App {
+
    fn init(&mut self, app: &mut Application<ComponentId, Message, NoUserEvent>) -> Result<()> {
+
        let theme = theme::default_dark();
+

+
        app.mount(
+
            ComponentId::ShortcutBar,
+
            ui::shortcut_bar(
+
                &theme,
+
                vec![
+
                    ui::shortcut(&theme, "tab", "section"),
+
                    ui::shortcut(&theme, "q", "quit"),
+
                ],
+
            )
+
            .to_boxed(),
+
            vec![],
+
        )?;
+

+
        // Add global key listener and subscribe to key events
+
        app.mount(
+
            ComponentId::GlobalListener,
+
            ui::global_listener().to_boxed(),
+
            vec![Sub::new(
+
                SubEventClause::Keyboard(KeyEvent {
+
                    code: Key::Char('q'),
+
                    modifiers: KeyModifiers::NONE,
+
                }),
+
                SubClause::Always,
+
            )],
+
        )?;
+

+
        // We need to give focus to a component then
+
        app.active(&ComponentId::ShortcutBar)?;
+

+
        Ok(())
+
    }
+

+
    fn view(
+
        &mut self,
+
        app: &mut Application<ComponentId, Message, NoUserEvent>,
+
        frame: &mut Frame,
+
    ) {
+
        let area = frame.size();
+
        let margin_h = 1u16;
+
        let shortcuts_h = app
+
            .query(&ComponentId::ShortcutBar, Attribute::Height)
+
            .ok()
+
            .flatten()
+
            .unwrap_or(AttrValue::Size(0))
+
            .unwrap_size();
+
        let workspaces_h = area
+
            .height
+
            .saturating_sub(shortcuts_h.saturating_add(margin_h));
+

+
        let layout = Layout::default()
+
            .direction(Direction::Vertical)
+
            .horizontal_margin(margin_h)
+
            .constraints(
+
                [
+
                    Constraint::Length(workspaces_h),
+
                    Constraint::Length(shortcuts_h),
+
                ]
+
                .as_ref(),
+
            )
+
            .split(area);
+

+
        app.view(&ComponentId::ShortcutBar, frame, layout[1]);
+
    }
+

+
    fn update(&mut self, app: &mut Application<ComponentId, Message, NoUserEvent>, interval: u64) {
+
        if let Ok(messages) = app.tick(PollStrategy::TryFor(Duration::from_millis(interval))) {
+
            for message in messages {
+
                match message {
+
                    Message::Quit => self.quit = true,
+
                }
+
            }
+
        }
+
    }
+

+
    fn quit(&self) -> bool {
+
        self.quit
+
    }
+
}
+

+
/// Since the framework does not know the type of messages that are being
+
/// passed around in the app, the following handlers need to be implemented for
+
/// each component used.
+
impl Component<Message, NoUserEvent> for Widget<GlobalListener> {
+
    fn on(&mut self, event: Event<NoUserEvent>) -> Option<Message> {
+
        match event {
+
            Event::Keyboard(KeyEvent {
+
                code: Key::Char('q'),
+
                ..
+
            }) => Some(Message::Quit),
+
            _ => None,
+
        }
+
    }
+
}
+

+
impl Component<Message, NoUserEvent> for Widget<ShortcutBar> {
+
    fn on(&mut self, _event: Event<NoUserEvent>) -> Option<Message> {
+
        None
+
    }
+
}
modified radicle-tui/src/lib.rs
@@ -7,7 +7,7 @@ use tuirealm::terminal::TerminalBridge;
use tuirealm::Frame;
use tuirealm::{Application, EventListenerCfg, NoUserEvent};

-
mod ui;
+
pub mod ui;

/// Trait that must be implemented by client applications in order to be run
/// as tui-application using tui-realm. Implementors act as models to the
modified radicle-tui/src/main.rs
@@ -1,6 +1,14 @@
use std::process;

+
use anyhow::{anyhow, Context};
+

+
use radicle::storage::ReadStorage;
+

+
use radicle_cli as cli;
use radicle_term as term;
+
use radicle_tui::Window;
+

+
mod app;

pub const NAME: &str = "radicle-tui";
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
@@ -47,6 +55,24 @@ impl Options {

fn execute() -> anyhow::Result<()> {
    let _ = Options::from_env()?;
+

+
    let (_, id) = radicle::rad::cwd()
+
        .map_err(|_| anyhow!("this command must be run in the context of a project"))?;
+

+
    let profile = cli::terminal::profile()?;
+

+
    let signer = cli::terminal::signer(&profile)?;
+
    let storage = &profile.storage;
+

+
    let payload = storage
+
        .get(signer.public_key(), id)?
+
        .context("No project with such `id` exists")?;
+

+
    let project = payload.project()?;
+

+
    let mut window = Window::default();
+
    window.run(&mut app::App::new(id, project), 1000 / FPS)?;
+

    Ok(())
}

modified radicle-tui/src/ui/components.rs
@@ -6,6 +6,9 @@ use tuirealm::{Frame, MockComponent, State, StateValue};
use super::layout;
use super::widget::{Widget, WidgetComponent};

+
/// Some user events need to be handled globally (e.g. user presses key `q` to quit
+
/// the application). This component can be used in conjunction with SubEventClause
+
/// to handle those events.
#[derive(Default)]
pub struct GlobalListener {}