chore: refactor app
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-10 12:20:43 +02:00
parent 86310c6764
commit f6a48540e1
Signed by: kjuulh
GPG Key ID: 9AA7BC13CE474394
6 changed files with 878 additions and 855 deletions

View File

@ -5,8 +5,8 @@ use ratatui::{
}; };
use crate::{ use crate::{
command_parser::CommandParser, commands::IntoCommand, components::GraphExplorer, command_parser::CommandParser, commands::IntoCommand,
state::SharedState, Msg, components::graph_explorer::GraphExplorer, state::SharedState, Msg,
}; };
use self::{ use self::{
@ -125,6 +125,10 @@ impl<'a> App<'a> {
self.command = None; self.command = None;
return Ok(msg.into_command()); return Ok(msg.into_command());
} }
if command.is_quit() {
return Ok(Msg::QuitApp.into_command());
}
} }
} }
} }

View File

@ -1,28 +1,35 @@
use std::ops::Deref; pub(crate) mod movement_graph;
pub(crate) mod render_graph;
use anyhow::Result; pub(crate) mod graph_explorer {
use hyperlog_core::log::{GraphItem, ItemState}; use anyhow::Result;
use itertools::Itertools; use hyperlog_core::log::GraphItem;
use ratatui::{prelude::*, widgets::*}; 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> { use super::movement_graph::{MovementGraph, MovementGraphItem};
use super::render_graph::RenderGraph;
pub struct GraphExplorer<'a> {
state: SharedState, state: SharedState,
pub inner: GraphExplorerState<'a>, pub inner: GraphExplorerState<'a>,
} }
pub struct GraphExplorerState<'a> { pub struct GraphExplorerState<'a> {
root: String, root: String,
current_path: Option<&'a str>, current_path: Option<&'a str>,
current_position: Vec<usize>, current_position: Vec<usize>,
graph: Option<GraphItem>, graph: Option<GraphItem>,
} }
impl<'a> GraphExplorer<'a> { impl<'a> GraphExplorer<'a> {
pub fn new(root: String, state: SharedState) -> Self { pub fn new(root: String, state: SharedState) -> Self {
Self { Self {
state, state,
@ -222,149 +229,9 @@ impl<'a> GraphExplorer<'a> {
Ok(()) Ok(())
} }
}
trait RenderGraph {
fn render_graph(&self, items: &[usize]) -> Vec<Line>;
fn render_graph_spans(&self, items: &[usize]) -> Vec<Vec<Span>>;
}
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<Line> {
// 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()); impl<'a> StatefulWidget for GraphExplorer<'a> {
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<Vec<Span>> {
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>; type State = GraphExplorerState<'a>;
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) { fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
@ -378,520 +245,5 @@ impl<'a> StatefulWidget for GraphExplorer<'a> {
para.render(area, buf); 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<MovementGraphItem>,
}
impl MovementGraph {
fn next_right(&self, items: &[usize]) -> Option<MovementGraphItem> {
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<Vec<usize>> {
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<Vec<usize>> {
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<String> {
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<Box<GraphItem>> for MovementGraph {
fn from(value: Box<GraphItem>) -> Self {
value.deref().clone().into()
}
}
impl From<GraphItem> 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::<Vec<_>>();
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(())
} }
} }

View File

@ -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<MovementGraphItem>,
}
impl MovementGraph {
pub fn next_right(&self, items: &[usize]) -> Option<MovementGraphItem> {
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<Vec<usize>> {
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<Vec<usize>> {
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<String> {
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<Box<GraphItem>> for MovementGraph {
fn from(value: Box<GraphItem>) -> Self {
value.deref().clone().into()
}
}
impl From<GraphItem> 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::<Vec<_>>();
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(())
}
}

View File

@ -0,0 +1,143 @@
use ratatui::prelude::*;
use super::movement_graph::{GraphItemType, MovementGraph};
pub trait RenderGraph {
fn render_graph(&self, items: &[usize]) -> Vec<Line>;
fn render_graph_spans(&self, items: &[usize]) -> Vec<Vec<Span>>;
}
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<Line> {
// 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<Vec<Span>> {
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
}
}

View File

@ -5,7 +5,7 @@ use std::{io::Stdout, time::Duration};
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use app::{render_app, App}; use app::{render_app, App};
use commands::IntoCommand; use commands::IntoCommand;
use components::GraphExplorer; use components::graph_explorer::GraphExplorer;
use crossterm::event::{self, Event, KeyCode}; use crossterm::event::{self, Event, KeyCode};
use hyperlog_core::state::State; use hyperlog_core::state::State;
use models::{EditMsg, Msg}; use models::{EditMsg, Msg};
@ -77,7 +77,6 @@ fn update(
let mut cmd = match &app.mode { let mut cmd = match &app.mode {
app::Mode::View => match key.code { app::Mode::View => match key.code {
KeyCode::Enter => app.update(Msg::Interact)?, KeyCode::Enter => app.update(Msg::Interact)?,
KeyCode::Char('q') => return Ok(UpdateConclusion::new(true)),
KeyCode::Char('l') => app.update(Msg::MoveRight)?, KeyCode::Char('l') => app.update(Msg::MoveRight)?,
KeyCode::Char('h') => app.update(Msg::MoveLeft)?, KeyCode::Char('h') => app.update(Msg::MoveLeft)?,
KeyCode::Char('j') => app.update(Msg::MoveDown)?, KeyCode::Char('j') => app.update(Msg::MoveDown)?,
@ -109,6 +108,10 @@ fn update(
let msg = cmd.into_command().execute(); let msg = cmd.into_command().execute();
match msg { match msg {
Some(msg) => { Some(msg) => {
if let Msg::QuitApp = msg {
return Ok(UpdateConclusion(true));
}
cmd = app.update(msg)?; cmd = app.update(msg)?;
} }
None => break, None => break,

View File

@ -8,6 +8,7 @@ pub enum Msg {
MoveLeft, MoveLeft,
MoveDown, MoveDown,
MoveUp, MoveUp,
QuitApp,
OpenCreateItemDialog, OpenCreateItemDialog,
OpenEditItemDialog { item: GraphItem }, OpenEditItemDialog { item: GraphItem },
Interact, Interact,