From 1b14a83841ee2f5cb8f377575614150795bbcc58 Mon Sep 17 00:00:00 2001 From: Daniel Leech Date: Sat, 8 Nov 2025 12:18:08 +0000 Subject: [PATCH 01/49] POC php process running --- src/app.rs | 15 +++++++++++++++ src/config.rs | 6 +++++- src/event/input.rs | 1 + 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/src/app.rs b/src/app.rs index fd88ad7..1e2cb28 100644 --- a/src/app.rs +++ b/src/app.rs @@ -34,6 +34,8 @@ use ratatui::widgets::Block; use ratatui::widgets::Padding; use ratatui::widgets::Paragraph; use ratatui::Terminal; +use tokio::process::Child; +use tokio::process::Command; use std::collections::hash_map::Entry; use std::collections::HashMap; use std::io; @@ -233,6 +235,7 @@ pub struct App { receiver: Receiver, quit: bool, sender: Sender, + php_process: Option, pub listening_status: ListenStatus, pub notification: Notification, @@ -275,6 +278,7 @@ impl App { notification: Notification::none(), receiver, sender: sender.clone(), + php_process: None, quit: false, history: History::default(), document_variables: DocumentVariables::default(), @@ -323,6 +327,8 @@ impl App { } }; + sender.send(AppEvent::Listening).await.unwrap(); + loop { match listener.accept().await { Ok(s) => match sender.send(AppEvent::ClientConnected(s.0)).await { @@ -376,6 +382,15 @@ impl App { match event { AppEvent::Tick => (), AppEvent::Quit => self.quit = true, + AppEvent::Listening => { + if let Some(script) = &self.config.cmd { + let cmd = script.first(); + if let Some(program) = cmd { + let process = Command::new(&program).args(&script[1..]).spawn().unwrap(); + self.php_process = Some(process); + } + } + }, AppEvent::ChangeView(view) => { self.view_current = view; } diff --git a/src/config.rs b/src/config.rs index aa0f32d..40f5d42 100644 --- a/src/config.rs +++ b/src/config.rs @@ -3,6 +3,8 @@ use clap::Parser; #[derive(Parser, Debug)] #[command(author, version, about, long_about = None)] struct Args { + #[arg(last = true)] + pub php_script: Option>, #[arg(short, long)] pub listen: Option, #[arg(long)] @@ -14,6 +16,7 @@ pub fn load_config() -> Config { Config { listen: args.listen.unwrap_or("0.0.0.0:9003".to_string()), log_path: args.log, + cmd: args.php_script, } } @@ -21,6 +24,7 @@ pub fn load_config() -> Config { pub struct Config { pub listen: String, pub log_path: Option, + pub cmd: Option>, } impl Default for Config { @@ -31,7 +35,7 @@ impl Default for Config { impl Config { pub fn new(listen: String) -> Config { - Config { listen , log_path: None} + Config { listen , log_path: None, cmd: None} } } diff --git a/src/event/input.rs b/src/event/input.rs index 5dbe4be..5277af9 100644 --- a/src/event/input.rs +++ b/src/event/input.rs @@ -55,6 +55,7 @@ pub enum AppEvent { EvalExecute, EvalRefresh, EvalStart, + Listening, } pub type EventSender = Sender; From f3826cbcb0c3516ce0661a4244a701040c6c43d8 Mon Sep 17 00:00:00 2001 From: Daniel Leech Date: Sat, 8 Nov 2025 14:42:19 +0000 Subject: [PATCH 02/49] Show PHP process output --- src/app.rs | 55 ++++++++++++++++++++++++++++++++++++++++------ src/console.rs | 19 ++++++++++++++++ src/main.rs | 1 + src/view/eval.rs | 45 ++++++++++++++----------------------- src/view/layout.rs | 2 +- 5 files changed, 86 insertions(+), 36 deletions(-) create mode 100644 src/console.rs diff --git a/src/app.rs b/src/app.rs index 1e2cb28..89295ea 100644 --- a/src/app.rs +++ b/src/app.rs @@ -2,6 +2,7 @@ use crate::analyzer::Analyser; use crate::analyzer::Analysis; use crate::analyzer::VariableRef; use crate::config::Config; +use crate::console::Console; use crate::dbgp::client::ContextGetResponse; use crate::dbgp::client::ContinuationResponse; use crate::dbgp::client::ContinuationStatus; @@ -12,6 +13,7 @@ use crate::event::input::AppEvent; use crate::notification::Notification; use crate::theme::Scheme; use crate::theme::Theme; +use crate::view::eval::draw_properties; use crate::view::eval::EvalDialog; use crate::view::help::HelpView; use crate::view::layout::LayoutView; @@ -30,16 +32,22 @@ use ratatui::layout::Rect; use ratatui::prelude::CrosstermBackend; use ratatui::style::Color; use ratatui::style::Style; +use ratatui::text::Line; use ratatui::widgets::Block; use ratatui::widgets::Padding; use ratatui::widgets::Paragraph; use ratatui::Terminal; +use tokio::io::AsyncBufReadExt; +use tokio::io::AsyncReadExt; +use tokio::io::BufReader; use tokio::process::Child; use tokio::process::Command; use std::collections::hash_map::Entry; use std::collections::HashMap; use std::io; use std::ops::DerefMut; +use std::process::Stdio; +use std::str::from_utf8; use std::sync::Arc; use tokio::net::TcpListener; use tokio::sync::mpsc::Receiver; @@ -130,6 +138,7 @@ impl HistoryEntry { match entry { Some(e) => e.source.clone(), None => SourceContext::default(), + } } @@ -237,6 +246,7 @@ pub struct App { sender: Sender, php_process: Option, + pub console: Console, pub listening_status: ListenStatus, pub notification: Notification, pub config: Config, @@ -268,7 +278,7 @@ pub struct App { } impl App { - pub fn new(config: Config, receiver: Receiver, sender: Sender) -> App { + pub fn new<'a>(config: Config, receiver: Receiver, sender: Sender) -> App { let client = Arc::new(Mutex::new(DbgpClient::new(None))); App { tick: 0, @@ -284,6 +294,7 @@ impl App { document_variables: DocumentVariables::default(), client: Arc::clone(&client), workspace: Workspace::new(Arc::clone(&client)), + console: Console::new(), counter: 0, context_depth: 4, @@ -361,6 +372,8 @@ impl App { return Ok(()); } + self.console.unload().await; + terminal.autoresize()?; terminal.draw(|frame| { LayoutView::draw(self, frame, frame.area()); @@ -384,11 +397,7 @@ impl App { AppEvent::Quit => self.quit = true, AppEvent::Listening => { if let Some(script) = &self.config.cmd { - let cmd = script.first(); - if let Some(program) = cmd { - let process = Command::new(&program).args(&script[1..]).spawn().unwrap(); - self.php_process = Some(process); - } + self.start_php_process(script.clone()); } }, AppEvent::ChangeView(view) => { @@ -578,7 +587,15 @@ impl App { ) .await?; - self.session_view.eval_state.response = Some(response); + let mut lines: Vec = Vec::new(); + draw_properties( + &self.theme(), + response.properties.defined_properties(), + &mut lines, + 0, + &mut Vec::new(), + ); + self.console.buffer.lock().await.append(&mut lines); self.sender.send(AppEvent::Snapshot()).await.unwrap(); } self.active_dialog = None; @@ -835,6 +852,30 @@ impl App { .scroll_to_line(entry.source(self.session_view.stack_depth()).line_no) } } + + fn start_php_process(&mut self, script: Vec) { + let cmd = script.first(); + if let Some(program) = cmd { + let mut process = Command::new(&program) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .args(&script[1..]) + .spawn() + .unwrap(); + let buffer = self.console.buffer.clone(); + let mut reader = BufReader::new(process.stdout.take().unwrap()); + self.php_process = Some(process); + task::spawn(async move { + loop { + let mut buf = [0; 10]; + let n = reader.read(&mut buf).await.expect("nope"); + if n > 0 { + buffer.lock().await.push(from_utf8(&buf[..n]).expect("fucl").to_string()); + } + } + }); + } + } } fn apply_scroll(scroll: (u16, u16), amount: (i16, i16), motion: i16) -> (u16, u16) { diff --git a/src/console.rs b/src/console.rs new file mode 100644 index 0000000..53fcf12 --- /dev/null +++ b/src/console.rs @@ -0,0 +1,19 @@ +use std::sync::Arc; + +use ratatui::text::Line; +use tokio::sync::Mutex; + +pub struct Console { + pub buffer: Arc>>, + pub lines: Vec, +} + +impl Console { + pub async fn unload(&mut self) { + self.lines.append(&mut self.buffer.lock().await.to_vec()); + self.buffer.lock().await.clear(); + } + pub fn new() -> Self { + Self { buffer: Arc::new(Mutex::new(vec![])), lines: vec![] } + } +} diff --git a/src/main.rs b/src/main.rs index 1145a6a..e7958b3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,6 +7,7 @@ pub mod view; pub mod analyzer; pub mod theme; pub mod workspace; +pub mod console; use app::App; use better_panic::Settings; diff --git a/src/view/eval.rs b/src/view/eval.rs index c61e01d..3a9375e 100644 --- a/src/view/eval.rs +++ b/src/view/eval.rs @@ -45,21 +45,13 @@ impl View for EvalComponent { area, ); } else { - let mut lines: Vec = Vec::new(); - draw_properties( - &app.theme(), - eval_entry.response.properties.defined_properties(), - &mut lines, - 0, - &mut Vec::new(), - ); - frame.render_widget( - Paragraph::new(lines).scroll(app.session_view.eval_state.scroll), - area, - ); } } } + frame.render_widget( + Paragraph::new(app.console.lines.join("\n")).scroll(app.session_view.eval_state.scroll).style(app.theme().source_line), + area, + ); } } @@ -121,7 +113,7 @@ impl View for EvalDialog { pub fn draw_properties( theme: &Scheme, properties: Vec<&Property>, - lines: &mut Vec, + lines: &mut Vec, level: usize, filter_path: &mut Vec<&str>, ) { @@ -134,18 +126,15 @@ pub fn draw_properties( } } let mut spans = vec![ - Span::raw(" ".repeat(level)), - Span::styled(property.name.to_string(), theme.syntax_label), - Span::raw(" ".to_string()), - Span::styled( - property.type_name(), - match property.property_type { - PropertyType::Object => theme.syntax_type_object, - _ => theme.syntax_type, - }, - ), - Span::raw(" = ".to_string()), - render_value(theme, property), + " ".repeat(level), + property.name.to_string(), + " ".to_string(), + property.type_name(), + " = ".to_string(), + match &property.value { + Some(s) => s.to_string(), + None => "".to_string(), + } ]; let delimiters = match property.property_type { @@ -154,14 +143,14 @@ pub fn draw_properties( }; if !property.children.is_empty() { - spans.push(Span::raw(delimiters.0).style(theme.syntax_brace)); + spans.push(delimiters.0.to_string()); } - lines.push(Line::from(spans)); + lines.push(spans.join("")); if !property.children.is_empty() { draw_properties(theme, property.children.defined_properties(), lines, level + 1, filter_path); - lines.push(Line::from(vec![Span::raw(format!("{}{}", " ".repeat(level), delimiters.1))]).style(theme.syntax_brace)); + lines.push(format!("{}{}", " ".repeat(level), delimiters.1)); } } } diff --git a/src/view/layout.rs b/src/view/layout.rs index b7c4678..9d5cf81 100644 --- a/src/view/layout.rs +++ b/src/view/layout.rs @@ -53,7 +53,7 @@ impl View for LayoutView { } } -fn notification_widget(app: &App) -> Paragraph<'_> { +fn notification_widget<'a>(app: &'a App) -> Paragraph<'a> { Paragraph::new(vec![Line::from(Span::styled( match app.notification.is_visible() { true => format!( From bd1934491e9178a15887e5650254dcaf5d8314f1 Mon Sep 17 00:00:00 2001 From: Daniel Leech Date: Sat, 8 Nov 2025 15:56:03 +0000 Subject: [PATCH 03/49] Drain --- src/console.rs | 19 +++++++++++++------ src/view/eval.rs | 2 +- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/src/console.rs b/src/console.rs index 53fcf12..39e659e 100644 --- a/src/console.rs +++ b/src/console.rs @@ -1,19 +1,26 @@ use std::sync::Arc; - -use ratatui::text::Line; use tokio::sync::Mutex; pub struct Console { pub buffer: Arc>>, - pub lines: Vec, + pub chunks: Vec, } impl Console { pub async fn unload(&mut self) { - self.lines.append(&mut self.buffer.lock().await.to_vec()); - self.buffer.lock().await.clear(); + self.chunks.append(&mut self.buffer.lock().await.drain(0..).collect()); } + pub fn new() -> Self { - Self { buffer: Arc::new(Mutex::new(vec![])), lines: vec![] } + Self { + buffer: Arc::new(Mutex::new(vec![])), + chunks: vec![] + } + } +} + +impl Default for Console { + fn default() -> Self { + Self::new() } } diff --git a/src/view/eval.rs b/src/view/eval.rs index 3a9375e..a7e2357 100644 --- a/src/view/eval.rs +++ b/src/view/eval.rs @@ -49,7 +49,7 @@ impl View for EvalComponent { } } frame.render_widget( - Paragraph::new(app.console.lines.join("\n")).scroll(app.session_view.eval_state.scroll).style(app.theme().source_line), + Paragraph::new(app.console.chunks.join("")).scroll(app.session_view.eval_state.scroll).style(app.theme().source_line), area, ); } From bc431a9fc83f79448047d0fccd514131073b93cf Mon Sep 17 00:00:00 2001 From: Daniel Leech Date: Sat, 8 Nov 2025 16:49:32 +0000 Subject: [PATCH 04/49] Switch channels --- src/app.rs | 15 +++++---- src/channel.rs | 74 +++++++++++++++++++++++++++++++++++++++++++++ src/console.rs | 26 ---------------- src/event/input.rs | 1 + src/main.rs | 2 +- src/view/eval.rs | 10 +++++- src/view/session.rs | 4 +++ 7 files changed, 98 insertions(+), 34 deletions(-) create mode 100644 src/channel.rs delete mode 100644 src/console.rs diff --git a/src/app.rs b/src/app.rs index 89295ea..7fe5b4a 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,8 +1,8 @@ use crate::analyzer::Analyser; use crate::analyzer::Analysis; use crate::analyzer::VariableRef; +use crate::channel::Channels; use crate::config::Config; -use crate::console::Console; use crate::dbgp::client::ContextGetResponse; use crate::dbgp::client::ContinuationResponse; use crate::dbgp::client::ContinuationStatus; @@ -246,7 +246,7 @@ pub struct App { sender: Sender, php_process: Option, - pub console: Console, + pub channels: Channels, pub listening_status: ListenStatus, pub notification: Notification, pub config: Config, @@ -294,7 +294,7 @@ impl App { document_variables: DocumentVariables::default(), client: Arc::clone(&client), workspace: Workspace::new(Arc::clone(&client)), - console: Console::new(), + channels: Channels::new(), counter: 0, context_depth: 4, @@ -372,7 +372,7 @@ impl App { return Ok(()); } - self.console.unload().await; + self.channels.unload().await; terminal.autoresize()?; terminal.draw(|frame| { @@ -570,6 +570,9 @@ impl App { self.active_dialog = Some(ActiveDialog::Eval); } } + AppEvent::NextChannel => { + self.session_view.eval_state.channel = (self.session_view.eval_state.channel + 1) % self.channels.count() + }, AppEvent::EvalCancel => { self.active_dialog = None; } @@ -595,7 +598,7 @@ impl App { 0, &mut Vec::new(), ); - self.console.buffer.lock().await.append(&mut lines); + self.channels.get_mut("eval").buffer.lock().await.append(&mut lines); self.sender.send(AppEvent::Snapshot()).await.unwrap(); } self.active_dialog = None; @@ -862,7 +865,7 @@ impl App { .args(&script[1..]) .spawn() .unwrap(); - let buffer = self.console.buffer.clone(); + let buffer = self.channels.get_mut("php").buffer.clone(); let mut reader = BufReader::new(process.stdout.take().unwrap()); self.php_process = Some(process); task::spawn(async move { diff --git a/src/channel.rs b/src/channel.rs new file mode 100644 index 0000000..6ea464d --- /dev/null +++ b/src/channel.rs @@ -0,0 +1,74 @@ +use std::{collections::HashMap, sync::Arc}; +use tokio::sync::Mutex; + +pub struct Channel { + pub buffer: Arc>>, + pub chunks: Vec, +} + +pub struct Channels { + channels: HashMap, + channel_by_offset: Vec, +} + +impl Channels { + pub fn names(&self) -> Vec<&str> + { + self.channels.keys().map(|t|t.as_str()).collect() + } + + pub fn get(&self, name: &str) -> Option<&Channel> { + self.channels.get(name) + } + + pub fn get_mut(&mut self, name: &str) -> &Channel { + self.channels.entry(name.to_string()).or_insert_with(|| { + self.channel_by_offset.push(name.to_string()); + Channel::new() + }) + } + + pub async fn unload(&mut self) { + for entry in self.channels.iter_mut() { + entry.1.unload().await + } + } + + pub fn new() -> Self { + Self{ + channels: HashMap::new(), + channel_by_offset: Vec::new(), + } + } + + pub(crate) fn count(&self) -> usize { + self.channels.keys().len() + } + + pub(crate) fn channel_by_offset(&self, channel: usize) -> Option<&Channel> { + match self.channel_by_offset.get(channel) { + Some(name) => self.channels.get(name), + None => None, + } + } +} + + +impl Channel { + pub async fn unload(&mut self) { + self.chunks.append(&mut self.buffer.lock().await.drain(0..).collect()); + } + + pub fn new() -> Self { + Self { + buffer: Arc::new(Mutex::new(vec![])), + chunks: vec![] + } + } +} + +impl Default for Channel { + fn default() -> Self { + Self::new() + } +} diff --git a/src/console.rs b/src/console.rs deleted file mode 100644 index 39e659e..0000000 --- a/src/console.rs +++ /dev/null @@ -1,26 +0,0 @@ -use std::sync::Arc; -use tokio::sync::Mutex; - -pub struct Console { - pub buffer: Arc>>, - pub chunks: Vec, -} - -impl Console { - pub async fn unload(&mut self) { - self.chunks.append(&mut self.buffer.lock().await.drain(0..).collect()); - } - - pub fn new() -> Self { - Self { - buffer: Arc::new(Mutex::new(vec![])), - chunks: vec![] - } - } -} - -impl Default for Console { - fn default() -> Self { - Self::new() - } -} diff --git a/src/event/input.rs b/src/event/input.rs index 5277af9..be64b13 100644 --- a/src/event/input.rs +++ b/src/event/input.rs @@ -56,6 +56,7 @@ pub enum AppEvent { EvalRefresh, EvalStart, Listening, + NextChannel, } pub type EventSender = Sender; diff --git a/src/main.rs b/src/main.rs index e7958b3..8f0b702 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,7 +7,7 @@ pub mod view; pub mod analyzer; pub mod theme; pub mod workspace; -pub mod console; +pub mod channel; use app::App; use better_panic::Settings; diff --git a/src/view/eval.rs b/src/view/eval.rs index a7e2357..7c6f2da 100644 --- a/src/view/eval.rs +++ b/src/view/eval.rs @@ -1,6 +1,7 @@ use super::centered_rect_absolute; use super::View; use crate::app::App; +use crate::channel::Channel; use crate::dbgp::client::EvalResponse; use crate::dbgp::client::Property; use crate::dbgp::client::PropertyType; @@ -25,6 +26,7 @@ pub struct EvalDialog {} pub struct EvalState { pub response: Option, pub input: Input, + pub channel: usize, pub scroll: (u16, u16), } @@ -48,8 +50,14 @@ impl View for EvalComponent { } } } + + ; + let channel = match app.channels.channel_by_offset(app.session_view.eval_state.channel) { + Some(c) => c, + None => &Channel::new(), + }; frame.render_widget( - Paragraph::new(app.console.chunks.join("")).scroll(app.session_view.eval_state.scroll).style(app.theme().source_line), + Paragraph::new(channel.chunks.join("")).scroll(app.session_view.eval_state.scroll).style(app.theme().source_line), area, ); } diff --git a/src/view/session.rs b/src/view/session.rs index c1d0c77..29f0e52 100644 --- a/src/view/session.rs +++ b/src/view/session.rs @@ -18,6 +18,7 @@ use ratatui::layout::Rect; use ratatui::widgets::Block; use ratatui::widgets::Borders; use ratatui::widgets::Clear; +use ratatui::widgets::Tabs; use ratatui::Frame; use std::cell::Cell; use std::rc::Rc; @@ -52,6 +53,7 @@ impl View for SessionView { KeyCode::Down => return Some(AppEvent::Scroll((multiplier, 0))), KeyCode::Char(char) => match char { 'e' => return Some(AppEvent::EvalStart), + 'c' => return Some(AppEvent::NextChannel), 'j' => return Some(AppEvent::Scroll((1, 0))), 'k' => return Some(AppEvent::Scroll((-1, 0))), 'J' => return Some(AppEvent::Scroll((10, 0))), @@ -225,6 +227,8 @@ fn build_pane_widget(frame: &mut Frame, app: &App, pane: &Pane, area: Rect, inde StackComponent::draw(app, frame, block.inner(area)); } ComponentType::Eval => { + let tabs = Tabs::new(app.channels.names()).select(app.session_view.eval_state.channel); + frame.render_widget(tabs, area); EvalComponent::draw(app, frame, block.inner(area)); } }; From 90b90c1f32dff12f5d6f97e0afe1a9865de473f7 Mon Sep 17 00:00:00 2001 From: Daniel Leech Date: Sun, 9 Nov 2025 08:50:19 +0000 Subject: [PATCH 05/49] Failing test for lines --- src/app.rs | 5 ++--- src/channel.rs | 35 ++++++++++++++++++++++++++++++++--- src/view/eval.rs | 46 ++-------------------------------------------- 3 files changed, 36 insertions(+), 50 deletions(-) diff --git a/src/app.rs b/src/app.rs index 7fe5b4a..f0148ff 100644 --- a/src/app.rs +++ b/src/app.rs @@ -595,10 +595,9 @@ impl App { &self.theme(), response.properties.defined_properties(), &mut lines, - 0, - &mut Vec::new(), + 0 ); - self.channels.get_mut("eval").buffer.lock().await.append(&mut lines); + self.channels.get_mut("eval").writeln(lines.join("\n")).await; self.sender.send(AppEvent::Snapshot()).await.unwrap(); } self.active_dialog = None; diff --git a/src/channel.rs b/src/channel.rs index 6ea464d..b6b6e10 100644 --- a/src/channel.rs +++ b/src/channel.rs @@ -3,7 +3,7 @@ use tokio::sync::Mutex; pub struct Channel { pub buffer: Arc>>, - pub chunks: Vec, + pub lines: Vec, } pub struct Channels { @@ -56,15 +56,25 @@ impl Channels { impl Channel { pub async fn unload(&mut self) { - self.chunks.append(&mut self.buffer.lock().await.drain(0..).collect()); + let chunks = &mut self.buffer.lock().await.drain(0..).collect(); + self.lines.append(chunks); } pub fn new() -> Self { Self { buffer: Arc::new(Mutex::new(vec![])), - chunks: vec![] + lines: vec![] } } + + pub(crate) async fn write(&self, join: String) { + self.buffer.lock().await.push(join); + } + + pub(crate) async fn writeln(&self, join: String) { + self.write(join).await; + self.buffer.lock().await.push("\n".to_string()); + } } impl Default for Channel { @@ -72,3 +82,22 @@ impl Default for Channel { Self::new() } } + +#[cfg(test)] +mod test { + use super::*; + + #[tokio::test] + pub async fn test_channel_lines() { + let mut channel = Channel::new(); + channel.write("foobar".to_string()).await; + channel.write("\nbarfoo\nbaz\none\ntwo".to_string()).await; + channel.write("baf\nbaz\n".to_string()).await; + + assert_eq!(0, channel.lines.len()); + channel.unload().await; + + assert_eq!(7, channel.lines.len()); + + } +} diff --git a/src/view/eval.rs b/src/view/eval.rs index 7c6f2da..784d4ef 100644 --- a/src/view/eval.rs +++ b/src/view/eval.rs @@ -57,7 +57,7 @@ impl View for EvalComponent { None => &Channel::new(), }; frame.render_widget( - Paragraph::new(channel.chunks.join("")).scroll(app.session_view.eval_state.scroll).style(app.theme().source_line), + Paragraph::new(channel.lines.join("")).scroll(app.session_view.eval_state.scroll).style(app.theme().source_line), area, ); } @@ -123,16 +123,8 @@ pub fn draw_properties( properties: Vec<&Property>, lines: &mut Vec, level: usize, - filter_path: &mut Vec<&str>, ) { - let filter = filter_path.pop(); - for property in properties { - if let Some(filter) = filter { - if !property.name.starts_with(filter) { - continue; - } - } let mut spans = vec![ " ".repeat(level), property.name.to_string(), @@ -157,7 +149,7 @@ pub fn draw_properties( lines.push(spans.join("")); if !property.children.is_empty() { - draw_properties(theme, property.children.defined_properties(), lines, level + 1, filter_path); + draw_properties(theme, property.children.defined_properties(), lines, level + 1); lines.push(format!("{}{}", " ".repeat(level), delimiters.1)); } } @@ -194,7 +186,6 @@ mod test { Vec::new(), &mut lines, 0, - &mut Vec::new(), ); assert_eq!(0, lines.len()); Ok(()) @@ -214,7 +205,6 @@ mod test { vec![&prop1], &mut lines, 0, - &mut Vec::new(), ); assert_eq!( vec!["foo string = \"\"{", " bar string = \"\"", "}",], @@ -226,36 +216,4 @@ mod test { Ok(()) } - #[test] - fn test_filter_property_multiple_level() -> Result<()> { - let mut lines = vec![]; - let mut prop1 = Property::default(); - let mut prop2 = Property::default(); - let prop3 = Property::default(); - - prop2.name = "bar".to_string(); - prop1.children = Properties::from_properties(vec![prop2]); - prop1.name = "foo".to_string(); - - // segments are reversed - let filter = &mut vec!["bar", "foo"]; - - draw_properties( - &Theme::SolarizedDark.scheme(), - vec![&prop1, &prop3], - &mut lines, - 0, - filter, - ); - - assert_eq!( - vec!["foo string = \"\"{", " bar string = \"\"", "}",], - lines - .iter() - .map(|l| { l.to_string() }) - .collect::>() - ); - - Ok(()) - } } From c1d6963a1fce60bfcb868f0c0b0b2fd1e508b71f Mon Sep 17 00:00:00 2001 From: Daniel Leech Date: Sun, 9 Nov 2025 09:11:16 +0000 Subject: [PATCH 06/49] Lines --- src/app.rs | 2 -- src/channel.rs | 9 +++++---- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/app.rs b/src/app.rs index f0148ff..d8b217a 100644 --- a/src/app.rs +++ b/src/app.rs @@ -32,12 +32,10 @@ use ratatui::layout::Rect; use ratatui::prelude::CrosstermBackend; use ratatui::style::Color; use ratatui::style::Style; -use ratatui::text::Line; use ratatui::widgets::Block; use ratatui::widgets::Padding; use ratatui::widgets::Paragraph; use ratatui::Terminal; -use tokio::io::AsyncBufReadExt; use tokio::io::AsyncReadExt; use tokio::io::BufReader; use tokio::process::Child; diff --git a/src/channel.rs b/src/channel.rs index b6b6e10..18e902c 100644 --- a/src/channel.rs +++ b/src/channel.rs @@ -56,8 +56,10 @@ impl Channels { impl Channel { pub async fn unload(&mut self) { - let chunks = &mut self.buffer.lock().await.drain(0..).collect(); - self.lines.append(chunks); + let chunks: Vec = self.buffer.lock().await.drain(0..).collect(); + let content = chunks.join(""); + let mut lines: Vec = content.lines().map(|s|s.to_string()).collect(); + self.lines.append(&mut lines); } pub fn new() -> Self { @@ -97,7 +99,6 @@ mod test { assert_eq!(0, channel.lines.len()); channel.unload().await; - assert_eq!(7, channel.lines.len()); - + assert_eq!(6, channel.lines.len()); } } From 6ae688102e688f267e50c444f6b68faf11273824 Mon Sep 17 00:00:00 2001 From: Daniel Leech Date: Sun, 9 Nov 2025 09:38:47 +0000 Subject: [PATCH 07/49] Fix line ending --- src/app.rs | 8 ++++++-- src/channel.rs | 45 ++++++++++++++++++++++++++++++++++++++++++++- src/view/eval.rs | 14 +++++++++----- src/view/session.rs | 6 +++--- 4 files changed, 62 insertions(+), 11 deletions(-) diff --git a/src/app.rs b/src/app.rs index d8b217a..4d877d0 100644 --- a/src/app.rs +++ b/src/app.rs @@ -868,9 +868,13 @@ impl App { task::spawn(async move { loop { let mut buf = [0; 10]; - let n = reader.read(&mut buf).await.expect("nope"); + let n = reader.read(&mut buf).await.expect("TODO: handle this error"); if n > 0 { - buffer.lock().await.push(from_utf8(&buf[..n]).expect("fucl").to_string()); + buffer.lock().await.push( + from_utf8(&buf[..n]).expect( + "TODO: handle this error" + ).to_string() + ); } } }); diff --git a/src/channel.rs b/src/channel.rs index 18e902c..79a1f82 100644 --- a/src/channel.rs +++ b/src/channel.rs @@ -59,6 +59,24 @@ impl Channel { let chunks: Vec = self.buffer.lock().await.drain(0..).collect(); let content = chunks.join(""); let mut lines: Vec = content.lines().map(|s|s.to_string()).collect(); + + // content.lines() will ignore trailing new lines. we explicitly + // add a new line if the last character was a new line. + if let Some(char) = content.chars().last() { + if char == '\n' { + lines.push("".to_string()); + } + } + + if lines.len() == 0 { + return; + } + if let Some(l) = &mut self.lines.last_mut() { + let first = lines.get(0).unwrap(); + l.push_str(first.as_str()); + self.lines.append(&mut lines[1..].to_vec()); + return; + } self.lines.append(&mut lines); } @@ -99,6 +117,31 @@ mod test { assert_eq!(0, channel.lines.len()); channel.unload().await; - assert_eq!(6, channel.lines.len()); + assert_eq!(7, channel.lines.len()); + } + + #[tokio::test] + pub async fn test_channel_lines_with_unterminated_previous() { + let mut channel = Channel::new(); + channel.write("foobar".to_string()).await; + channel.unload().await; + assert_eq!(1, channel.lines.len()); + channel.write("barfoo".to_string()).await; + channel.unload().await; + assert_eq!(1, channel.lines.len()); + channel.write("barfoo\n".to_string()).await; + channel.unload().await; + assert_eq!(2, channel.lines.len()); + } + + #[tokio::test] + pub async fn test_channel_lines_with_nothing() { + let mut channel = Channel::new(); + channel.unload().await; + + assert_eq!(0, channel.lines.len()); + channel.write("".to_string()).await; + channel.unload().await; + assert_eq!(0, channel.lines.len()); } } diff --git a/src/view/eval.rs b/src/view/eval.rs index 784d4ef..5fcc1f7 100644 --- a/src/view/eval.rs +++ b/src/view/eval.rs @@ -19,7 +19,7 @@ use ratatui::Frame; use tui_input::backend::crossterm::EventHandler; use tui_input::Input; -pub struct EvalComponent {} +pub struct ChannelsComponent {} pub struct EvalDialog {} #[derive(Default)] @@ -30,7 +30,7 @@ pub struct EvalState { pub scroll: (u16, u16), } -impl View for EvalComponent { +impl View for ChannelsComponent { fn handle(_app: &mut App, event: AppEvent) -> Option { match event { AppEvent::Scroll(scroll) => Some(AppEvent::ScrollEval(scroll)), @@ -51,13 +51,17 @@ impl View for EvalComponent { } } - ; - let channel = match app.channels.channel_by_offset(app.session_view.eval_state.channel) { + let channel = match app.channels.channel_by_offset( + app.session_view.eval_state.channel + ) { Some(c) => c, None => &Channel::new(), }; + frame.render_widget( - Paragraph::new(channel.lines.join("")).scroll(app.session_view.eval_state.scroll).style(app.theme().source_line), + Paragraph::new(channel.lines.join("\n")).scroll( + app.session_view.eval_state.scroll + ).style(app.theme().source_line), area, ); } diff --git a/src/view/session.rs b/src/view/session.rs index 29f0e52..cf2f17d 100644 --- a/src/view/session.rs +++ b/src/view/session.rs @@ -1,5 +1,5 @@ use super::context::ContextComponent; -use super::eval::EvalComponent; +use super::eval::ChannelsComponent; use super::eval::EvalState; use super::source::SourceComponent; use super::stack::StackComponent; @@ -164,7 +164,7 @@ fn delegate_event_to_pane(app: &mut App, event: AppEvent) -> Option { ComponentType::Source => SourceComponent::handle(app, event), ComponentType::Context => ContextComponent::handle(app, event), ComponentType::Stack => StackComponent::handle(app, event), - ComponentType::Eval => EvalComponent::handle(app, event), + ComponentType::Eval => ChannelsComponent::handle(app, event), } } @@ -229,7 +229,7 @@ fn build_pane_widget(frame: &mut Frame, app: &App, pane: &Pane, area: Rect, inde ComponentType::Eval => { let tabs = Tabs::new(app.channels.names()).select(app.session_view.eval_state.channel); frame.render_widget(tabs, area); - EvalComponent::draw(app, frame, block.inner(area)); + ChannelsComponent::draw(app, frame, block.inner(area)); } }; } From 4f391caee0ac2e0c183e761710f4396770096263 Mon Sep 17 00:00:00 2001 From: Daniel Leech Date: Sun, 9 Nov 2025 11:33:47 +0000 Subject: [PATCH 08/49] Show stdout and stderr --- src/app.rs | 59 ++++++++++++++++++++++++++++++++++++++-------- src/channel.rs | 4 ++++ src/event/input.rs | 1 + src/view/eval.rs | 17 +++++++++++++ 4 files changed, 71 insertions(+), 10 deletions(-) diff --git a/src/app.rs b/src/app.rs index 4d877d0..b3ebc75 100644 --- a/src/app.rs +++ b/src/app.rs @@ -571,6 +571,9 @@ impl App { AppEvent::NextChannel => { self.session_view.eval_state.channel = (self.session_view.eval_state.channel + 1) % self.channels.count() }, + AppEvent::FocusChannel(name) => { + self.session_view.eval_state.focus(&self.channels, name); + }, AppEvent::EvalCancel => { self.active_dialog = None; } @@ -596,6 +599,7 @@ impl App { 0 ); self.channels.get_mut("eval").writeln(lines.join("\n")).await; + self.sender.send(AppEvent::FocusChannel("eval".to_string())).await.unwrap(); self.sender.send(AppEvent::Snapshot()).await.unwrap(); } self.active_dialog = None; @@ -862,22 +866,57 @@ impl App { .args(&script[1..]) .spawn() .unwrap(); + let buffer = self.channels.get_mut("php").buffer.clone(); - let mut reader = BufReader::new(process.stdout.take().unwrap()); - self.php_process = Some(process); + let sender = self.sender.clone(); + + let mut stdoutreader = BufReader::new(process.stdout.take().unwrap()); + let mut stderrreader = BufReader::new(process.stderr.take().unwrap()); + task::spawn(async move { loop { - let mut buf = [0; 10]; - let n = reader.read(&mut buf).await.expect("TODO: handle this error"); - if n > 0 { - buffer.lock().await.push( - from_utf8(&buf[..n]).expect( - "TODO: handle this error" - ).to_string() - ); + let mut buf = [0; 255]; + let read = stdoutreader.read(&mut buf).await.expect( + "TODO: handle this error" + ); + + if read == 0 { + continue; } + buffer.lock().await.push( + from_utf8(&buf[..read]).expect( + "TODO: handle this error" + ).to_string() + ); + sender.send( + AppEvent::FocusChannel("php".to_string()) + ).await.unwrap(); } }); + let sender = self.sender.clone(); + let buffer = self.channels.get_mut("php").buffer.clone(); + task::spawn(async move { + loop { + let mut buf = [0; 255]; + let read = stderrreader.read(&mut buf).await.expect( + "TODO: handle this error" + ); + + if read == 0 { + continue; + } + buffer.lock().await.push( + from_utf8(&buf[..read]).expect( + "TODO: handle this error" + ).to_string() + ); + sender.send( + AppEvent::FocusChannel("php".to_string()) + ).await.unwrap(); + } + }); + + self.php_process = Some(process); } } } diff --git a/src/channel.rs b/src/channel.rs index 79a1f82..2cfe732 100644 --- a/src/channel.rs +++ b/src/channel.rs @@ -51,6 +51,10 @@ impl Channels { None => None, } } + + pub(crate) fn offset_by_name(&self, name: String) -> Option { + self.channel_by_offset.iter().position(|n|*n==name) + } } diff --git a/src/event/input.rs b/src/event/input.rs index be64b13..ab85527 100644 --- a/src/event/input.rs +++ b/src/event/input.rs @@ -57,6 +57,7 @@ pub enum AppEvent { EvalStart, Listening, NextChannel, + FocusChannel(String), } pub type EventSender = Sender; diff --git a/src/view/eval.rs b/src/view/eval.rs index 5fcc1f7..4aacfdd 100644 --- a/src/view/eval.rs +++ b/src/view/eval.rs @@ -1,7 +1,10 @@ +use std::cell::Cell; + use super::centered_rect_absolute; use super::View; use crate::app::App; use crate::channel::Channel; +use crate::channel::Channels; use crate::dbgp::client::EvalResponse; use crate::dbgp::client::Property; use crate::dbgp::client::PropertyType; @@ -25,10 +28,20 @@ pub struct EvalDialog {} #[derive(Default)] pub struct EvalState { pub response: Option, + pub eval_area: Cell, pub input: Input, pub channel: usize, pub scroll: (u16, u16), } +impl EvalState { + pub(crate) fn focus(&mut self, channels: &Channels, name: String) { + self.channel = channels.offset_by_name(name).unwrap_or(0); + let channel = channels.channel_by_offset(self.channel).expect("channel does not exist!"); + self.scroll.1 = 0; + let area = self.eval_area.get(); + self.scroll.0 = (channel.lines.len() as i16 - area.height as i16).max(0) as u16; + } +} impl View for ChannelsComponent { fn handle(_app: &mut App, event: AppEvent) -> Option { @@ -58,6 +71,10 @@ impl View for ChannelsComponent { None => &Channel::new(), }; + // make the app aware of the channel area so we can + // scroll it correctly when its updated + app.session_view.eval_state.eval_area.set(area); + frame.render_widget( Paragraph::new(channel.lines.join("\n")).scroll( app.session_view.eval_state.scroll From 74a24681aa23ecde663407e8edb04ceea4c296d4 Mon Sep 17 00:00:00 2001 From: Daniel Leech Date: Sun, 9 Nov 2025 11:49:32 +0000 Subject: [PATCH 09/49] Extract crappy code to own module --- src/app.rs | 66 ++------------------------------------- src/main.rs | 1 + src/php_process.rs | 77 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 80 insertions(+), 64 deletions(-) create mode 100644 src/php_process.rs diff --git a/src/app.rs b/src/app.rs index b3ebc75..0cb5cd5 100644 --- a/src/app.rs +++ b/src/app.rs @@ -11,6 +11,7 @@ use crate::dbgp::client::EvalResponse; use crate::dbgp::client::Property; use crate::event::input::AppEvent; use crate::notification::Notification; +use crate::php_process; use crate::theme::Scheme; use crate::theme::Theme; use crate::view::eval::draw_properties; @@ -395,7 +396,7 @@ impl App { AppEvent::Quit => self.quit = true, AppEvent::Listening => { if let Some(script) = &self.config.cmd { - self.start_php_process(script.clone()); + self.php_process = php_process::start(&mut self.channels, script, self.sender.clone()); } }, AppEvent::ChangeView(view) => { @@ -856,69 +857,6 @@ impl App { .scroll_to_line(entry.source(self.session_view.stack_depth()).line_no) } } - - fn start_php_process(&mut self, script: Vec) { - let cmd = script.first(); - if let Some(program) = cmd { - let mut process = Command::new(&program) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .args(&script[1..]) - .spawn() - .unwrap(); - - let buffer = self.channels.get_mut("php").buffer.clone(); - let sender = self.sender.clone(); - - let mut stdoutreader = BufReader::new(process.stdout.take().unwrap()); - let mut stderrreader = BufReader::new(process.stderr.take().unwrap()); - - task::spawn(async move { - loop { - let mut buf = [0; 255]; - let read = stdoutreader.read(&mut buf).await.expect( - "TODO: handle this error" - ); - - if read == 0 { - continue; - } - buffer.lock().await.push( - from_utf8(&buf[..read]).expect( - "TODO: handle this error" - ).to_string() - ); - sender.send( - AppEvent::FocusChannel("php".to_string()) - ).await.unwrap(); - } - }); - let sender = self.sender.clone(); - let buffer = self.channels.get_mut("php").buffer.clone(); - task::spawn(async move { - loop { - let mut buf = [0; 255]; - let read = stderrreader.read(&mut buf).await.expect( - "TODO: handle this error" - ); - - if read == 0 { - continue; - } - buffer.lock().await.push( - from_utf8(&buf[..read]).expect( - "TODO: handle this error" - ).to_string() - ); - sender.send( - AppEvent::FocusChannel("php".to_string()) - ).await.unwrap(); - } - }); - - self.php_process = Some(process); - } - } } fn apply_scroll(scroll: (u16, u16), amount: (i16, i16), motion: i16) -> (u16, u16) { diff --git a/src/main.rs b/src/main.rs index 8f0b702..a251197 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,6 +8,7 @@ pub mod analyzer; pub mod theme; pub mod workspace; pub mod channel; +pub mod php_process; use app::App; use better_panic::Settings; diff --git a/src/php_process.rs b/src/php_process.rs new file mode 100644 index 0000000..c7687d0 --- /dev/null +++ b/src/php_process.rs @@ -0,0 +1,77 @@ +use std::{process::Stdio, str::from_utf8}; +use tokio::io::AsyncReadExt; + +use tokio::process::Child; +use tokio::{io::BufReader, process::Command, sync::mpsc::Sender, task}; + +use crate::{channel::Channels, event::input::AppEvent}; + +pub fn start( + channels: &mut Channels, + script: &Vec, + parent_sender: Sender, +) -> Option { + let cmd = script.first(); + if let Some(program) = cmd { + let mut process = Command::new(&program) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .args(&script[1..]) + .spawn() + .unwrap(); + + let buffer = channels.get_mut("php").buffer.clone(); + let sender = parent_sender.clone(); + + let mut stdoutreader = BufReader::new(process.stdout.take().unwrap()); + let mut stderrreader = BufReader::new(process.stderr.take().unwrap()); + + task::spawn(async move { + loop { + let mut buf = [0; 255]; + let read = stdoutreader + .read(&mut buf) + .await + .expect("TODO: handle this error"); + + handle_read(buffer.clone(), sender.clone(), read, buf).await; + } + }); + + let buffer = channels.get_mut("php").buffer.clone(); + let sender = parent_sender.clone(); + task::spawn(async move { + loop { + let mut buf = [0; 255]; + let read = stderrreader + .read(&mut buf) + .await + .expect("TODO: handle this error"); + handle_read(buffer.clone(), sender.clone(), read, buf).await; + } + }); + + return Some(process); + } + return None; +} + +async fn handle_read( + buffer: std::sync::Arc>>, + sender: Sender, + read: usize, + buf: [u8; 255] +) { + if read == 0 { + return; + } + buffer.lock().await.push( + from_utf8(&buf[..read]) + .expect("TODO: handle this error") + .to_string(), + ); + sender + .send(AppEvent::FocusChannel("php".to_string())) + .await + .unwrap(); +} From ee64240eded89c8f7e85642ccf3c6186d91b5ff8 Mon Sep 17 00:00:00 2001 From: Daniel Leech Date: Sun, 9 Nov 2025 15:59:10 +0000 Subject: [PATCH 10/49] OK --- src/app.rs | 24 +++++++++++++++-------- src/php_process.rs | 48 ++++++++++++++++++++++++--------------------- src/view/eval.rs | 14 +------------ src/view/session.rs | 9 ++++++--- 4 files changed, 49 insertions(+), 46 deletions(-) diff --git a/src/app.rs b/src/app.rs index 0cb5cd5..47cc8ab 100644 --- a/src/app.rs +++ b/src/app.rs @@ -593,15 +593,23 @@ impl App { .await?; let mut lines: Vec = Vec::new(); - draw_properties( - &self.theme(), - response.properties.defined_properties(), - &mut lines, - 0 - ); - self.channels.get_mut("eval").writeln(lines.join("\n")).await; + match response.error { + Some(e) => { + self.channels.get_mut("eval").writeln( + format!("[{}] {}", e.code, e.message), + ).await; + }, + None => { + draw_properties( + &self.theme(), + response.properties.defined_properties(), + &mut lines, + 0 + ); + self.channels.get_mut("eval").writeln(lines.join("\n")).await; + } + }; self.sender.send(AppEvent::FocusChannel("eval".to_string())).await.unwrap(); - self.sender.send(AppEvent::Snapshot()).await.unwrap(); } self.active_dialog = None; } diff --git a/src/php_process.rs b/src/php_process.rs index c7687d0..0ac8f98 100644 --- a/src/php_process.rs +++ b/src/php_process.rs @@ -20,34 +20,36 @@ pub fn start( .spawn() .unwrap(); - let buffer = channels.get_mut("php").buffer.clone(); - let sender = parent_sender.clone(); - let mut stdoutreader = BufReader::new(process.stdout.take().unwrap()); - let mut stderrreader = BufReader::new(process.stderr.take().unwrap()); + let buffer = channels.get_mut("stdout").buffer.clone(); + let sender = parent_sender.clone(); task::spawn(async move { loop { let mut buf = [0; 255]; - let read = stdoutreader + match stdoutreader .read(&mut buf) - .await - .expect("TODO: handle this error"); + .await { + Ok(read) => handle_read(buffer.clone(), sender.clone(), read, "stdout".to_string(), buf).await, + Err(_) => (), + } - handle_read(buffer.clone(), sender.clone(), read, buf).await; + ; } }); - let buffer = channels.get_mut("php").buffer.clone(); + let mut stderrreader = BufReader::new(process.stderr.take().unwrap()); + let buffer = channels.get_mut("stderr").buffer.clone(); let sender = parent_sender.clone(); task::spawn(async move { loop { let mut buf = [0; 255]; - let read = stderrreader + match stderrreader .read(&mut buf) - .await - .expect("TODO: handle this error"); - handle_read(buffer.clone(), sender.clone(), read, buf).await; + .await { + Ok(read) => handle_read(buffer.clone(), sender.clone(), read, "stderr".to_string(), buf).await, + Err(_) => (), + } } }); @@ -60,18 +62,20 @@ async fn handle_read( buffer: std::sync::Arc>>, sender: Sender, read: usize, + channel: String, buf: [u8; 255] ) { if read == 0 { return; } - buffer.lock().await.push( - from_utf8(&buf[..read]) - .expect("TODO: handle this error") - .to_string(), - ); - sender - .send(AppEvent::FocusChannel("php".to_string())) - .await - .unwrap(); + match from_utf8(&buf[..read]) { + Ok(s) => { + buffer.lock().await.push(s.to_string()); + sender + .send(AppEvent::FocusChannel(channel)) + .await + .unwrap_or_default(); + }, + Err(_) => (), + }; } diff --git a/src/view/eval.rs b/src/view/eval.rs index 4aacfdd..c570eaa 100644 --- a/src/view/eval.rs +++ b/src/view/eval.rs @@ -52,18 +52,6 @@ impl View for ChannelsComponent { } fn draw(app: &App, frame: &mut Frame, area: Rect) { - if let Some(entry) = &app.history.current() { - if let Some(eval_entry) = &entry.eval { - if let Some(error) = &eval_entry.response.error { - frame.render_widget( - Paragraph::new(error.message.clone()).style(app.theme().notification_error), - area, - ); - } else { - } - } - } - let channel = match app.channels.channel_by_offset( app.session_view.eval_state.channel ) { @@ -149,7 +137,7 @@ pub fn draw_properties( let mut spans = vec![ " ".repeat(level), property.name.to_string(), - " ".to_string(), + if property.name.len() > 0 { " ".to_string() } else {"".to_string()}, property.type_name(), " = ".to_string(), match &property.value { diff --git a/src/view/session.rs b/src/view/session.rs index cf2f17d..c50b92c 100644 --- a/src/view/session.rs +++ b/src/view/session.rs @@ -14,7 +14,9 @@ use crossterm::event::KeyCode; use crossterm::event::KeyModifiers; use ratatui::layout::Constraint; use ratatui::layout::Layout; +use ratatui::layout::Offset; use ratatui::layout::Rect; +use ratatui::widgets::block::Title; use ratatui::widgets::Block; use ratatui::widgets::Borders; use ratatui::widgets::Clear; @@ -198,12 +200,13 @@ fn build_pane_widget(frame: &mut Frame, app: &App, pane: &Pane, area: Rect, inde ), ComponentType::Eval => match app.history.current() { Some(entry) => format!( - "Eval: {}", + "Eval: {} {}", if let Some(eval) = &entry.eval { eval.expr.clone() } else { "Press 'e' to enter an expression".to_string() - } + }, + ", 'c' to change channel", ), None => "".to_string(), }, @@ -228,7 +231,7 @@ fn build_pane_widget(frame: &mut Frame, app: &App, pane: &Pane, area: Rect, inde } ComponentType::Eval => { let tabs = Tabs::new(app.channels.names()).select(app.session_view.eval_state.channel); - frame.render_widget(tabs, area); + frame.render_widget(tabs, area.offset(Offset{x: 1, y: 0})); ChannelsComponent::draw(app, frame, block.inner(area)); } }; From 6ac8b2ca93ec8d57e817013ad85d669f62744d8c Mon Sep 17 00:00:00 2001 From: Daniel Leech Date: Sun, 9 Nov 2025 16:28:57 +0000 Subject: [PATCH 11/49] Clippy --- src/app.rs | 8 +--- src/channel.rs | 10 ++++- src/php_process.rs | 89 ++++++++++++++++++++------------------------- src/view/eval.rs | 7 +--- src/view/session.rs | 1 - 5 files changed, 50 insertions(+), 65 deletions(-) diff --git a/src/app.rs b/src/app.rs index 47cc8ab..3a86763 100644 --- a/src/app.rs +++ b/src/app.rs @@ -37,16 +37,11 @@ use ratatui::widgets::Block; use ratatui::widgets::Padding; use ratatui::widgets::Paragraph; use ratatui::Terminal; -use tokio::io::AsyncReadExt; -use tokio::io::BufReader; use tokio::process::Child; -use tokio::process::Command; use std::collections::hash_map::Entry; use std::collections::HashMap; use std::io; use std::ops::DerefMut; -use std::process::Stdio; -use std::str::from_utf8; use std::sync::Arc; use tokio::net::TcpListener; use tokio::sync::mpsc::Receiver; @@ -277,7 +272,7 @@ pub struct App { } impl App { - pub fn new<'a>(config: Config, receiver: Receiver, sender: Sender) -> App { + pub fn new(config: Config, receiver: Receiver, sender: Sender) -> App { let client = Arc::new(Mutex::new(DbgpClient::new(None))); App { tick: 0, @@ -601,7 +596,6 @@ impl App { }, None => { draw_properties( - &self.theme(), response.properties.defined_properties(), &mut lines, 0 diff --git a/src/channel.rs b/src/channel.rs index 2cfe732..2542dff 100644 --- a/src/channel.rs +++ b/src/channel.rs @@ -11,6 +11,12 @@ pub struct Channels { channel_by_offset: Vec, } +impl Default for Channels { + fn default() -> Self { + Self::new() + } +} + impl Channels { pub fn names(&self) -> Vec<&str> { @@ -72,11 +78,11 @@ impl Channel { } } - if lines.len() == 0 { + if lines.is_empty() { return; } if let Some(l) = &mut self.lines.last_mut() { - let first = lines.get(0).unwrap(); + let first = lines.first().unwrap(); l.push_str(first.as_str()); self.lines.append(&mut lines[1..].to_vec()); return; diff --git a/src/php_process.rs b/src/php_process.rs index 0ac8f98..806204b 100644 --- a/src/php_process.rs +++ b/src/php_process.rs @@ -8,54 +8,46 @@ use crate::{channel::Channels, event::input::AppEvent}; pub fn start( channels: &mut Channels, - script: &Vec, + script: &[String], parent_sender: Sender, ) -> Option { - let cmd = script.first(); - if let Some(program) = cmd { - let mut process = Command::new(&program) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .args(&script[1..]) - .spawn() - .unwrap(); + let program = script.first()?; - let mut stdoutreader = BufReader::new(process.stdout.take().unwrap()); - let buffer = channels.get_mut("stdout").buffer.clone(); - let sender = parent_sender.clone(); + let mut process = Command::new(program) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .args(&script[1..]) + .spawn() + .unwrap(); - task::spawn(async move { - loop { - let mut buf = [0; 255]; - match stdoutreader - .read(&mut buf) - .await { - Ok(read) => handle_read(buffer.clone(), sender.clone(), read, "stdout".to_string(), buf).await, - Err(_) => (), - } + let mut stdoutreader = BufReader::new(process.stdout.take().unwrap()); + let buffer = channels.get_mut("stdout").buffer.clone(); + let sender = parent_sender.clone(); - ; - } - }); + task::spawn(async move { + loop { + let mut buf = [0; 255]; + if let Ok(read) = stdoutreader + .read(&mut buf) + .await { handle_read(buffer.clone(), sender.clone(), read, "stdout".to_string(), buf).await } - let mut stderrreader = BufReader::new(process.stderr.take().unwrap()); - let buffer = channels.get_mut("stderr").buffer.clone(); - let sender = parent_sender.clone(); - task::spawn(async move { - loop { - let mut buf = [0; 255]; - match stderrreader - .read(&mut buf) - .await { - Ok(read) => handle_read(buffer.clone(), sender.clone(), read, "stderr".to_string(), buf).await, - Err(_) => (), - } - } - }); + ; + } + }); - return Some(process); - } - return None; + let mut stderrreader = BufReader::new(process.stderr.take().unwrap()); + let buffer = channels.get_mut("stderr").buffer.clone(); + let sender = parent_sender.clone(); + task::spawn(async move { + loop { + let mut buf = [0; 255]; + if let Ok(read) = stderrreader + .read(&mut buf) + .await { handle_read(buffer.clone(), sender.clone(), read, "stderr".to_string(), buf).await } + } + }); + + Some(process) } async fn handle_read( @@ -68,14 +60,11 @@ async fn handle_read( if read == 0 { return; } - match from_utf8(&buf[..read]) { - Ok(s) => { - buffer.lock().await.push(s.to_string()); - sender - .send(AppEvent::FocusChannel(channel)) - .await - .unwrap_or_default(); - }, - Err(_) => (), + if let Ok(s) = from_utf8(&buf[..read]) { + buffer.lock().await.push(s.to_string()); + sender + .send(AppEvent::FocusChannel(channel)) + .await + .unwrap_or_default(); }; } diff --git a/src/view/eval.rs b/src/view/eval.rs index c570eaa..ffb1dfc 100644 --- a/src/view/eval.rs +++ b/src/view/eval.rs @@ -128,7 +128,6 @@ impl View for EvalDialog { } pub fn draw_properties( - theme: &Scheme, properties: Vec<&Property>, lines: &mut Vec, level: usize, @@ -137,7 +136,7 @@ pub fn draw_properties( let mut spans = vec![ " ".repeat(level), property.name.to_string(), - if property.name.len() > 0 { " ".to_string() } else {"".to_string()}, + if !property.name.is_empty() { " ".to_string() } else {"".to_string()}, property.type_name(), " = ".to_string(), match &property.value { @@ -158,7 +157,7 @@ pub fn draw_properties( lines.push(spans.join("")); if !property.children.is_empty() { - draw_properties(theme, property.children.defined_properties(), lines, level + 1); + draw_properties(property.children.defined_properties(), lines, level + 1); lines.push(format!("{}{}", " ".repeat(level), delimiters.1)); } } @@ -191,7 +190,6 @@ mod test { fn test_draw_properties_empty() -> Result<()> { let mut lines = vec![]; draw_properties( - &Theme::SolarizedDark.scheme(), Vec::new(), &mut lines, 0, @@ -210,7 +208,6 @@ mod test { prop1.name = "foo".to_string(); draw_properties( - &Theme::SolarizedDark.scheme(), vec![&prop1], &mut lines, 0, diff --git a/src/view/session.rs b/src/view/session.rs index c50b92c..0e14037 100644 --- a/src/view/session.rs +++ b/src/view/session.rs @@ -16,7 +16,6 @@ use ratatui::layout::Constraint; use ratatui::layout::Layout; use ratatui::layout::Offset; use ratatui::layout::Rect; -use ratatui::widgets::block::Title; use ratatui::widgets::Block; use ratatui::widgets::Borders; use ratatui::widgets::Clear; From 94f50dae929f4232fc59dbdfe9a7ea8ab20cebc5 Mon Sep 17 00:00:00 2001 From: Daniel Leech Date: Sun, 9 Nov 2025 22:13:40 +0000 Subject: [PATCH 12/49] Refactor --- src/app.rs | 20 +++++-- src/event/input.rs | 1 + src/php_process.rs | 144 ++++++++++++++++++++++++++++----------------- 3 files changed, 108 insertions(+), 57 deletions(-) diff --git a/src/app.rs b/src/app.rs index 3a86763..083410e 100644 --- a/src/app.rs +++ b/src/app.rs @@ -12,6 +12,7 @@ use crate::dbgp::client::Property; use crate::event::input::AppEvent; use crate::notification::Notification; use crate::php_process; +use crate::php_process::ProcessEvent; use crate::theme::Scheme; use crate::theme::Theme; use crate::view::eval::draw_properties; @@ -38,6 +39,7 @@ use ratatui::widgets::Padding; use ratatui::widgets::Paragraph; use ratatui::Terminal; use tokio::process::Child; +use tokio::sync::mpsc; use std::collections::hash_map::Entry; use std::collections::HashMap; use std::io; @@ -238,7 +240,6 @@ pub struct App { receiver: Receiver, quit: bool, sender: Sender, - php_process: Option, pub channels: Channels, pub listening_status: ListenStatus, @@ -269,11 +270,14 @@ pub struct App { pub analyzed_files: AnalyzedFiles, pub stack_max_context_fetch: u16, + php_tx: Sender, } impl App { pub fn new(config: Config, receiver: Receiver, sender: Sender) -> App { let client = Arc::new(Mutex::new(DbgpClient::new(None))); + let (php_tx, php_rx) = mpsc::channel::(1024); + php_process::process_manager_start(php_rx, sender.clone()); App { tick: 0, listening_status: ListenStatus::Listening, @@ -282,7 +286,7 @@ impl App { notification: Notification::none(), receiver, sender: sender.clone(), - php_process: None, + php_tx, quit: false, history: History::default(), document_variables: DocumentVariables::default(), @@ -315,7 +319,7 @@ impl App { ) -> Result<()> { let sender = self.sender.clone(); let config = self.config.clone(); - + // spawn connection listener co-routine task::spawn(async move { let listener = match TcpListener::bind(config.listen.clone()).await { @@ -391,7 +395,7 @@ impl App { AppEvent::Quit => self.quit = true, AppEvent::Listening => { if let Some(script) = &self.config.cmd { - self.php_process = php_process::start(&mut self.channels, script, self.sender.clone()); + self.php_tx.send(ProcessEvent::Start(script.to_vec())).await?; } }, AppEvent::ChangeView(view) => { @@ -570,6 +574,14 @@ impl App { AppEvent::FocusChannel(name) => { self.session_view.eval_state.focus(&self.channels, name); }, + AppEvent::ChannelLog(channel, chunk) => { + let buffer = self.channels.get_mut(channel.as_str()).buffer.clone(); + buffer.lock().await.push(chunk.to_string()); + self.sender + .send(AppEvent::FocusChannel(channel)) + .await + .unwrap_or_default(); + }, AppEvent::EvalCancel => { self.active_dialog = None; } diff --git a/src/event/input.rs b/src/event/input.rs index ab85527..04369ac 100644 --- a/src/event/input.rs +++ b/src/event/input.rs @@ -58,6 +58,7 @@ pub enum AppEvent { Listening, NextChannel, FocusChannel(String), + ChannelLog(String,String), } pub type EventSender = Sender; diff --git a/src/php_process.rs b/src/php_process.rs index 806204b..b1a52c1 100644 --- a/src/php_process.rs +++ b/src/php_process.rs @@ -2,69 +2,107 @@ use std::{process::Stdio, str::from_utf8}; use tokio::io::AsyncReadExt; use tokio::process::Child; +use tokio::select; +use tokio::sync::mpsc::Receiver; use tokio::{io::BufReader, process::Command, sync::mpsc::Sender, task}; use crate::{channel::Channels, event::input::AppEvent}; -pub fn start( - channels: &mut Channels, - script: &[String], +#[derive(Debug)] +pub enum ProcessEvent { + Start(Vec), + Stop, + Restart, +} + +pub fn process_manager_start( + mut receiver: Receiver, parent_sender: Sender, -) -> Option { - let program = script.first()?; +) { + task::spawn(async move { + loop { + let cmd = receiver.recv().await; + let event = match cmd { + Some(event) => event, + None => continue, + }; + let args = match event { + ProcessEvent::Start(args) => args, + ProcessEvent::Stop => continue, + ProcessEvent::Restart => continue, + }; - let mut process = Command::new(program) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .args(&script[1..]) - .spawn() - .unwrap(); + let program = match args.first() { + Some(arg) => arg, + None => continue, + }; - let mut stdoutreader = BufReader::new(process.stdout.take().unwrap()); - let buffer = channels.get_mut("stdout").buffer.clone(); - let sender = parent_sender.clone(); + let mut process = Command::new(program) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .args(&args[1..]) + .spawn() + .unwrap(); - task::spawn(async move { - loop { - let mut buf = [0; 255]; - if let Ok(read) = stdoutreader - .read(&mut buf) - .await { handle_read(buffer.clone(), sender.clone(), read, "stdout".to_string(), buf).await } + let mut stdoutreader = BufReader::new(process.stdout.take().unwrap()); + let mut stderrreader = BufReader::new(process.stderr.take().unwrap()); + let sender = parent_sender.clone(); - ; - } - }); + task::spawn(async move { + loop { + let mut stdout_buff = [0; 255]; + let mut stderr_buff = [0; 255]; - let mut stderrreader = BufReader::new(process.stderr.take().unwrap()); - let buffer = channels.get_mut("stderr").buffer.clone(); - let sender = parent_sender.clone(); - task::spawn(async move { - loop { - let mut buf = [0; 255]; - if let Ok(read) = stderrreader - .read(&mut buf) - .await { handle_read(buffer.clone(), sender.clone(), read, "stderr".to_string(), buf).await } + select! { + read = stdoutreader.read(&mut stdout_buff) => { + if let Ok(s) = from_utf8(&stdout_buff[..read.unwrap()]) { + if s.len() > 0 { + sender + .send(AppEvent::ChannelLog("stdout".to_string(), s.to_string())) + .await + .unwrap_or_default(); + } + }; + }, + read = stderrreader.read(&mut stderr_buff) => { + if let Ok(s) = from_utf8(&stderr_buff[..read.unwrap()]) { + if s.len() > 0 { + sender + .send(AppEvent::ChannelLog("stderr".to_string(), s.to_string())) + .await + .unwrap_or_default(); + } + }; + }, + }; + } + }); + + loop { + select! { + _ = process.wait() => { + return; + }, + cmd = receiver.recv() => { + let event = match cmd { + Some(event) => event, + None => continue, + }; + match event { + ProcessEvent::Start(_) => continue, + ProcessEvent::Stop => { + process.kill().await.unwrap_or_default(); + break; + }, + ProcessEvent::Restart => { + // TODO: restart + process.kill().await.unwrap_or_default(); + break; + }, + }; + }, + }; + } } }); - - Some(process) -} - -async fn handle_read( - buffer: std::sync::Arc>>, - sender: Sender, - read: usize, - channel: String, - buf: [u8; 255] -) { - if read == 0 { - return; - } - if let Ok(s) = from_utf8(&buf[..read]) { - buffer.lock().await.push(s.to_string()); - sender - .send(AppEvent::FocusChannel(channel)) - .await - .unwrap_or_default(); - }; } From b9ed6c831a730cb9d10dcd960e6408f9379f2a50 Mon Sep 17 00:00:00 2001 From: Daniel Leech Date: Sun, 9 Nov 2025 22:29:57 +0000 Subject: [PATCH 13/49] Support restarting --- src/app.rs | 8 +++++++- src/event/input.rs | 1 + src/php_process.rs | 16 ++++------------ src/view/session.rs | 2 ++ 4 files changed, 14 insertions(+), 13 deletions(-) diff --git a/src/app.rs b/src/app.rs index 083410e..097dc45 100644 --- a/src/app.rs +++ b/src/app.rs @@ -38,7 +38,6 @@ use ratatui::widgets::Block; use ratatui::widgets::Padding; use ratatui::widgets::Paragraph; use ratatui::Terminal; -use tokio::process::Child; use tokio::sync::mpsc; use std::collections::hash_map::Entry; use std::collections::HashMap; @@ -582,6 +581,13 @@ impl App { .await .unwrap_or_default(); }, + AppEvent::RestartProcess => { + self.sender.send(AppEvent::Disconnect).await?; + self.sender.send(AppEvent::Listen).await?; + self.php_tx.send(ProcessEvent::Stop).await?; + let cmd = self.config.clone().cmd; + self.php_tx.send(ProcessEvent::Start(cmd.unwrap())).await?; + }, AppEvent::EvalCancel => { self.active_dialog = None; } diff --git a/src/event/input.rs b/src/event/input.rs index 04369ac..887f2e4 100644 --- a/src/event/input.rs +++ b/src/event/input.rs @@ -59,6 +59,7 @@ pub enum AppEvent { NextChannel, FocusChannel(String), ChannelLog(String,String), + RestartProcess, } pub type EventSender = Sender; diff --git a/src/php_process.rs b/src/php_process.rs index b1a52c1..eda1252 100644 --- a/src/php_process.rs +++ b/src/php_process.rs @@ -1,18 +1,16 @@ use std::{process::Stdio, str::from_utf8}; use tokio::io::AsyncReadExt; -use tokio::process::Child; use tokio::select; use tokio::sync::mpsc::Receiver; use tokio::{io::BufReader, process::Command, sync::mpsc::Sender, task}; -use crate::{channel::Channels, event::input::AppEvent}; +use crate::event::input::AppEvent; #[derive(Debug)] pub enum ProcessEvent { Start(Vec), Stop, - Restart, } pub fn process_manager_start( @@ -29,7 +27,6 @@ pub fn process_manager_start( let args = match event { ProcessEvent::Start(args) => args, ProcessEvent::Stop => continue, - ProcessEvent::Restart => continue, }; let program = match args.first() { @@ -56,7 +53,7 @@ pub fn process_manager_start( select! { read = stdoutreader.read(&mut stdout_buff) => { if let Ok(s) = from_utf8(&stdout_buff[..read.unwrap()]) { - if s.len() > 0 { + if !s.is_empty() { sender .send(AppEvent::ChannelLog("stdout".to_string(), s.to_string())) .await @@ -66,7 +63,7 @@ pub fn process_manager_start( }, read = stderrreader.read(&mut stderr_buff) => { if let Ok(s) = from_utf8(&stderr_buff[..read.unwrap()]) { - if s.len() > 0 { + if !s.is_empty() { sender .send(AppEvent::ChannelLog("stderr".to_string(), s.to_string())) .await @@ -81,7 +78,7 @@ pub fn process_manager_start( loop { select! { _ = process.wait() => { - return; + break; }, cmd = receiver.recv() => { let event = match cmd { @@ -94,11 +91,6 @@ pub fn process_manager_start( process.kill().await.unwrap_or_default(); break; }, - ProcessEvent::Restart => { - // TODO: restart - process.kill().await.unwrap_or_default(); - break; - }, }; }, }; diff --git a/src/view/session.rs b/src/view/session.rs index 0e14037..e2d279d 100644 --- a/src/view/session.rs +++ b/src/view/session.rs @@ -75,6 +75,7 @@ impl View for SessionView { '+' => Some(AppEvent::ContextDepth(1)), '-' => Some(AppEvent::ContextDepth(-1)), 'r' => Some(AppEvent::Run), + 'R' => Some(AppEvent::RestartProcess), 'n' => Some(AppEvent::StepInto), 'N' => Some(AppEvent::StepOver), 'o' => Some(AppEvent::StepOut), @@ -90,6 +91,7 @@ impl View for SessionView { 'n' => Some(AppEvent::HistoryNext), 'p' => Some(AppEvent::HistoryPrevious), 'd' => Some(AppEvent::Disconnect), + 'R' => Some(AppEvent::RestartProcess), 'b' => escape(app), _ => None, }, From 2a2ae99da4f28ae7a9db6d72dfba784182748493 Mon Sep 17 00:00:00 2001 From: Daniel Leech Date: Sun, 9 Nov 2025 22:45:40 +0000 Subject: [PATCH 14/49] Show error when process terminates abnormally --- src/app.rs | 11 +++++++++-- src/event/input.rs | 1 + src/php_process.rs | 19 ++++++++++++++++++- src/view/help.rs | 1 + 4 files changed, 29 insertions(+), 3 deletions(-) diff --git a/src/app.rs b/src/app.rs index 097dc45..c83d9b9 100644 --- a/src/app.rs +++ b/src/app.rs @@ -573,6 +573,9 @@ impl App { AppEvent::FocusChannel(name) => { self.session_view.eval_state.focus(&self.channels, name); }, + AppEvent::NotifyError(message) => { + self.notification = Notification::error(message); + }, AppEvent::ChannelLog(channel, chunk) => { let buffer = self.channels.get_mut(channel.as_str()).buffer.clone(); buffer.lock().await.push(chunk.to_string()); @@ -585,8 +588,12 @@ impl App { self.sender.send(AppEvent::Disconnect).await?; self.sender.send(AppEvent::Listen).await?; self.php_tx.send(ProcessEvent::Stop).await?; - let cmd = self.config.clone().cmd; - self.php_tx.send(ProcessEvent::Start(cmd.unwrap())).await?; + match self.config.clone().cmd { + Some(cmd) => { + self.php_tx.send(ProcessEvent::Start(cmd)).await?; + } + None => (), + }; }, AppEvent::EvalCancel => { self.active_dialog = None; diff --git a/src/event/input.rs b/src/event/input.rs index 887f2e4..8e9547f 100644 --- a/src/event/input.rs +++ b/src/event/input.rs @@ -60,6 +60,7 @@ pub enum AppEvent { FocusChannel(String), ChannelLog(String,String), RestartProcess, + NotifyError(String), } pub type EventSender = Sender; diff --git a/src/php_process.rs b/src/php_process.rs index eda1252..96a8c12 100644 --- a/src/php_process.rs +++ b/src/php_process.rs @@ -75,9 +75,26 @@ pub fn process_manager_start( } }); + let sender = parent_sender.clone(); loop { select! { - _ = process.wait() => { + exit_code = process.wait() => { + match exit_code { + Ok(exit_code) => { + if exit_code.code().unwrap_or_default() != 0 { + let _ = sender.send( + AppEvent::NotifyError( + format!( + "Process '{:?}' exited with code {}", + args, + exit_code.code().unwrap_or_default() + ) + ) + ).await; + } + }, + Err(e) => (), + } break; }, cmd = receiver.recv() => { diff --git a/src/view/help.rs b/src/view/help.rs index cb35d89..0011a71 100644 --- a/src/view/help.rs +++ b/src/view/help.rs @@ -35,6 +35,7 @@ Help for you - press any key to return. Key mappings (prefix with number to repeat): [r] run +[R] restart php script (if provided) [n] next / step into [N] step over [p] previous (switches to history mode if in current mode) From 042d591af8053a5a7ce8b37886469a0d2689a0ae Mon Sep 17 00:00:00 2001 From: Daniel Leech Date: Sun, 9 Nov 2025 23:07:08 +0000 Subject: [PATCH 15/49] Clippy --- src/app.rs | 7 ++----- src/php_process.rs | 2 +- src/view/eval.rs | 6 +++++- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/app.rs b/src/app.rs index c83d9b9..a926e9e 100644 --- a/src/app.rs +++ b/src/app.rs @@ -588,11 +588,8 @@ impl App { self.sender.send(AppEvent::Disconnect).await?; self.sender.send(AppEvent::Listen).await?; self.php_tx.send(ProcessEvent::Stop).await?; - match self.config.clone().cmd { - Some(cmd) => { - self.php_tx.send(ProcessEvent::Start(cmd)).await?; - } - None => (), + if let Some(cmd) = self.config.clone().cmd { + self.php_tx.send(ProcessEvent::Start(cmd)).await?; }; }, AppEvent::EvalCancel => { diff --git a/src/php_process.rs b/src/php_process.rs index 96a8c12..79ee556 100644 --- a/src/php_process.rs +++ b/src/php_process.rs @@ -93,7 +93,7 @@ pub fn process_manager_start( ).await; } }, - Err(e) => (), + Err(_) => (), } break; }, diff --git a/src/view/eval.rs b/src/view/eval.rs index ffb1dfc..f55d3d1 100644 --- a/src/view/eval.rs +++ b/src/view/eval.rs @@ -213,7 +213,11 @@ mod test { 0, ); assert_eq!( - vec!["foo string = \"\"{", " bar string = \"\"", "}",], + vec![ + "foo string = \"{", + " bar string = \"\"", + "}", + ], lines .iter() .map(|l| { l.to_string() }) From 8e9a455e57b30fd30a50abff55d87725c3339577 Mon Sep 17 00:00:00 2001 From: Daniel Leech Date: Wed, 12 Nov 2025 19:34:54 +0000 Subject: [PATCH 16/49] Fix channel names --- Cargo.lock | 10 + Cargo.toml | 1 + debug.loh | 29 + demo/demo.tape | 108 ++ example/demo/demo.gif | Bin 0 -> 597378 bytes php/large.php | 2536 +++++++++++++++++++++++++++++++++++++++++ php/test.php | 7 +- src/channel.rs | 2 +- 8 files changed, 2690 insertions(+), 3 deletions(-) create mode 100644 debug.loh create mode 100644 demo/demo.tape create mode 100644 example/demo/demo.gif create mode 100644 php/large.php diff --git a/Cargo.lock b/Cargo.lock index 2e97599..a07fb1b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -339,6 +339,7 @@ dependencies = [ "clap", "crossterm", "log", + "ordered_hash_map", "pretty_assertions", "ratatui", "serde", @@ -608,6 +609,15 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "ordered_hash_map" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6c699f8a30f345785be969deed7eee4c73a5de58c7faf61d6a3251ef798ff61" +dependencies = [ + "hashbrown", +] + [[package]] name = "parking_lot" version = "0.12.3" diff --git a/Cargo.toml b/Cargo.toml index 1cc77f8..673d0db 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,7 @@ better-panic = "0.3.0" clap = { version = "4.5.35", features = ["derive"] } crossterm = "0.28.1" log = "0.4.27" +ordered_hash_map = "0.5.0" pretty_assertions = "1.4.1" ratatui = "0.29.0" serde = { version = "1.0.219", features = ["derive"] } diff --git a/debug.loh b/debug.loh new file mode 100644 index 0000000..e68b31e --- /dev/null +++ b/debug.loh @@ -0,0 +1,29 @@ +[00:00:00.000] (7ffa3507f440) INFO Handling event Startup +[00:00:00.000] (7ffa2edf66c0) TRACE registering event source with poller: token=Token(140711450119680), interests=READABLE | WRITABLE +[00:00:00.648] (7ffa34dff6c0) TRACE registering event source with poller: token=Token(140712523862528), interests=READABLE | WRITABLE +[00:00:00.648] (7ffa3507f440) INFO Handling event ClientConnected(PollEvented { io: Some(TcpStream { addr: 127.0.0.1:9003, peer: 127.0.0.1:47680, fd: 14 }) }) +[00:00:00.648] (7ffa3507f440) DEBUG [dbgp] << + +[00:00:00.648] (7ffa3507f440) INFO setting feature max_depth to "4" +[00:00:00.648] (7ffa3507f440) DEBUG [dbgp] >> feature_set -i 0 -n max_depth -v 4 +[00:00:00.648] (7ffa3507f440) DEBUG [dbgp] << + +[00:00:00.648] (7ffa3507f440) INFO setting feature extended_properties to "1" +[00:00:00.648] (7ffa3507f440) DEBUG [dbgp] >> feature_set -i 1 -n extended_properties -v 1 +[00:00:00.648] (7ffa3507f440) DEBUG [dbgp] << + +[00:00:00.649] (7ffa3507f440) DEBUG [dbgp] >> source -i 2 -f file:///home/daniel/www/dantleech/debug-tui/php/test.php +[00:00:00.652] (7ffa3507f440) DEBUG [dbgp] << + +[00:00:06.466] (7ffa3507f440) INFO Handling event Input(KeyEvent { code: Char('r'), modifiers: KeyModifiers(0x0), kind: Press, state: KeyEventState(0x0) }) +[00:00:06.478] (7ffa3507f440) INFO Handling event Run +[00:00:06.478] (7ffa34dff6c0) INFO Running iteration 0/1 +[00:00:06.478] (7ffa34dff6c0) DEBUG [dbgp] >> run -i 3 +[00:00:06.478] (7ffa2eff76c0) DEBUG [dbgp] << + +[00:00:06.481] (7ffa3507f440) INFO Handling event UpdateStatus(Stopping) +[00:00:06.484] (7ffa3507f440) INFO Handling event Disconnect +[00:00:06.484] (7ffa3507f440) TRACE deregistering event source from poller +[00:00:06.487] (7ffa3507f440) INFO Handling event ChangeSessionViewMode(History) +[00:00:09.778] (7ffa3507f440) INFO Handling event Quit +[00:00:09.779] (7ffa347fc6c0) TRACE deregistering event source from poller diff --git a/demo/demo.tape b/demo/demo.tape new file mode 100644 index 0000000..9713a85 --- /dev/null +++ b/demo/demo.tape @@ -0,0 +1,108 @@ +# VHS documentation +# +# Output: +# Output .gif Create a GIF output at the given +# Output .mp4 Create an MP4 output at the given +# Output .webm Create a WebM output at the given +# +# Require: +# Require Ensure a program is on the $PATH to proceed +# +# Settings: +# Set FontSize Set the font size of the terminal +# Set FontFamily Set the font family of the terminal +# Set Height Set the height of the terminal +# Set Width Set the width of the terminal +# Set LetterSpacing Set the font letter spacing (tracking) +# Set LineHeight Set the font line height +# Set LoopOffset % Set the starting frame offset for the GIF loop +# Set Theme Set the theme of the terminal +# Set Padding Set the padding of the terminal +# Set Framerate Set the framerate of the recording +# Set PlaybackSpeed Set the playback speed of the recording +# Set MarginFill Set the file or color the margin will be filled with. +# Set Margin Set the size of the margin. Has no effect if MarginFill isn't set. +# Set BorderRadius Set terminal border radius, in pixels. +# Set WindowBar Set window bar type. (one of: Rings, RingsRight, Colorful, ColorfulRight) +# Set WindowBarSize Set window bar size, in pixels. Default is 40. +# Set TypingSpeed