From f9e95b08691d128230dbff5506a6fae5037f0745 Mon Sep 17 00:00:00 2001 From: Patrick Lorio Date: Wed, 15 Apr 2026 07:57:05 -0700 Subject: [PATCH] Remove log capture from CLI TUI - Drop the tracing log capture layer and buffer - Simplify TUI navigation to tunnel list only - Update help text and selection handling --- packages/playit-cli/src/client.rs | 16 -- packages/playit-cli/src/main.rs | 11 +- packages/playit-cli/src/ui/log_capture.rs | 160 ------------- packages/playit-cli/src/ui/mod.rs | 13 -- packages/playit-cli/src/ui/tui_app.rs | 259 ++++------------------ packages/playit-cli/src/ui/widgets.rs | 7 - 6 files changed, 38 insertions(+), 428 deletions(-) delete mode 100644 packages/playit-cli/src/ui/log_capture.rs diff --git a/packages/playit-cli/src/client.rs b/packages/playit-cli/src/client.rs index 3c4b9d59..4a1ceb3a 100644 --- a/packages/playit-cli/src/client.rs +++ b/packages/playit-cli/src/client.rs @@ -10,7 +10,6 @@ use playit_ipc::model::{ use playitd::manager::{ensure_installed_service_running, stop_installed_service}; use crate::ui::UI; -use crate::ui::log_capture::{LogEntry, LogLevel as UiLogLevel}; use crate::ui::tui_app::{ AccountStatusInfo, AgentData, ConnectionStats, NoticeInfo, PendingTunnelInfo, TunnelInfo, }; @@ -599,21 +598,6 @@ async fn apply_update(ui: &mut UI, update: ServiceUpdate, stdout_mode: bool) { entry.target, entry.message ); - } else if let Some(log_capture) = ui.log_capture() { - let level = match entry.level { - ServiceLogLevel::Error => UiLogLevel::Error, - ServiceLogLevel::Warn => UiLogLevel::Warn, - ServiceLogLevel::Info => UiLogLevel::Info, - ServiceLogLevel::Debug => UiLogLevel::Debug, - ServiceLogLevel::Trace => UiLogLevel::Trace, - }; - - log_capture.push(LogEntry { - level, - target: entry.target, - message: entry.message, - timestamp: entry.timestamp, - }); } } } diff --git a/packages/playit-cli/src/main.rs b/packages/playit-cli/src/main.rs index c0a56a05..95180a7c 100644 --- a/packages/playit-cli/src/main.rs +++ b/packages/playit-cli/src/main.rs @@ -23,7 +23,6 @@ use playit_api_client::http_client::HttpClientError; use playit_api_client::{PlayitApi, api::*}; use crate::signal_handle::get_signal_handle; -use crate::ui::log_capture::LogCaptureLayer; use crate::ui::{UI, UISettings}; pub static API_BASE: LazyLock = @@ -179,7 +178,6 @@ async fn run_cli() -> Result { // Use log-only mode if stdout flag is set or if attach --stdout was requested. let use_log_only = log_only || attach_stdout; - // Create UI first so we can get its log capture let mut ui = UI::new(UISettings { auto_answer: None, log_only: use_log_only, @@ -202,14 +200,7 @@ async fn run_cli() -> Result { Some(guard) } false => { - // TUI mode - set up log capture layer with filter - if let Some(log_capture) = ui.log_capture() { - let capture_layer = LogCaptureLayer::new(log_capture); - tracing_subscriber::registry() - .with(log_filter) - .with(capture_layer) - .init(); - } + tracing_subscriber::registry().with(log_filter).init(); None } }; diff --git a/packages/playit-cli/src/ui/log_capture.rs b/packages/playit-cli/src/ui/log_capture.rs deleted file mode 100644 index 65c645c8..00000000 --- a/packages/playit-cli/src/ui/log_capture.rs +++ /dev/null @@ -1,160 +0,0 @@ -use std::collections::VecDeque; -use std::sync::{Arc, Mutex}; -use tracing::{Event, Subscriber}; -use tracing_subscriber::Layer; -use tracing_subscriber::layer::Context; - -/// A log entry captured from tracing -#[derive(Clone, Debug)] -pub struct LogEntry { - pub timestamp: u64, - pub level: LogLevel, - pub target: String, - pub message: String, -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum LogLevel { - Trace, - Debug, - Info, - Warn, - Error, -} - -impl LogLevel { - pub fn as_str(&self) -> &'static str { - match self { - LogLevel::Trace => "TRACE", - LogLevel::Debug => "DEBUG", - LogLevel::Info => "INFO", - LogLevel::Warn => "WARN", - LogLevel::Error => "ERROR", - } - } -} - -impl From<&tracing::Level> for LogLevel { - fn from(level: &tracing::Level) -> Self { - match *level { - tracing::Level::TRACE => LogLevel::Trace, - tracing::Level::DEBUG => LogLevel::Debug, - tracing::Level::INFO => LogLevel::Info, - tracing::Level::WARN => LogLevel::Warn, - tracing::Level::ERROR => LogLevel::Error, - } - } -} - -/// Ring buffer for captured log entries -pub struct LogCapture { - entries: Mutex>, - capacity: usize, -} - -impl LogCapture { - pub fn new(capacity: usize) -> Arc { - Arc::new(LogCapture { - entries: Mutex::new(VecDeque::with_capacity(capacity)), - capacity, - }) - } - - /// Add a log entry to the buffer - pub fn push(&self, entry: LogEntry) { - let mut entries = self.entries.lock().unwrap(); - if entries.len() >= self.capacity { - entries.pop_front(); - } - entries.push_back(entry); - } - - /// Get a snapshot of all log entries - pub fn get_entries(&self) -> Vec { - let entries = self.entries.lock().unwrap(); - entries.iter().cloned().collect() - } - - /// Get the number of log entries - pub fn len(&self) -> usize { - self.entries.lock().unwrap().len() - } - - /// Check if the log buffer is empty - pub fn is_empty(&self) -> bool { - self.entries.lock().unwrap().is_empty() - } - - /// Clear all log entries - #[allow(dead_code)] - pub fn clear(&self) { - self.entries.lock().unwrap().clear(); - } -} - -/// Tracing layer that captures log events into a LogCapture buffer -pub struct LogCaptureLayer { - capture: Arc, -} - -impl LogCaptureLayer { - pub fn new(capture: Arc) -> Self { - LogCaptureLayer { capture } - } -} - -impl Layer for LogCaptureLayer { - fn on_event(&self, event: &Event<'_>, _ctx: Context<'_, S>) { - use playit_agent_core::utils::now_milli; - - let metadata = event.metadata(); - let level = LogLevel::from(metadata.level()); - let target = metadata.target().to_string(); - - // Extract the message from the event - let mut visitor = MessageVisitor::default(); - event.record(&mut visitor); - - let entry = LogEntry { - timestamp: now_milli(), - level, - target, - message: visitor.message, - }; - - self.capture.push(entry); - } -} - -/// Visitor to extract the message field from a tracing event -#[derive(Default)] -struct MessageVisitor { - message: String, -} - -impl tracing::field::Visit for MessageVisitor { - fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn std::fmt::Debug) { - if field.name() == "message" { - self.message = format!("{:?}", value); - } else { - // Fallback: use any debug value if no message field - if !self.message.is_empty() { - self.message.push_str(", "); - } - self.message - .push_str(&format!("{}={:?}", field.name(), value)); - } - } - - fn record_str(&mut self, field: &tracing::field::Field, value: &str) { - if field.name() == "message" { - self.message = value.to_string(); - } else { - if !self.message.is_empty() { - self.message.push_str(", "); - } - self.message - .push_str(&format!("{}={}", field.name(), value)); - } - } -} diff --git a/packages/playit-cli/src/ui/mod.rs b/packages/playit-cli/src/ui/mod.rs index 3176932b..558d5290 100644 --- a/packages/playit-cli/src/ui/mod.rs +++ b/packages/playit-cli/src/ui/mod.rs @@ -1,16 +1,12 @@ -use std::sync::Arc; - use crossterm::event::{self, Event, KeyCode, KeyEvent}; use playit_agent_core::utils::now_milli; use crate::CliError; use crate::signal_handle::get_signal_handle; -pub mod log_capture; pub mod tui_app; pub mod widgets; -pub use log_capture::LogCapture; pub use tui_app::{AgentData, ConnectionStats, TuiApp}; /// UI mode - either TUI (interactive) or log-only (stdout) @@ -75,15 +71,6 @@ impl UI { } } - /// Get the log capture for TUI mode - pub fn log_capture(&self) -> Option> { - if let UI::Tui(tui) = self { - Some(tui.log_capture()) - } else { - None - } - } - /// Run one iteration of the TUI event loop /// Returns Ok(true) if should continue, Ok(false) if should quit pub fn tick_tui(&mut self) -> Result { diff --git a/packages/playit-cli/src/ui/tui_app.rs b/packages/playit-cli/src/ui/tui_app.rs index e6400e45..bd8dc22a 100644 --- a/packages/playit-cli/src/ui/tui_app.rs +++ b/packages/playit-cli/src/ui/tui_app.rs @@ -1,5 +1,4 @@ use std::io::{self, Stdout, Write, stdin, stdout}; -use std::sync::Arc; use std::time::Duration; use crossterm::{ @@ -11,13 +10,12 @@ use ratatui::{ Frame, Terminal, backend::CrosstermBackend, layout::{Constraint, Direction, Layout, Rect}, - style::{Color, Modifier, Style, Stylize}, + style::{Color, Modifier, Style}, text::{Line, Span}, widgets::{Block, Borders, List, ListItem, ListState, Paragraph, Wrap}, }; use super::UISettings; -use super::log_capture::{LogCapture, LogEntry, LogLevel}; use super::widgets::{render_header, render_help_bar, render_stats_bar}; use crate::CliError; use crate::signal_handle::get_signal_handle; @@ -75,13 +73,6 @@ pub struct ConnectionStats { pub active_udp: u32, } -/// Which panel is currently focused -#[derive(Clone, Copy, Debug, PartialEq)] -pub enum FocusedPanel { - Tunnels, - Logs, -} - /// UI mode for TuiApp #[derive(Clone, Debug, PartialEq)] pub enum TuiMode { @@ -94,16 +85,12 @@ pub enum TuiMode { /// Main TUI application state pub struct TuiApp { settings: UISettings, - log_capture: Arc, agent_data: AgentData, stats: ConnectionStats, // UI state mode: TuiMode, - focused_panel: FocusedPanel, tunnel_list_state: ListState, - log_scroll: usize, - log_follow: bool, // Auto-scroll logs when at bottom should_quit: bool, quit_confirm: bool, @@ -115,29 +102,27 @@ impl TuiApp { pub fn new(settings: UISettings) -> Self { TuiApp { settings, - log_capture: LogCapture::new(500), agent_data: AgentData::default(), stats: ConnectionStats::default(), mode: TuiMode::Setup { message: "Initializing...".to_string(), }, - focused_panel: FocusedPanel::Tunnels, tunnel_list_state: ListState::default(), - log_scroll: 0, - log_follow: true, // Start with follow mode enabled should_quit: false, quit_confirm: false, terminal: None, } } - pub fn log_capture(&self) -> Arc { - self.log_capture.clone() - } - pub fn update_agent_data(&mut self, data: AgentData) { self.agent_data = data; - // Switch to running mode when we get agent data + let tunnel_count = self.tunnel_item_count(); + let selection = match (self.tunnel_list_state.selected(), tunnel_count) { + (_, 0) => None, + (Some(selected), count) => Some(selected.min(count - 1)), + (None, _) => Some(0), + }; + self.tunnel_list_state.select(selection); self.mode = TuiMode::Running; } @@ -150,11 +135,6 @@ impl TuiApp { self.mode = TuiMode::Setup { message }; } - /// Switch to running mode - pub fn set_running_mode(&mut self) { - self.mode = TuiMode::Running; - } - /// Initialize the terminal for TUI mode fn init_terminal(&mut self) -> io::Result<()> { enable_raw_mode()?; @@ -188,11 +168,6 @@ impl TuiApp { self.restore_terminal().map_err(CliError::RenderError) } - /// Check if the TUI should quit - pub fn should_quit(&self) -> bool { - self.should_quit - } - /// Run one iteration of the TUI (draw + handle events) /// Returns Ok(true) if should continue, Ok(false) if should quit pub fn tick(&mut self) -> Result { @@ -221,22 +196,6 @@ impl TuiApp { Ok(!self.should_quit) } - /// Run the TUI event loop (blocking) - pub async fn run(&mut self) -> Result<(), CliError> { - self.init()?; - - loop { - if !self.tick()? { - break; - } - // Yield to allow other tasks to run - tokio::task::yield_now().await; - } - - self.shutdown()?; - Ok(()) - } - fn handle_key_event(&mut self, key: KeyEvent) { // Handle quit confirmation if self.quit_confirm { @@ -262,14 +221,6 @@ impl TuiApp { self.quit_confirm = true; } - // Navigation - KeyCode::Tab => { - self.focused_panel = match self.focused_panel { - FocusedPanel::Tunnels => FocusedPanel::Logs, - FocusedPanel::Logs => FocusedPanel::Tunnels, - }; - } - // Scrolling KeyCode::Char('j') | KeyCode::Down => self.scroll_down(), KeyCode::Char('k') | KeyCode::Up => self.scroll_up(), @@ -291,77 +242,41 @@ impl TuiApp { } fn scroll_down(&mut self) { - match self.focused_panel { - FocusedPanel::Tunnels => { - let total = self.agent_data.tunnels.len(); - if total > 0 { - let i = match self.tunnel_list_state.selected() { - Some(i) => (i + 1).min(total - 1), - None => 0, - }; - self.tunnel_list_state.select(Some(i)); - } - } - FocusedPanel::Logs => { - let total = self.log_capture.len(); - if self.log_scroll < total.saturating_sub(1) { - self.log_scroll += 1; - // Re-enable follow if we scrolled to the bottom - if self.log_scroll >= total.saturating_sub(1) { - self.log_follow = true; - } - } - } + let total = self.tunnel_item_count(); + if total > 0 { + let i = match self.tunnel_list_state.selected() { + Some(i) => (i + 1).min(total - 1), + None => 0, + }; + self.tunnel_list_state.select(Some(i)); } } fn scroll_up(&mut self) { - match self.focused_panel { - FocusedPanel::Tunnels => { - let i = match self.tunnel_list_state.selected() { - Some(i) => i.saturating_sub(1), - None => 0, - }; - self.tunnel_list_state.select(Some(i)); - } - FocusedPanel::Logs => { - self.log_scroll = self.log_scroll.saturating_sub(1); - // Disable follow when scrolling up - self.log_follow = false; - } - } + let i = match self.tunnel_list_state.selected() { + Some(i) => i.saturating_sub(1), + None => 0, + }; + self.tunnel_list_state.select(Some(i)); } fn scroll_to_top(&mut self) { - match self.focused_panel { - FocusedPanel::Tunnels => { - self.tunnel_list_state.select(Some(0)); - } - FocusedPanel::Logs => { - self.log_scroll = 0; - // Disable follow when going to top - self.log_follow = false; - } + if self.tunnel_item_count() > 0 { + self.tunnel_list_state.select(Some(0)); } } fn scroll_to_bottom(&mut self) { - match self.focused_panel { - FocusedPanel::Tunnels => { - let total = self.agent_data.tunnels.len(); - if total > 0 { - self.tunnel_list_state.select(Some(total - 1)); - } - } - FocusedPanel::Logs => { - let total = self.log_capture.len(); - self.log_scroll = total.saturating_sub(1); - // Enable follow when going to bottom - self.log_follow = true; - } + let total = self.tunnel_item_count(); + if total > 0 { + self.tunnel_list_state.select(Some(total - 1)); } } + fn tunnel_item_count(&self) -> usize { + self.agent_data.tunnels.len() + self.agent_data.pending_tunnels.len() + } + fn draw(&mut self) -> io::Result<()> { let terminal = self.terminal.as_mut().unwrap(); @@ -369,20 +284,7 @@ impl TuiApp { let agent_data = self.agent_data.clone(); let stats = self.stats.clone(); let start_time = agent_data.start_time; - let focused_panel = self.focused_panel; let quit_confirm = self.quit_confirm; - let log_entries = self.log_capture.get_entries(); - let log_follow = self.log_follow; - - // Auto-scroll to bottom if following logs - let log_scroll = if log_follow { - let total = log_entries.len(); - self.log_scroll = total.saturating_sub(1); - self.log_scroll - } else { - self.log_scroll - }; - let mut tunnel_list_state = self.tunnel_list_state.clone(); terminal.draw(|frame| { @@ -399,45 +301,22 @@ impl TuiApp { } } - // Main layout: Header, Content, Stats, Logs, Help + // Main layout: Header, Tunnels, Stats, Help let chunks = Layout::default() .direction(Direction::Vertical) .constraints([ - Constraint::Length(3), // Header - Constraint::Min(8), // Tunnels - Constraint::Length(3), // Stats - Constraint::Length(10), // Logs - Constraint::Length(1), // Help bar + Constraint::Length(3), + Constraint::Min(8), + Constraint::Length(3), + Constraint::Length(1), ]) .split(area); - // Render header render_header(frame, chunks[0], &agent_data, start_time); - // Render tunnel list - Self::render_tunnels( - frame, - chunks[1], - &agent_data, - focused_panel == FocusedPanel::Tunnels, - &mut tunnel_list_state, - ); - - // Render stats bar + Self::render_tunnels(frame, chunks[1], &agent_data, &mut tunnel_list_state); render_stats_bar(frame, chunks[2], &stats); - - // Render log panel - Self::render_logs( - frame, - chunks[3], - &log_entries, - log_scroll, - focused_panel == FocusedPanel::Logs, - log_follow, - ); - - // Render help bar - render_help_bar(frame, chunks[4], quit_confirm); + render_help_bar(frame, chunks[3], quit_confirm); })?; self.tunnel_list_state = tunnel_list_state; @@ -449,19 +328,12 @@ impl TuiApp { frame: &mut Frame, area: Rect, agent_data: &AgentData, - focused: bool, list_state: &mut ListState, ) { - let border_style = if focused { - Style::default().fg(Color::Cyan) - } else { - Style::default().fg(Color::DarkGray) - }; - let block = Block::default() .title(" Tunnels ") .borders(Borders::ALL) - .border_style(border_style); + .border_style(Style::default().fg(Color::DarkGray)); if agent_data.tunnels.is_empty() && agent_data.pending_tunnels.is_empty() { let msg = if agent_data.agent_id.is_empty() { @@ -519,63 +391,6 @@ impl TuiApp { frame.render_stateful_widget(list, area, list_state); } - fn render_logs( - frame: &mut Frame, - area: Rect, - log_entries: &[LogEntry], - scroll: usize, - focused: bool, - following: bool, - ) { - let border_style = if focused { - Style::default().fg(Color::Cyan) - } else { - Style::default().fg(Color::DarkGray) - }; - - let title = if following { - format!(" Logs ({}) [following] ", log_entries.len()) - } else { - format!(" Logs ({}) ", log_entries.len()) - }; - - let block = Block::default() - .title(title) - .borders(Borders::ALL) - .border_style(border_style); - - let inner_height = area.height.saturating_sub(2) as usize; - let start = scroll.min(log_entries.len().saturating_sub(inner_height)); - let visible_entries = log_entries.iter().skip(start).take(inner_height); - - let lines: Vec = visible_entries - .map(|entry| { - let level_style = match entry.level { - LogLevel::Error => Style::default().fg(Color::Red).bold(), - LogLevel::Warn => Style::default().fg(Color::Yellow).bold(), - LogLevel::Info => Style::default().fg(Color::Green), - LogLevel::Debug => Style::default().fg(Color::Blue), - LogLevel::Trace => Style::default().fg(Color::DarkGray), - }; - - Line::from(vec![ - Span::styled(format!("[{}] ", entry.level.as_str()), level_style), - Span::styled( - format!( - "{}: ", - entry.target.split("::").last().unwrap_or(&entry.target) - ), - Style::default().fg(Color::DarkGray), - ), - Span::raw(&entry.message), - ]) - }) - .collect(); - - let paragraph = Paragraph::new(lines).block(block); - frame.render_widget(paragraph, area); - } - fn render_setup_screen(frame: &mut Frame, area: Rect, message: &str, quit_confirm: bool) { use ratatui::layout::Alignment; diff --git a/packages/playit-cli/src/ui/widgets.rs b/packages/playit-cli/src/ui/widgets.rs index 9c6f54a4..8f109015 100644 --- a/packages/playit-cli/src/ui/widgets.rs +++ b/packages/playit-cli/src/ui/widgets.rs @@ -192,13 +192,6 @@ pub fn render_help_bar(frame: &mut Frame, area: Rect, quit_confirm: bool) { .add_modifier(Modifier::BOLD), ), Span::raw(" Scroll "), - Span::styled( - "Tab", - Style::default() - .fg(Color::Cyan) - .add_modifier(Modifier::BOLD), - ), - Span::raw(" Switch Panel "), Span::styled( "g/G", Style::default()