feat: left right movement done
Signed-off-by: kjuulh <contact@kjuulh.io>
This commit is contained in:
parent
d7e21a256d
commit
9de5e6bbff
2
Cargo.lock
generated
2
Cargo.lock
generated
@ -919,8 +919,10 @@ dependencies = [
|
|||||||
"crossterm",
|
"crossterm",
|
||||||
"directories",
|
"directories",
|
||||||
"hyperlog-core",
|
"hyperlog-core",
|
||||||
|
"itertools",
|
||||||
"ratatui",
|
"ratatui",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"similar-asserts",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
|
@ -14,3 +14,4 @@ clap = { version = "4", features = ["derive", "env"] }
|
|||||||
dotenv = { version = "0.15" }
|
dotenv = { version = "0.15" }
|
||||||
axum = { version = "0.7" }
|
axum = { version = "0.7" }
|
||||||
serde_json = "1.0.116"
|
serde_json = "1.0.116"
|
||||||
|
itertools = "0.12.1"
|
||||||
|
@ -3,6 +3,7 @@ use crate::{
|
|||||||
storage::Storage,
|
storage::Storage,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
pub struct State {
|
pub struct State {
|
||||||
engine: SharedEngine,
|
engine: SharedEngine,
|
||||||
pub storage: Storage,
|
pub storage: Storage,
|
||||||
|
@ -11,7 +11,11 @@ tokio.workspace = true
|
|||||||
tracing.workspace = true
|
tracing.workspace = true
|
||||||
tracing-subscriber.workspace = true
|
tracing-subscriber.workspace = true
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
|
itertools.workspace = true
|
||||||
|
|
||||||
ratatui = "0.26.2"
|
ratatui = "0.26.2"
|
||||||
crossterm = { version = "0.27.0", features = ["event-stream"] }
|
crossterm = { version = "0.27.0", features = ["event-stream"] }
|
||||||
directories = "5.0.1"
|
directories = "5.0.1"
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
similar-asserts = "1.5.0"
|
||||||
|
86
crates/hyperlog-tui/src/app.rs
Normal file
86
crates/hyperlog-tui/src/app.rs
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
use ratatui::{
|
||||||
|
prelude::*,
|
||||||
|
widgets::{Block, Borders, Padding, Paragraph},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{components::GraphExplorer, state::SharedState, Msg};
|
||||||
|
|
||||||
|
pub struct App<'a> {
|
||||||
|
state: SharedState,
|
||||||
|
|
||||||
|
graph_explorer: GraphExplorer<'a>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> App<'a> {
|
||||||
|
pub fn new(state: SharedState, graph_explorer: GraphExplorer<'a>) -> Self {
|
||||||
|
Self {
|
||||||
|
state,
|
||||||
|
graph_explorer,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update(&mut self, msg: Msg) -> anyhow::Result<()> {
|
||||||
|
tracing::trace!("handling msg: {:?}", msg);
|
||||||
|
|
||||||
|
match msg {
|
||||||
|
Msg::MoveRight => self.graph_explorer.move_right(),
|
||||||
|
Msg::MoveLeft => self.graph_explorer.move_left(),
|
||||||
|
Msg::MoveDown => self.graph_explorer.move_down(),
|
||||||
|
Msg::MoveUp => self.graph_explorer.move_up(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Widget for &mut App<'a> {
|
||||||
|
fn render(self, area: Rect, buf: &mut Buffer)
|
||||||
|
where
|
||||||
|
Self: Sized,
|
||||||
|
{
|
||||||
|
StatefulWidget::render(
|
||||||
|
GraphExplorer::new(self.state.clone()),
|
||||||
|
area,
|
||||||
|
buf,
|
||||||
|
&mut self.graph_explorer.inner,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render_app(frame: &mut Frame, state: &mut App) {
|
||||||
|
let chunks =
|
||||||
|
Layout::vertical(vec![Constraint::Length(2), Constraint::Min(0)]).split(frame.size());
|
||||||
|
|
||||||
|
let heading = Paragraph::new(text::Line::from(
|
||||||
|
Span::styled("hyperlog", Style::default()).fg(Color::Green),
|
||||||
|
));
|
||||||
|
let block_heading = Block::default().borders(Borders::BOTTOM);
|
||||||
|
|
||||||
|
frame.render_widget(heading.block(block_heading), chunks[0]);
|
||||||
|
|
||||||
|
let Rect { width, height, .. } = chunks[1];
|
||||||
|
|
||||||
|
let height = height as usize;
|
||||||
|
let width = width as usize;
|
||||||
|
|
||||||
|
let mut lines = Vec::new();
|
||||||
|
for y in 0..height {
|
||||||
|
if !y % 2 == 0 {
|
||||||
|
lines.push(text::Line::default());
|
||||||
|
} else {
|
||||||
|
lines.push(text::Line::raw(" ~ ".repeat(width / 3)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let _background = Paragraph::new(lines);
|
||||||
|
|
||||||
|
let _bg_block = Block::default()
|
||||||
|
.fg(Color::DarkGray)
|
||||||
|
.bold()
|
||||||
|
.padding(Padding {
|
||||||
|
left: 4,
|
||||||
|
right: 4,
|
||||||
|
top: 2,
|
||||||
|
bottom: 2,
|
||||||
|
});
|
||||||
|
//frame.render_widget(background.block(bg_block), chunks[1]);
|
||||||
|
|
||||||
|
frame.render_widget(state, chunks[1])
|
||||||
|
}
|
289
crates/hyperlog-tui/src/components.rs
Normal file
289
crates/hyperlog-tui/src/components.rs
Normal file
@ -0,0 +1,289 @@
|
|||||||
|
use std::{collections::HashMap, ops::Deref};
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use hyperlog_core::log::GraphItem;
|
||||||
|
use itertools::Itertools;
|
||||||
|
use ratatui::{prelude::*, widgets::*};
|
||||||
|
|
||||||
|
use crate::state::SharedState;
|
||||||
|
|
||||||
|
pub struct GraphExplorer<'a> {
|
||||||
|
state: SharedState,
|
||||||
|
|
||||||
|
pub inner: GraphExplorerState<'a>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct GraphExplorerState<'a> {
|
||||||
|
current_path: Option<&'a str>,
|
||||||
|
current_postition: Vec<usize>,
|
||||||
|
|
||||||
|
graph: Option<GraphItem>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GraphExplorer<'_> {
|
||||||
|
pub fn new(state: SharedState) -> Self {
|
||||||
|
Self {
|
||||||
|
state,
|
||||||
|
inner: GraphExplorerState::<'_> {
|
||||||
|
current_path: None,
|
||||||
|
current_postition: Vec::new(),
|
||||||
|
graph: None,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_graph(&mut self) -> Result<&mut Self> {
|
||||||
|
let graph = self
|
||||||
|
.state
|
||||||
|
.querier
|
||||||
|
.get(
|
||||||
|
"something",
|
||||||
|
self.inner
|
||||||
|
.current_path
|
||||||
|
.map(|p| p.split('.').collect::<Vec<_>>())
|
||||||
|
.unwrap_or_default(),
|
||||||
|
)
|
||||||
|
.ok_or(anyhow::anyhow!("graph should've had an item"))?;
|
||||||
|
|
||||||
|
self.inner.graph = Some(graph);
|
||||||
|
|
||||||
|
Ok(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn linearize_graph(&self) -> Option<MovementGraph> {
|
||||||
|
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() {
|
||||||
|
let position_items = &self.inner.current_postition;
|
||||||
|
|
||||||
|
if let Some(next_item) = graph.next_right(position_items) {
|
||||||
|
self.inner.current_postition.push(next_item.index);
|
||||||
|
tracing::trace!("found next item: {:?}", self.inner.current_postition);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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_postition.pop() {
|
||||||
|
tracing::trace!(
|
||||||
|
"found last item: {:?}, popped: {}",
|
||||||
|
self.inner.current_postition,
|
||||||
|
last
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn move_up(&self) -> Result<()> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn move_down(&self) -> Result<()> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
if let Ok(graph) = serde_json::to_string_pretty(graph) {
|
||||||
|
let lines = graph
|
||||||
|
.split('\n')
|
||||||
|
.take(height)
|
||||||
|
.map(Line::raw)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
let para = Paragraph::new(lines);
|
||||||
|
|
||||||
|
para.render(area, buf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(PartialEq, Eq, Debug, Clone)]
|
||||||
|
struct MovementGraphItem {
|
||||||
|
index: usize,
|
||||||
|
name: String,
|
||||||
|
values: MovementGraph,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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(),
|
||||||
|
})
|
||||||
|
.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::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(),
|
||||||
|
Box::new(GraphItem::Section(BTreeMap::from([
|
||||||
|
(
|
||||||
|
"00".to_string(),
|
||||||
|
Box::new(GraphItem::Section(BTreeMap::new())),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"01".to_string(),
|
||||||
|
Box::new(GraphItem::Section(BTreeMap::from([
|
||||||
|
(
|
||||||
|
"010".to_string(),
|
||||||
|
Box::new(GraphItem::Item {
|
||||||
|
title: "some-title".into(),
|
||||||
|
description: "some-desc".into(),
|
||||||
|
state: ItemState::NotDone,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"011".to_string(),
|
||||||
|
Box::new(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(),
|
||||||
|
values: MovementGraph {
|
||||||
|
items: vec![
|
||||||
|
MovementGraphItem {
|
||||||
|
index: 0,
|
||||||
|
name: "00".into(),
|
||||||
|
values: MovementGraph::default()
|
||||||
|
},
|
||||||
|
MovementGraphItem {
|
||||||
|
index: 1,
|
||||||
|
name: "01".into(),
|
||||||
|
values: MovementGraph {
|
||||||
|
items: vec![
|
||||||
|
MovementGraphItem {
|
||||||
|
index: 0,
|
||||||
|
name: "010".into(),
|
||||||
|
values: MovementGraph::default(),
|
||||||
|
},
|
||||||
|
MovementGraphItem {
|
||||||
|
index: 1,
|
||||||
|
name: "011".into(),
|
||||||
|
values: MovementGraph::default(),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
actual
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,80 +1,23 @@
|
|||||||
use std::{
|
use std::{io::Stdout, time::Duration};
|
||||||
io::{self, Stdout},
|
|
||||||
ops::{Deref, DerefMut},
|
|
||||||
time::Duration,
|
|
||||||
};
|
|
||||||
|
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use crossterm::{
|
use app::{render_app, App};
|
||||||
event::{self, Event, KeyCode},
|
use components::GraphExplorer;
|
||||||
execute,
|
use crossterm::event::{self, Event, KeyCode};
|
||||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
|
||||||
};
|
|
||||||
use hyperlog_core::{log::GraphItem, state::State};
|
|
||||||
use ratatui::{backend::CrosstermBackend, prelude::*, widgets::*, Frame, Terminal};
|
|
||||||
|
|
||||||
use crate::state::SharedState;
|
|
||||||
|
|
||||||
struct TerminalInstance {
|
|
||||||
terminal: Terminal<CrosstermBackend<Stdout>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TerminalInstance {
|
|
||||||
fn new() -> Result<Self> {
|
|
||||||
Ok(Self {
|
|
||||||
terminal: setup_terminal().context("setup failed")?,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Deref for TerminalInstance {
|
|
||||||
type Target = Terminal<CrosstermBackend<Stdout>>;
|
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
|
||||||
&self.terminal
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DerefMut for TerminalInstance {
|
|
||||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
|
||||||
&mut self.terminal
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Drop for TerminalInstance {
|
|
||||||
fn drop(&mut self) {
|
|
||||||
if let Err(e) = restore_terminal(&mut self.terminal).context("restore terminal failed") {
|
|
||||||
tracing::error!("failed to restore terminal: {}", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mod state {
|
|
||||||
use std::{ops::Deref, sync::Arc};
|
|
||||||
|
|
||||||
use hyperlog_core::state::State;
|
use hyperlog_core::state::State;
|
||||||
|
use models::Msg;
|
||||||
|
use ratatui::{backend::CrosstermBackend, Terminal};
|
||||||
|
|
||||||
#[derive(Clone)]
|
use crate::{state::SharedState, terminal::TerminalInstance};
|
||||||
pub struct SharedState {
|
|
||||||
state: Arc<State>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Deref for SharedState {
|
pub mod models;
|
||||||
type Target = State;
|
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
pub(crate) mod app;
|
||||||
&self.state
|
pub(crate) mod components;
|
||||||
}
|
pub(crate) mod state;
|
||||||
}
|
|
||||||
|
|
||||||
impl From<State> for SharedState {
|
mod logging;
|
||||||
fn from(value: State) -> Self {
|
mod terminal;
|
||||||
Self {
|
|
||||||
state: Arc::new(value),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn execute(state: State) -> Result<()> {
|
pub async fn execute(state: State) -> Result<()> {
|
||||||
tracing::debug!("starting hyperlog tui");
|
tracing::debug!("starting hyperlog tui");
|
||||||
@ -90,35 +33,6 @@ pub async fn execute(state: State) -> Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct App<'a> {
|
|
||||||
state: SharedState,
|
|
||||||
|
|
||||||
graph_explorer: GraphExplorer<'a>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> App<'a> {
|
|
||||||
pub fn new(state: SharedState, graph_explorer: GraphExplorer<'a>) -> Self {
|
|
||||||
Self {
|
|
||||||
state,
|
|
||||||
graph_explorer,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> Widget for &mut App<'a> {
|
|
||||||
fn render(self, area: Rect, buf: &mut Buffer)
|
|
||||||
where
|
|
||||||
Self: Sized,
|
|
||||||
{
|
|
||||||
StatefulWidget::render(
|
|
||||||
GraphExplorer::new(self.state.clone()),
|
|
||||||
area,
|
|
||||||
buf,
|
|
||||||
&mut self.graph_explorer.inner,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn run(terminal: &mut Terminal<CrosstermBackend<Stdout>>, state: SharedState) -> Result<()> {
|
fn run(terminal: &mut Terminal<CrosstermBackend<Stdout>>, state: SharedState) -> Result<()> {
|
||||||
let mut graph_explorer = GraphExplorer::new(state.clone());
|
let mut graph_explorer = GraphExplorer::new(state.clone());
|
||||||
graph_explorer.update_graph()?;
|
graph_explorer.update_graph()?;
|
||||||
@ -126,142 +40,51 @@ fn run(terminal: &mut Terminal<CrosstermBackend<Stdout>>, state: SharedState) ->
|
|||||||
let mut app = App::new(state.clone(), graph_explorer);
|
let mut app = App::new(state.clone(), graph_explorer);
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
terminal.draw(|f| crate::render_app(f, &mut app))?;
|
terminal.draw(|f| render_app(f, &mut app))?;
|
||||||
if should_quit()? {
|
|
||||||
|
if update(terminal, &mut app)?.should_quit() {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_app(frame: &mut Frame, state: &mut App) {
|
pub struct UpdateConclusion(bool);
|
||||||
let chunks =
|
|
||||||
Layout::vertical(vec![Constraint::Length(2), Constraint::Min(0)]).split(frame.size());
|
|
||||||
|
|
||||||
let heading = Paragraph::new(text::Line::from(
|
impl UpdateConclusion {
|
||||||
Span::styled("hyperlog", Style::default()).fg(Color::Green),
|
pub fn new(should_quit: bool) -> Self {
|
||||||
));
|
Self(should_quit)
|
||||||
let block_heading = Block::default().borders(Borders::BOTTOM);
|
|
||||||
|
|
||||||
frame.render_widget(heading.block(block_heading), chunks[0]);
|
|
||||||
|
|
||||||
let Rect { width, height, .. } = chunks[1];
|
|
||||||
|
|
||||||
let height = height as usize;
|
|
||||||
let width = width as usize;
|
|
||||||
|
|
||||||
let mut lines = Vec::new();
|
|
||||||
for y in 0..height {
|
|
||||||
if !y % 2 == 0 {
|
|
||||||
lines.push(text::Line::default());
|
|
||||||
} else {
|
|
||||||
lines.push(text::Line::raw(" ~ ".repeat(width / 3)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let background = Paragraph::new(lines);
|
|
||||||
|
|
||||||
let bg_block = Block::default()
|
|
||||||
.fg(Color::DarkGray)
|
|
||||||
.bold()
|
|
||||||
.padding(Padding {
|
|
||||||
left: 4,
|
|
||||||
right: 4,
|
|
||||||
top: 2,
|
|
||||||
bottom: 2,
|
|
||||||
});
|
|
||||||
//frame.render_widget(background.block(bg_block), chunks[1]);
|
|
||||||
|
|
||||||
frame.render_widget(state, chunks[1])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct GraphExplorer<'a> {
|
pub fn should_quit(self) -> bool {
|
||||||
state: SharedState,
|
self.0
|
||||||
|
|
||||||
inner: GraphExplorerState<'a>,
|
|
||||||
}
|
|
||||||
|
|
||||||
struct GraphExplorerState<'a> {
|
|
||||||
current_path: Option<&'a str>,
|
|
||||||
|
|
||||||
graph: Option<GraphItem>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl GraphExplorer<'_> {
|
|
||||||
pub fn new(state: SharedState) -> Self {
|
|
||||||
Self {
|
|
||||||
state,
|
|
||||||
inner: GraphExplorerState::<'_> {
|
|
||||||
current_path: None,
|
|
||||||
graph: None,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update_graph(&mut self) -> Result<&mut Self> {
|
fn update(
|
||||||
let graph = self
|
_terminal: &mut Terminal<CrosstermBackend<Stdout>>,
|
||||||
.state
|
app: &mut App,
|
||||||
.querier
|
) -> Result<UpdateConclusion> {
|
||||||
.get(
|
|
||||||
"something",
|
|
||||||
self.inner
|
|
||||||
.current_path
|
|
||||||
.map(|p| p.split('.').collect::<Vec<_>>())
|
|
||||||
.unwrap_or_default(),
|
|
||||||
)
|
|
||||||
.ok_or(anyhow::anyhow!("graph should've had an item"))?;
|
|
||||||
|
|
||||||
self.inner.graph = Some(graph);
|
|
||||||
|
|
||||||
Ok(self)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
|
||||||
if let Ok(graph) = serde_json::to_string_pretty(graph) {
|
|
||||||
let lines = graph
|
|
||||||
.split('\n')
|
|
||||||
.take(height)
|
|
||||||
.map(Line::raw)
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
let para = Paragraph::new(lines);
|
|
||||||
|
|
||||||
para.render(area, buf);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn should_quit() -> Result<bool> {
|
|
||||||
if event::poll(Duration::from_millis(250)).context("event poll failed")? {
|
if event::poll(Duration::from_millis(250)).context("event poll failed")? {
|
||||||
if let Event::Key(key) = event::read().context("event read failed")? {
|
if let Event::Key(key) = event::read().context("event read failed")? {
|
||||||
return Ok(KeyCode::Char('q') == key.code);
|
match key.code {
|
||||||
|
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)?;
|
||||||
|
}
|
||||||
|
KeyCode::Char('k') => {
|
||||||
|
app.update(Msg::MoveUp)?;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(false)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn setup_terminal() -> Result<Terminal<CrosstermBackend<Stdout>>> {
|
Ok(UpdateConclusion::new(false))
|
||||||
let mut stdout = io::stdout();
|
|
||||||
enable_raw_mode().context("failed to enable raw mode")?;
|
|
||||||
execute!(stdout, EnterAlternateScreen).context("unable to enter alternate screen")?;
|
|
||||||
Terminal::new(CrosstermBackend::new(stdout)).context("creating terminal failed")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Restore the terminal. This is where you disable raw mode, leave the alternate screen, and show
|
|
||||||
/// the cursor.
|
|
||||||
fn restore_terminal(terminal: &mut Terminal<CrosstermBackend<Stdout>>) -> Result<()> {
|
|
||||||
disable_raw_mode().context("failed to disable raw mode")?;
|
|
||||||
execute!(terminal.backend_mut(), LeaveAlternateScreen)
|
|
||||||
.context("unable to switch to main screen")?;
|
|
||||||
terminal.show_cursor().context("unable to show cursor")
|
|
||||||
}
|
|
||||||
|
|
||||||
mod logging;
|
|
||||||
|
7
crates/hyperlog-tui/src/models.rs
Normal file
7
crates/hyperlog-tui/src/models.rs
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
#[derive(Debug)]
|
||||||
|
pub enum Msg {
|
||||||
|
MoveRight,
|
||||||
|
MoveLeft,
|
||||||
|
MoveDown,
|
||||||
|
MoveUp,
|
||||||
|
}
|
24
crates/hyperlog-tui/src/state.rs
Normal file
24
crates/hyperlog-tui/src/state.rs
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
use std::{ops::Deref, sync::Arc};
|
||||||
|
|
||||||
|
use hyperlog_core::state::State;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct SharedState {
|
||||||
|
state: Arc<State>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Deref for SharedState {
|
||||||
|
type Target = State;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.state
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<State> for SharedState {
|
||||||
|
fn from(value: State) -> Self {
|
||||||
|
Self {
|
||||||
|
state: Arc::new(value),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
61
crates/hyperlog-tui/src/terminal.rs
Normal file
61
crates/hyperlog-tui/src/terminal.rs
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
use std::{
|
||||||
|
io::{self, Stdout},
|
||||||
|
ops::{Deref, DerefMut},
|
||||||
|
};
|
||||||
|
|
||||||
|
use anyhow::{Context, Result};
|
||||||
|
use crossterm::{
|
||||||
|
execute,
|
||||||
|
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||||
|
};
|
||||||
|
use ratatui::{backend::CrosstermBackend, Terminal};
|
||||||
|
|
||||||
|
pub struct TerminalInstance {
|
||||||
|
terminal: Terminal<CrosstermBackend<Stdout>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TerminalInstance {
|
||||||
|
pub fn new() -> Result<Self> {
|
||||||
|
Ok(Self {
|
||||||
|
terminal: setup_terminal().context("setup failed")?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Deref for TerminalInstance {
|
||||||
|
type Target = Terminal<CrosstermBackend<Stdout>>;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.terminal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DerefMut for TerminalInstance {
|
||||||
|
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||||
|
&mut self.terminal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for TerminalInstance {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
if let Err(e) = restore_terminal(&mut self.terminal).context("restore terminal failed") {
|
||||||
|
tracing::error!("failed to restore terminal: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn setup_terminal() -> Result<Terminal<CrosstermBackend<Stdout>>> {
|
||||||
|
let mut stdout = io::stdout();
|
||||||
|
enable_raw_mode().context("failed to enable raw mode")?;
|
||||||
|
execute!(stdout, EnterAlternateScreen).context("unable to enter alternate screen")?;
|
||||||
|
Terminal::new(CrosstermBackend::new(stdout)).context("creating terminal failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Restore the terminal. This is where you disable raw mode, leave the alternate screen, and show
|
||||||
|
/// the cursor.
|
||||||
|
fn restore_terminal(terminal: &mut Terminal<CrosstermBackend<Stdout>>) -> Result<()> {
|
||||||
|
disable_raw_mode().context("failed to disable raw mode")?;
|
||||||
|
execute!(terminal.backend_mut(), LeaveAlternateScreen)
|
||||||
|
.context("unable to switch to main screen")?;
|
||||||
|
terminal.show_cursor().context("unable to show cursor")
|
||||||
|
}
|
@ -15,7 +15,7 @@ impl Deref for SharedState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub struct State {
|
pub struct State {
|
||||||
pub db: Pool<Postgres>,
|
pub _db: Pool<Postgres>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl State {
|
impl State {
|
||||||
@ -32,6 +32,6 @@ impl State {
|
|||||||
|
|
||||||
let _ = sqlx::query("SELECT 1;").fetch_one(&db).await?;
|
let _ = sqlx::query("SELECT 1;").fetch_one(&db).await?;
|
||||||
|
|
||||||
Ok(Self { db })
|
Ok(Self { _db: db })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user