diff --git a/Cargo.lock b/Cargo.lock index 0248e347..f6a607cf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2812,6 +2812,7 @@ dependencies = [ "tokio-stream", "tower 0.5.0", "tracing", + "tui-logger", "whoami", ] @@ -6289,6 +6290,20 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "tui-logger" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "828e67459779fbbeef5d4d65e1862d03a2d0c673e074a93fc3eb5171e1438038" +dependencies = [ + "chrono", + "fxhash", + "lazy_static", + "log", + "parking_lot", + "ratatui", +] + [[package]] name = "tungstenite" version = "0.24.0" diff --git a/crates/kftray-server/src/main.rs b/crates/kftray-server/src/main.rs index 6060b84f..eae2dbbc 100644 --- a/crates/kftray-server/src/main.rs +++ b/crates/kftray-server/src/main.rs @@ -138,4 +138,5 @@ async fn main() { } warn!("Shutdown initiated..."); + info!("Exiting...") } diff --git a/crates/kftui/Cargo.toml b/crates/kftui/Cargo.toml index feea991d..a18c64a4 100644 --- a/crates/kftui/Cargo.toml +++ b/crates/kftui/Cargo.toml @@ -58,6 +58,7 @@ kftray-commons = { path = "../kftray-commons" } kftray-portforward = { path = "../kftray-portforward" } ratatui = { version = "0.28.1", features = ["unstable-widget-ref"] } crossterm = { version = "0.28.1", optional = false } +tui-logger = "0.13.1" # https://github.com/tatounee/ratatui-explorer/pull/2/files ratatui-explorer = { git = "https://github.com/hcavarsan/ratatui-explorer", branch = "master" } @@ -66,4 +67,4 @@ built = "0.7.4" [build-dependencies] -built = "0.7" \ No newline at end of file +built = "0.7" diff --git a/crates/kftui/src/core/logging.rs b/crates/kftui/src/core/logging.rs deleted file mode 100644 index e1fc38f7..00000000 --- a/crates/kftui/src/core/logging.rs +++ /dev/null @@ -1,69 +0,0 @@ -use std::fs::OpenOptions; -use std::io::Write; -use std::sync::{ - Arc, - Mutex, -}; - -use kftray_commons::utils::config_dir::get_app_log_path; -use log::{ - Metadata, - Record, -}; -use once_cell::sync::Lazy; - -pub struct AppLogger { - pub buffer: Arc>, - pub file: Option>>, -} - -impl AppLogger { - fn new(buffer: Arc>, file: Option>>) -> Self { - AppLogger { buffer, file } - } -} - -impl log::Log for AppLogger { - fn enabled(&self, metadata: &Metadata) -> bool { - metadata.level() <= log::Level::Info && metadata.target().starts_with("kftray_portforward") - } - - fn log(&self, record: &Record) { - if self.enabled(record.metadata()) { - let log_entry = format!("{}\n", record.args()); - - { - let mut buffer = self.buffer.lock().unwrap(); - buffer.push_str(&log_entry); - } - - if let Some(file) = &self.file { - let mut file = file.lock().unwrap(); - if let Err(_e) = file.write_all(log_entry.as_bytes()) {} - } - } - } - - fn flush(&self) {} -} - -pub static LOGGER: Lazy = Lazy::new(|| { - let buffer = Arc::new(Mutex::new(String::new())); - let file = if std::env::var("KFTUI_LOGS").unwrap_or_default() == "enabled" { - get_app_log_path().ok().and_then(|path| { - OpenOptions::new() - .create(true) - .append(true) - .open(path) - .ok() - .map(|file| Arc::new(Mutex::new(file))) - }) - } else { - None - }; - AppLogger::new(buffer, file) -}); - -pub fn init_logger() -> Result<(), Box> { - Ok(log::set_logger(&*LOGGER).map(|()| log::set_max_level(log::LevelFilter::Info))?) -} diff --git a/crates/kftui/src/core/mod.rs b/crates/kftui/src/core/mod.rs index 7dd150a2..5994d2e5 100644 --- a/crates/kftui/src/core/mod.rs +++ b/crates/kftui/src/core/mod.rs @@ -1,4 +1,3 @@ -pub mod logging; pub mod port_forward; pub mod built_info { diff --git a/crates/kftui/src/main.rs b/crates/kftui/src/main.rs index 44be657d..2d1d718f 100644 --- a/crates/kftui/src/main.rs +++ b/crates/kftui/src/main.rs @@ -2,13 +2,12 @@ mod core; mod tui; mod utils; - -use core::logging::init_logger; - use tui::run_tui; #[tokio::main] async fn main() -> Result<(), Box> { - init_logger()?; + tui_logger::init_logger(log::LevelFilter::Debug).unwrap(); + tui_logger::set_default_level(log::LevelFilter::Debug); + run_tui().await } diff --git a/crates/kftui/src/tui/app.rs b/crates/kftui/src/tui/app.rs index 3a4e4a4c..03dab49d 100644 --- a/crates/kftui/src/tui/app.rs +++ b/crates/kftui/src/tui/app.rs @@ -67,7 +67,6 @@ async fn run_app( let mut config_states = read_config_states().await.unwrap_or_default(); app.update_configs(&configs, &config_states); - fetch_new_logs(app).await; terminal.draw(|f| { draw_ui(f, app, &config_states); @@ -81,25 +80,3 @@ async fn run_app( } Ok(()) } - -async fn fetch_new_logs(app: &mut App) { - let new_logs = { - let mut buffer = app.stdout_output.lock().unwrap(); - let new_logs = buffer.clone(); - buffer.clear(); - new_logs - }; - - let mut log_content = app.log_content.lock().unwrap(); - let was_at_bottom = app.log_scroll_offset == app.log_scroll_max_offset; - - log_content.push_str(&new_logs); - - let log_lines: Vec<&str> = log_content.lines().collect(); - let log_height = app.visible_rows; - app.log_scroll_max_offset = log_lines.len().saturating_sub(log_height); - - if was_at_bottom { - app.log_scroll_offset = app.log_scroll_max_offset; - } -} diff --git a/crates/kftui/src/tui/input/mod.rs b/crates/kftui/src/tui/input/mod.rs index 69a5e07f..ff46328a 100644 --- a/crates/kftui/src/tui/input/mod.rs +++ b/crates/kftui/src/tui/input/mod.rs @@ -4,10 +4,6 @@ mod popup; use std::collections::HashSet; use std::io; -use std::sync::{ - Arc, - Mutex, -}; use crossterm::event::{ self, @@ -21,6 +17,7 @@ use kftray_commons::models::{ config_model::Config, config_state_model::ConfigState, }; +use log::LevelFilter; pub use popup::*; use ratatui::widgets::ListState; use ratatui::widgets::TableState; @@ -28,8 +25,9 @@ use ratatui_explorer::{ FileExplorer, Theme, }; +use tui_logger::TuiWidgetEvent; +use tui_logger::TuiWidgetState; -use crate::core::logging::LOGGER; use crate::core::port_forward::stop_all_port_forward_and_exit; use crate::tui::input::navigation::handle_auto_add_configs; use crate::tui::input::navigation::handle_context_selection; @@ -86,10 +84,7 @@ pub struct App { pub file_content: Option, pub stopped_configs: Vec, pub running_configs: Vec, - pub stdout_output: Arc>, pub error_message: Option, - pub log_scroll_offset: usize, - pub log_scroll_max_offset: usize, pub active_component: ActiveComponent, pub selected_menu_item: usize, pub delete_confirmation_message: Option, @@ -97,10 +92,10 @@ pub struct App { pub visible_rows: usize, pub table_state_stopped: TableState, pub table_state_running: TableState, - pub log_content: Arc>, pub contexts: Vec, pub selected_context_index: usize, pub context_list_state: ListState, + pub logger_state: TuiWidgetState, } impl App { @@ -108,7 +103,7 @@ impl App { let theme = Theme::default().add_default_title(); let import_file_explorer = FileExplorer::with_theme(theme.clone()).unwrap(); let export_file_explorer = FileExplorer::with_theme(theme).unwrap(); - let stdout_output = LOGGER.buffer.clone(); + let logger_state = TuiWidgetState::new().set_default_display_level(LevelFilter::Warn); let mut app = Self { details_scroll_offset: 0, @@ -127,10 +122,7 @@ impl App { file_content: None, stopped_configs: Vec::new(), running_configs: Vec::new(), - stdout_output, error_message: None, - log_scroll_offset: 0, - log_scroll_max_offset: 0, active_component: ActiveComponent::StoppedTable, selected_menu_item: 0, delete_confirmation_message: None, @@ -138,10 +130,10 @@ impl App { visible_rows: 0, table_state_stopped: TableState::default(), table_state_running: TableState::default(), - log_content: Arc::new(Mutex::new(String::new())), contexts: Vec::new(), selected_context_index: 0, context_list_state: ListState::default(), + logger_state, }; if let Ok((_, height)) = size() { @@ -371,13 +363,6 @@ fn scroll_page_up(app: &mut App) { app.table_state_running .select(Some(app.selected_row_running)); } - ActiveComponent::Logs => { - if app.log_scroll_offset >= app.visible_rows { - app.log_scroll_offset -= app.visible_rows; - } else { - app.log_scroll_offset = 0; - } - } ActiveComponent::Details => { if app.details_scroll_offset >= app.visible_rows { app.details_scroll_offset -= app.visible_rows; @@ -411,13 +396,6 @@ fn scroll_page_down(app: &mut App) { app.table_state_running .select(Some(app.selected_row_running)); } - ActiveComponent::Logs => { - if app.log_scroll_offset + app.visible_rows < app.log_scroll_max_offset { - app.log_scroll_offset += app.visible_rows; - } else { - app.log_scroll_offset = app.log_scroll_max_offset; - } - } ActiveComponent::Details => { if app.details_scroll_offset + app.visible_rows < app.details_scroll_max_offset { app.details_scroll_offset += app.visible_rows; @@ -604,10 +582,6 @@ async fn handle_details_input(app: &mut App, key: KeyCode) -> io::Result<()> { } async fn handle_logs_input(app: &mut App, key: KeyCode) -> io::Result<()> { - if handle_common_hotkeys(app, key).await? { - return Ok(()); - } - match key { KeyCode::Left => app.active_component = ActiveComponent::Details, KeyCode::Up => { @@ -616,12 +590,16 @@ async fn handle_logs_input(app: &mut App, key: KeyCode) -> io::Result<()> { clear_selection(app); select_first_row(app); } - KeyCode::PageUp => { - scroll_page_up(app); - } - KeyCode::PageDown => { - scroll_page_down(app); - } + // Pass key events to the logger state + KeyCode::PageUp => app.logger_state.transition(TuiWidgetEvent::PrevPageKey), + KeyCode::PageDown => app.logger_state.transition(TuiWidgetEvent::NextPageKey), + KeyCode::Down => app.logger_state.transition(TuiWidgetEvent::DownKey), + KeyCode::Char('+') => app.logger_state.transition(TuiWidgetEvent::PlusKey), + KeyCode::Char('-') => app.logger_state.transition(TuiWidgetEvent::MinusKey), + KeyCode::Char(' ') => app.logger_state.transition(TuiWidgetEvent::SpaceKey), + KeyCode::Esc => app.logger_state.transition(TuiWidgetEvent::EscapeKey), + KeyCode::Char('h') => app.logger_state.transition(TuiWidgetEvent::HideKey), + KeyCode::Char('f') => app.logger_state.transition(TuiWidgetEvent::FocusKey), _ => {} } Ok(()) @@ -629,10 +607,6 @@ async fn handle_logs_input(app: &mut App, key: KeyCode) -> io::Result<()> { async fn handle_common_hotkeys(app: &mut App, key: KeyCode) -> io::Result { match key { - KeyCode::Char('c') => { - clear_stdout_output(app); - Ok(true) - } KeyCode::Char('q') => { app.state = AppState::ShowAbout; Ok(true) @@ -672,11 +646,6 @@ fn toggle_row_selection(app: &mut App) { } } -fn clear_stdout_output(app: &mut App) { - let mut stdout_output = app.stdout_output.lock().unwrap(); - stdout_output.clear(); -} - async fn handle_port_forwarding(app: &mut App) -> io::Result<()> { let (selected_rows, configs, selected_row) = match app.active_table { ActiveTable::Stopped => ( diff --git a/crates/kftui/src/tui/ui/draw.rs b/crates/kftui/src/tui/ui/draw.rs index 9ccb71eb..ba1012c4 100644 --- a/crates/kftui/src/tui/ui/draw.rs +++ b/crates/kftui/src/tui/ui/draw.rs @@ -1,8 +1,4 @@ use kftray_commons::models::config_state_model::ConfigState; -use ratatui::widgets::Paragraph; -use ratatui::widgets::Scrollbar; -use ratatui::widgets::ScrollbarOrientation; -use ratatui::widgets::ScrollbarState; use ratatui::{ layout::{ Constraint, @@ -11,6 +7,7 @@ use ratatui::{ Rect, }, style::{ + Color, Modifier, Style, }, @@ -26,6 +23,8 @@ use ratatui::{ }, Frame, }; +use tui_logger::TuiLoggerLevelOutput; +use tui_logger::TuiLoggerWidget; use crate::tui::input::ActiveTable; use crate::tui::input::{ @@ -37,7 +36,6 @@ use crate::tui::ui::render_context_selection_popup; use crate::tui::ui::render_delete_confirmation_popup; use crate::tui::ui::render_details; use crate::tui::ui::MAUVE; -use crate::tui::ui::SURFACE2; use crate::tui::ui::{ centered_rect, draw_configs_tab, @@ -190,28 +188,17 @@ pub fn draw_ui(f: &mut Frame, app: &mut App, config_states: &[ConfigState]) { } } -pub fn render_logs(f: &mut Frame, app: &mut App, area: Rect, has_focus: bool) { - let logs = { - let log_content = app.log_content.lock().unwrap(); - log_content.clone() - }; - - let log_lines: Vec<&str> = logs.lines().collect(); - let log_height = area.height as usize; - app.log_scroll_max_offset = log_lines.len().saturating_sub(log_height); - - if app.log_scroll_offset > app.log_scroll_max_offset { - app.log_scroll_offset = app.log_scroll_max_offset; +fn log_level_to_color(level: log::Level) -> Style { + match level { + log::Level::Error => Style::default().fg(Color::Red), + log::Level::Warn => Style::default().fg(Color::Yellow), + log::Level::Info => Style::default().fg(Color::Green), + log::Level::Debug => Style::default().fg(Color::Cyan), + log::Level::Trace => Style::default().fg(Color::Blue), } +} - let visible_logs: String = log_lines - .iter() - .skip(app.log_scroll_offset) - .take(log_height) - .cloned() - .collect::>() - .join("\n"); - +pub fn render_logs(f: &mut Frame, app: &mut App, area: Rect, has_focus: bool) { let focus_color = if has_focus { YELLOW } else { TEXT }; let border_modifier = if has_focus { Modifier::BOLD @@ -219,7 +206,7 @@ pub fn render_logs(f: &mut Frame, app: &mut App, area: Rect, has_focus: bool) { Modifier::empty() }; - let logs_paragraph = Paragraph::new(visible_logs) + let logs_widget = TuiLoggerWidget::default() .block( Block::default() .borders(Borders::ALL) @@ -231,27 +218,16 @@ pub fn render_logs(f: &mut Frame, app: &mut App, area: Rect, has_focus: bool) { ), ) .style(Style::default().fg(TEXT).bg(BASE)) - .wrap(ratatui::widgets::Wrap { trim: true }); - - f.render_widget(logs_paragraph, area); + .state(&app.logger_state) + .output_separator('|') + .output_timestamp(Some(" %H:%M ".to_string())) + .output_level(Some(TuiLoggerLevelOutput::Long)) + .style(Style::default().fg(Color::White)) + .style_error(log_level_to_color(log::Level::Error)) + .style_warn(log_level_to_color(log::Level::Warn)); - let scrollbar = Scrollbar::new(ScrollbarOrientation::VerticalRight) - .begin_symbol(None) - .track_symbol(None) - .end_symbol(None) - .style(Style::default().fg(SURFACE2).bg(BASE)); - let mut scrollbar_state = ScrollbarState::new(app.log_scroll_max_offset) - .position(app.log_scroll_offset) - .viewport_content_length(log_height); - let scrollbar_area = Rect { - x: area.x + area.width - 1, - y: area.y.saturating_add(2), - height: area.height.saturating_sub(2), - width: 1, - }; - f.render_stateful_widget(scrollbar, scrollbar_area, &mut scrollbar_state); + f.render_widget(logs_widget, area); } - pub fn draw_header(f: &mut Frame, app: &App, area: Rect) { let menu_titles = ["Help", "Auto Import", "Import", "Export", "About", "Quit"]; let menu: Vec = menu_titles