Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
16 changes: 14 additions & 2 deletions src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -190,6 +191,7 @@ impl ListenStatus {
}

pub struct App {
tick: u8,
receiver: Receiver<AppEvent>,
quit: bool,
sender: Sender<AppEvent>,
Expand Down Expand Up @@ -226,6 +228,7 @@ impl App {
pub fn new(config: Config, receiver: Receiver<AppEvent>, sender: Sender<AppEvent>) -> App {
let client = Arc::new(Mutex::new(DbgpClient::new(None)));
App {
tick: 0,
listening_status: ListenStatus::Listening,
config,
input_plurality: vec![],
Expand Down Expand Up @@ -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"),
}
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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?;
Expand Down
11 changes: 11 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
}
44 changes: 42 additions & 2 deletions src/notification.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand All @@ -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
}
Expand All @@ -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(),
}
}
Expand All @@ -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());
}
}
2 changes: 1 addition & 1 deletion src/theme.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()),
Expand Down
2 changes: 1 addition & 1 deletion src/view/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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",
];
Expand Down
2 changes: 1 addition & 1 deletion src/view/help.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
63 changes: 35 additions & 28 deletions src/view/layout.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]),
Expand All @@ -44,14 +45,34 @@ 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(
format!(
" 󱘖 {} ",
match app.listening_status {
ListenStatus::Connected => "connected".to_string(),

ListenStatus::Listening => app.config.listen.to_string(),
ListenStatus::Refusing => "refusing".to_string(),
},
Expand All @@ -76,39 +97,25 @@ 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 {
SessionViewMode::Current => app.theme().widget_mode_debug,
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(),
},
),
])])
}