tui: Implement basic application
Signed-off-by: Erik Kundt <erik@zirkular.io>
This commit is contained in:
parent
62867d26bd
commit
bd4efee897
|
|
@ -2133,6 +2133,8 @@ version = "0.1.0"
|
|||
dependencies = [
|
||||
"anyhow",
|
||||
"lexopt",
|
||||
"radicle",
|
||||
"radicle-cli",
|
||||
"radicle-term",
|
||||
"tui-realm-stdlib",
|
||||
"tuirealm",
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(())
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue