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",
|
||||
"directories",
|
||||
"hyperlog-core",
|
||||
"itertools",
|
||||
"ratatui",
|
||||
"serde_json",
|
||||
"similar-asserts",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
|
@ -14,3 +14,4 @@ clap = { version = "4", features = ["derive", "env"] }
|
||||
dotenv = { version = "0.15" }
|
||||
axum = { version = "0.7" }
|
||||
serde_json = "1.0.116"
|
||||
itertools = "0.12.1"
|
||||
|
@ -3,6 +3,7 @@ use crate::{
|
||||
storage::Storage,
|
||||
};
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub struct State {
|
||||
engine: SharedEngine,
|
||||
pub storage: Storage,
|
||||
|
@ -11,7 +11,11 @@ tokio.workspace = true
|
||||
tracing.workspace = true
|
||||
tracing-subscriber.workspace = true
|
||||
serde_json.workspace = true
|
||||
itertools.workspace = true
|
||||
|
||||
ratatui = "0.26.2"
|
||||
crossterm = { version = "0.27.0", features = ["event-stream"] }
|
||||
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::{
|
||||
io::{self, Stdout},
|
||||
ops::{Deref, DerefMut},
|
||||
time::Duration,
|
||||
};
|
||||
use std::{io::Stdout, time::Duration};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use crossterm::{
|
||||
event::{self, Event, KeyCode},
|
||||
execute,
|
||||
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 app::{render_app, App};
|
||||
use components::GraphExplorer;
|
||||
use crossterm::event::{self, Event, KeyCode};
|
||||
use hyperlog_core::state::State;
|
||||
use models::Msg;
|
||||
use ratatui::{backend::CrosstermBackend, Terminal};
|
||||
|
||||
use crate::state::SharedState;
|
||||
use crate::{state::SharedState, terminal::TerminalInstance};
|
||||
|
||||
struct TerminalInstance {
|
||||
terminal: Terminal<CrosstermBackend<Stdout>>,
|
||||
}
|
||||
pub mod models;
|
||||
|
||||
impl TerminalInstance {
|
||||
fn new() -> Result<Self> {
|
||||
Ok(Self {
|
||||
terminal: setup_terminal().context("setup failed")?,
|
||||
})
|
||||
}
|
||||
}
|
||||
pub(crate) mod app;
|
||||
pub(crate) mod components;
|
||||
pub(crate) mod state;
|
||||
|
||||
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;
|
||||
|
||||
#[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),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
mod logging;
|
||||
mod terminal;
|
||||
|
||||
pub async fn execute(state: State) -> Result<()> {
|
||||
tracing::debug!("starting hyperlog tui");
|
||||
@ -90,35 +33,6 @@ pub async fn execute(state: State) -> Result<()> {
|
||||
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<()> {
|
||||
let mut graph_explorer = GraphExplorer::new(state.clone());
|
||||
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);
|
||||
|
||||
loop {
|
||||
terminal.draw(|f| crate::render_app(f, &mut app))?;
|
||||
if should_quit()? {
|
||||
terminal.draw(|f| render_app(f, &mut app))?;
|
||||
|
||||
if update(terminal, &mut app)?.should_quit() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn render_app(frame: &mut Frame, state: &mut App) {
|
||||
let chunks =
|
||||
Layout::vertical(vec![Constraint::Length(2), Constraint::Min(0)]).split(frame.size());
|
||||
pub struct UpdateConclusion(bool);
|
||||
|
||||
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])
|
||||
}
|
||||
|
||||
struct GraphExplorer<'a> {
|
||||
state: SharedState,
|
||||
|
||||
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,
|
||||
},
|
||||
}
|
||||
impl UpdateConclusion {
|
||||
pub fn new(should_quit: bool) -> Self {
|
||||
Self(should_quit)
|
||||
}
|
||||
|
||||
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)
|
||||
pub fn should_quit(self) -> bool {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
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 update(
|
||||
_terminal: &mut Terminal<CrosstermBackend<Stdout>>,
|
||||
app: &mut App,
|
||||
) -> Result<UpdateConclusion> {
|
||||
if event::poll(Duration::from_millis(250)).context("event poll failed")? {
|
||||
if let Event::Key(key) = event::read().context("event read failed")? {
|
||||
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)?;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn should_quit() -> Result<bool> {
|
||||
if event::poll(Duration::from_millis(250)).context("event poll failed")? {
|
||||
if let Event::Key(key) = event::read().context("event read failed")? {
|
||||
return Ok(KeyCode::Char('q') == key.code);
|
||||
}
|
||||
}
|
||||
Ok(false)
|
||||
Ok(UpdateConclusion::new(false))
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
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 db: Pool<Postgres>,
|
||||
pub _db: Pool<Postgres>,
|
||||
}
|
||||
|
||||
impl State {
|
||||
@ -32,6 +32,6 @@ impl State {
|
||||
|
||||
let _ = sqlx::query("SELECT 1;").fetch_one(&db).await?;
|
||||
|
||||
Ok(Self { db })
|
||||
Ok(Self { _db: db })
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user