feat: allow async function in command
All checks were successful
continuous-integration/drone/push Build is passing

Signed-off-by: kjuulh <contact@kjuulh.io>
This commit is contained in:
Kasper Juul Hermansen 2024-05-12 12:58:54 +02:00
parent 4a0fcd1bbb
commit cf26422673
Signed by: kjuulh
GPG Key ID: 57B6E1465221F912
14 changed files with 390 additions and 107 deletions

31
Cargo.lock generated
View File

@ -687,6 +687,21 @@ dependencies = [
"percent-encoding", "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]] [[package]]
name = "futures-channel" name = "futures-channel"
version = "0.3.30" version = "0.3.30"
@ -731,6 +746,17 @@ version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" 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]] [[package]]
name = "futures-sink" name = "futures-sink"
version = "0.3.30" version = "0.3.30"
@ -749,8 +775,10 @@ version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48"
dependencies = [ dependencies = [
"futures-channel",
"futures-core", "futures-core",
"futures-io", "futures-io",
"futures-macro",
"futures-sink", "futures-sink",
"futures-task", "futures-task",
"memchr", "memchr",
@ -1118,8 +1146,10 @@ dependencies = [
"crossterm", "crossterm",
"directories", "directories",
"dirs", "dirs",
"futures",
"human-panic", "human-panic",
"hyperlog-core", "hyperlog-core",
"hyperlog-protos",
"itertools", "itertools",
"ratatui", "ratatui",
"ropey", "ropey",
@ -1128,6 +1158,7 @@ dependencies = [
"similar-asserts", "similar-asserts",
"tempfile", "tempfile",
"tokio", "tokio",
"tonic",
"tracing", "tracing",
"tracing-subscriber", "tracing-subscriber",
] ]

View File

@ -20,6 +20,7 @@ serde_json = "1.0.117"
itertools = "0.12.1" itertools = "0.12.1"
uuid = { version = "1.8.0", features = ["v4"] } uuid = { version = "1.8.0", features = ["v4"] }
tonic = "0.11.0" tonic = "0.11.0"
futures = { version = "0.3.30" }
[workspace.package] [workspace.package]
version = "0.1.0" version = "0.1.0"

View File

@ -15,16 +15,16 @@ message SectionGraphItem {
map<string, GraphItem> items = 1; map<string, GraphItem> items = 1;
} }
enum ItemState { message ItemStateNotDone {}
UNSPECIFIED = 0; message ItemStateDone {}
NOT_DONE = 1;
DONE = 2;
}
message ItemGraphItem { message ItemGraphItem {
string title = 1; string title = 1;
string description = 2; string description = 2;
ItemState state = 3; oneof item_state {
ItemStateNotDone not_done = 3;
ItemStateDone done = 4;
}
} }
message GraphItem { message GraphItem {
@ -42,6 +42,6 @@ message GetRequest {
} }
message GetReply { message GetReply {
repeated GraphItem items = 1; GraphItem item = 1;
} }

View File

@ -32,28 +32,14 @@ impl Graph for Server {
tracing::trace!("get: req({:?})", msg); tracing::trace!("get: req({:?})", msg);
Ok(Response::new(GetReply { Ok(Response::new(GetReply {
items: vec![ item: Some(GraphItem {
GraphItem { path: "some.path".into(),
path: "some.path".into(), contents: Some(graph_item::Contents::Item(ItemGraphItem {
contents: Some(graph_item::Contents::Item(ItemGraphItem { title: "some-title".into(),
title: "some-title".into(), description: "some-description".into(),
description: "some-description".into(), item_state: Some(item_graph_item::ItemState::Done(ItemStateDone {})),
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(),
})),
},
],
})) }))
} }
} }

View File

@ -6,6 +6,7 @@ repository = "https://git.front.kjuulh.io/kjuulh/hyperlog"
[dependencies] [dependencies]
hyperlog-core.workspace = true hyperlog-core.workspace = true
hyperlog-protos.workspace = true
anyhow.workspace = true anyhow.workspace = true
tokio.workspace = true tokio.workspace = true
@ -14,6 +15,8 @@ tracing-subscriber.workspace = true
serde.workspace = true serde.workspace = true
serde_json.workspace = true serde_json.workspace = true
itertools.workspace = true itertools.workspace = true
tonic.workspace = true
futures.workspace = true
ratatui = "0.26.2" ratatui = "0.26.2"
crossterm = { version = "0.27.0", features = ["event-stream"] } crossterm = { version = "0.27.0", features = ["event-stream"] }

View File

@ -10,6 +10,7 @@ pub enum Commands {
ShowAll, ShowAll,
HideDone, HideDone,
Test,
} }
impl Commands { impl Commands {
@ -42,6 +43,7 @@ impl CommandParser {
"e" | "edit" => Some(Commands::Edit), "e" | "edit" => Some(Commands::Edit),
"show-all" => Some(Commands::ShowAll), "show-all" => Some(Commands::ShowAll),
"hide-done" => Some(Commands::HideDone), "hide-done" => Some(Commands::HideDone),
"test" => Some(Commands::Test),
_ => None, _ => None,
}, },
None => None, None => None,

View File

@ -1,3 +1,5 @@
use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender};
use crate::models::Msg; use crate::models::Msg;
pub trait IntoCommand { pub trait IntoCommand {
@ -6,7 +8,7 @@ pub trait IntoCommand {
impl IntoCommand for () { impl IntoCommand for () {
fn into_command(self) -> Command { 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<Msg>;
pub struct Command { pub struct Command {
func: Box<dyn FnOnce() -> Option<Msg>>, func: Box<CommandFunc>,
} }
impl Command { impl Command {
pub fn new<T: FnOnce() -> Option<Msg> + 'static>(f: T) -> Self { pub fn new<T: FnOnce(Dispatch) -> Option<Msg> + 'static>(f: T) -> Self {
Self { func: Box::new(f) } Self { func: Box::new(f) }
} }
pub fn execute(self) -> Option<Msg> { pub fn execute(self, dispatch: Dispatch) -> Option<Msg> {
self.func.call_once(()) 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<Msg>,
}
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<Msg>,
}
impl Receiver {
pub async fn next(&mut self) -> Option<Msg> {
self.receiver.recv().await
} }
} }

View File

@ -3,7 +3,11 @@ use hyperlog_core::log::GraphItem;
use ratatui::{prelude::*, widgets::*}; use ratatui::{prelude::*, widgets::*};
use crate::{ 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, state::SharedState,
}; };
@ -184,7 +188,7 @@ impl<'a> GraphExplorer<'a> {
} }
} }
pub fn execute_command(&mut self, command: &Commands) -> anyhow::Result<Option<Msg>> { pub fn execute_command(&mut self, command: &Commands) -> anyhow::Result<Option<Command>> {
match command { match command {
Commands::Archive => { Commands::Archive => {
if !self.get_current_path().is_empty() { if !self.get_current_path().is_empty() {
@ -220,7 +224,9 @@ impl<'a> GraphExplorer<'a> {
GraphItemType::Item { .. } => { GraphItemType::Item { .. } => {
if let Some(item) = self.state.querier.get(&self.inner.root, path) { if let Some(item) = self.state.querier.get(&self.inner.root, path) {
if let GraphItem::Item { .. } = item { 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 => { Commands::HideDone => {
self.inner.display_options.filter_by = FilterBy::NotDone; 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
})));
}
_ => (), _ => (),
} }

View File

@ -26,7 +26,7 @@ impl State {
events: events.clone(), events: events.clone(),
commander: Commander::new(engine.clone(), storage, events)?, commander: Commander::new(engine.clone(), storage, events)?,
querier: Querier::new(engine), querier: Querier::local(&engine),
}) })
} }
} }

View File

@ -1,14 +1,15 @@
#![feature(map_try_insert)] #![feature(map_try_insert)]
#![feature(fn_traits)] #![feature(fn_traits)]
use std::{io::Stdout, time::Duration}; use std::io::Stdout;
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use app::{render_app, App}; use app::{render_app, App};
use commands::IntoCommand; use commands::{Dispatch, IntoCommand, Receiver};
use components::graph_explorer::GraphExplorer; use components::graph_explorer::GraphExplorer;
use core_state::State; 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 models::{EditMsg, Msg};
use ratatui::{backend::CrosstermBackend, Terminal}; use ratatui::{backend::CrosstermBackend, Terminal};
@ -43,12 +44,12 @@ pub async fn execute(state: State) -> Result<()> {
let state = SharedState::from(state); let state = SharedState::from(state);
let mut terminal = TerminalInstance::new()?; let mut terminal = TerminalInstance::new()?;
run(&mut terminal, state).context("app loop failed")?; run(&mut terminal, state).await.context("app loop failed")?;
Ok(()) Ok(())
} }
fn run(terminal: &mut Terminal<CrosstermBackend<Stdout>>, state: SharedState) -> Result<()> { async fn run(terminal: &mut Terminal<CrosstermBackend<Stdout>>, state: SharedState) -> Result<()> {
let root = match state.querier.get_available_roots() { let root = match state.querier.get_available_roots() {
// TODO: maybe present choose root screen // TODO: maybe present choose root screen
Some(roots) => roots.first().cloned().unwrap(), Some(roots) => roots.first().cloned().unwrap(),
@ -62,11 +63,22 @@ fn run(terminal: &mut Terminal<CrosstermBackend<Stdout>>, state: SharedState) ->
graph_explorer.update_graph()?; graph_explorer.update_graph()?;
let mut app = App::new(&root, state.clone(), graph_explorer); 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 { loop {
terminal.draw(|f| render_app(f, &mut app))?; 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; break;
} }
} }
@ -85,53 +97,104 @@ impl UpdateConclusion {
} }
} }
fn update( async fn update<'a>(
_terminal: &mut Terminal<CrosstermBackend<Stdout>>, _terminal: &mut Terminal<CrosstermBackend<Stdout>>,
app: &mut App, app: &mut App<'a>,
dispatch: &Dispatch,
receiver: &mut Receiver,
event_stream: &mut crossterm::event::EventStream,
) -> Result<UpdateConclusion> { ) -> Result<UpdateConclusion> {
if event::poll(Duration::from_millis(250)).context("event poll failed")? { let cross_event = event_stream.next().fuse();
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)),
},
app::Mode::Command | app::Mode::Insert => match key.code { let mut handle_key_event = |maybe_event| -> anyhow::Result<UpdateConclusion> {
KeyCode::Backspace => app.update(Msg::Edit(EditMsg::Delete))?, match maybe_event {
KeyCode::Enter => app.update(Msg::Edit(EditMsg::InsertNewLine))?, Some(Ok(e)) => {
KeyCode::Tab => app.update(Msg::Edit(EditMsg::InsertTab))?, if let Event::Key(key) = e {
KeyCode::Delete => app.update(Msg::Edit(EditMsg::DeleteNext))?, if key.kind == KeyEventKind::Press {
KeyCode::Char(c) => app.update(Msg::Edit(EditMsg::InsertChar(c)))?, let mut cmd = match &app.mode {
KeyCode::Left => app.update(Msg::Edit(EditMsg::MoveLeft))?, app::Mode::View => match key.code {
KeyCode::Right => app.update(Msg::Edit(EditMsg::MoveRight))?, KeyCode::Enter => app.update(Msg::Interact)?,
KeyCode::Esc => app.update(Msg::EnterViewMode)?, KeyCode::Char('l') => app.update(Msg::MoveRight)?,
_ => return Ok(UpdateConclusion(false)), 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 { app::Mode::Command | app::Mode::Insert => match key.code {
let msg = cmd.into_command().execute(); KeyCode::Backspace => app.update(Msg::Edit(EditMsg::Delete))?,
match msg { KeyCode::Enter => app.update(Msg::Edit(EditMsg::InsertNewLine))?,
Some(msg) => { KeyCode::Tab => app.update(Msg::Edit(EditMsg::InsertTab))?,
if let Msg::QuitApp = msg { KeyCode::Delete => app.update(Msg::Edit(EditMsg::DeleteNext))?,
return Ok(UpdateConclusion(true)); 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,
}
} }
} }
} }

View File

@ -24,7 +24,7 @@ pub enum Msg {
impl IntoCommand for Msg { impl IntoCommand for Msg {
fn into_command(self) -> crate::commands::Command { fn into_command(self) -> crate::commands::Command {
Command::new(|| Some(self)) Command::new(|_| Some(self))
} }
} }

View File

@ -2,17 +2,22 @@ use hyperlog_core::log::GraphItem;
use crate::shared_engine::SharedEngine; use crate::shared_engine::SharedEngine;
mod local;
mod remote;
enum QuerierVariant {
Local(local::Querier),
}
pub struct Querier { pub struct Querier {
engine: SharedEngine, variant: QuerierVariant,
} }
impl Querier { impl Querier {
pub fn new(engine: SharedEngine) -> Self { pub fn local(engine: &SharedEngine) -> Self {
Self { engine } Self {
} variant: QuerierVariant::Local(local::Querier::new(engine)),
}
pub fn get_available_roots(&self) -> Option<Vec<String>> {
self.engine.get_roots()
} }
pub fn get( pub fn get(
@ -20,20 +25,14 @@ impl Querier {
root: &str, root: &str,
path: impl IntoIterator<Item = impl Into<String>>, path: impl IntoIterator<Item = impl Into<String>>,
) -> Option<GraphItem> { ) -> Option<GraphItem> {
let path = path match &self.variant {
.into_iter() QuerierVariant::Local(querier) => querier.get(root, path),
.map(|i| i.into()) }
.filter(|i| !i.is_empty()) }
.collect::<Vec<String>>();
tracing::debug!( pub fn get_available_roots(&self) -> Option<Vec<String>> {
"quering: root:({}), path:({}), len: ({}))", match &self.variant {
root, QuerierVariant::Local(querier) => querier.get_available_roots(),
path.join("."), }
path.len()
);
self.engine
.get(root, &path.iter().map(|i| i.as_str()).collect::<Vec<_>>())
} }
} }

View File

@ -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<Vec<String>> {
self.engine.get_roots()
}
pub fn get(
&self,
root: &str,
path: impl IntoIterator<Item = impl Into<String>>,
) -> Option<GraphItem> {
let path = path
.into_iter()
.map(|i| i.into())
.filter(|i| !i.is_empty())
.collect::<Vec<String>>();
tracing::debug!(
"quering: root:({}), path:({}), len: ({}))",
root,
path.join("."),
path.len()
);
self.engine
.get(root, &path.iter().map(|i| i.as_str()).collect::<Vec<_>>())
}
}

View File

@ -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<Self> {
let channel = Channel::from_static("http://localhost:4000")
.connect()
.await?;
Ok(Self { channel })
}
pub async fn get_available_roots(&self) -> Option<Vec<String>> {
//self.engine.get_roots()
todo!()
}
pub async fn get(
&self,
root: &str,
path: impl IntoIterator<Item = impl Into<String>>,
) -> anyhow::Result<Option<GraphItem>> {
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<GraphItem> {
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 &section.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,
}
}