tui: Implement basic application

Signed-off-by: Erik Kundt <erik@zirkular.io>
This commit is contained in:
Erik Kundt 2023-02-22 09:53:07 +01:00 committed by Alexis Sellier
parent 62867d26bd
commit bd4efee897
No known key found for this signature in database
6 changed files with 194 additions and 1 deletions

2
Cargo.lock generated
View File

@ -2133,6 +2133,8 @@ version = "0.1.0"
dependencies = [
"anyhow",
"lexopt",
"radicle",
"radicle-cli",
"radicle-term",
"tui-realm-stdlib",
"tuirealm",

View File

@ -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"

154
radicle-tui/src/app.rs Normal file
View File

@ -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
}
}

View File

@ -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

View File

@ -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(())
}

View File

@ -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 {}