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",
]
[[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",
]

View File

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

View File

@ -15,16 +15,16 @@ message SectionGraphItem {
map<string, GraphItem> 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;
}

View File

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

View File

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

View File

@ -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<Msg>;
pub struct Command {
func: Box<dyn FnOnce() -> Option<Msg>>,
func: Box<CommandFunc>,
}
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) }
}
pub fn execute(self) -> Option<Msg> {
self.func.call_once(())
pub fn execute(self, dispatch: Dispatch) -> Option<Msg> {
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 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<Option<Msg>> {
pub fn execute_command(&mut self, command: &Commands) -> anyhow::Result<Option<Command>> {
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
})));
}
_ => (),
}

View File

@ -26,7 +26,7 @@ impl State {
events: events.clone(),
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(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<CrosstermBackend<Stdout>>, state: SharedState) -> Result<()> {
async fn run(terminal: &mut Terminal<CrosstermBackend<Stdout>>, 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<CrosstermBackend<Stdout>>, 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,12 +97,20 @@ impl UpdateConclusion {
}
}
fn update(
async fn update<'a>(
_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> {
if event::poll(Duration::from_millis(250)).context("event poll failed")? {
if let Event::Key(key) = event::read().context("event read failed")? {
let cross_event = event_stream.next().fuse();
let mut handle_key_event = |maybe_event| -> anyhow::Result<UpdateConclusion> {
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)?,
@ -113,7 +133,9 @@ fn update(
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::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)?,
@ -122,7 +144,7 @@ fn update(
};
loop {
let msg = cmd.into_command().execute();
let msg = cmd.into_command().execute(dispatch.clone());
match msg {
Some(msg) => {
if let Msg::QuitApp = msg {
@ -136,6 +158,47 @@ fn update(
}
}
}
}
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,
}
}
}
}
}
Ok(UpdateConclusion::new(false))
}

View File

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

View File

@ -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 local(engine: &SharedEngine) -> Self {
Self {
variant: QuerierVariant::Local(local::Querier::new(engine)),
}
pub fn get_available_roots(&self) -> Option<Vec<String>> {
self.engine.get_roots()
}
pub fn get(
@ -20,20 +25,14 @@ impl Querier {
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>>();
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::<Vec<_>>())
pub fn get_available_roots(&self) -> Option<Vec<String>> {
match &self.variant {
QuerierVariant::Local(querier) => querier.get_available_roots(),
}
}
}

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