diff --git a/Cargo.lock b/Cargo.lock index f3e11e3..8b7e348 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -687,6 +687,21 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "futures" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + [[package]] name = "futures-channel" version = "0.3.30" @@ -731,6 +746,17 @@ version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" +[[package]] +name = "futures-macro" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.62", +] + [[package]] name = "futures-sink" version = "0.3.30" @@ -749,8 +775,10 @@ version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" dependencies = [ + "futures-channel", "futures-core", "futures-io", + "futures-macro", "futures-sink", "futures-task", "memchr", @@ -1118,8 +1146,10 @@ dependencies = [ "crossterm", "directories", "dirs", + "futures", "human-panic", "hyperlog-core", + "hyperlog-protos", "itertools", "ratatui", "ropey", @@ -1128,6 +1158,7 @@ dependencies = [ "similar-asserts", "tempfile", "tokio", + "tonic", "tracing", "tracing-subscriber", ] diff --git a/Cargo.toml b/Cargo.toml index 2e6493a..fbc9e5f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,7 @@ serde_json = "1.0.117" itertools = "0.12.1" uuid = { version = "1.8.0", features = ["v4"] } tonic = "0.11.0" +futures = { version = "0.3.30" } [workspace.package] version = "0.1.0" diff --git a/crates/hyperlog-protos/proto/hyperlog.proto b/crates/hyperlog-protos/proto/hyperlog.proto index 5358ea2..9a41397 100644 --- a/crates/hyperlog-protos/proto/hyperlog.proto +++ b/crates/hyperlog-protos/proto/hyperlog.proto @@ -15,16 +15,16 @@ message SectionGraphItem { map items = 1; } -enum ItemState { - UNSPECIFIED = 0; - NOT_DONE = 1; - DONE = 2; -} +message ItemStateNotDone {} +message ItemStateDone {} message ItemGraphItem { string title = 1; string description = 2; - ItemState state = 3; + oneof item_state { + ItemStateNotDone not_done = 3; + ItemStateDone done = 4; + } } message GraphItem { @@ -42,6 +42,6 @@ message GetRequest { } message GetReply { - repeated GraphItem items = 1; + GraphItem item = 1; } diff --git a/crates/hyperlog-server/src/external_grpc.rs b/crates/hyperlog-server/src/external_grpc.rs index b01afc7..59fb353 100644 --- a/crates/hyperlog-server/src/external_grpc.rs +++ b/crates/hyperlog-server/src/external_grpc.rs @@ -32,28 +32,14 @@ impl Graph for Server { tracing::trace!("get: req({:?})", msg); Ok(Response::new(GetReply { - items: vec![ - GraphItem { - path: "some.path".into(), - contents: Some(graph_item::Contents::Item(ItemGraphItem { - title: "some-title".into(), - description: "some-description".into(), - state: ItemState::NotDone as i32, - })), - }, - GraphItem { - path: "some.path.section".into(), - contents: Some(graph_item::Contents::Section(SectionGraphItem { - items: HashMap::new(), - })), - }, - GraphItem { - path: "some.path".into(), - contents: Some(graph_item::Contents::User(UserGraphItem { - items: HashMap::new(), - })), - }, - ], + item: Some(GraphItem { + path: "some.path".into(), + contents: Some(graph_item::Contents::Item(ItemGraphItem { + title: "some-title".into(), + description: "some-description".into(), + item_state: Some(item_graph_item::ItemState::Done(ItemStateDone {})), + })), + }), })) } } diff --git a/crates/hyperlog-tui/Cargo.toml b/crates/hyperlog-tui/Cargo.toml index 631286a..c7f382b 100644 --- a/crates/hyperlog-tui/Cargo.toml +++ b/crates/hyperlog-tui/Cargo.toml @@ -6,6 +6,7 @@ repository = "https://git.front.kjuulh.io/kjuulh/hyperlog" [dependencies] hyperlog-core.workspace = true +hyperlog-protos.workspace = true anyhow.workspace = true tokio.workspace = true @@ -14,6 +15,8 @@ tracing-subscriber.workspace = true serde.workspace = true serde_json.workspace = true itertools.workspace = true +tonic.workspace = true +futures.workspace = true ratatui = "0.26.2" crossterm = { version = "0.27.0", features = ["event-stream"] } diff --git a/crates/hyperlog-tui/src/command_parser.rs b/crates/hyperlog-tui/src/command_parser.rs index d38fe6a..272454a 100644 --- a/crates/hyperlog-tui/src/command_parser.rs +++ b/crates/hyperlog-tui/src/command_parser.rs @@ -10,6 +10,7 @@ pub enum Commands { ShowAll, HideDone, + Test, } impl Commands { @@ -42,6 +43,7 @@ impl CommandParser { "e" | "edit" => Some(Commands::Edit), "show-all" => Some(Commands::ShowAll), "hide-done" => Some(Commands::HideDone), + "test" => Some(Commands::Test), _ => None, }, None => None, diff --git a/crates/hyperlog-tui/src/commands.rs b/crates/hyperlog-tui/src/commands.rs index 680978e..9b9ab47 100644 --- a/crates/hyperlog-tui/src/commands.rs +++ b/crates/hyperlog-tui/src/commands.rs @@ -1,3 +1,5 @@ +use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender}; + use crate::models::Msg; pub trait IntoCommand { @@ -6,7 +8,7 @@ pub trait IntoCommand { impl IntoCommand for () { fn into_command(self) -> Command { - Command::new(|| None) + Command::new(|_| None) } } @@ -16,16 +18,47 @@ impl IntoCommand for Command { } } +type CommandFunc = dyn FnOnce(Dispatch) -> Option; + pub struct Command { - func: Box Option>, + func: Box, } impl Command { - pub fn new Option + 'static>(f: T) -> Self { + pub fn new Option + 'static>(f: T) -> Self { Self { func: Box::new(f) } } - pub fn execute(self) -> Option { - self.func.call_once(()) + pub fn execute(self, dispatch: Dispatch) -> Option { + self.func.call_once((dispatch,)) + } +} + +pub fn create_dispatch() -> (Dispatch, Receiver) { + let (tx, rx) = tokio::sync::mpsc::unbounded_channel(); + + (Dispatch { sender: tx }, Receiver { receiver: rx }) +} + +#[derive(Clone)] +pub struct Dispatch { + sender: UnboundedSender, +} + +impl Dispatch { + pub fn send(&self, msg: Msg) { + if let Err(e) = self.sender.send(msg) { + tracing::warn!("failed to send event: {}", e); + } + } +} + +pub struct Receiver { + receiver: UnboundedReceiver, +} + +impl Receiver { + pub async fn next(&mut self) -> Option { + self.receiver.recv().await } } diff --git a/crates/hyperlog-tui/src/components/graph_explorer.rs b/crates/hyperlog-tui/src/components/graph_explorer.rs index 3dbc165..6d6d97f 100644 --- a/crates/hyperlog-tui/src/components/graph_explorer.rs +++ b/crates/hyperlog-tui/src/components/graph_explorer.rs @@ -3,7 +3,11 @@ use hyperlog_core::log::GraphItem; use ratatui::{prelude::*, widgets::*}; use crate::{ - command_parser::Commands, commander, components::movement_graph::GraphItemType, models::Msg, + command_parser::Commands, + commander, + commands::{Command, IntoCommand}, + components::movement_graph::GraphItemType, + models::Msg, state::SharedState, }; @@ -184,7 +188,7 @@ impl<'a> GraphExplorer<'a> { } } - pub fn execute_command(&mut self, command: &Commands) -> anyhow::Result> { + pub fn execute_command(&mut self, command: &Commands) -> anyhow::Result> { match command { Commands::Archive => { if !self.get_current_path().is_empty() { @@ -220,7 +224,9 @@ impl<'a> GraphExplorer<'a> { GraphItemType::Item { .. } => { if let Some(item) = self.state.querier.get(&self.inner.root, path) { if let GraphItem::Item { .. } = item { - return Ok(Some(Msg::OpenEditItemDialog { item })); + return Ok(Some( + Msg::OpenEditItemDialog { item }.into_command(), + )); } } } @@ -233,6 +239,18 @@ impl<'a> GraphExplorer<'a> { Commands::HideDone => { self.inner.display_options.filter_by = FilterBy::NotDone; } + Commands::Test => { + return Ok(Some(Command::new(|dispatch| { + tokio::spawn(async move { + dispatch.send(Msg::MoveDown); + tokio::time::sleep(std::time::Duration::from_secs(2)).await; + dispatch.send(Msg::EnterViewMode); + }); + + None + }))); + } + _ => (), } diff --git a/crates/hyperlog-tui/src/core_state.rs b/crates/hyperlog-tui/src/core_state.rs index f99299f..55734c3 100644 --- a/crates/hyperlog-tui/src/core_state.rs +++ b/crates/hyperlog-tui/src/core_state.rs @@ -26,7 +26,7 @@ impl State { events: events.clone(), commander: Commander::new(engine.clone(), storage, events)?, - querier: Querier::new(engine), + querier: Querier::local(&engine), }) } } diff --git a/crates/hyperlog-tui/src/lib.rs b/crates/hyperlog-tui/src/lib.rs index 2142cc9..f8ab2ed 100644 --- a/crates/hyperlog-tui/src/lib.rs +++ b/crates/hyperlog-tui/src/lib.rs @@ -1,14 +1,15 @@ #![feature(map_try_insert)] #![feature(fn_traits)] -use std::{io::Stdout, time::Duration}; +use std::io::Stdout; use anyhow::{Context, Result}; use app::{render_app, App}; -use commands::IntoCommand; +use commands::{Dispatch, IntoCommand, Receiver}; use components::graph_explorer::GraphExplorer; use core_state::State; -use crossterm::event::{self, Event, KeyCode}; +use crossterm::event::{Event, KeyCode, KeyEventKind}; +use futures::{FutureExt, StreamExt}; use models::{EditMsg, Msg}; use ratatui::{backend::CrosstermBackend, Terminal}; @@ -43,12 +44,12 @@ pub async fn execute(state: State) -> Result<()> { let state = SharedState::from(state); let mut terminal = TerminalInstance::new()?; - run(&mut terminal, state).context("app loop failed")?; + run(&mut terminal, state).await.context("app loop failed")?; Ok(()) } -fn run(terminal: &mut Terminal>, state: SharedState) -> Result<()> { +async fn run(terminal: &mut Terminal>, state: SharedState) -> Result<()> { let root = match state.querier.get_available_roots() { // TODO: maybe present choose root screen Some(roots) => roots.first().cloned().unwrap(), @@ -62,11 +63,22 @@ fn run(terminal: &mut Terminal>, state: SharedState) -> graph_explorer.update_graph()?; let mut app = App::new(&root, state.clone(), graph_explorer); + let (dispatch, mut receiver) = commands::create_dispatch(); + let mut event_stream = crossterm::event::EventStream::new(); loop { terminal.draw(|f| render_app(f, &mut app))?; - if update(terminal, &mut app)?.should_quit() { + if update( + terminal, + &mut app, + &dispatch, + &mut receiver, + &mut event_stream, + ) + .await? + .should_quit() + { break; } } @@ -85,53 +97,104 @@ impl UpdateConclusion { } } -fn update( +async fn update<'a>( _terminal: &mut Terminal>, - app: &mut App, + app: &mut App<'a>, + dispatch: &Dispatch, + receiver: &mut Receiver, + event_stream: &mut crossterm::event::EventStream, ) -> Result { - if event::poll(Duration::from_millis(250)).context("event poll failed")? { - if let Event::Key(key) = event::read().context("event read failed")? { - let mut cmd = match &app.mode { - app::Mode::View => match key.code { - KeyCode::Enter => app.update(Msg::Interact)?, - KeyCode::Char('l') => app.update(Msg::MoveRight)?, - KeyCode::Char('h') => app.update(Msg::MoveLeft)?, - KeyCode::Char('j') => app.update(Msg::MoveDown)?, - KeyCode::Char('k') => app.update(Msg::MoveUp)?, - KeyCode::Char('a') => { - // TODO: batch commands - app.update(Msg::OpenCreateItemDialog)?; - app.update(Msg::EnterInsertMode)? - } - KeyCode::Char('i') => app.update(Msg::EnterInsertMode)?, - KeyCode::Char(':') => app.update(Msg::EnterCommandMode)?, - _ => return Ok(UpdateConclusion(false)), - }, + let cross_event = event_stream.next().fuse(); - app::Mode::Command | app::Mode::Insert => match key.code { - KeyCode::Backspace => app.update(Msg::Edit(EditMsg::Delete))?, - KeyCode::Enter => app.update(Msg::Edit(EditMsg::InsertNewLine))?, - KeyCode::Tab => app.update(Msg::Edit(EditMsg::InsertTab))?, - KeyCode::Delete => app.update(Msg::Edit(EditMsg::DeleteNext))?, - KeyCode::Char(c) => app.update(Msg::Edit(EditMsg::InsertChar(c)))?, - KeyCode::Left => app.update(Msg::Edit(EditMsg::MoveLeft))?, - KeyCode::Right => app.update(Msg::Edit(EditMsg::MoveRight))?, - KeyCode::Esc => app.update(Msg::EnterViewMode)?, - _ => return Ok(UpdateConclusion(false)), - }, - }; + let mut handle_key_event = |maybe_event| -> anyhow::Result { + match maybe_event { + Some(Ok(e)) => { + if let Event::Key(key) = e { + if key.kind == KeyEventKind::Press { + let mut cmd = match &app.mode { + app::Mode::View => match key.code { + KeyCode::Enter => app.update(Msg::Interact)?, + KeyCode::Char('l') => app.update(Msg::MoveRight)?, + KeyCode::Char('h') => app.update(Msg::MoveLeft)?, + KeyCode::Char('j') => app.update(Msg::MoveDown)?, + KeyCode::Char('k') => app.update(Msg::MoveUp)?, + KeyCode::Char('a') => { + // TODO: batch commands + app.update(Msg::OpenCreateItemDialog)?; + app.update(Msg::EnterInsertMode)? + } + KeyCode::Char('i') => app.update(Msg::EnterInsertMode)?, + KeyCode::Char(':') => app.update(Msg::EnterCommandMode)?, + _ => return Ok(UpdateConclusion(false)), + }, - loop { - let msg = cmd.into_command().execute(); - match msg { - Some(msg) => { - if let Msg::QuitApp = msg { - return Ok(UpdateConclusion(true)); + app::Mode::Command | app::Mode::Insert => match key.code { + KeyCode::Backspace => app.update(Msg::Edit(EditMsg::Delete))?, + KeyCode::Enter => app.update(Msg::Edit(EditMsg::InsertNewLine))?, + KeyCode::Tab => app.update(Msg::Edit(EditMsg::InsertTab))?, + KeyCode::Delete => app.update(Msg::Edit(EditMsg::DeleteNext))?, + KeyCode::Char(c) => { + app.update(Msg::Edit(EditMsg::InsertChar(c)))? + } + KeyCode::Left => app.update(Msg::Edit(EditMsg::MoveLeft))?, + KeyCode::Right => app.update(Msg::Edit(EditMsg::MoveRight))?, + KeyCode::Esc => app.update(Msg::EnterViewMode)?, + _ => return Ok(UpdateConclusion(false)), + }, + }; + + loop { + let msg = cmd.into_command().execute(dispatch.clone()); + match msg { + Some(msg) => { + if let Msg::QuitApp = msg { + return Ok(UpdateConclusion(true)); + } + + cmd = app.update(msg)?; + } + None => break, + } } - - cmd = app.update(msg)?; } - None => break, + } + } + Some(Err(e)) => { + tracing::warn!("failed to send event: {}", e); + } + None => {} + } + + Ok(UpdateConclusion(false)) + }; + + tokio::select! { + maybe_event = cross_event => { + let conclusion = handle_key_event(maybe_event)?; + + return Ok(conclusion) + }, + + msg = receiver.next() => { + if let Some(msg) = msg { + if let Msg::QuitApp = msg { + return Ok(UpdateConclusion(true)); + } + + let mut cmd = app.update(msg)?; + + loop { + let msg = cmd.into_command().execute(dispatch.clone()); + match msg { + Some(msg) => { + if let Msg::QuitApp = msg { + return Ok(UpdateConclusion(true)); + } + + cmd = app.update(msg)?; + } + None => break, + } } } } diff --git a/crates/hyperlog-tui/src/models.rs b/crates/hyperlog-tui/src/models.rs index 7eee074..59f655e 100644 --- a/crates/hyperlog-tui/src/models.rs +++ b/crates/hyperlog-tui/src/models.rs @@ -24,7 +24,7 @@ pub enum Msg { impl IntoCommand for Msg { fn into_command(self) -> crate::commands::Command { - Command::new(|| Some(self)) + Command::new(|_| Some(self)) } } diff --git a/crates/hyperlog-tui/src/querier.rs b/crates/hyperlog-tui/src/querier.rs index 28c59a3..b0a7b57 100644 --- a/crates/hyperlog-tui/src/querier.rs +++ b/crates/hyperlog-tui/src/querier.rs @@ -2,17 +2,22 @@ use hyperlog_core::log::GraphItem; use crate::shared_engine::SharedEngine; +mod local; +mod remote; + +enum QuerierVariant { + Local(local::Querier), +} + pub struct Querier { - engine: SharedEngine, + variant: QuerierVariant, } impl Querier { - pub fn new(engine: SharedEngine) -> Self { - Self { engine } - } - - pub fn get_available_roots(&self) -> Option> { - self.engine.get_roots() + pub fn local(engine: &SharedEngine) -> Self { + Self { + variant: QuerierVariant::Local(local::Querier::new(engine)), + } } pub fn get( @@ -20,20 +25,14 @@ impl Querier { root: &str, path: impl IntoIterator>, ) -> Option { - let path = path - .into_iter() - .map(|i| i.into()) - .filter(|i| !i.is_empty()) - .collect::>(); + match &self.variant { + QuerierVariant::Local(querier) => querier.get(root, path), + } + } - tracing::debug!( - "quering: root:({}), path:({}), len: ({}))", - root, - path.join("."), - path.len() - ); - - self.engine - .get(root, &path.iter().map(|i| i.as_str()).collect::>()) + pub fn get_available_roots(&self) -> Option> { + match &self.variant { + QuerierVariant::Local(querier) => querier.get_available_roots(), + } } } diff --git a/crates/hyperlog-tui/src/querier/local.rs b/crates/hyperlog-tui/src/querier/local.rs new file mode 100644 index 0000000..b5a3811 --- /dev/null +++ b/crates/hyperlog-tui/src/querier/local.rs @@ -0,0 +1,41 @@ +use hyperlog_core::log::GraphItem; + +use crate::shared_engine::SharedEngine; + +pub struct Querier { + engine: SharedEngine, +} + +impl Querier { + pub fn new(engine: &SharedEngine) -> Self { + Self { + engine: engine.clone(), + } + } + + pub fn get_available_roots(&self) -> Option> { + self.engine.get_roots() + } + + pub fn get( + &self, + root: &str, + path: impl IntoIterator>, + ) -> Option { + let path = path + .into_iter() + .map(|i| i.into()) + .filter(|i| !i.is_empty()) + .collect::>(); + + tracing::debug!( + "quering: root:({}), path:({}), len: ({}))", + root, + path.join("."), + path.len() + ); + + self.engine + .get(root, &path.iter().map(|i| i.as_str()).collect::>()) + } +} diff --git a/crates/hyperlog-tui/src/querier/remote.rs b/crates/hyperlog-tui/src/querier/remote.rs new file mode 100644 index 0000000..cdd4fdc --- /dev/null +++ b/crates/hyperlog-tui/src/querier/remote.rs @@ -0,0 +1,106 @@ +use std::collections::BTreeMap; + +use hyperlog_core::log::GraphItem; +use hyperlog_protos::hyperlog::{graph_client::GraphClient, graph_item::Contents, GetRequest}; +use itertools::Itertools; +use tonic::transport::Channel; + +use crate::shared_engine::SharedEngine; + +pub struct Querier { + channel: Channel, +} + +impl Querier { + pub async fn new() -> anyhow::Result { + let channel = Channel::from_static("http://localhost:4000") + .connect() + .await?; + + Ok(Self { channel }) + } + + pub async fn get_available_roots(&self) -> Option> { + //self.engine.get_roots() + todo!() + } + + pub async fn get( + &self, + root: &str, + path: impl IntoIterator>, + ) -> anyhow::Result> { + let paths = path.into_iter().map(|i| i.into()).collect_vec(); + + tracing::debug!( + "quering: root:({}), path:({}), len: ({}))", + root, + paths.join("."), + paths.len() + ); + + let channel = self.channel.clone(); + + let mut client = GraphClient::new(channel); + + let request = tonic::Request::new(GetRequest { + root: root.into(), + paths, + }); + + let response = client.get(request).await?; + + let graph_item = response.into_inner(); + + if let Some(item) = graph_item.item { + Ok(transform_proto_to_local(&item)) + } else { + Ok(None) + } + } +} + +fn transform_proto_to_local(input: &hyperlog_protos::hyperlog::GraphItem) -> Option { + match &input.contents { + Some(item) => match item { + Contents::User(user) => { + let mut items = BTreeMap::new(); + + for (key, value) in &user.items { + if let Some(item) = transform_proto_to_local(value) { + items.insert(key.clone(), item); + } + } + + Some(GraphItem::User(items)) + } + Contents::Section(section) => { + let mut items = BTreeMap::new(); + + for (key, value) in §ion.items { + if let Some(item) = transform_proto_to_local(value) { + items.insert(key.clone(), item); + } + } + + Some(GraphItem::Section(items)) + } + Contents::Item(item) => Some(GraphItem::Item { + title: item.title.clone(), + description: item.description.clone(), + state: match &item.item_state { + Some(state) => match state { + hyperlog_protos::hyperlog::item_graph_item::ItemState::NotDone(_) => { + hyperlog_core::log::ItemState::NotDone + } + hyperlog_protos::hyperlog::item_graph_item::ItemState::Done(_) => { + hyperlog_core::log::ItemState::Done + } + }, + None => hyperlog_core::log::ItemState::NotDone, + }, + }), + }, + None => None, + } +}