feat: with async commands instead of inline mutations phew.
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 21:07:21 +02:00
parent 2d63d3ad4c
commit 9bb5bc9e87
Signed by: kjuulh
GPG Key ID: 57B6E1465221F912
19 changed files with 589 additions and 141 deletions

View File

@ -3,9 +3,14 @@ syntax = "proto3";
package hyperlog;
service Graph {
rpc GetAvailableRoots(GetAvailableRootsRequest) returns (GetAvailableRootsResponse);
rpc Get(GetRequest) returns (GetReply);
}
message GetAvailableRootsRequest {}
message GetAvailableRootsResponse {
repeated string roots = 1;
}
message UserGraphItem {
map<string, GraphItem> items = 1;

View File

@ -2,7 +2,7 @@ use hyperlog_protos::hyperlog::{
graph_server::{Graph, GraphServer},
*,
};
use std::net::SocketAddr;
use std::{collections::HashMap, net::SocketAddr};
use tonic::{transport, Response};
use crate::{
@ -33,15 +33,37 @@ impl Graph for Server {
Ok(Response::new(GetReply {
item: Some(GraphItem {
path: "some.path".into(),
path: "kjuulh".into(),
contents: Some(graph_item::Contents::User(UserGraphItem {
items: HashMap::from([(
"some".to_string(),
GraphItem {
path: "some".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 {})),
item_state: Some(item_graph_item::ItemState::NotDone(
ItemStateNotDone {},
)),
})),
},
)]),
})),
}),
}))
}
async fn get_available_roots(
&self,
request: tonic::Request<GetAvailableRootsRequest>,
) -> std::result::Result<tonic::Response<GetAvailableRootsResponse>, tonic::Status> {
let req = request.into_inner();
tracing::trace!("get available roots: req({:?})", req);
Ok(Response::new(GetAvailableRootsResponse {
roots: vec!["kjuulh".into()],
}))
}
}
pub trait ServerExt {

View File

@ -6,9 +6,9 @@ use ratatui::{
use crate::{
command_parser::CommandParser,
commander,
commands::{batch::BatchCommand, IntoCommand},
components::graph_explorer::GraphExplorer,
models::IOEvent,
state::SharedState,
Msg,
};
@ -30,10 +30,10 @@ pub enum Dialog {
}
impl Dialog {
pub fn get_command(&self) -> Option<commander::Command> {
pub fn get_command(&self) -> Option<impl IntoCommand> {
match self {
Dialog::CreateItem { state } => state.get_command(),
Dialog::EditItem { state } => state.get_command(),
Dialog::CreateItem { state } => state.get_command().map(|c| c.into_command()),
Dialog::EditItem { state } => state.get_command().map(|c| c.into_command()),
}
}
}
@ -89,6 +89,12 @@ impl<'a> App<'a> {
let mut batch = BatchCommand::default();
match &msg {
Msg::ItemCreated(IOEvent::Success(()))
| Msg::ItemUpdated(IOEvent::Success(()))
| Msg::SectionCreated(IOEvent::Success(()))
| Msg::ItemToggled(IOEvent::Success(())) => {
batch.with(self.graph_explorer.new_update_graph());
}
Msg::MoveRight => self.graph_explorer.move_right()?,
Msg::MoveLeft => self.graph_explorer.move_left()?,
Msg::MoveDown => self.graph_explorer.move_down()?,
@ -117,11 +123,9 @@ impl<'a> App<'a> {
if command.is_write() {
if let Some(dialog) = &self.dialog {
if let Some(output) = dialog.get_command() {
self.state.commander.execute(output)?;
batch.with(output.into_command());
}
}
batch.with(self.graph_explorer.new_update_graph());
}
if command.is_quit() {
@ -172,7 +176,7 @@ impl<'a> App<'a> {
self.focus = AppFocus::Dialog;
self.dialog = Some(Dialog::CreateItem {
state: CreateItemState::new(root, path),
state: CreateItemState::new(&self.state, root, path),
});
}
}
@ -183,7 +187,7 @@ impl<'a> App<'a> {
let path = self.graph_explorer.get_current_path();
self.dialog = Some(Dialog::EditItem {
state: EditItemState::new(root, path, item),
state: EditItemState::new(&self.state, root, path, item),
});
self.command = None;
self.focus = AppFocus::Dialog;

View File

@ -1,7 +1,12 @@
use hyperlog_core::log::ItemState;
use itertools::Itertools;
use ratatui::{prelude::*, widgets::*};
use crate::{commander, models::Msg};
use crate::{
commands::{create_item::CreateItemCommandExt, IntoCommand},
models::Msg,
state::SharedState,
};
use super::{InputBuffer, InputField};
@ -23,10 +28,16 @@ pub struct CreateItemState {
description: InputBuffer,
focused: CreateItemFocused,
state: SharedState,
}
impl CreateItemState {
pub fn new(root: impl Into<String>, path: impl IntoIterator<Item = impl Into<String>>) -> Self {
pub fn new(
state: &SharedState,
root: impl Into<String>,
path: impl IntoIterator<Item = impl Into<String>>,
) -> Self {
let root = root.into();
let path = path.into_iter().map(|p| p.into()).collect_vec();
@ -37,6 +48,8 @@ impl CreateItemState {
title: Default::default(),
description: Default::default(),
focused: Default::default(),
state: state.clone(),
}
}
@ -61,7 +74,7 @@ impl CreateItemState {
Ok(())
}
pub fn get_command(&self) -> Option<commander::Command> {
pub fn get_command(&self) -> Option<impl IntoCommand> {
let title = self.title.string();
let description = self.description.string();
@ -69,13 +82,21 @@ impl CreateItemState {
let mut path = self.path.clone();
path.push(title.replace([' ', '.'], "-"));
Some(commander::Command::CreateItem {
root: self.root.clone(),
path,
title: title.trim().into(),
description: description.trim().into(),
state: hyperlog_core::log::ItemState::NotDone,
})
Some(self.state.create_item_command().command(
&self.root,
&path.iter().map(|i| i.as_str()).collect_vec(),
title.trim(),
description.trim(),
&ItemState::NotDone,
))
// Some(commander::Command::CreateItem {
// root: self.root.clone(),
// path,
// title: title.trim().into(),
// description: description.trim().into(),
// state: hyperlog_core::log::ItemState::NotDone,
// })
} else {
None
}

View File

@ -2,7 +2,11 @@ use hyperlog_core::log::GraphItem;
use itertools::Itertools;
use ratatui::{prelude::*, widgets::*};
use crate::{commander, models::Msg};
use crate::{
commands::{update_item::UpdateItemCommandExt, IntoCommand},
models::Msg,
state::SharedState,
};
use super::{InputBuffer, InputField};
@ -26,10 +30,13 @@ pub struct EditItemState {
item: GraphItem,
focused: EditItemFocused,
state: SharedState,
}
impl EditItemState {
pub fn new(
state: &SharedState,
root: impl Into<String>,
path: impl IntoIterator<Item = impl Into<String>>,
item: &GraphItem,
@ -47,6 +54,8 @@ impl EditItemState {
title.set_position(title_len);
Self {
state: state.clone(),
root,
path,
@ -82,24 +91,36 @@ impl EditItemState {
Ok(())
}
pub fn get_command(&self) -> Option<commander::Command> {
pub fn get_command(&self) -> Option<impl IntoCommand> {
let title = self.title.string();
let description = self.description.string();
if !title.is_empty() {
let path = self.path.clone();
Some(commander::Command::UpdateItem {
root: self.root.clone(),
path,
title: title.trim().into(),
description: description.trim().into(),
state: match &self.item {
Some(self.state.update_item_command().command(
&self.root,
&path.iter().map(|s| s.as_str()).collect_vec(),
title.trim(),
description.trim(),
match &self.item {
GraphItem::User(_) => Default::default(),
GraphItem::Section(_) => Default::default(),
GraphItem::Item { state, .. } => state.clone(),
},
})
))
// Some(commander::Command::UpdateItem {
// root: self.root.clone(),
// path,
// title: title.trim().into(),
// description: description.trim().into(),
// state: match &self.item {
// GraphItem::User(_) => Default::default(),
// GraphItem::Section(_) => Default::default(),
// GraphItem::Item { state, .. } => state.clone(),
// },
// })
} else {
None
}

View File

@ -1,6 +1,4 @@
use std::collections::BTreeMap;
use hyperlog_core::log::{GraphItem, ItemState};
use hyperlog_core::log::ItemState;
use serde::Serialize;
use crate::{events::Events, shared_engine::SharedEngine, storage::Storage};
@ -39,6 +37,40 @@ pub enum Command {
},
}
#[derive(Clone)]
enum CommanderVariant {
Local(local::Commander),
}
#[derive(Clone)]
pub struct Commander {
variant: CommanderVariant,
}
impl Commander {
pub fn local(engine: SharedEngine, storage: Storage, events: Events) -> anyhow::Result<Self> {
Ok(Self {
variant: CommanderVariant::Local(local::Commander::new(engine, storage, events)?),
})
}
pub async fn execute(&self, cmd: Command) -> anyhow::Result<()> {
match &self.variant {
CommanderVariant::Local(commander) => commander.execute(cmd),
}
}
}
mod local {
use std::collections::BTreeMap;
use hyperlog_core::log::GraphItem;
use crate::{events::Events, shared_engine::SharedEngine, storage::Storage};
use super::Command;
#[derive(Clone)]
pub struct Commander {
engine: SharedEngine,
storage: Storage,
@ -115,3 +147,4 @@ impl Commander {
Ok(())
}
}
}

View File

@ -2,7 +2,11 @@ use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender};
pub mod batch;
pub mod create_item;
pub mod create_section;
pub mod toggle_item;
pub mod update_graph;
pub mod update_item;
use crate::models::Msg;

View File

@ -0,0 +1,75 @@
use hyperlog_core::log::ItemState;
use itertools::Itertools;
use crate::{
commander::{self, Commander},
models::IOEvent,
state::SharedState,
};
pub struct CreateItemCommand {
commander: Commander,
}
impl CreateItemCommand {
pub fn new(commander: Commander) -> Self {
Self { commander }
}
pub fn command(
self,
root: &str,
path: &[&str],
title: &str,
description: &str,
state: &ItemState,
) -> super::Command {
let root = root.to_owned();
let path = path.iter().map(|s| s.to_string()).collect_vec();
let title = title.to_string();
let description = description.to_string();
let state = state.clone();
super::Command::new(|dispatch| {
tokio::spawn(async move {
dispatch.send(crate::models::Msg::ItemCreated(IOEvent::Initialized));
match self
.commander
.execute(commander::Command::CreateItem {
root,
path,
title,
description,
state,
})
.await
{
Ok(()) => {
#[cfg(debug_assertions)]
{
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
}
dispatch.send(crate::models::Msg::ItemCreated(IOEvent::Success(())));
}
Err(e) => {
dispatch.send(crate::models::Msg::ItemCreated(IOEvent::Failure(
e.to_string(),
)));
}
}
});
None
})
}
}
pub trait CreateItemCommandExt {
fn create_item_command(&self) -> CreateItemCommand;
}
impl CreateItemCommandExt for SharedState {
fn create_item_command(&self) -> CreateItemCommand {
CreateItemCommand::new(self.commander.clone())
}
}

View File

@ -0,0 +1,59 @@
use itertools::Itertools;
use crate::{
commander::{self, Commander},
models::IOEvent,
state::SharedState,
};
pub struct CreateSectionCommand {
commander: Commander,
}
impl CreateSectionCommand {
pub fn new(commander: Commander) -> Self {
Self { commander }
}
pub fn command(self, root: &str, path: &[&str]) -> super::Command {
let root = root.to_owned();
let path = path.iter().map(|s| s.to_string()).collect_vec();
super::Command::new(|dispatch| {
tokio::spawn(async move {
dispatch.send(crate::models::Msg::SectionCreated(IOEvent::Initialized));
match self
.commander
.execute(commander::Command::CreateSection { root, path })
.await
{
Ok(()) => {
#[cfg(debug_assertions)]
{
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
}
dispatch.send(crate::models::Msg::SectionCreated(IOEvent::Success(())));
}
Err(e) => {
dispatch.send(crate::models::Msg::SectionCreated(IOEvent::Failure(
e.to_string(),
)));
}
}
});
None
})
}
}
pub trait CreateSectionCommandExt {
fn create_section_command(&self) -> CreateSectionCommand;
}
impl CreateSectionCommandExt for SharedState {
fn create_section_command(&self) -> CreateSectionCommand {
CreateSectionCommand::new(self.commander.clone())
}
}

View File

@ -0,0 +1,59 @@
use itertools::Itertools;
use crate::{
commander::{self, Commander},
models::IOEvent,
state::SharedState,
};
pub struct ToggleItemCommand {
commander: Commander,
}
impl ToggleItemCommand {
pub fn new(commander: Commander) -> Self {
Self { commander }
}
pub fn command(self, root: &str, path: &[&str]) -> super::Command {
let root = root.to_owned();
let path = path.iter().map(|s| s.to_string()).collect_vec();
super::Command::new(|dispatch| {
tokio::spawn(async move {
dispatch.send(crate::models::Msg::ItemToggled(IOEvent::Initialized));
match self
.commander
.execute(commander::Command::ToggleItem { root, path })
.await
{
Ok(()) => {
#[cfg(debug_assertions)]
{
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
}
dispatch.send(crate::models::Msg::ItemToggled(IOEvent::Success(())));
}
Err(e) => {
dispatch.send(crate::models::Msg::ItemToggled(IOEvent::Failure(
e.to_string(),
)));
}
}
});
None
})
}
}
pub trait ToggleItemCommandExt {
fn toggle_item_command(&self) -> ToggleItemCommand;
}
impl ToggleItemCommandExt for SharedState {
fn toggle_item_command(&self) -> ToggleItemCommand {
ToggleItemCommand::new(self.commander.clone())
}
}

View File

@ -0,0 +1,76 @@
use hyperlog_core::log::ItemState;
use itertools::Itertools;
use crate::{
commander::{self, Commander},
models::IOEvent,
state::SharedState,
};
pub struct UpdateItemCommand {
commander: Commander,
}
impl UpdateItemCommand {
pub fn new(commander: Commander) -> Self {
Self { commander }
}
pub fn command(
self,
root: &str,
path: &[&str],
title: &str,
description: &str,
state: ItemState,
) -> super::Command {
let root = root.to_owned();
let path = path.iter().map(|s| s.to_string()).collect_vec();
let title = title.to_string();
let description = description.to_string();
let state = state.clone();
super::Command::new(|dispatch| {
tokio::spawn(async move {
dispatch.send(crate::models::Msg::ItemUpdated(IOEvent::Initialized));
match self
.commander
.execute(commander::Command::UpdateItem {
root,
path,
title,
description,
state,
})
.await
{
Ok(()) => {
#[cfg(debug_assertions)]
{
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
}
dispatch.send(crate::models::Msg::ItemUpdated(IOEvent::Success(())));
}
Err(e) => {
dispatch.send(crate::models::Msg::ItemUpdated(IOEvent::Failure(
e.to_string(),
)));
}
}
});
None
})
}
}
pub trait UpdateItemCommandExt {
fn update_item_command(&self) -> UpdateItemCommand;
}
impl UpdateItemCommandExt for SharedState {
fn update_item_command(&self) -> UpdateItemCommand {
UpdateItemCommand::new(self.commander.clone())
}
}

View File

@ -5,8 +5,11 @@ use ratatui::{prelude::*, widgets::*};
use crate::{
command_parser::Commands,
commander,
commands::{update_graph::UpdateGraphCommandExt, Command, IntoCommand},
commands::{
batch::BatchCommand, create_section::CreateSectionCommandExt,
toggle_item::ToggleItemCommandExt, update_graph::UpdateGraphCommandExt, Command,
IntoCommand,
},
components::movement_graph::GraphItemType,
models::{GraphUpdatedEvent, Msg},
state::SharedState,
@ -227,6 +230,8 @@ impl<'a> GraphExplorer<'a> {
}
pub fn execute_command(&mut self, command: &Commands) -> anyhow::Result<Option<Command>> {
let mut batch = BatchCommand::default();
match command {
Commands::Archive => {
if !self.get_current_path().is_empty() {
@ -238,12 +243,19 @@ impl<'a> GraphExplorer<'a> {
let mut path = self.get_current_path();
path.push(name.replace(" ", "-").replace(".", "-"));
self.state
.commander
.execute(commander::Command::CreateSection {
root: self.inner.root.clone(),
path,
})?;
// self.state
// .commander
// .execute(commander::Command::CreateSection {
// root: self.inner.root.clone(),
// path,
// })?;
let cmd = self.state.create_section_command().command(
&self.inner.root,
&path.iter().map(|i| i.as_str()).collect_vec(),
);
batch.with(cmd.into_command());
}
}
Commands::Edit => {
@ -294,24 +306,37 @@ impl<'a> GraphExplorer<'a> {
//self.update_graph()?;
Ok(Some(self.new_update_graph()))
Ok(Some(batch.into_command()))
}
pub(crate) fn interact(&mut self) -> anyhow::Result<Command> {
let mut batch = BatchCommand::default();
if !self.get_current_path().is_empty() {
tracing::info!("toggling state of items");
self.state
.commander
.execute(commander::Command::ToggleItem {
root: self.inner.root.to_string(),
path: self.get_current_path(),
})?;
// self.state
// .commander
// .execute(commander::Command::ToggleItem {
// root: self.inner.root.to_string(),
// path: self.get_current_path(),
// })?;
let cmd = self.state.toggle_item_command().command(
&self.inner.root,
&self
.get_current_path()
.iter()
.map(|i| i.as_str())
.collect_vec(),
);
batch.with(cmd.into_command());
}
//self.update_graph()?;
Ok(self.new_update_graph())
Ok(batch.into_command())
}
}

View File

@ -24,17 +24,23 @@ impl State {
let engine = storage.load()?;
let events = Events::default();
let engine = SharedEngine::from(engine);
let querier = match backend {
Backend::Local => Querier::local(&engine),
Backend::Remote => Querier::remote().await?,
};
let commander = match backend {
Backend::Local => Commander::local(engine.clone(), storage.clone(), events.clone())?,
Backend::Remote => todo!(),
};
Ok(Self {
engine: engine.clone(),
storage: storage.clone(),
events: events.clone(),
commander: Commander::new(engine.clone(), storage, events)?,
commander,
querier,
})
}

View File

@ -51,7 +51,7 @@ pub async fn execute(state: State) -> 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_async().await? {
// TODO: maybe present choose root screen
Some(roots) => roots.first().cloned().unwrap(),
None => {

View File

@ -22,6 +22,19 @@ pub enum Msg {
Edit(EditMsg),
GraphUpdated(GraphUpdatedEvent),
ItemCreated(IOEvent<()>),
ItemUpdated(IOEvent<()>),
SectionCreated(IOEvent<()>),
ItemToggled(IOEvent<()>),
}
#[derive(Debug)]
pub enum IOEvent<T> {
Initialized,
Optimistic(T),
Success(T),
Failure(String),
}
#[derive(Debug)]

View File

@ -61,7 +61,7 @@ impl Querier {
pub async fn get_available_roots_async(&self) -> anyhow::Result<Option<Vec<String>>> {
match &self.variant {
QuerierVariant::Local(querier) => Ok(querier.get_available_roots()),
QuerierVariant::Remote(querier) => Ok(querier.get_available_roots().await),
QuerierVariant::Remote(querier) => querier.get_available_roots().await,
}
}
}

View File

@ -36,7 +36,10 @@ impl Querier {
path.len()
);
self.engine
.get(root, &path.iter().map(|i| i.as_str()).collect::<Vec<_>>())
let item = self
.engine
.get(root, &path.iter().map(|i| i.as_str()).collect::<Vec<_>>());
item
}
}

View File

@ -1,7 +1,9 @@
use std::collections::BTreeMap;
use hyperlog_core::log::GraphItem;
use hyperlog_protos::hyperlog::{graph_client::GraphClient, graph_item::Contents, GetRequest};
use hyperlog_protos::hyperlog::{
graph_client::GraphClient, graph_item::Contents, GetAvailableRootsRequest, GetRequest,
};
use itertools::Itertools;
use tonic::transport::Channel;
@ -21,9 +23,21 @@ impl Querier {
Ok(Self { channel })
}
pub async fn get_available_roots(&self) -> Option<Vec<String>> {
//self.engine.get_roots()
todo!()
pub async fn get_available_roots(&self) -> anyhow::Result<Option<Vec<String>>> {
let channel = self.channel.clone();
let mut client = GraphClient::new(channel);
let request = tonic::Request::new(GetAvailableRootsRequest {});
let response = client.get_available_roots(request).await?;
let roots = response.into_inner();
if roots.roots.is_empty() {
Ok(None)
} else {
Ok(Some(roots.roots))
}
}
pub async fn get(
@ -54,7 +68,8 @@ impl Querier {
let graph_item = response.into_inner();
if let Some(item) = graph_item.item {
Ok(transform_proto_to_local(&item))
let local_graph = transform_proto_to_local(&item);
Ok(local_graph)
} else {
Ok(None)
}

View File

@ -115,11 +115,16 @@ pub async fn execute() -> anyhow::Result<()> {
Some(Commands::Exec { commands }) => {
let state = State::new(backend.into()).await?;
match commands {
ExecCommands::CreateRoot { root } => state
ExecCommands::CreateRoot { root } => {
state
.commander
.execute(commander::Command::CreateRoot { root })?,
.execute(commander::Command::CreateRoot { root })
.await?
}
ExecCommands::CreateSection { root, path } => {
state.commander.execute(commander::Command::CreateSection {
state
.commander
.execute(commander::Command::CreateSection {
root,
path: path
.unwrap_or_default()
@ -127,7 +132,8 @@ pub async fn execute() -> anyhow::Result<()> {
.map(|s| s.to_string())
.filter(|s| !s.is_empty())
.collect::<Vec<String>>(),
})?
})
.await?
}
}
}
@ -152,7 +158,8 @@ pub async fn execute() -> anyhow::Result<()> {
let state = State::new(backend.into()).await?;
state
.commander
.execute(commander::Command::CreateRoot { root: name })?;
.execute(commander::Command::CreateRoot { root: name })
.await?;
println!("Root was successfully created, now run:\n\n$ hyperlog");
}
Some(Commands::Info {}) => {