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 = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"lexopt",
|
"lexopt",
|
||||||
|
"radicle",
|
||||||
|
"radicle-cli",
|
||||||
"radicle-term",
|
"radicle-term",
|
||||||
"tui-realm-stdlib",
|
"tui-realm-stdlib",
|
||||||
"tuirealm",
|
"tuirealm",
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,14 @@ lexopt = { version = "0.2" }
|
||||||
tuirealm = { version = "1.8.0", default-features = false, features = [ "with-termion" ] }
|
tuirealm = { version = "1.8.0", default-features = false, features = [ "with-termion" ] }
|
||||||
tui-realm-stdlib = { version = "1.2.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]
|
[dependencies.radicle-term]
|
||||||
version = "0"
|
version = "0"
|
||||||
path = "../radicle-term"
|
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::Frame;
|
||||||
use tuirealm::{Application, EventListenerCfg, NoUserEvent};
|
use tuirealm::{Application, EventListenerCfg, NoUserEvent};
|
||||||
|
|
||||||
mod ui;
|
pub mod ui;
|
||||||
|
|
||||||
/// Trait that must be implemented by client applications in order to be run
|
/// 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
|
/// as tui-application using tui-realm. Implementors act as models to the
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,14 @@
|
||||||
use std::process;
|
use std::process;
|
||||||
|
|
||||||
|
use anyhow::{anyhow, Context};
|
||||||
|
|
||||||
|
use radicle::storage::ReadStorage;
|
||||||
|
|
||||||
|
use radicle_cli as cli;
|
||||||
use radicle_term as term;
|
use radicle_term as term;
|
||||||
|
use radicle_tui::Window;
|
||||||
|
|
||||||
|
mod app;
|
||||||
|
|
||||||
pub const NAME: &str = "radicle-tui";
|
pub const NAME: &str = "radicle-tui";
|
||||||
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
|
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||||
|
|
@ -47,6 +55,24 @@ impl Options {
|
||||||
|
|
||||||
fn execute() -> anyhow::Result<()> {
|
fn execute() -> anyhow::Result<()> {
|
||||||
let _ = Options::from_env()?;
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,9 @@ use tuirealm::{Frame, MockComponent, State, StateValue};
|
||||||
use super::layout;
|
use super::layout;
|
||||||
use super::widget::{Widget, WidgetComponent};
|
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)]
|
#[derive(Default)]
|
||||||
pub struct GlobalListener {}
|
pub struct GlobalListener {}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue