From f6a48540e118500bdf412e08a877967e88cbb3b9 Mon Sep 17 00:00:00 2001 From: kjuulh Date: Fri, 10 May 2024 12:20:43 +0200 Subject: [PATCH] chore: refactor app Signed-off-by: kjuulh --- crates/hyperlog-tui/src/app.rs | 8 +- crates/hyperlog-tui/src/components.rs | 1054 ++++------------- .../src/components/movement_graph.rs | 520 ++++++++ .../src/components/render_graph.rs | 143 +++ crates/hyperlog-tui/src/lib.rs | 7 +- crates/hyperlog-tui/src/models.rs | 1 + 6 files changed, 878 insertions(+), 855 deletions(-) create mode 100644 crates/hyperlog-tui/src/components/movement_graph.rs create mode 100644 crates/hyperlog-tui/src/components/render_graph.rs diff --git a/crates/hyperlog-tui/src/app.rs b/crates/hyperlog-tui/src/app.rs index 68530f9..6635460 100644 --- a/crates/hyperlog-tui/src/app.rs +++ b/crates/hyperlog-tui/src/app.rs @@ -5,8 +5,8 @@ use ratatui::{ }; use crate::{ - command_parser::CommandParser, commands::IntoCommand, components::GraphExplorer, - state::SharedState, Msg, + command_parser::CommandParser, commands::IntoCommand, + components::graph_explorer::GraphExplorer, state::SharedState, Msg, }; use self::{ @@ -125,6 +125,10 @@ impl<'a> App<'a> { self.command = None; return Ok(msg.into_command()); } + + if command.is_quit() { + return Ok(Msg::QuitApp.into_command()); + } } } } diff --git a/crates/hyperlog-tui/src/components.rs b/crates/hyperlog-tui/src/components.rs index 755e321..9b987c2 100644 --- a/crates/hyperlog-tui/src/components.rs +++ b/crates/hyperlog-tui/src/components.rs @@ -1,897 +1,249 @@ -use std::ops::Deref; +pub(crate) mod movement_graph; +pub(crate) mod render_graph; -use anyhow::Result; -use hyperlog_core::log::{GraphItem, ItemState}; -use itertools::Itertools; -use ratatui::{prelude::*, widgets::*}; +pub(crate) mod graph_explorer { + use anyhow::Result; + use hyperlog_core::log::GraphItem; + use ratatui::{prelude::*, widgets::*}; -use crate::{command_parser::Commands, models::Msg, state::SharedState}; + use crate::{ + command_parser::Commands, components::movement_graph::GraphItemType, models::Msg, + state::SharedState, + }; -pub struct GraphExplorer<'a> { - state: SharedState, + use super::movement_graph::{MovementGraph, MovementGraphItem}; + use super::render_graph::RenderGraph; - pub inner: GraphExplorerState<'a>, -} + pub struct GraphExplorer<'a> { + state: SharedState, -pub struct GraphExplorerState<'a> { - root: String, - - current_path: Option<&'a str>, - current_position: Vec, - - graph: Option, -} - -impl<'a> GraphExplorer<'a> { - pub fn new(root: String, state: SharedState) -> Self { - Self { - state, - inner: GraphExplorerState::<'a> { - root, - current_path: None, - current_position: Vec::new(), - graph: None, - }, - } + pub inner: GraphExplorerState<'a>, } - pub fn update_graph(&mut self) -> Result<&mut Self> { - let now = std::time::SystemTime::now(); + pub struct GraphExplorerState<'a> { + root: String, - let graph = self - .state - .querier - .get( - &self.inner.root, - self.inner - .current_path - .map(|p| p.split('.').collect::>()) - .unwrap_or_default(), - ) - .ok_or(anyhow::anyhow!("graph should've had an item"))?; + current_path: Option<&'a str>, + current_position: Vec, - self.inner.graph = Some(graph); - - let elapsed = now.elapsed()?; - tracing::trace!("Graph.update_graph took: {}nanos", elapsed.as_nanos()); - - Ok(self) + graph: Option, } - fn linearize_graph(&self) -> Option { - self.inner.graph.clone().map(|g| g.into()) - } - - /// Will only incrmeent to the next level - /// - /// Current: 0.1.0 - /// Available: 0.1.0.[0,1,2] - /// Choses: 0.1.0.0 else nothing - pub(crate) fn move_right(&mut self) -> Result<()> { - if let Some(graph) = self.linearize_graph() { - tracing::debug!("graph: {:?}", graph); - let position_items = &self.inner.current_position; - - if let Some(next_item) = graph.next_right(position_items) { - self.inner.current_position.push(next_item.index); - tracing::trace!("found next item: {:?}", self.inner.current_position); + impl<'a> GraphExplorer<'a> { + pub fn new(root: String, state: SharedState) -> Self { + Self { + state, + inner: GraphExplorerState::<'a> { + root, + current_path: None, + current_position: Vec::new(), + graph: None, + }, } } - Ok(()) - } + pub fn update_graph(&mut self) -> Result<&mut Self> { + let now = std::time::SystemTime::now(); - /// Will only incrmeent to the next level - /// - /// Current: 0.1.0 - /// Available: 0.[0,1,2].0 - /// Choses: 0.1 else nothing - pub(crate) fn move_left(&mut self) -> Result<()> { - if let Some(last) = self.inner.current_position.pop() { - tracing::trace!( - "found last item: {:?}, popped: {}", - self.inner.current_position, - last - ); + let graph = self + .state + .querier + .get( + &self.inner.root, + self.inner + .current_path + .map(|p| p.split('.').collect::>()) + .unwrap_or_default(), + ) + .ok_or(anyhow::anyhow!("graph should've had an item"))?; + + self.inner.graph = Some(graph); + + let elapsed = now.elapsed()?; + tracing::trace!("Graph.update_graph took: {}nanos", elapsed.as_nanos()); + + Ok(self) } - Ok(()) - } - - /// Will move up if a sibling exists, or up to the most common sibling between sections - /// - /// Current: 0.1.1 - /// Available: 0.[0.[0,1],1.[0,1]] - /// Chose: 0.1.0 again 0.0 We don't choose a subitem in the next three instead we just find the most common sibling - pub(crate) fn move_up(&mut self) -> Result<()> { - if let Some(graph) = self.linearize_graph() { - let position_items = &self.inner.current_position; - - if let Some(next_item) = graph.next_up(position_items) { - self.inner.current_position = next_item; - tracing::trace!("found next up: {:?}", self.inner.current_position) - } + fn linearize_graph(&self) -> Option { + self.inner.graph.clone().map(|g| g.into()) } - Ok(()) - } + /// Will only incrmeent to the next level + /// + /// Current: 0.1.0 + /// Available: 0.1.0.[0,1,2] + /// Choses: 0.1.0.0 else nothing + pub(crate) fn move_right(&mut self) -> Result<()> { + if let Some(graph) = self.linearize_graph() { + tracing::debug!("graph: {:?}", graph); + let position_items = &self.inner.current_position; - /// Will move down if a sibling exists, or down to the most common sibling between sections - /// - /// Current: 0.0.0 - /// Available: 0.[0.[0,1],1.[0,1]] - /// Chose: 0.0.1 again 0.1 - pub(crate) fn move_down(&mut self) -> Result<()> { - if let Some(graph) = self.linearize_graph() { - let position_items = &self.inner.current_position; - - if let Some(next_item) = graph.next_down(position_items) { - self.inner.current_position = next_item; - tracing::trace!("found next down: {:?}", self.inner.current_position) - } - } - - Ok(()) - } - - pub(crate) fn get_current_path(&self) -> Vec { - let graph = self.linearize_graph(); - let position_items = &self.inner.current_position; - - if let Some(graph) = graph { - graph.to_current_path(position_items) - } else { - Vec::new() - } - } - - fn get_current_item(&self) -> Option { - let graph = self.linearize_graph(); - - if let Some(graph) = graph { - graph.get_graph_item(&self.inner.current_position).cloned() - } else { - None - } - } - - pub fn execute_command(&mut self, command: &Commands) -> anyhow::Result> { - match command { - Commands::Archive => { - if !self.get_current_path().is_empty() { - tracing::debug!("archiving path: {:?}", self.get_current_path()) + if let Some(next_item) = graph.next_right(position_items) { + self.inner.current_position.push(next_item.index); + tracing::trace!("found next item: {:?}", self.inner.current_position); } } - Commands::CreateSection { name } => { - if !name.is_empty() { - let mut path = self.get_current_path(); - path.push(name.replace(" ", "-").replace(".", "-")); - self.state.commander.execute( - hyperlog_core::commander::Command::CreateSection { - root: self.inner.root.clone(), - path, - }, - )?; + Ok(()) + } + + /// Will only incrmeent to the next level + /// + /// Current: 0.1.0 + /// Available: 0.[0,1,2].0 + /// Choses: 0.1 else nothing + pub(crate) fn move_left(&mut self) -> Result<()> { + if let Some(last) = self.inner.current_position.pop() { + tracing::trace!( + "found last item: {:?}, popped: {}", + self.inner.current_position, + last + ); + } + + Ok(()) + } + + /// Will move up if a sibling exists, or up to the most common sibling between sections + /// + /// Current: 0.1.1 + /// Available: 0.[0.[0,1],1.[0,1]] + /// Chose: 0.1.0 again 0.0 We don't choose a subitem in the next three instead we just find the most common sibling + pub(crate) fn move_up(&mut self) -> Result<()> { + if let Some(graph) = self.linearize_graph() { + let position_items = &self.inner.current_position; + + if let Some(next_item) = graph.next_up(position_items) { + self.inner.current_position = next_item; + tracing::trace!("found next up: {:?}", self.inner.current_position) } } - Commands::Edit => { - if let Some(item) = self.get_current_item() { - let path = self.get_current_path(); - tracing::debug!( - "found item to edit: path: {}, item: {}", - path.join("."), - item.name - ); - match item.item_type { - GraphItemType::Section => { - todo!("cannot edit section at the moment") - } - 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 })); + Ok(()) + } + + /// Will move down if a sibling exists, or down to the most common sibling between sections + /// + /// Current: 0.0.0 + /// Available: 0.[0.[0,1],1.[0,1]] + /// Chose: 0.0.1 again 0.1 + pub(crate) fn move_down(&mut self) -> Result<()> { + if let Some(graph) = self.linearize_graph() { + let position_items = &self.inner.current_position; + + if let Some(next_item) = graph.next_down(position_items) { + self.inner.current_position = next_item; + tracing::trace!("found next down: {:?}", self.inner.current_position) + } + } + + Ok(()) + } + + pub(crate) fn get_current_path(&self) -> Vec { + let graph = self.linearize_graph(); + let position_items = &self.inner.current_position; + + if let Some(graph) = graph { + graph.to_current_path(position_items) + } else { + Vec::new() + } + } + + fn get_current_item(&self) -> Option { + let graph = self.linearize_graph(); + + if let Some(graph) = graph { + graph.get_graph_item(&self.inner.current_position).cloned() + } else { + None + } + } + + pub fn execute_command(&mut self, command: &Commands) -> anyhow::Result> { + match command { + Commands::Archive => { + if !self.get_current_path().is_empty() { + tracing::debug!("archiving path: {:?}", self.get_current_path()) + } + } + Commands::CreateSection { name } => { + if !name.is_empty() { + let mut path = self.get_current_path(); + path.push(name.replace(" ", "-").replace(".", "-")); + + self.state.commander.execute( + hyperlog_core::commander::Command::CreateSection { + root: self.inner.root.clone(), + path, + }, + )?; + } + } + Commands::Edit => { + if let Some(item) = self.get_current_item() { + let path = self.get_current_path(); + + tracing::debug!( + "found item to edit: path: {}, item: {}", + path.join("."), + item.name + ); + match item.item_type { + GraphItemType::Section => { + todo!("cannot edit section at the moment") + } + 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 })); + } } } } } } + _ => (), } - _ => (), + + self.update_graph()?; + + Ok(None) } - self.update_graph()?; + pub(crate) fn interact(&mut self) -> anyhow::Result<()> { + if !self.get_current_path().is_empty() { + tracing::info!("toggling state of items"); - Ok(None) - } + self.state + .commander + .execute(hyperlog_core::commander::Command::ToggleItem { + root: self.inner.root.to_string(), + path: self.get_current_path(), + })?; + } - pub(crate) fn interact(&mut self) -> anyhow::Result<()> { - if !self.get_current_path().is_empty() { - tracing::info!("toggling state of items"); + self.update_graph()?; - self.state - .commander - .execute(hyperlog_core::commander::Command::ToggleItem { - root: self.inner.root.to_string(), - path: self.get_current_path(), - })?; + Ok(()) } - - self.update_graph()?; - - Ok(()) } -} -trait RenderGraph { - fn render_graph(&self, items: &[usize]) -> Vec; - fn render_graph_spans(&self, items: &[usize]) -> Vec>; -} + impl<'a> StatefulWidget for GraphExplorer<'a> { + type State = GraphExplorerState<'a>; -impl RenderGraph for MovementGraph { - /// render_graph takes each level of items, renders them, and finally renders a strongly set selector for the current item the user is on - /// This is done from buttom up, and composed via. string padding - fn render_graph(&self, items: &[usize]) -> Vec { - // Gets the inner content of the strings + fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) { + let Rect { height, .. } = area; + let _height = height as usize; - let mut lines = Vec::new(); - - for item in &self.items { - let prefix = match item.item_type { - GraphItemType::Section => "- ", - GraphItemType::Item { done } => { - if done { - "- [x]" - } else { - "- [ ]" - } - } - }; - - match items.split_first().map(|(first, rest)| { - if item.index == *first { - (true, rest) - } else { - (false, rest) - } - }) { - Some((true, rest)) => { - if rest.is_empty() { - lines.push( - Line::raw(format!("{} {}", prefix, item.name)) - .style(Style::new().bold().white()), - ); - } else { - lines.push( - Line::raw(format!("{} {}", prefix, item.name)) - .patch_style(Style::new().dark_gray()), - ); - } - - lines.push("".into()); - - let embedded_sections = item.values.render_graph_spans(rest); - for section in &embedded_sections { - let mut line = vec![Span::raw(" ".repeat(4))]; - line.extend_from_slice(section); - lines.push(Line::from(line)); - } - } - _ => { - lines.push( - Line::raw(format!("{} {}", prefix, item.name)) - .patch_style(Style::new().dark_gray()), - ); - - lines.push("".into()); - - let embedded_sections = item.values.render_graph_spans(&[]); - for section in &embedded_sections { - let mut line = vec![Span::raw(" ".repeat(4))]; - line.extend_from_slice(section); - lines.push(Line::from(line)); - } - } + if let Some(graph) = &state.graph { + let movement_graph: MovementGraph = graph.clone().into(); + let lines = movement_graph.render_graph(&state.current_position); + let para = Paragraph::new(lines); + para.render(area, buf); } } - - lines - } - - fn render_graph_spans(&self, items: &[usize]) -> Vec> { - let mut lines = Vec::new(); - - for item in &self.items { - let prefix = match item.item_type { - GraphItemType::Section => "-", - GraphItemType::Item { done } => { - if done { - "- [x]" - } else { - "- [ ]" - } - } - }; - match items.split_first().map(|(first, rest)| { - if item.index == *first { - (true, rest) - } else { - (false, rest) - } - }) { - Some((true, rest)) => { - let mut line = Vec::new(); - if rest.is_empty() { - line.push( - Span::raw(format!("{} {}", prefix, item.name)) - .style(Style::new().bold().white()), - ); - } else { - line.push( - Span::raw(format!("{} {}", prefix, item.name)) - .patch_style(Style::new().dark_gray()), - ); - } - - lines.push(line); - lines.push(vec!["".into()]); - - let embedded_sections = item.values.render_graph_spans(rest); - for section in &embedded_sections { - let mut line = vec![Span::raw(" ".repeat(4))]; - line.extend_from_slice(section); - lines.push(line); - } - } - _ => { - lines.push(vec![Span::raw(format!("{prefix} {}", item.name)) - .patch_style(Style::new().dark_gray())]); - - lines.push(vec!["".into()]); - - let embedded_sections = item.values.render_graph_spans(&[]); - for section in &embedded_sections { - let mut line = vec![Span::raw(" ".repeat(4))]; - line.extend_from_slice(section); - lines.push(line); - } - } - } - } - - lines - } -} - -impl<'a> StatefulWidget for GraphExplorer<'a> { - type State = GraphExplorerState<'a>; - - fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) { - let Rect { height, .. } = area; - let _height = height as usize; - - if let Some(graph) = &state.graph { - let movement_graph: MovementGraph = graph.clone().into(); - let lines = movement_graph.render_graph(&state.current_position); - let para = Paragraph::new(lines); - para.render(area, buf); - } - } -} - -#[derive(PartialEq, Eq, Debug, Clone)] -enum GraphItemType { - Section, - Item { done: bool }, -} - -#[derive(PartialEq, Eq, Debug, Clone)] -struct MovementGraphItem { - index: usize, - name: String, - values: MovementGraph, - - item_type: GraphItemType, -} - -#[derive(Default, PartialEq, Eq, Debug, Clone)] -struct MovementGraph { - items: Vec, -} - -impl MovementGraph { - fn next_right(&self, items: &[usize]) -> Option { - match items.split_first() { - Some((current_index, rest)) => match self.items.get(*current_index) { - Some(next_item) => next_item.values.next_right(rest), - None => None, - }, - None => self.items.first().cloned(), - } - } - - fn next_up(&self, items: &[usize]) -> Option> { - match items.split_last() { - Some((0, _)) => None, - Some((current_index, rest)) => { - let mut vec = rest.to_vec(); - vec.push(current_index - 1); - - Some(vec) - } - // May need to reduce this to an Some(Vec::default()) instead - //None => Some(self.items.iter().map(|i| i.index).collect_vec()), - None => None, - } - } - - fn next_down(&self, items: &[usize]) -> Option> { - match items.split_last() { - Some((current_index, rest)) => { - if let Some(current_item) = self.get_graph(rest) { - if *current_index + 1 < current_item.items.len() { - let mut vec = rest.to_vec(); - vec.push(current_index + 1); - - Some(vec) - } else { - None - } - } else { - None - } - } - // May need to reduce this to an Some(Vec::default()) instead - //None => Some(self.items.iter().map(|i| i.index).collect_vec()), - None => None, - } - } - - fn get_graph(&self, items: &[usize]) -> Option<&MovementGraph> { - match items.split_first() { - Some((first, rest)) => match self.items.get(*first).map(|s| &s.values) { - Some(next_graph) => next_graph.get_graph(rest), - None => Some(self), - }, - None => Some(self), - } - } - - fn get_graph_item(&self, items: &[usize]) -> Option<&MovementGraphItem> { - match items.split_first() { - Some((first, rest)) => match self.items.get(*first) { - Some(next_graph) => match next_graph.values.get_graph_item(rest) { - Some(graph_item) => Some(graph_item), - None => Some(next_graph), - }, - None => None, - }, - None => None, - } - } - - fn to_current_path(&self, position_items: &[usize]) -> Vec { - match position_items.split_first() { - Some((first, rest)) => match self.items.get(*first) { - Some(item) => { - let mut current = vec![item.name.clone()]; - let mut next = item.values.to_current_path(rest); - current.append(&mut next); - - current - } - None => Vec::new(), - }, - None => Vec::new(), - } - } -} - -impl From> for MovementGraph { - fn from(value: Box) -> Self { - value.deref().clone().into() - } -} - -impl From for MovementGraph { - fn from(value: GraphItem) -> Self { - let mut graph = MovementGraph::default(); - - match value { - GraphItem::User(sections) | GraphItem::Section(sections) => { - let graph_items = sections - .iter() - .sorted_by(|(a, _), (b, _)| Ord::cmp(a, b)) - .enumerate() - .map(|(i, (key, value))| MovementGraphItem { - index: i, - name: key.clone(), - values: value.clone().into(), - item_type: match value { - GraphItem::User(_) => GraphItemType::Section, - GraphItem::Section(_) => GraphItemType::Section, - GraphItem::Item { state, .. } => GraphItemType::Item { - done: matches!(state, ItemState::Done), - }, - }, - }) - .collect::>(); - - graph.items = graph_items; - } - GraphItem::Item { .. } => {} - } - - graph - } -} - -#[cfg(test)] -mod test { - use std::collections::BTreeMap; - - use hyperlog_core::log::{GraphItem, ItemState}; - use similar_asserts::assert_eq; - - use crate::components::{GraphItemType, MovementGraphItem}; - - use super::MovementGraph; - - /// Lets say we've got a graph - /// ```json - /// { - /// "type": "user", - /// "something": { - /// "type": "section", - /// "something": { - /// "type": "section", - /// "something-else": { - /// "type": "section", - /// "blabla": { - /// "type": "section" - /// } - /// } - /// } - /// } - /// } - /// ``` - /// We can get something out like - /// [ - /// 0: {key: something, values: [ - /// 0: {key: something, values: [ - /// ... - /// ]} - /// ]} - /// ] - #[test] - fn test_can_transform_to_movement_graph() { - let graph = GraphItem::User(BTreeMap::from([( - "0".to_string(), - GraphItem::Section(BTreeMap::from([ - ("00".to_string(), GraphItem::Section(BTreeMap::new())), - ( - "01".to_string(), - GraphItem::Section(BTreeMap::from([ - ( - "010".to_string(), - GraphItem::Item { - title: "some-title".into(), - description: "some-desc".into(), - state: ItemState::NotDone, - }, - ), - ( - "011".to_string(), - GraphItem::Item { - title: "some-title".into(), - description: "some-desc".into(), - state: ItemState::NotDone, - }, - ), - ])), - ), - ])), - )])); - - let actual: MovementGraph = graph.into(); - - assert_eq!( - MovementGraph { - items: vec![MovementGraphItem { - index: 0, - name: "0".into(), - item_type: GraphItemType::Section, - values: MovementGraph { - items: vec![ - MovementGraphItem { - index: 0, - name: "00".into(), - values: MovementGraph::default(), - item_type: GraphItemType::Section, - }, - MovementGraphItem { - index: 1, - name: "01".into(), - item_type: GraphItemType::Section, - values: MovementGraph { - items: vec![ - MovementGraphItem { - index: 0, - name: "010".into(), - values: MovementGraph::default(), - item_type: GraphItemType::Item { done: false }, - }, - MovementGraphItem { - index: 1, - name: "011".into(), - values: MovementGraph::default(), - item_type: GraphItemType::Item { done: false }, - }, - ] - } - }, - ] - } - }] - }, - actual - ); - } - - #[test] - fn test_get_graph_item() -> anyhow::Result<()> { - let graph = MovementGraph { - items: vec![ - MovementGraphItem { - index: 0, - name: "0".into(), - item_type: GraphItemType::Section, - values: MovementGraph { - items: vec![ - MovementGraphItem { - index: 0, - name: "0".into(), - values: MovementGraph::default(), - item_type: GraphItemType::Section, - }, - MovementGraphItem { - index: 1, - name: "0".into(), - values: MovementGraph::default(), - item_type: GraphItemType::Section, - }, - ], - }, - }, - MovementGraphItem { - index: 1, - name: "0".into(), - item_type: GraphItemType::Section, - values: MovementGraph { - items: vec![ - MovementGraphItem { - index: 0, - name: "0".into(), - values: MovementGraph::default(), - item_type: GraphItemType::Section, - }, - MovementGraphItem { - index: 1, - name: "0".into(), - values: MovementGraph::default(), - item_type: GraphItemType::Section, - }, - MovementGraphItem { - index: 2, - name: "0".into(), - values: MovementGraph::default(), - item_type: GraphItemType::Section, - }, - ], - }, - }, - MovementGraphItem { - index: 2, - name: "0".into(), - item_type: GraphItemType::Section, - values: MovementGraph { - items: vec![ - MovementGraphItem { - index: 0, - name: "0".into(), - values: MovementGraph::default(), - item_type: GraphItemType::Section, - }, - MovementGraphItem { - index: 1, - name: "0".into(), - values: MovementGraph::default(), - item_type: GraphItemType::Section, - }, - ], - }, - }, - ], - }; - - let actual_default = graph.get_graph(&[]); - assert_eq!(Some(&graph), actual_default); - - let actual_first = graph.get_graph(&[0]); - assert_eq!(graph.items.first().map(|i| &i.values), actual_first); - - let actual_second = graph.get_graph(&[1]); - assert_eq!(graph.items.get(1).map(|i| &i.values), actual_second); - - let actual_nested = graph.get_graph(&[0, 0]); - assert_eq!( - graph - .items - .first() - .and_then(|i| i.values.items.first()) - .map(|i| &i.values), - actual_nested - ); - - let actual_nested = graph.get_graph(&[0, 1]); - assert_eq!( - graph - .items - .first() - .and_then(|i| i.values.items.get(1)) - .map(|i| &i.values), - actual_nested - ); - - let actual_nested = graph.get_graph(&[1, 2]); - assert_eq!( - graph - .items - .get(1) - .and_then(|i| i.values.items.get(2)) - .map(|i| &i.values), - actual_nested - ); - - Ok(()) - } - - #[test] - fn can_next_down() -> anyhow::Result<()> { - let graph = MovementGraph { - items: vec![ - MovementGraphItem { - index: 0, - name: "0".into(), - item_type: GraphItemType::Section, - values: MovementGraph { - items: vec![MovementGraphItem { - index: 0, - name: "0".into(), - item_type: GraphItemType::Section, - values: MovementGraph::default(), - }], - }, - }, - MovementGraphItem { - index: 1, - name: "1".into(), - item_type: GraphItemType::Section, - values: MovementGraph { - items: vec![ - MovementGraphItem { - index: 0, - name: "0".into(), - item_type: GraphItemType::Section, - values: MovementGraph::default(), - }, - MovementGraphItem { - index: 1, - name: "1".into(), - item_type: GraphItemType::Section, - values: MovementGraph::default(), - }, - ], - }, - }, - MovementGraphItem { - index: 2, - name: "2".into(), - item_type: GraphItemType::Section, - values: MovementGraph { - items: vec![ - MovementGraphItem { - index: 0, - name: "0".into(), - item_type: GraphItemType::Section, - values: MovementGraph::default(), - }, - MovementGraphItem { - index: 1, - name: "1".into(), - item_type: GraphItemType::Section, - values: MovementGraph::default(), - }, - MovementGraphItem { - index: 2, - name: "2".into(), - item_type: GraphItemType::Section, - values: MovementGraph::default(), - }, - ], - }, - }, - ], - }; - - let actual = graph.next_down(&[]); - assert_eq!(None, actual); - - let actual = graph.next_down(&[0]); - assert_eq!(Some(vec![1]), actual); - - let actual = graph.next_down(&[1]); - assert_eq!(Some(vec![2]), actual); - - let actual = graph.next_down(&[2]); - assert_eq!(None, actual); - - let graph = MovementGraph { - items: vec![ - MovementGraphItem { - index: 0, - name: "other".into(), - item_type: GraphItemType::Section, - values: MovementGraph { - items: vec![MovementGraphItem { - index: 0, - name: "other".into(), - item_type: GraphItemType::Section, - values: MovementGraph { - items: vec![MovementGraphItem { - index: 0, - name: "other".into(), - item_type: GraphItemType::Section, - values: MovementGraph { items: vec![] }, - }], - }, - }], - }, - }, - MovementGraphItem { - index: 1, - name: "some".into(), - item_type: GraphItemType::Section, - values: MovementGraph { items: vec![] }, - }, - MovementGraphItem { - index: 2, - name: "something".into(), - item_type: GraphItemType::Section, - values: MovementGraph { - items: vec![ - MovementGraphItem { - index: 0, - name: "else".into(), - item_type: GraphItemType::Section, - values: MovementGraph { items: vec![] }, - }, - MovementGraphItem { - index: 1, - name: "third".into(), - item_type: GraphItemType::Section, - values: MovementGraph { items: vec![] }, - }, - ], - }, - }, - ], - }; - - let actual = graph.next_down(&[0]); - assert_eq!(Some(vec![1]), actual); - - Ok(()) } } diff --git a/crates/hyperlog-tui/src/components/movement_graph.rs b/crates/hyperlog-tui/src/components/movement_graph.rs new file mode 100644 index 0000000..5e04358 --- /dev/null +++ b/crates/hyperlog-tui/src/components/movement_graph.rs @@ -0,0 +1,520 @@ +use std::ops::Deref; + +use hyperlog_core::log::{GraphItem, ItemState}; +use itertools::Itertools; + +#[derive(PartialEq, Eq, Debug, Clone)] +pub enum GraphItemType { + Section, + Item { done: bool }, +} + +#[derive(PartialEq, Eq, Debug, Clone)] +pub struct MovementGraphItem { + pub index: usize, + pub name: String, + pub values: MovementGraph, + + pub item_type: GraphItemType, +} + +#[derive(Default, PartialEq, Eq, Debug, Clone)] +pub struct MovementGraph { + pub items: Vec, +} + +impl MovementGraph { + pub fn next_right(&self, items: &[usize]) -> Option { + match items.split_first() { + Some((current_index, rest)) => match self.items.get(*current_index) { + Some(next_item) => next_item.values.next_right(rest), + None => None, + }, + None => self.items.first().cloned(), + } + } + + pub fn next_up(&self, items: &[usize]) -> Option> { + match items.split_last() { + Some((0, _)) => None, + Some((current_index, rest)) => { + let mut vec = rest.to_vec(); + vec.push(current_index - 1); + + Some(vec) + } + // May need to reduce this to an Some(Vec::default()) instead + //None => Some(self.items.iter().map(|i| i.index).collect_vec()), + None => None, + } + } + + pub fn next_down(&self, items: &[usize]) -> Option> { + match items.split_last() { + Some((current_index, rest)) => { + if let Some(current_item) = self.get_graph(rest) { + if *current_index + 1 < current_item.items.len() { + let mut vec = rest.to_vec(); + vec.push(current_index + 1); + + Some(vec) + } else { + None + } + } else { + None + } + } + // May need to reduce this to an Some(Vec::default()) instead + //None => Some(self.items.iter().map(|i| i.index).collect_vec()), + None => None, + } + } + + fn get_graph(&self, items: &[usize]) -> Option<&MovementGraph> { + match items.split_first() { + Some((first, rest)) => match self.items.get(*first).map(|s| &s.values) { + Some(next_graph) => next_graph.get_graph(rest), + None => Some(self), + }, + None => Some(self), + } + } + + pub fn get_graph_item(&self, items: &[usize]) -> Option<&MovementGraphItem> { + match items.split_first() { + Some((first, rest)) => match self.items.get(*first) { + Some(next_graph) => match next_graph.values.get_graph_item(rest) { + Some(graph_item) => Some(graph_item), + None => Some(next_graph), + }, + None => None, + }, + None => None, + } + } + + pub fn to_current_path(&self, position_items: &[usize]) -> Vec { + match position_items.split_first() { + Some((first, rest)) => match self.items.get(*first) { + Some(item) => { + let mut current = vec![item.name.clone()]; + let mut next = item.values.to_current_path(rest); + current.append(&mut next); + + current + } + None => Vec::new(), + }, + None => Vec::new(), + } + } +} + +impl From> for MovementGraph { + fn from(value: Box) -> Self { + value.deref().clone().into() + } +} + +impl From for MovementGraph { + fn from(value: GraphItem) -> Self { + let mut graph = MovementGraph::default(); + + match value { + GraphItem::User(sections) | GraphItem::Section(sections) => { + let graph_items = sections + .iter() + .sorted_by(|(a, _), (b, _)| Ord::cmp(a, b)) + .enumerate() + .map(|(i, (key, value))| MovementGraphItem { + index: i, + name: key.clone(), + values: value.clone().into(), + item_type: match value { + GraphItem::User(_) => GraphItemType::Section, + GraphItem::Section(_) => GraphItemType::Section, + GraphItem::Item { state, .. } => GraphItemType::Item { + done: matches!(state, ItemState::Done), + }, + }, + }) + .collect::>(); + + graph.items = graph_items; + } + GraphItem::Item { .. } => {} + } + + graph + } +} + +#[cfg(test)] +mod test { + use std::collections::BTreeMap; + + use hyperlog_core::log::{GraphItem, ItemState}; + use similar_asserts::assert_eq; + + use crate::components::movement_graph::{GraphItemType, MovementGraphItem}; + + use super::MovementGraph; + + /// Lets say we've got a graph + /// ```json + /// { + /// "type": "user", + /// "something": { + /// "type": "section", + /// "something": { + /// "type": "section", + /// "something-else": { + /// "type": "section", + /// "blabla": { + /// "type": "section" + /// } + /// } + /// } + /// } + /// } + /// ``` + /// We can get something out like + /// [ + /// 0: {key: something, values: [ + /// 0: {key: something, values: [ + /// ... + /// ]} + /// ]} + /// ] + #[test] + fn test_can_transform_to_movement_graph() { + let graph = GraphItem::User(BTreeMap::from([( + "0".to_string(), + GraphItem::Section(BTreeMap::from([ + ("00".to_string(), GraphItem::Section(BTreeMap::new())), + ( + "01".to_string(), + GraphItem::Section(BTreeMap::from([ + ( + "010".to_string(), + GraphItem::Item { + title: "some-title".into(), + description: "some-desc".into(), + state: ItemState::NotDone, + }, + ), + ( + "011".to_string(), + GraphItem::Item { + title: "some-title".into(), + description: "some-desc".into(), + state: ItemState::NotDone, + }, + ), + ])), + ), + ])), + )])); + + let actual: MovementGraph = graph.into(); + + assert_eq!( + MovementGraph { + items: vec![MovementGraphItem { + index: 0, + name: "0".into(), + item_type: GraphItemType::Section, + values: MovementGraph { + items: vec![ + MovementGraphItem { + index: 0, + name: "00".into(), + values: MovementGraph::default(), + item_type: GraphItemType::Section, + }, + MovementGraphItem { + index: 1, + name: "01".into(), + item_type: GraphItemType::Section, + values: MovementGraph { + items: vec![ + MovementGraphItem { + index: 0, + name: "010".into(), + values: MovementGraph::default(), + item_type: GraphItemType::Item { done: false }, + }, + MovementGraphItem { + index: 1, + name: "011".into(), + values: MovementGraph::default(), + item_type: GraphItemType::Item { done: false }, + }, + ] + } + }, + ] + } + }] + }, + actual + ); + } + + #[test] + fn test_get_graph_item() -> anyhow::Result<()> { + let graph = MovementGraph { + items: vec![ + MovementGraphItem { + index: 0, + name: "0".into(), + item_type: GraphItemType::Section, + values: MovementGraph { + items: vec![ + MovementGraphItem { + index: 0, + name: "0".into(), + values: MovementGraph::default(), + item_type: GraphItemType::Section, + }, + MovementGraphItem { + index: 1, + name: "0".into(), + values: MovementGraph::default(), + item_type: GraphItemType::Section, + }, + ], + }, + }, + MovementGraphItem { + index: 1, + name: "0".into(), + item_type: GraphItemType::Section, + values: MovementGraph { + items: vec![ + MovementGraphItem { + index: 0, + name: "0".into(), + values: MovementGraph::default(), + item_type: GraphItemType::Section, + }, + MovementGraphItem { + index: 1, + name: "0".into(), + values: MovementGraph::default(), + item_type: GraphItemType::Section, + }, + MovementGraphItem { + index: 2, + name: "0".into(), + values: MovementGraph::default(), + item_type: GraphItemType::Section, + }, + ], + }, + }, + MovementGraphItem { + index: 2, + name: "0".into(), + item_type: GraphItemType::Section, + values: MovementGraph { + items: vec![ + MovementGraphItem { + index: 0, + name: "0".into(), + values: MovementGraph::default(), + item_type: GraphItemType::Section, + }, + MovementGraphItem { + index: 1, + name: "0".into(), + values: MovementGraph::default(), + item_type: GraphItemType::Section, + }, + ], + }, + }, + ], + }; + + let actual_default = graph.get_graph(&[]); + assert_eq!(Some(&graph), actual_default); + + let actual_first = graph.get_graph(&[0]); + assert_eq!(graph.items.first().map(|i| &i.values), actual_first); + + let actual_second = graph.get_graph(&[1]); + assert_eq!(graph.items.get(1).map(|i| &i.values), actual_second); + + let actual_nested = graph.get_graph(&[0, 0]); + assert_eq!( + graph + .items + .first() + .and_then(|i| i.values.items.first()) + .map(|i| &i.values), + actual_nested + ); + + let actual_nested = graph.get_graph(&[0, 1]); + assert_eq!( + graph + .items + .first() + .and_then(|i| i.values.items.get(1)) + .map(|i| &i.values), + actual_nested + ); + + let actual_nested = graph.get_graph(&[1, 2]); + assert_eq!( + graph + .items + .get(1) + .and_then(|i| i.values.items.get(2)) + .map(|i| &i.values), + actual_nested + ); + + Ok(()) + } + + #[test] + fn can_next_down() -> anyhow::Result<()> { + let graph = MovementGraph { + items: vec![ + MovementGraphItem { + index: 0, + name: "0".into(), + item_type: GraphItemType::Section, + values: MovementGraph { + items: vec![MovementGraphItem { + index: 0, + name: "0".into(), + item_type: GraphItemType::Section, + values: MovementGraph::default(), + }], + }, + }, + MovementGraphItem { + index: 1, + name: "1".into(), + item_type: GraphItemType::Section, + values: MovementGraph { + items: vec![ + MovementGraphItem { + index: 0, + name: "0".into(), + item_type: GraphItemType::Section, + values: MovementGraph::default(), + }, + MovementGraphItem { + index: 1, + name: "1".into(), + item_type: GraphItemType::Section, + values: MovementGraph::default(), + }, + ], + }, + }, + MovementGraphItem { + index: 2, + name: "2".into(), + item_type: GraphItemType::Section, + values: MovementGraph { + items: vec![ + MovementGraphItem { + index: 0, + name: "0".into(), + item_type: GraphItemType::Section, + values: MovementGraph::default(), + }, + MovementGraphItem { + index: 1, + name: "1".into(), + item_type: GraphItemType::Section, + values: MovementGraph::default(), + }, + MovementGraphItem { + index: 2, + name: "2".into(), + item_type: GraphItemType::Section, + values: MovementGraph::default(), + }, + ], + }, + }, + ], + }; + + let actual = graph.next_down(&[]); + assert_eq!(None, actual); + + let actual = graph.next_down(&[0]); + assert_eq!(Some(vec![1]), actual); + + let actual = graph.next_down(&[1]); + assert_eq!(Some(vec![2]), actual); + + let actual = graph.next_down(&[2]); + assert_eq!(None, actual); + + let graph = MovementGraph { + items: vec![ + MovementGraphItem { + index: 0, + name: "other".into(), + item_type: GraphItemType::Section, + values: MovementGraph { + items: vec![MovementGraphItem { + index: 0, + name: "other".into(), + item_type: GraphItemType::Section, + values: MovementGraph { + items: vec![MovementGraphItem { + index: 0, + name: "other".into(), + item_type: GraphItemType::Section, + values: MovementGraph { items: vec![] }, + }], + }, + }], + }, + }, + MovementGraphItem { + index: 1, + name: "some".into(), + item_type: GraphItemType::Section, + values: MovementGraph { items: vec![] }, + }, + MovementGraphItem { + index: 2, + name: "something".into(), + item_type: GraphItemType::Section, + values: MovementGraph { + items: vec![ + MovementGraphItem { + index: 0, + name: "else".into(), + item_type: GraphItemType::Section, + values: MovementGraph { items: vec![] }, + }, + MovementGraphItem { + index: 1, + name: "third".into(), + item_type: GraphItemType::Section, + values: MovementGraph { items: vec![] }, + }, + ], + }, + }, + ], + }; + + let actual = graph.next_down(&[0]); + assert_eq!(Some(vec![1]), actual); + + Ok(()) + } +} diff --git a/crates/hyperlog-tui/src/components/render_graph.rs b/crates/hyperlog-tui/src/components/render_graph.rs new file mode 100644 index 0000000..f55793e --- /dev/null +++ b/crates/hyperlog-tui/src/components/render_graph.rs @@ -0,0 +1,143 @@ +use ratatui::prelude::*; + +use super::movement_graph::{GraphItemType, MovementGraph}; + +pub trait RenderGraph { + fn render_graph(&self, items: &[usize]) -> Vec; + fn render_graph_spans(&self, items: &[usize]) -> Vec>; +} + +impl RenderGraph for MovementGraph { + /// render_graph takes each level of items, renders them, and finally renders a strongly set selector for the current item the user is on + /// This is done from buttom up, and composed via. string padding + fn render_graph(&self, items: &[usize]) -> Vec { + // Gets the inner content of the strings + + let mut lines = Vec::new(); + + for item in &self.items { + let prefix = match item.item_type { + GraphItemType::Section => "- ", + GraphItemType::Item { done } => { + if done { + "- [x]" + } else { + "- [ ]" + } + } + }; + + match items.split_first().map(|(first, rest)| { + if item.index == *first { + (true, rest) + } else { + (false, rest) + } + }) { + Some((true, rest)) => { + if rest.is_empty() { + lines.push( + Line::raw(format!("{} {}", prefix, item.name)) + .style(Style::new().bold().white()), + ); + } else { + lines.push( + Line::raw(format!("{} {}", prefix, item.name)) + .patch_style(Style::new().dark_gray()), + ); + } + + lines.push("".into()); + + let embedded_sections = item.values.render_graph_spans(rest); + for section in &embedded_sections { + let mut line = vec![Span::raw(" ".repeat(4))]; + line.extend_from_slice(section); + lines.push(Line::from(line)); + } + } + _ => { + lines.push( + Line::raw(format!("{} {}", prefix, item.name)) + .patch_style(Style::new().dark_gray()), + ); + + lines.push("".into()); + + let embedded_sections = item.values.render_graph_spans(&[]); + for section in &embedded_sections { + let mut line = vec![Span::raw(" ".repeat(4))]; + line.extend_from_slice(section); + lines.push(Line::from(line)); + } + } + } + } + + lines + } + + fn render_graph_spans(&self, items: &[usize]) -> Vec> { + let mut lines = Vec::new(); + + for item in &self.items { + let prefix = match item.item_type { + GraphItemType::Section => "-", + GraphItemType::Item { done } => { + if done { + "- [x]" + } else { + "- [ ]" + } + } + }; + match items.split_first().map(|(first, rest)| { + if item.index == *first { + (true, rest) + } else { + (false, rest) + } + }) { + Some((true, rest)) => { + let mut line = Vec::new(); + if rest.is_empty() { + line.push( + Span::raw(format!("{} {}", prefix, item.name)) + .style(Style::new().bold().white()), + ); + } else { + line.push( + Span::raw(format!("{} {}", prefix, item.name)) + .patch_style(Style::new().dark_gray()), + ); + } + + lines.push(line); + lines.push(vec!["".into()]); + + let embedded_sections = item.values.render_graph_spans(rest); + for section in &embedded_sections { + let mut line = vec![Span::raw(" ".repeat(4))]; + line.extend_from_slice(section); + lines.push(line); + } + } + _ => { + lines.push(vec![Span::raw(format!("{prefix} {}", item.name)) + .patch_style(Style::new().dark_gray())]); + + lines.push(vec!["".into()]); + + let embedded_sections = item.values.render_graph_spans(&[]); + for section in &embedded_sections { + let mut line = vec![Span::raw(" ".repeat(4))]; + line.extend_from_slice(section); + lines.push(line); + } + } + } + } + + lines + } +} diff --git a/crates/hyperlog-tui/src/lib.rs b/crates/hyperlog-tui/src/lib.rs index 335f4d8..9e74cfd 100644 --- a/crates/hyperlog-tui/src/lib.rs +++ b/crates/hyperlog-tui/src/lib.rs @@ -5,7 +5,7 @@ use std::{io::Stdout, time::Duration}; use anyhow::{Context, Result}; use app::{render_app, App}; use commands::IntoCommand; -use components::GraphExplorer; +use components::graph_explorer::GraphExplorer; use crossterm::event::{self, Event, KeyCode}; use hyperlog_core::state::State; use models::{EditMsg, Msg}; @@ -77,7 +77,6 @@ fn update( let mut cmd = match &app.mode { app::Mode::View => match key.code { KeyCode::Enter => app.update(Msg::Interact)?, - KeyCode::Char('q') => return Ok(UpdateConclusion::new(true)), KeyCode::Char('l') => app.update(Msg::MoveRight)?, KeyCode::Char('h') => app.update(Msg::MoveLeft)?, KeyCode::Char('j') => app.update(Msg::MoveDown)?, @@ -109,6 +108,10 @@ fn update( let msg = cmd.into_command().execute(); 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 7978b66..7eee074 100644 --- a/crates/hyperlog-tui/src/models.rs +++ b/crates/hyperlog-tui/src/models.rs @@ -8,6 +8,7 @@ pub enum Msg { MoveLeft, MoveDown, MoveUp, + QuitApp, OpenCreateItemDialog, OpenEditItemDialog { item: GraphItem }, Interact,