diff --git a/CHANGELOG.md b/CHANGELOG.md index a55c308..0e697f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ CHANGELOG main ---- +- Improve notification display - Do not accept connections while an existing session is running - Filter properties with dot notation in context pane (`f`) - Stack traversal - select stack and inspect stack frames in current mode. diff --git a/README.md b/README.md index 23cf980..4585a48 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ Debug-TUI Interactive [Xdebug](https://xdebug.org) step-debugging client your terminal. -![Image](https://github.com/user-attachments/assets/21627682-b2f1-4622-b67d-ff6cd32e4363) +![Demo](https://github.com/user-attachments/assets/1a8a1d1b-d01b-4d71-9d35-c65e546e8c24) - **Travel forwards**: step over, into and out. - **Travel backwards**: it's not quite time travel, but you can revisit diff --git a/src/app.rs b/src/app.rs index 19ff680..7a1be49 100644 --- a/src/app.rs +++ b/src/app.rs @@ -22,6 +22,7 @@ use anyhow::Result; use crossterm::event::KeyCode; use log::info; use log::warn; +use log::error; use ratatui::layout::Rect; use ratatui::prelude::CrosstermBackend; use ratatui::style::Color; @@ -190,6 +191,7 @@ impl ListenStatus { } pub struct App { + tick: u8, receiver: Receiver, quit: bool, sender: Sender, @@ -226,6 +228,7 @@ impl App { pub fn new(config: Config, receiver: Receiver, sender: Sender) -> App { let client = Arc::new(Mutex::new(DbgpClient::new(None))); App { + tick: 0, listening_status: ListenStatus::Listening, config, input_plurality: vec![], @@ -281,7 +284,13 @@ impl App { loop { match listener.accept().await { Ok(s) => { - sender.send(AppEvent::ClientConnected(s.0)).await.unwrap(); + match sender.send(AppEvent::ClientConnected(s.0)).await { + Ok(_) => (), + Err(e) => error!( + "Could not send connection event: {}", + e.to_string() + ), + } } Err(_) => panic!("Could not connect"), } @@ -321,7 +330,9 @@ impl App { event: AppEvent, ) -> Result<()> { match event { - AppEvent::Tick => (), + AppEvent::Tick => { + self.tick = self.tick.wrapping_add(1); + }, _ => info!("Handling event {:?}", event), }; match event { @@ -382,6 +393,7 @@ impl App { if self.listening_status != ListenStatus::Listening { self.notification = Notification::warning("refused incoming connection".to_string()); } else { + self.notification = Notification::info("connected".to_string()); let filepath = { let mut client = self.client.lock().await; let response = client.deref_mut().connect(s).await?; diff --git a/src/config.rs b/src/config.rs index 18beb2c..a236744 100644 --- a/src/config.rs +++ b/src/config.rs @@ -34,3 +34,14 @@ impl Config { Config { listen , log_path: None} } } + +#[cfg(test)] +mod test { + use crate::notification::Notification; + + #[test] + fn test_countdown_char() -> () { + let notification = Notification::info("Hello".to_string()); + notification.countdown_char(); + } +} diff --git a/src/notification.rs b/src/notification.rs index 2631b11..10e71af 100644 --- a/src/notification.rs +++ b/src/notification.rs @@ -13,6 +13,9 @@ pub struct Notification { expires: SystemTime, } impl Notification { + const DURATION: u64 = 5; + const BLOCKS: [char; 8] = ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█']; + pub(crate) fn none() -> Notification { Notification { message: "".to_string(), @@ -31,6 +34,17 @@ impl Notification { } } + pub fn countdown_char(&self) -> char { + match self.expires.duration_since(SystemTime::now()) { + Ok(duration) => { + let pct = duration.as_secs_f64() / Self::DURATION as f64; + let block_offset = (8.0 * pct).ceil(); + Self::BLOCKS[block_offset as usize - 1] + } + Err(_) => ' ', + } + } + pub fn is_visible(&self) -> bool { SystemTime::now() < self.expires } @@ -41,7 +55,7 @@ impl Notification { message, level: NotificationLevel::Info, expires: SystemTime::now() - .checked_add(Duration::from_secs(5)) + .checked_add(Duration::from_secs(Self::DURATION)) .unwrap(), } } @@ -52,8 +66,34 @@ impl Notification { message, level: NotificationLevel::Warning, expires: SystemTime::now() - .checked_add(Duration::from_secs(5)) + .checked_add(Duration::from_secs(Self::DURATION)) .unwrap(), } } } + +#[cfg(test)] +mod test { + use super::*; + use pretty_assertions::assert_eq; + + #[test] + fn test_countdown_char() -> () { + let notification = Notification::info("hello".to_string()); + assert_eq!('█', notification.countdown_char()); + + let notification = Notification { + message: "hello".to_string(), + level: NotificationLevel::Info, + expires: SystemTime::now(), + }; + assert_eq!(' ', notification.countdown_char()); + + let notification = Notification { + message: "hello".to_string(), + level: NotificationLevel::Info, + expires: SystemTime::now() - Duration::from_secs(10), + }; + assert_eq!(' ', notification.countdown_char()); + } +} diff --git a/src/theme.rs b/src/theme.rs index 7907b26..bb14cae 100644 --- a/src/theme.rs +++ b/src/theme.rs @@ -48,7 +48,7 @@ impl Theme { syntax_brace: Style::default().fg(Solarized::Base01.to_color()), notification_info: Style::default().fg(Solarized::Green.to_color()), notification_error: Style::default().fg(Solarized::Red.to_color()), - notification_warning: Style::default().fg(Color::Yellow), + notification_warning: Style::default().fg(Solarized::Yellow.to_color()), pane_border_active: Style::default().fg(Solarized::Base01.to_color()), pane_border_inactive: Style::default().fg(Solarized::Base02.to_color()), source_line: Style::default().fg(Solarized::Base1.to_color()), diff --git a/src/view/context.rs b/src/view/context.rs index 1e58032..3efd185 100644 --- a/src/view/context.rs +++ b/src/view/context.rs @@ -219,7 +219,7 @@ mod test { prop1.name = "foo".to_string(); // segments are reversed - let mut filter = &mut vec![ + let filter = &mut vec![ "bar", "foo", ]; diff --git a/src/view/help.rs b/src/view/help.rs index aef43a5..315665b 100644 --- a/src/view/help.rs +++ b/src/view/help.rs @@ -14,7 +14,7 @@ impl View for HelpView { if app.listening_status == ListenStatus::Connected{ Some(AppEvent::ChangeView(SelectedView::Session)) } else { - Some(AppEvent::ChangeView(SelectedView::Listen)) + Some(AppEvent::Listen) } }, _ => None diff --git a/src/view/layout.rs b/src/view/layout.rs index 1b19ece..7c659dc 100644 --- a/src/view/layout.rs +++ b/src/view/layout.rs @@ -35,6 +35,7 @@ impl View for LayoutView { f.render_widget(Block::default().style(app.theme().background), area); f.render_widget(status_widget(app), rows[0]); + f.render_widget(notification_widget(app), rows[0]); match app.view_current { SelectedView::Listen => ListenView::draw(app, f, rows[1]), @@ -44,6 +45,26 @@ impl View for LayoutView { } } +fn notification_widget(app: &App) -> Paragraph<'_> { + Paragraph::new(vec![Line::from(Span::styled( + match app.notification.is_visible() { + true => format!( + "{} {}", + app.notification.message.clone(), + app.notification.countdown_char() + ), + false => "".to_string(), + }, + match app.notification.level { + NotificationLevel::Error => app.theme().notification_error, + NotificationLevel::Warning => app.theme().notification_warning, + NotificationLevel::Info => app.theme().notification_info, + NotificationLevel::None => Style::default(), + }, + )) + .alignment(ratatui::layout::Alignment::Right)]) +} + fn status_widget(app: &App) -> Paragraph { Paragraph::new(vec![Line::from(vec![ Span::styled( @@ -51,7 +72,7 @@ fn status_widget(app: &App) -> Paragraph { " 󱘖 {} ", match app.listening_status { ListenStatus::Connected => "connected".to_string(), - + ListenStatus::Listening => app.config.listen.to_string(), ListenStatus::Refusing => "refusing".to_string(), }, @@ -76,21 +97,19 @@ fn status_widget(app: &App) -> Paragraph { true => format!("  {} / ∞", app.history.offset + 1), false => "  0 / 0".to_string(), }, - SessionViewMode::History => { - match app.listening_status { - ListenStatus::Connected => format!( - "  {} / {} history [p] to go back [n] to go forwards [b] to return", - app.history.offset + 1, - app.history.len() - ), - ListenStatus::Refusing => format!( - "  {} / {} terminated [p] to go back [n] to go forwards [b] to listen", - app.history.offset + 1, - app.history.len() - ), - ListenStatus::Listening => String::new(), - } - } + SessionViewMode::History => match app.listening_status { + ListenStatus::Connected => format!( + "  {} / {} history [p] to go back [n] to go forwards [b] to return", + app.history.offset + 1, + app.history.len() + ), + ListenStatus::Refusing => format!( + "  {} / {} terminated [p] to go back [n] to go forwards [b] to listen", + app.history.offset + 1, + app.history.len() + ), + ListenStatus::Listening => String::new(), + }, }) .to_string(), match app.session_view.mode { @@ -98,17 +117,5 @@ fn status_widget(app: &App) -> Paragraph { SessionViewMode::History => app.theme().widget_mode_history, }, ), - Span::styled( - match app.notification.is_visible() { - true => format!(" {} ", app.notification.message.clone()), - false => "".to_string(), - }, - match app.notification.level { - NotificationLevel::Error => app.theme().notification_error, - NotificationLevel::Warning => app.theme().notification_warning, - NotificationLevel::Info => app.theme().notification_info, - NotificationLevel::None => Style::default(), - }, - ), ])]) }