2024-05-01 22:17:39 +02:00
|
|
|
use std::{
|
|
|
|
io::{self, Stdout},
|
|
|
|
ops::{Deref, DerefMut},
|
|
|
|
time::Duration,
|
|
|
|
};
|
|
|
|
|
|
|
|
use anyhow::{Context, Result};
|
|
|
|
use crossterm::{
|
|
|
|
event::{self, Event, KeyCode},
|
|
|
|
execute,
|
|
|
|
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
|
|
|
};
|
2024-05-01 23:26:25 +02:00
|
|
|
use hyperlog_core::{log::GraphItem, state::State};
|
2024-05-01 22:17:39 +02:00
|
|
|
use ratatui::{backend::CrosstermBackend, prelude::*, widgets::*, Frame, Terminal};
|
|
|
|
|
2024-05-01 23:26:25 +02:00
|
|
|
use crate::state::SharedState;
|
|
|
|
|
2024-05-01 22:17:39 +02:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-05-01 23:26:25 +02:00
|
|
|
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),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
pub async fn execute(state: State) -> Result<()> {
|
2024-05-01 22:17:39 +02:00
|
|
|
tracing::debug!("starting hyperlog tui");
|
|
|
|
|
|
|
|
logging::initialize_panic_handler()?;
|
|
|
|
logging::initialize_logging()?;
|
|
|
|
|
2024-05-01 23:26:25 +02:00
|
|
|
let state = SharedState::from(state);
|
|
|
|
|
2024-05-01 22:17:39 +02:00
|
|
|
let mut terminal = TerminalInstance::new()?;
|
|
|
|
run(&mut terminal, state).context("app loop failed")?;
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
2024-05-01 23:26:25 +02:00
|
|
|
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()?;
|
|
|
|
|
|
|
|
let mut app = App::new(state.clone(), graph_explorer);
|
|
|
|
|
2024-05-01 22:17:39 +02:00
|
|
|
loop {
|
2024-05-01 23:26:25 +02:00
|
|
|
terminal.draw(|f| crate::render_app(f, &mut app))?;
|
2024-05-01 22:17:39 +02:00
|
|
|
if should_quit()? {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
2024-05-01 23:26:25 +02:00
|
|
|
fn render_app(frame: &mut Frame, state: &mut App) {
|
2024-05-01 22:17:39 +02:00
|
|
|
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,
|
|
|
|
});
|
2024-05-01 23:26:25 +02:00
|
|
|
//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>,
|
|
|
|
}
|
2024-05-01 22:17:39 +02:00
|
|
|
|
2024-05-01 23:26:25 +02:00
|
|
|
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> {
|
|
|
|
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)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl<'a> StatefulWidget for GraphExplorer<'a> {
|
|
|
|
type State = GraphExplorerState<'a>;
|
2024-05-01 22:17:39 +02:00
|
|
|
|
2024-05-01 23:26:25 +02:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2024-05-01 22:17:39 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
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)
|
|
|
|
}
|
|
|
|
|
|
|
|
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;
|