16
crates/hyperlog-tui/Cargo.toml
Normal file
16
crates/hyperlog-tui/Cargo.toml
Normal file
@@ -0,0 +1,16 @@
|
||||
[package]
|
||||
name = "hyperlog-tui"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
hyperlog-core.workspace = true
|
||||
|
||||
anyhow.workspace = true
|
||||
tokio.workspace = true
|
||||
tracing.workspace = true
|
||||
tracing-subscriber.workspace = true
|
||||
|
||||
ratatui = "0.26.2"
|
||||
crossterm = { version = "0.27.0", features = ["event-stream"] }
|
||||
directories = "5.0.1"
|
138
crates/hyperlog-tui/src/lib.rs
Normal file
138
crates/hyperlog-tui/src/lib.rs
Normal file
@@ -0,0 +1,138 @@
|
||||
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},
|
||||
};
|
||||
use hyperlog_core::state::State;
|
||||
use ratatui::{backend::CrosstermBackend, prelude::*, widgets::*, Frame, Terminal};
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn execute(state: &State) -> Result<()> {
|
||||
tracing::debug!("starting hyperlog tui");
|
||||
|
||||
logging::initialize_panic_handler()?;
|
||||
logging::initialize_logging()?;
|
||||
|
||||
let mut terminal = TerminalInstance::new()?;
|
||||
run(&mut terminal, state).context("app loop failed")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run(terminal: &mut Terminal<CrosstermBackend<Stdout>>, state: &State) -> Result<()> {
|
||||
loop {
|
||||
terminal.draw(|f| crate::render_app(f, &state))?;
|
||||
if should_quit()? {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn render_app(frame: &mut Frame, state: &State) {
|
||||
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,
|
||||
});
|
||||
|
||||
if let Some(graph) = state.querier.get("something", Vec::<String>::new()) {}
|
||||
|
||||
frame.render_widget(background.block(bg_block), chunks[1]);
|
||||
}
|
||||
|
||||
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;
|
77
crates/hyperlog-tui/src/logging.rs
Normal file
77
crates/hyperlog-tui/src/logging.rs
Normal file
@@ -0,0 +1,77 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use directories::ProjectDirs;
|
||||
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, Layer};
|
||||
|
||||
pub fn initialize_logging() -> anyhow::Result<()> {
|
||||
let project = match ProjectDirs::from("io", "kjuulh", env!("CARGO_PKG_NAME")) {
|
||||
Some(p) => p.data_local_dir().to_path_buf(),
|
||||
None => PathBuf::from(".").join(".data"),
|
||||
};
|
||||
|
||||
std::fs::create_dir_all(&project)?;
|
||||
let log_path = project.join("hyperlog.log");
|
||||
|
||||
//println!("logging to: {}", log_path.display());
|
||||
|
||||
let log_file = std::fs::File::create(log_path)?;
|
||||
std::env::set_var(
|
||||
"RUST_LOG",
|
||||
std::env::var("RUST_LOG")
|
||||
.or_else(|_| std::env::var("HYPERLOG_LOG_LEVEL"))
|
||||
.unwrap_or_else(|_| format!("{}=info", env!("CARGO_CRATE_NAME"))),
|
||||
);
|
||||
|
||||
let file_subscriber = tracing_subscriber::fmt::layer()
|
||||
.with_file(true)
|
||||
.with_line_number(true)
|
||||
.with_writer(log_file)
|
||||
.with_target(false)
|
||||
.with_ansi(false)
|
||||
.with_filter(tracing_subscriber::filter::EnvFilter::from_default_env());
|
||||
tracing_subscriber::registry()
|
||||
.with(file_subscriber)
|
||||
//.with(ErrorLayer::default())
|
||||
.init();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn initialize_panic_handler() -> anyhow::Result<()> {
|
||||
std::panic::set_hook(Box::new(move |panic_info| {
|
||||
// if let Ok(mut t) = crate::tui::Tui::new() {
|
||||
// if let Err(r) = t.exit() {
|
||||
// tracing::error!("Unable to exit Terminal: {:?}", r);
|
||||
// }
|
||||
// }
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
{
|
||||
use human_panic::{handle_dump, print_msg, Metadata};
|
||||
let meta = Metadata {
|
||||
version: env!("CARGO_PKG_VERSION").into(),
|
||||
name: env!("CARGO_PKG_NAME").into(),
|
||||
authors: env!("CARGO_PKG_AUTHORS").replace(':', ", ").into(),
|
||||
homepage: env!("CARGO_PKG_HOMEPAGE").into(),
|
||||
};
|
||||
|
||||
let file_path = handle_dump(&meta, panic_info);
|
||||
// prints human-panic message
|
||||
print_msg(file_path, &meta)
|
||||
.expect("human-panic: printing error message to console failed");
|
||||
//eprintln!("{}", panic_hook.panic_report(panic_info)); // prints color-eyre stack trace to stderr
|
||||
}
|
||||
let msg = format!("{}", panic_info);
|
||||
tracing::error!("Error: {}", msg);
|
||||
|
||||
// #[cfg(debug_assertions)]
|
||||
// {
|
||||
// // Better Panic stacktrace that is only enabled when debugging.
|
||||
// better_panic::Settings::auto()
|
||||
// .most_recent_first(false)
|
||||
// .lineno_suffix(true)
|
||||
// .verbosity(better_panic::Verbosity::Full)
|
||||
// .create_panic_handler()(panic_info);
|
||||
// }
|
||||
}));
|
||||
Ok(())
|
||||
}
|
Reference in New Issue
Block a user