diff --git a/Cargo.lock b/Cargo.lock index 13f9e465..e6af743b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2133,6 +2133,8 @@ version = "0.1.0" dependencies = [ "anyhow", "lexopt", + "radicle", + "radicle-cli", "radicle-term", "tui-realm-stdlib", "tuirealm", diff --git a/radicle-tui/Cargo.toml b/radicle-tui/Cargo.toml index 194fca3b..8c736d90 100644 --- a/radicle-tui/Cargo.toml +++ b/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" diff --git a/radicle-tui/src/app.rs b/radicle-tui/src/app.rs new file mode 100644 index 00000000..b2f4bc78 --- /dev/null +++ b/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 for App { + fn init(&mut self, app: &mut Application) -> 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, + 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, 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 for Widget { + fn on(&mut self, event: Event) -> Option { + match event { + Event::Keyboard(KeyEvent { + code: Key::Char('q'), + .. + }) => Some(Message::Quit), + _ => None, + } + } +} + +impl Component for Widget { + fn on(&mut self, _event: Event) -> Option { + None + } +} diff --git a/radicle-tui/src/lib.rs b/radicle-tui/src/lib.rs index 36ea0ff0..e6dfd6d1 100644 --- a/radicle-tui/src/lib.rs +++ b/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 diff --git a/radicle-tui/src/main.rs b/radicle-tui/src/main.rs index 4e548250..0caa1e39 100644 --- a/radicle-tui/src/main.rs +++ b/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(()) } diff --git a/radicle-tui/src/ui/components.rs b/radicle-tui/src/ui/components.rs index a78f5d80..a8c394f3 100644 --- a/radicle-tui/src/ui/components.rs +++ b/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 {}