From 4779eebdceb242afe6d6f188cbd0385a9b631994 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Thu, 1 Sep 2022 17:50:22 -0700 Subject: [PATCH 01/12] fix shift-enter --- crates/terminal/src/mappings/keys.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/terminal/src/mappings/keys.rs b/crates/terminal/src/mappings/keys.rs index da730e62968d6e3d15ae860068b56aab2c55a22c..7e0c6e3d17ce8c3ca1d26859abf1217430f9be0f 100644 --- a/crates/terminal/src/mappings/keys.rs +++ b/crates/terminal/src/mappings/keys.rs @@ -57,6 +57,7 @@ pub fn to_esc_str(keystroke: &Keystroke, mode: &TermMode) -> Option { ("tab", Modifiers::None) => Some("\x09".to_string()), ("escape", Modifiers::None) => Some("\x1b".to_string()), ("enter", Modifiers::None) => Some("\x0d".to_string()), + ("enter", Modifiers::Shift) => Some("\x0d".to_string()), ("backspace", Modifiers::None) => Some("\x7f".to_string()), //Interesting escape codes ("tab", Modifiers::Shift) => Some("\x1b[Z".to_string()), From 7730039e3158ccfc1f65039ff1d0e3cb4d73dde2 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Thu, 1 Sep 2022 20:07:30 -0700 Subject: [PATCH 02/12] Sketched out program manager API --- crates/gpui/src/app.rs | 21 ++++++++++++++ crates/terminal/src/modal.rs | 56 +++++++++++++++++++++++++++++++++++- 2 files changed, 76 insertions(+), 1 deletion(-) diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 4c8bed04919d50a1dbe1b9a30b0246f5ac7ce8de..e9091d74c8001c59c03beb6c2e1f4101ae567035 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -5083,6 +5083,7 @@ impl Drop for AnyModelHandle { } } +#[derive(Hash, PartialEq, Eq, Debug)] pub struct AnyWeakModelHandle { model_id: usize, model_type: TypeId, @@ -5092,6 +5093,26 @@ impl AnyWeakModelHandle { pub fn upgrade(&self, cx: &impl UpgradeModelHandle) -> Option { cx.upgrade_any_model_handle(self) } + pub fn model_type(&self) -> TypeId { + self.model_type + } + + fn is(&self) -> bool { + TypeId::of::() == self.model_type + } + + pub fn downcast(&self) -> Option> { + if self.is::() { + let result = Some(WeakModelHandle { + model_id: self.model_id, + model_type: PhantomData, + }); + + result + } else { + None + } + } } impl From> for AnyWeakModelHandle { diff --git a/crates/terminal/src/modal.rs b/crates/terminal/src/modal.rs index 504a4b84ab12c8665afdb0b2fa95db199f5d4998..97dcd40afdb439cb557f882ef4885b777463112c 100644 --- a/crates/terminal/src/modal.rs +++ b/crates/terminal/src/modal.rs @@ -1,4 +1,9 @@ -use gpui::{ModelHandle, ViewContext}; +use std::{ + any::TypeId, + collections::{HashMap, HashSet}, +}; + +use gpui::{AnyWeakModelHandle, Entity, ModelHandle, ViewContext, WeakModelHandle}; use settings::{Settings, WorkingDirectory}; use workspace::Workspace; @@ -9,10 +14,59 @@ use crate::{ Event, Terminal, }; +// TODO: Need to put this basic structure in workspace, and make 'program handles' +// based off of the 'searchable item' pattern except with models this way, the workspace's clients +// can register their models as programs. +// Programs are: +// - Kept alive by the program manager, they need to emit an event to get dropped from it +// - Can be interacted with directly, (closed, activated), etc, bypassing associated view(s) +// - Have special rendering methods that the program manager offers to fill out the status bar +// - Can emit events for the program manager which: +// - Add a jewel (notification, change, etc.) +// - Drop the program +// - ??? +// - Program Manager is kept in a global, listens for window drop so it can drop all it's program handles +// - Start by making up the infrastructure, then just render the first item as the modal terminal in it's spot +// update), + +struct ProgramManager { + window_to_programs: HashMap>, +} + +impl ProgramManager { + pub fn add_program(&mut self, window: usize, program: WeakModelHandle) { + let mut programs = if let Some(programs) = self.window_to_programs.remove(&window) { + programs + } else { + HashSet::default() + }; + + programs.insert(AnyWeakModelHandle::from(program)); + self.window_to_programs.insert(window, programs); + } + + pub fn get_programs( + &self, + window: &usize, + ) -> impl Iterator> + '_ { + self.window_to_programs + .get(window) + .into_iter() + .flat_map(|programs| { + programs + .iter() + .filter(|program| program.model_type() != TypeId::of::()) + .map(|program| program.downcast().unwrap()) + }) + } +} + #[derive(Debug)] struct StoredTerminal(ModelHandle); pub fn deploy_modal(workspace: &mut Workspace, _: &DeployModal, cx: &mut ViewContext) { + // cx.window_id() + // Pull the terminal connection out of the global if it has been stored let possible_terminal = cx.update_default_global::, _, _>(|possible_connection, _| { From 1502c19208429ce7956c3ca57d15a64af498c959 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Fri, 2 Sep 2022 15:47:35 -0700 Subject: [PATCH 03/12] Polished scrolling significantly --- crates/gpui/src/platform/event.rs | 11 ++++ crates/gpui/src/platform/mac/event.rs | 13 +++- crates/terminal/src/terminal.rs | 94 +++++++++++++++++---------- 3 files changed, 81 insertions(+), 37 deletions(-) diff --git a/crates/gpui/src/platform/event.rs b/crates/gpui/src/platform/event.rs index 3e0fe4a53a408f87a525d31154ccb688efdc47e7..48043ac9186b0701ab7d08fe86de30941ae56dda 100644 --- a/crates/gpui/src/platform/event.rs +++ b/crates/gpui/src/platform/event.rs @@ -19,6 +19,15 @@ pub struct ModifiersChangedEvent { pub cmd: bool, } +/// The phase of a touch motion event. +/// Based on the winit enum of the same name, +#[derive(Clone, Copy, Debug)] +pub enum TouchPhase { + Started, + Moved, + Ended, +} + #[derive(Clone, Copy, Debug, Default)] pub struct ScrollWheelEvent { pub position: Vector2F, @@ -28,6 +37,8 @@ pub struct ScrollWheelEvent { pub alt: bool, pub shift: bool, pub cmd: bool, + /// If the platform supports returning the phase of a scroll wheel event, it will be stored here + pub phase: Option, } #[derive(Hash, PartialEq, Eq, Copy, Clone, Debug)] diff --git a/crates/gpui/src/platform/mac/event.rs b/crates/gpui/src/platform/mac/event.rs index c6f838b431ac232463501be6f21e1ad8aea38e25..51524f4b15742733b8e865ade79f6d7266b6fc83 100644 --- a/crates/gpui/src/platform/mac/event.rs +++ b/crates/gpui/src/platform/mac/event.rs @@ -3,10 +3,10 @@ use crate::{ keymap::Keystroke, platform::{Event, NavigationDirection}, KeyDownEvent, KeyUpEvent, ModifiersChangedEvent, MouseButton, MouseButtonEvent, - MouseMovedEvent, ScrollWheelEvent, + MouseMovedEvent, ScrollWheelEvent, TouchPhase, }; use cocoa::{ - appkit::{NSEvent, NSEventModifierFlags, NSEventType}, + appkit::{NSEvent, NSEventModifierFlags, NSEventPhase, NSEventType}, base::{id, YES}, foundation::NSString as _, }; @@ -150,6 +150,14 @@ impl Event { NSEventType::NSScrollWheel => window_height.map(|window_height| { let modifiers = native_event.modifierFlags(); + let phase = match native_event.phase() { + NSEventPhase::NSEventPhaseMayBegin | NSEventPhase::NSEventPhaseBegan => { + Some(TouchPhase::Started) + } + NSEventPhase::NSEventPhaseEnded => Some(TouchPhase::Ended), + _ => Some(TouchPhase::Moved), + }; + Self::ScrollWheel(ScrollWheelEvent { position: vec2f( native_event.locationInWindow().x as f32, @@ -159,6 +167,7 @@ impl Event { native_event.scrollingDeltaX() as f32, native_event.scrollingDeltaY() as f32, ), + phase, precise: native_event.hasPreciseScrollingDeltas() == YES, ctrl: modifiers.contains(NSEventModifierFlags::NSControlKeyMask), alt: modifiers.contains(NSEventModifierFlags::NSAlternateKeyMask), diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 42ec061290a31a39e0b7bc1dfb07859ee81255bb..7a35ea1a3ed01a68e9498031faf70865ebeca136 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -72,7 +72,7 @@ pub fn init(cx: &mut MutableAppContext) { ///Scrolling is unbearably sluggish by default. Alacritty supports a configurable ///Scroll multiplier that is set to 3 by default. This will be removed when I ///Implement scroll bars. -const ALACRITTY_SCROLL_MULTIPLIER: f32 = 3.; +const SCROLL_MULTIPLIER: f32 = 4.; const MAX_SEARCH_LINES: usize = 100; const DEBUG_TERMINAL_WIDTH: f32 = 500.; const DEBUG_TERMINAL_HEIGHT: f32 = 30.; @@ -381,6 +381,7 @@ impl TerminalBuilder { shell_pid, foreground_process_info: None, breadcrumb_text: String::new(), + scroll_px: 0., }; Ok(TerminalBuilder { @@ -500,6 +501,7 @@ pub struct Terminal { shell_pid: u32, shell_fd: u32, foreground_process_info: Option, + scroll_px: f32, } impl Terminal { @@ -893,44 +895,66 @@ impl Terminal { ///Scroll the terminal pub fn scroll_wheel(&mut self, e: &ScrollWheelEvent, origin: Vector2F) { - if self.mouse_mode(e.shift) { - //TODO: Currently this only sends the current scroll reports as they come in. Alacritty - //Sends the *entire* scroll delta on *every* scroll event, only resetting it when - //The scroll enters 'TouchPhase::Started'. Do I need to replicate this? - //This would be consistent with a scroll model based on 'distance from origin'... - let scroll_lines = (e.delta.y() / self.cur_size.line_height) as i32; - let point = mouse_point( - e.position.sub(origin), - self.cur_size, - self.last_content.display_offset, - ); - - if let Some(scrolls) = - scroll_report(point, scroll_lines as i32, e, self.last_content.mode) + let mouse_mode = self.mouse_mode(e.shift); + + if let Some(scroll_lines) = self.determine_scroll_lines(e, mouse_mode) { + if mouse_mode { + let point = mouse_point( + e.position.sub(origin), + self.cur_size, + self.last_content.display_offset, + ); + + if let Some(scrolls) = + scroll_report(point, scroll_lines as i32, e, self.last_content.mode) + { + for scroll in scrolls { + self.pty_tx.notify(scroll); + } + }; + } else if self + .last_content + .mode + .contains(TermMode::ALT_SCREEN | TermMode::ALTERNATE_SCROLL) + && !e.shift { - for scroll in scrolls { - self.pty_tx.notify(scroll); + self.pty_tx.notify(alt_scroll(scroll_lines)) + } else { + if scroll_lines != 0 { + let scroll = AlacScroll::Delta(scroll_lines); + + self.events.push_back(InternalEvent::Scroll(scroll)); } - }; - } else if self - .last_content - .mode - .contains(TermMode::ALT_SCREEN | TermMode::ALTERNATE_SCROLL) - && !e.shift - { - //TODO: See above TODO, also applies here. - let scroll_lines = - ((e.delta.y() * ALACRITTY_SCROLL_MULTIPLIER) / self.cur_size.line_height) as i32; - - self.pty_tx.notify(alt_scroll(scroll_lines)) - } else { - let scroll_lines = - ((e.delta.y() * ALACRITTY_SCROLL_MULTIPLIER) / self.cur_size.line_height) as i32; - if scroll_lines != 0 { - let scroll = AlacScroll::Delta(scroll_lines); + } + } + } + + fn determine_scroll_lines(&mut self, e: &ScrollWheelEvent, mouse_mode: bool) -> Option { + let scroll_multiplier = if mouse_mode { 1. } else { SCROLL_MULTIPLIER }; + + match e.phase { + /* Reset scroll state on started */ + Some(gpui::TouchPhase::Started) => { + self.scroll_px = 0.; + None + } + /* Calculate the appropriate scroll lines */ + Some(gpui::TouchPhase::Moved) => { + let old_offset = (self.scroll_px / self.cur_size.line_height) as i32; + + self.scroll_px += e.delta.y() * scroll_multiplier; + + let new_offset = (self.scroll_px / self.cur_size.line_height) as i32; + + // Whenever we hit the edges, reset our stored scroll to 0 + // so we can respond to changes in direction quickly + self.scroll_px %= self.cur_size.height; - self.events.push_back(InternalEvent::Scroll(scroll)); + Some(new_offset - old_offset) } + /* Fall back to delta / line_height */ + None => Some(((e.delta.y() * scroll_multiplier) / self.cur_size.line_height) as i32), + _ => None, } } From 1375c5f1a154a43995d76dea3ee526789347884a Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Fri, 2 Sep 2022 16:45:58 -0700 Subject: [PATCH 04/12] Began program manager, made terminal modals per-window --- crates/terminal/src/modal.rs | 83 +++++-------------------------- crates/workspace/src/programs.rs | 77 ++++++++++++++++++++++++++++ crates/workspace/src/workspace.rs | 4 ++ 3 files changed, 93 insertions(+), 71 deletions(-) create mode 100644 crates/workspace/src/programs.rs diff --git a/crates/terminal/src/modal.rs b/crates/terminal/src/modal.rs index 97dcd40afdb439cb557f882ef4885b777463112c..bf83196a97eb4e2e713248b6287565e504a65990 100644 --- a/crates/terminal/src/modal.rs +++ b/crates/terminal/src/modal.rs @@ -1,11 +1,6 @@ -use std::{ - any::TypeId, - collections::{HashMap, HashSet}, -}; - -use gpui::{AnyWeakModelHandle, Entity, ModelHandle, ViewContext, WeakModelHandle}; +use gpui::{ModelHandle, ViewContext}; use settings::{Settings, WorkingDirectory}; -use workspace::Workspace; +use workspace::{programs::ProgramManager, Workspace}; use crate::{ terminal_container_view::{ @@ -14,73 +9,20 @@ use crate::{ Event, Terminal, }; -// TODO: Need to put this basic structure in workspace, and make 'program handles' -// based off of the 'searchable item' pattern except with models this way, the workspace's clients -// can register their models as programs. -// Programs are: -// - Kept alive by the program manager, they need to emit an event to get dropped from it -// - Can be interacted with directly, (closed, activated), etc, bypassing associated view(s) -// - Have special rendering methods that the program manager offers to fill out the status bar -// - Can emit events for the program manager which: -// - Add a jewel (notification, change, etc.) -// - Drop the program -// - ??? -// - Program Manager is kept in a global, listens for window drop so it can drop all it's program handles -// - Start by making up the infrastructure, then just render the first item as the modal terminal in it's spot -// update), - -struct ProgramManager { - window_to_programs: HashMap>, -} - -impl ProgramManager { - pub fn add_program(&mut self, window: usize, program: WeakModelHandle) { - let mut programs = if let Some(programs) = self.window_to_programs.remove(&window) { - programs - } else { - HashSet::default() - }; - - programs.insert(AnyWeakModelHandle::from(program)); - self.window_to_programs.insert(window, programs); - } - - pub fn get_programs( - &self, - window: &usize, - ) -> impl Iterator> + '_ { - self.window_to_programs - .get(window) - .into_iter() - .flat_map(|programs| { - programs - .iter() - .filter(|program| program.model_type() != TypeId::of::()) - .map(|program| program.downcast().unwrap()) - }) - } -} - -#[derive(Debug)] -struct StoredTerminal(ModelHandle); - pub fn deploy_modal(workspace: &mut Workspace, _: &DeployModal, cx: &mut ViewContext) { - // cx.window_id() + let window = cx.window_id(); // Pull the terminal connection out of the global if it has been stored - let possible_terminal = - cx.update_default_global::, _, _>(|possible_connection, _| { - possible_connection.take() - }); + let possible_terminal = ProgramManager::remove::(window, cx); - if let Some(StoredTerminal(stored_terminal)) = possible_terminal { + if let Some(terminal_handle) = possible_terminal { workspace.toggle_modal(cx, |_, cx| { // Create a view from the stored connection if the terminal modal is not already shown - cx.add_view(|cx| TerminalContainer::from_terminal(stored_terminal.clone(), true, cx)) + cx.add_view(|cx| TerminalContainer::from_terminal(terminal_handle.clone(), true, cx)) }); // Toggle Modal will dismiss the terminal modal if it is currently shown, so we must // store the terminal back in the global - cx.set_global::>(Some(StoredTerminal(stored_terminal.clone()))); + ProgramManager::insert_or_replace::(window, terminal_handle, cx); } else { // No connection was stored, create a new terminal if let Some(closed_terminal_handle) = workspace.toggle_modal(cx, |workspace, cx| { @@ -101,21 +43,19 @@ pub fn deploy_modal(workspace: &mut Workspace, _: &DeployModal, cx: &mut ViewCon cx.subscribe(&terminal_handle, on_event).detach(); // Set the global immediately if terminal construction was successful, // in case the user opens the command palette - cx.set_global::>(Some(StoredTerminal( - terminal_handle.clone(), - ))); + ProgramManager::insert_or_replace::(window, terminal_handle, cx); } this }) { - // Terminal modal was dismissed. Store terminal if the terminal view is connected + // Terminal modal was dismissed and the terminal view is connected, store the terminal if let TerminalContainerContent::Connected(connected) = &closed_terminal_handle.read(cx).content { let terminal_handle = connected.read(cx).handle(); // Set the global immediately if terminal construction was successful, // in case the user opens the command palette - cx.set_global::>(Some(StoredTerminal(terminal_handle))); + ProgramManager::insert_or_replace::(window, terminal_handle, cx); } } } @@ -129,7 +69,8 @@ pub fn on_event( ) { // Dismiss the modal if the terminal quit if let Event::CloseTerminal = event { - cx.set_global::>(None); + ProgramManager::remove::(cx.window_id(), cx); + if workspace.modal::().is_some() { workspace.dismiss_modal(cx) } diff --git a/crates/workspace/src/programs.rs b/crates/workspace/src/programs.rs new file mode 100644 index 0000000000000000000000000000000000000000..36169ea4c70ad174e666669349656ea2c70e9aa9 --- /dev/null +++ b/crates/workspace/src/programs.rs @@ -0,0 +1,77 @@ +// TODO: Need to put this basic structure in workspace, and make 'program handles' +// based off of the 'searchable item' pattern except with models. This way, the workspace's clients +// can register their models as programs with a specific identity and capable of notifying the workspace +// Programs are: +// - Kept alive by the program manager, they need to emit an event to get dropped from it +// - Can be interacted with directly, (closed, activated, etc.) by the program manager, bypassing +// associated view(s) +// - Have special rendering methods that the program manager requires them to implement to fill out +// the status bar +// - Can emit events for the program manager which: +// - Add a jewel (notification, change, etc.) +// - Drop the program +// - ??? +// - Program Manager is kept in a global, listens for window drop so it can drop all it's program handles + +use collections::HashMap; +use gpui::{AnyModelHandle, Entity, ModelHandle, View, ViewContext}; + +/// This struct is going to be the starting point for the 'program manager' feature that will +/// eventually be implemented to provide a collaborative way of engaging with identity-having +/// features like the terminal. +pub struct ProgramManager { + // TODO: Make this a hashset or something + modals: HashMap, +} + +impl ProgramManager { + pub fn insert_or_replace( + window: usize, + program: ModelHandle, + cx: &mut ViewContext, + ) -> Option { + cx.update_global::(|pm, _| { + pm.insert_or_replace_internal::(window, program) + }) + } + + pub fn remove( + window: usize, + cx: &mut ViewContext, + ) -> Option> { + cx.update_global::(|pm, _| pm.remove_internal::(window)) + } + + pub fn new() -> Self { + Self { + modals: Default::default(), + } + } + + /// Inserts or replaces the model at the given location. + fn insert_or_replace_internal( + &mut self, + window: usize, + program: ModelHandle, + ) -> Option { + self.modals.insert(window, AnyModelHandle::from(program)) + } + + /// Remove the program associated with this window, if it's of the given type + fn remove_internal(&mut self, window: usize) -> Option> { + let program = self.modals.remove(&window); + if let Some(program) = program { + if program.is::() { + // Guaranteed to be some, but leave it in the option + // anyway for the API + program.downcast() + } else { + // Model is of the incorrect type, put it back + self.modals.insert(window, program); + None + } + } else { + None + } + } +} diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index ac24467343165e67db2a2ac105b48fb03674bac6..94424f63fa0bf873b9e260252f878d6c4a2e413f 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -5,6 +5,7 @@ /// specific locations. pub mod pane; pub mod pane_group; +pub mod programs; pub mod searchable; pub mod sidebar; mod status_bar; @@ -36,6 +37,7 @@ use log::error; pub use pane::*; pub use pane_group::*; use postage::prelude::Stream; +use programs::ProgramManager; use project::{fs, Fs, Project, ProjectEntryId, ProjectPath, ProjectStore, Worktree, WorktreeId}; use searchable::SearchableItemHandle; use serde::Deserialize; @@ -146,6 +148,8 @@ impl_actions!(workspace, [ToggleProjectOnline, ActivatePane]); pub fn init(app_state: Arc, cx: &mut MutableAppContext) { pane::init(cx); + cx.set_global(ProgramManager::new()); + cx.add_global_action(open); cx.add_global_action({ let app_state = Arc::downgrade(&app_state); From aabc6ce8bdf4ab133ac4dcea3fc28b7623355979 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Fri, 2 Sep 2022 18:22:53 -0700 Subject: [PATCH 05/12] Fixed terminal title showing program, WIP showing exe arguments --- crates/terminal/src/terminal.rs | 57 ++++++++++++++++++------------- crates/workspace/src/workspace.rs | 5 +-- 2 files changed, 36 insertions(+), 26 deletions(-) diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 7a35ea1a3ed01a68e9498031faf70865ebeca136..7b504de4ae40aa326cb4b6a321aa46ffdccaf510 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -537,16 +537,42 @@ impl Terminal { } AlacTermEvent::Wakeup => { cx.emit(Event::Wakeup); - cx.notify(); + + if self.update_process_info() { + cx.emit(Event::TitleChanged) + } } AlacTermEvent::ColorRequest(idx, fun_ptr) => { self.events .push_back(InternalEvent::ColorRequest(*idx, fun_ptr.clone())); - cx.notify(); //Immediately schedule a render to respond to the color request } } } + /// Update the cached process info, returns whether the Zed-relevant info has changed + fn update_process_info(&mut self) -> bool { + let mut pid = unsafe { libc::tcgetpgrp(self.shell_fd as i32) }; + if pid < 0 { + pid = self.shell_pid as i32; + } + + if let Some(process_info) = LocalProcessInfo::with_root_pid(pid as u32) { + let res = self + .foreground_process_info + .as_ref() + .map(|old_info| { + process_info.cwd != old_info.cwd || process_info.name != old_info.name + }) + .unwrap_or(true); + + self.foreground_process_info = Some(process_info.clone()); + + res + } else { + false + } + } + ///Takes events from Alacritty and translates them to behavior on this view fn process_terminal_event( &mut self, @@ -680,7 +706,7 @@ impl Terminal { let mut terminal = if let Some(term) = term.try_lock_unfair() { term } else if self.last_synced.elapsed().as_secs_f32() > 0.25 { - term.lock_unfair() + term.lock_unfair() //It's been too long, force block } else if let None = self.sync_task { //Skip this frame let delay = cx.background().timer(Duration::from_millis(16)); @@ -701,24 +727,15 @@ impl Terminal { return; }; + if self.update_process_info() { + cx.emit(Event::TitleChanged); + } + //Note that the ordering of events matters for event processing while let Some(e) = self.events.pop_front() { self.process_terminal_event(&e, &mut terminal, cx) } - if let Some(process_info) = self.compute_process_info() { - let should_emit_title_changed = self - .foreground_process_info - .as_ref() - .map(|old_info| { - process_info.cwd != old_info.cwd || process_info.name != old_info.name - }) - .unwrap_or(true); - if should_emit_title_changed { - cx.emit(Event::TitleChanged) - } - self.foreground_process_info = Some(process_info.clone()); - } self.last_content = Self::make_content(&terminal); self.last_synced = Instant::now(); } @@ -984,14 +1001,6 @@ impl Terminal { make_search_matches(&term, &searcher).collect() }) } - - fn compute_process_info(&self) -> Option { - let mut pid = unsafe { libc::tcgetpgrp(self.shell_fd as i32) }; - if pid < 0 { - pid = self.shell_pid as i32; - } - LocalProcessInfo::with_root_pid(pid as u32) - } } impl Drop for Terminal { diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 94424f63fa0bf873b9e260252f878d6c4a2e413f..a527671525614ffb4ca33ff69c8eb1696e94d566 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -146,10 +146,11 @@ impl_internal_actions!( impl_actions!(workspace, [ToggleProjectOnline, ActivatePane]); pub fn init(app_state: Arc, cx: &mut MutableAppContext) { - pane::init(cx); - + // Initialize the program manager immediately cx.set_global(ProgramManager::new()); + pane::init(cx); + cx.add_global_action(open); cx.add_global_action({ let app_state = Arc::downgrade(&app_state); From 90bae80bb28c163db59d9f4a4ab2312a2c89a199 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Fri, 2 Sep 2022 19:48:34 -0700 Subject: [PATCH 06/12] WIP fixing arguments in title bug, need to validate fix, add to wezterm, push to our copy, refresh our cargo, and make a PR for wezterm. TODO: Learn how to do c-style buffer munging. --- Cargo.lock | 1 - crates/procinfo/Cargo.toml | 23 ++ crates/procinfo/src/lib.rs | 93 +++++++ crates/procinfo/src/linux.rs | 139 +++++++++++ crates/procinfo/src/macos.rs | 231 ++++++++++++++++++ crates/procinfo/src/windows.rs | 419 ++++++++++++++++++++++++++++++++ crates/terminal/Cargo.toml | 2 +- crates/terminal/src/terminal.rs | 124 +++------- 8 files changed, 943 insertions(+), 89 deletions(-) create mode 100644 crates/procinfo/Cargo.toml create mode 100644 crates/procinfo/src/lib.rs create mode 100644 crates/procinfo/src/linux.rs create mode 100644 crates/procinfo/src/macos.rs create mode 100644 crates/procinfo/src/windows.rs diff --git a/Cargo.lock b/Cargo.lock index 0a5d85d3b3b2af8d49278b21270a8609605b62fa..377d845f815b481ed5683873bc3ba94f89d9bf56 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3824,7 +3824,6 @@ dependencies = [ [[package]] name = "procinfo" version = "0.1.0" -source = "git+https://github.com/zed-industries/wezterm?rev=40a7dbf93542fbe4178c2e4b4bd438126a6432b9#40a7dbf93542fbe4178c2e4b4bd438126a6432b9" dependencies = [ "libc", "log", diff --git a/crates/procinfo/Cargo.toml b/crates/procinfo/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..567b390bf4eb91f1410896f4125a389ae2132da0 --- /dev/null +++ b/crates/procinfo/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "procinfo" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[features] + +[dependencies] +libc = "0.2" +log = "0.4" + +[target."cfg(windows)".dependencies] +ntapi = "0.3" +winapi = { version = "0.3", features = [ + "handleapi", + "memoryapi", + "psapi", + "processthreadsapi", + "shellapi", + "tlhelp32", +]} + diff --git a/crates/procinfo/src/lib.rs b/crates/procinfo/src/lib.rs new file mode 100644 index 0000000000000000000000000000000000000000..cfbec762b8bffca9140384d3fd3e377bdc8a60d9 --- /dev/null +++ b/crates/procinfo/src/lib.rs @@ -0,0 +1,93 @@ +use std::collections::{HashMap, HashSet}; +use std::path::PathBuf; +mod linux; +mod macos; +mod windows; + +#[derive(Debug, Copy, Clone)] +pub enum LocalProcessStatus { + Idle, + Run, + Sleep, + Stop, + Zombie, + Tracing, + Dead, + Wakekill, + Waking, + Parked, + LockBlocked, + Unknown, +} + +#[derive(Debug, Clone)] +pub struct LocalProcessInfo { + /// The process identifier + pub pid: u32, + /// The parent process identifier + pub ppid: u32, + /// The COMM name of the process. May not bear any relation to + /// the executable image name. May be changed at runtime by + /// the process. + /// Many systems truncate this + /// field to 15-16 characters. + pub name: String, + /// Path to the executable image + pub executable: PathBuf, + /// The argument vector. + /// Some systems allow changing the argv block at runtime + /// eg: setproctitle(). + pub argv: Vec, + /// The current working directory for the process, or an empty + /// path if it was not accessible for some reason. + pub cwd: PathBuf, + /// The status of the process. Not all possible values are + /// portably supported on all systems. + pub status: LocalProcessStatus, + /// A clock value in unspecified system dependent units that + /// indicates the relative age of the process. + pub start_time: u64, + /// The console handle associated with the process, if any. + #[cfg(windows)] + pub console: u64, + /// Child processes, keyed by pid + pub children: HashMap, +} +#[cfg(feature = "lua")] +luahelper::impl_lua_conversion_dynamic!(LocalProcessInfo); + +impl LocalProcessInfo { + /// Walk this sub-tree of processes and return a unique set + /// of executable base names. eg: `foo/bar` and `woot/bar` + /// produce a set containing just `bar`. + pub fn flatten_to_exe_names(&self) -> HashSet { + let mut names = HashSet::new(); + + fn flatten(item: &LocalProcessInfo, names: &mut HashSet) { + if let Some(exe) = item.executable.file_name() { + names.insert(exe.to_string_lossy().into_owned()); + } + for proc in item.children.values() { + flatten(proc, names); + } + } + + flatten(self, &mut names); + names + } + + #[cfg(not(any(target_os = "macos", target_os = "linux", windows)))] + pub fn with_root_pid(_pid: u32) -> Option { + None + } + + #[cfg(not(any(target_os = "macos", target_os = "linux", windows)))] + pub fn current_working_dir(_pid: u32) -> Option { + None + } + + #[cfg(not(any(target_os = "macos", target_os = "linux", windows)))] + pub fn executable_path(_pid: u32) -> Option { + None + } +} diff --git a/crates/procinfo/src/linux.rs b/crates/procinfo/src/linux.rs new file mode 100644 index 0000000000000000000000000000000000000000..9211f61551824758cda46aa47b0fcf906cac7dd6 --- /dev/null +++ b/crates/procinfo/src/linux.rs @@ -0,0 +1,139 @@ +#![cfg(target_os = "linux")] +use super::*; + +impl From<&str> for LocalProcessStatus { + fn from(s: &str) -> Self { + match s { + "R" => Self::Run, + "S" => Self::Sleep, + "D" => Self::Idle, + "Z" => Self::Zombie, + "T" => Self::Stop, + "t" => Self::Tracing, + "X" | "x" => Self::Dead, + "K" => Self::Wakekill, + "W" => Self::Waking, + "P" => Self::Parked, + _ => Self::Unknown, + } + } +} + +impl LocalProcessInfo { + pub fn current_working_dir(pid: u32) -> Option { + std::fs::read_link(format!("/proc/{}/cwd", pid)).ok() + } + + pub fn executable_path(pid: u32) -> Option { + std::fs::read_link(format!("/proc/{}/exe", pid)).ok() + } + + pub fn with_root_pid(pid: u32) -> Option { + use libc::pid_t; + + let pid = pid as pid_t; + + fn all_pids() -> Vec { + let mut pids = vec![]; + if let Ok(dir) = std::fs::read_dir("/proc") { + for entry in dir { + if let Ok(entry) = entry { + if let Ok(file_type) = entry.file_type() { + if file_type.is_dir() { + if let Some(name) = entry.file_name().to_str() { + if let Ok(pid) = name.parse::() { + pids.push(pid); + } + } + } + } + } + } + } + pids + } + + struct LinuxStat { + pid: pid_t, + name: String, + status: String, + ppid: pid_t, + // Time process started after boot, measured in ticks + starttime: u64, + } + + fn info_for_pid(pid: pid_t) -> Option { + let data = std::fs::read_to_string(format!("/proc/{}/stat", pid)).ok()?; + let (_pid_space, name) = data.split_once('(')?; + let (name, fields) = name.rsplit_once(')')?; + let fields = fields.split_whitespace().collect::>(); + + Some(LinuxStat { + pid, + name: name.to_string(), + status: fields.get(0)?.to_string(), + ppid: fields.get(1)?.parse().ok()?, + starttime: fields.get(20)?.parse().ok()?, + }) + } + + fn exe_for_pid(pid: pid_t) -> PathBuf { + std::fs::read_link(format!("/proc/{}/exe", pid)).unwrap_or_else(|_| PathBuf::new()) + } + + fn cwd_for_pid(pid: pid_t) -> PathBuf { + LocalProcessInfo::current_working_dir(pid as u32).unwrap_or_else(|| PathBuf::new()) + } + + fn parse_cmdline(pid: pid_t) -> Vec { + let data = match std::fs::read(format!("/proc/{}/cmdline", pid)) { + Ok(data) => data, + Err(_) => return vec![], + }; + + let mut args = vec![]; + + let data = data.strip_suffix(&[0]).unwrap_or(&data); + + for arg in data.split(|&c| c == 0) { + args.push(String::from_utf8_lossy(arg).to_owned().to_string()); + } + + args + } + + let procs: Vec<_> = all_pids().into_iter().filter_map(info_for_pid).collect(); + + fn build_proc(info: &LinuxStat, procs: &[LinuxStat]) -> LocalProcessInfo { + let mut children = HashMap::new(); + + for kid in procs { + if kid.ppid == info.pid { + children.insert(kid.pid as u32, build_proc(kid, procs)); + } + } + + let executable = exe_for_pid(info.pid); + let name = info.name.clone(); + let argv = parse_cmdline(info.pid); + + LocalProcessInfo { + pid: info.pid as _, + ppid: info.ppid as _, + name, + executable, + cwd: cwd_for_pid(info.pid), + argv, + start_time: info.starttime, + status: info.status.as_str().into(), + children, + } + } + + if let Some(info) = procs.iter().find(|info| info.pid == pid) { + Some(build_proc(info, &procs)) + } else { + None + } + } +} diff --git a/crates/procinfo/src/macos.rs b/crates/procinfo/src/macos.rs new file mode 100644 index 0000000000000000000000000000000000000000..5d2de1c399f9260bb5b7f05931852ae7722e30c2 --- /dev/null +++ b/crates/procinfo/src/macos.rs @@ -0,0 +1,231 @@ +#![cfg(target_os = "macos")] +use super::*; +use std::ffi::{OsStr, OsString}; +use std::os::unix::ffi::{OsStrExt, OsStringExt}; + +impl From for LocalProcessStatus { + fn from(s: u32) -> Self { + match s { + 1 => Self::Idle, + 2 => Self::Run, + 3 => Self::Sleep, + 4 => Self::Stop, + 5 => Self::Zombie, + _ => Self::Unknown, + } + } +} + +impl LocalProcessInfo { + pub fn current_working_dir(pid: u32) -> Option { + let mut pathinfo: libc::proc_vnodepathinfo = unsafe { std::mem::zeroed() }; + let size = std::mem::size_of_val(&pathinfo) as libc::c_int; + let ret = unsafe { + libc::proc_pidinfo( + pid as _, + libc::PROC_PIDVNODEPATHINFO, + 0, + &mut pathinfo as *mut _ as *mut _, + size, + ) + }; + if ret != size { + return None; + } + + // Workaround a workaround for an old rustc version supported by libc; + // the type of vip_path should just be [c_char; MAXPATHLEN] but it + // is defined as a horrible nested array by the libc crate: + // `[[c_char; 32]; 32]`. + // Urgh. Let's re-cast it as the correct kind of slice. + let vip_path = unsafe { + std::slice::from_raw_parts( + pathinfo.pvi_cdir.vip_path.as_ptr() as *const u8, + libc::MAXPATHLEN as usize, + ) + }; + let nul = vip_path.iter().position(|&c| c == 0)?; + Some(OsStr::from_bytes(&vip_path[0..nul]).into()) + } + + pub fn executable_path(pid: u32) -> Option { + let mut buffer: Vec = Vec::with_capacity(libc::PROC_PIDPATHINFO_MAXSIZE as _); + let x = unsafe { + libc::proc_pidpath( + pid as _, + buffer.as_mut_ptr() as *mut _, + libc::PROC_PIDPATHINFO_MAXSIZE as _, + ) + }; + if x <= 0 { + return None; + } + + unsafe { buffer.set_len(x as usize) }; + Some(OsString::from_vec(buffer).into()) + } + + pub fn with_root_pid(pid: u32) -> Option { + /// Enumerate all current process identifiers + fn all_pids() -> Vec { + let num_pids = unsafe { libc::proc_listallpids(std::ptr::null_mut(), 0) }; + if num_pids < 1 { + return vec![]; + } + + // Give a bit of padding to avoid looping if processes are spawning + // rapidly while we're trying to collect this info + const PADDING: usize = 32; + let mut pids: Vec = Vec::with_capacity(num_pids as usize + PADDING); + loop { + let n = unsafe { + libc::proc_listallpids( + pids.as_mut_ptr() as *mut _, + (pids.capacity() * std::mem::size_of::()) as _, + ) + }; + + if n < 1 { + return vec![]; + } + + let n = n as usize; + + if n > pids.capacity() { + pids.reserve(n + PADDING); + continue; + } + + unsafe { pids.set_len(n) }; + return pids; + } + } + + /// Obtain info block for a pid. + /// Note that the process could have gone away since we first + /// observed the pid and the time we call this, so we must + /// be able to tolerate this failing. + fn info_for_pid(pid: libc::pid_t) -> Option { + let mut info: libc::proc_bsdinfo = unsafe { std::mem::zeroed() }; + let wanted_size = std::mem::size_of::() as _; + let res = unsafe { + libc::proc_pidinfo( + pid, + libc::PROC_PIDTBSDINFO, + 0, + &mut info as *mut _ as *mut _, + wanted_size, + ) + }; + + if res == wanted_size { + Some(info) + } else { + None + } + } + + fn cwd_for_pid(pid: libc::pid_t) -> PathBuf { + LocalProcessInfo::current_working_dir(pid as _).unwrap_or_else(PathBuf::new) + } + + fn exe_and_args_for_pid_sysctl(pid: libc::pid_t) -> Option<(PathBuf, Vec)> { + use libc::c_int; + let mut size = 64 * 1024; + let mut buf: Vec = Vec::with_capacity(size); + let mut mib = [libc::CTL_KERN, libc::KERN_PROCARGS2, pid as c_int]; + + let res = unsafe { + libc::sysctl( + mib.as_mut_ptr(), + mib.len() as _, + buf.as_mut_ptr() as *mut _, + &mut size, + std::ptr::null_mut(), + 0, + ) + }; + if res == -1 { + return None; + } + if size < (std::mem::size_of::() * 2) { + // Not big enough + return None; + } + unsafe { buf.set_len(size) }; + + // The data in our buffer is laid out like this: + // argc - c_int + // exe_path - NUL terminated string + // argv[0] - NUL terminated string + // argv[1] - NUL terminated string + // ... + // argv[n] - NUL terminated string + // envp[0] - NUL terminated string + // ... + + let mut ptr = &buf[0..size]; + + let argc: c_int = unsafe { std::ptr::read(ptr.as_ptr() as *const c_int) }; + ptr = &ptr[std::mem::size_of::()..]; + + fn consume_cstr(ptr: &mut &[u8]) -> Option { + let nul = ptr.iter().position(|&c| c == 0)?; + let s = String::from_utf8_lossy(&ptr[0..nul]).to_owned().to_string(); + *ptr = ptr.get(nul + 1..)?; + Some(s) + } + + let exe_path = consume_cstr(&mut ptr)?.into(); + + let mut args = vec![]; + for _ in 0..argc { + args.push(consume_cstr(&mut ptr)?); + } + + dbg!(&exe_path); + dbg!(&args); + Some((exe_path, args)) + } + + fn exe_for_pid(pid: libc::pid_t) -> PathBuf { + LocalProcessInfo::executable_path(pid as _).unwrap_or_else(PathBuf::new) + } + + let procs: Vec<_> = all_pids().into_iter().filter_map(info_for_pid).collect(); + + fn build_proc(info: &libc::proc_bsdinfo, procs: &[libc::proc_bsdinfo]) -> LocalProcessInfo { + let mut children = HashMap::new(); + + for kid in procs { + if kid.pbi_ppid == info.pbi_pid { + children.insert(kid.pbi_pid, build_proc(kid, procs)); + } + } + + let (executable, argv) = exe_and_args_for_pid_sysctl(info.pbi_pid as _) + .unwrap_or_else(|| (exe_for_pid(info.pbi_pid as _), vec![])); + + let name = unsafe { std::ffi::CStr::from_ptr(info.pbi_comm.as_ptr() as _) }; + let name = name.to_str().unwrap_or("").to_string(); + + LocalProcessInfo { + pid: info.pbi_pid, + ppid: info.pbi_ppid, + name, + executable, + cwd: cwd_for_pid(info.pbi_pid as _), + argv, + start_time: info.pbi_start_tvsec, + status: LocalProcessStatus::from(info.pbi_status), + children, + } + } + + if let Some(info) = procs.iter().find(|info| info.pbi_pid == pid) { + Some(build_proc(info, &procs)) + } else { + None + } + } +} diff --git a/crates/procinfo/src/windows.rs b/crates/procinfo/src/windows.rs new file mode 100644 index 0000000000000000000000000000000000000000..bebcd0157a089a4972348c4be06f00cfbbfb2edd --- /dev/null +++ b/crates/procinfo/src/windows.rs @@ -0,0 +1,419 @@ +#![cfg(windows)] +use super::*; +use ntapi::ntpebteb::PEB; +use ntapi::ntpsapi::{ + NtQueryInformationProcess, ProcessBasicInformation, ProcessWow64Information, + PROCESS_BASIC_INFORMATION, +}; +use ntapi::ntrtl::RTL_USER_PROCESS_PARAMETERS; +use ntapi::ntwow64::RTL_USER_PROCESS_PARAMETERS32; +use std::ffi::OsString; +use std::mem::MaybeUninit; +use std::os::windows::ffi::OsStringExt; +use winapi::shared::minwindef::{DWORD, FILETIME, LPVOID, MAX_PATH}; +use winapi::shared::ntdef::{FALSE, NT_SUCCESS}; +use winapi::um::handleapi::CloseHandle; +use winapi::um::memoryapi::ReadProcessMemory; +use winapi::um::processthreadsapi::{GetCurrentProcessId, GetProcessTimes, OpenProcess}; +use winapi::um::shellapi::CommandLineToArgvW; +use winapi::um::tlhelp32::*; +use winapi::um::winbase::{LocalFree, QueryFullProcessImageNameW}; +use winapi::um::winnt::{HANDLE, PROCESS_QUERY_INFORMATION, PROCESS_VM_READ}; + +/// Manages a Toolhelp32 snapshot handle +struct Snapshot(HANDLE); + +impl Snapshot { + pub fn new() -> Option { + let handle = unsafe { CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0) }; + if handle.is_null() { + None + } else { + Some(Self(handle)) + } + } + + pub fn iter(&self) -> ProcIter { + ProcIter { + snapshot: &self, + first: true, + } + } + + pub fn entries() -> Vec { + match Self::new() { + Some(snapshot) => snapshot.iter().collect(), + None => vec![], + } + } +} + +impl Drop for Snapshot { + fn drop(&mut self) { + unsafe { CloseHandle(self.0) }; + } +} + +struct ProcIter<'a> { + snapshot: &'a Snapshot, + first: bool, +} + +impl<'a> Iterator for ProcIter<'a> { + type Item = PROCESSENTRY32W; + + fn next(&mut self) -> Option { + let mut entry: PROCESSENTRY32W = unsafe { std::mem::zeroed() }; + entry.dwSize = std::mem::size_of::() as _; + let res = if self.first { + self.first = false; + unsafe { Process32FirstW(self.snapshot.0, &mut entry) } + } else { + unsafe { Process32NextW(self.snapshot.0, &mut entry) } + }; + if res == 0 { + None + } else { + Some(entry) + } + } +} + +fn wstr_to_path(slice: &[u16]) -> PathBuf { + match slice.iter().position(|&c| c == 0) { + Some(nul) => OsString::from_wide(&slice[..nul]), + None => OsString::from_wide(slice), + } + .into() +} + +fn wstr_to_string(slice: &[u16]) -> String { + wstr_to_path(slice).to_string_lossy().into_owned() +} + +struct ProcParams { + argv: Vec, + cwd: PathBuf, + console: HANDLE, +} + +/// A handle to an opened process +struct ProcHandle { + pid: u32, + proc: HANDLE, +} + +impl ProcHandle { + pub fn new(pid: u32) -> Option { + if pid == unsafe { GetCurrentProcessId() } { + // Avoid the potential for deadlock if we're examining ourselves + log::trace!("ProcHandle::new({}): skip because it is my own pid", pid); + return None; + } + let options = PROCESS_QUERY_INFORMATION | PROCESS_VM_READ; + log::trace!("ProcHandle::new({}): OpenProcess", pid); + let handle = unsafe { OpenProcess(options, FALSE as _, pid) }; + log::trace!("ProcHandle::new({}): OpenProcess -> {:?}", pid, handle); + if handle.is_null() { + return None; + } + Some(Self { pid, proc: handle }) + } + + /// Returns the executable image for the process + pub fn executable(&self) -> Option { + let mut buf = [0u16; MAX_PATH + 1]; + let mut len = buf.len() as DWORD; + let res = unsafe { QueryFullProcessImageNameW(self.proc, 0, buf.as_mut_ptr(), &mut len) }; + if res == 0 { + None + } else { + Some(wstr_to_path(&buf)) + } + } + + /// Wrapper around NtQueryInformationProcess that fetches `what` as `T` + fn query_proc(&self, what: u32) -> Option { + let mut data = MaybeUninit::::uninit(); + let res = unsafe { + NtQueryInformationProcess( + self.proc, + what, + data.as_mut_ptr() as _, + std::mem::size_of::() as _, + std::ptr::null_mut(), + ) + }; + if !NT_SUCCESS(res) { + return None; + } + let data = unsafe { data.assume_init() }; + Some(data) + } + + /// Read a `T` from the target process at the specified address + fn read_struct(&self, addr: LPVOID) -> Option { + let mut data = MaybeUninit::::uninit(); + let res = unsafe { + ReadProcessMemory( + self.proc, + addr as _, + data.as_mut_ptr() as _, + std::mem::size_of::() as _, + std::ptr::null_mut(), + ) + }; + if res == 0 { + return None; + } + let data = unsafe { data.assume_init() }; + Some(data) + } + + /// If the process is a 32-bit process running on Win64, return the address + /// of its process parameters. + /// Otherwise, return None to indicate a native win64 process. + fn get_peb32_addr(&self) -> Option { + let peb32_addr: LPVOID = self.query_proc(ProcessWow64Information)?; + if peb32_addr.is_null() { + None + } else { + Some(peb32_addr) + } + } + + /// Returns the cwd and args for the process + pub fn get_params(&self) -> Option { + match self.get_peb32_addr() { + Some(peb32) => self.get_params_32(peb32), + None => self.get_params_64(), + } + } + + fn get_basic_info(&self) -> Option { + self.query_proc(ProcessBasicInformation) + } + + fn get_peb(&self, info: &PROCESS_BASIC_INFORMATION) -> Option { + self.read_struct(info.PebBaseAddress as _) + } + + fn get_proc_params(&self, peb: &PEB) -> Option { + self.read_struct(peb.ProcessParameters as _) + } + + /// Returns the cwd and args for a 64 bit process + fn get_params_64(&self) -> Option { + let info = self.get_basic_info()?; + let peb = self.get_peb(&info)?; + let params = self.get_proc_params(&peb)?; + + let cmdline = self.read_process_wchar( + params.CommandLine.Buffer as _, + params.CommandLine.Length as _, + )?; + let cwd = self.read_process_wchar( + params.CurrentDirectory.DosPath.Buffer as _, + params.CurrentDirectory.DosPath.Length as _, + )?; + + Some(ProcParams { + argv: cmd_line_to_argv(&cmdline), + cwd: wstr_to_path(&cwd), + console: params.ConsoleHandle, + }) + } + + fn get_proc_params_32(&self, peb32: LPVOID) -> Option { + self.read_struct(peb32) + } + + /// Returns the cwd and args for a 32 bit process + fn get_params_32(&self, peb32: LPVOID) -> Option { + let params = self.get_proc_params_32(peb32)?; + + let cmdline = self.read_process_wchar( + params.CommandLine.Buffer as _, + params.CommandLine.Length as _, + )?; + let cwd = self.read_process_wchar( + params.CurrentDirectory.DosPath.Buffer as _, + params.CurrentDirectory.DosPath.Length as _, + )?; + + Some(ProcParams { + argv: cmd_line_to_argv(&cmdline), + cwd: wstr_to_path(&cwd), + console: params.ConsoleHandle as _, + }) + } + + /// Copies a sized WSTR from the address in the process + fn read_process_wchar(&self, ptr: LPVOID, byte_size: usize) -> Option> { + if byte_size > MAX_PATH * 4 { + // Defend against implausibly large paths, just in + // case we're reading the wrong offset into a kernel struct + return None; + } + + let mut buf = vec![0u16; byte_size / 2]; + let mut bytes_read = 0; + + let res = unsafe { + ReadProcessMemory( + self.proc, + ptr as _, + buf.as_mut_ptr() as _, + byte_size, + &mut bytes_read, + ) + }; + if res == 0 { + return None; + } + + // In the unlikely event that we have a short read, + // truncate the buffer to fit. + let wide_chars_read = bytes_read / 2; + buf.resize(wide_chars_read, 0); + + // Ensure that it is NUL terminated + match buf.iter().position(|&c| c == 0) { + Some(n) => { + // Truncate to include existing NUL but no later chars + buf.resize(n + 1, 0); + } + None => { + // Add a NUL + buf.push(0); + } + } + + Some(buf) + } + + /// Retrieves the start time of the process + fn start_time(&self) -> Option { + const fn empty() -> FILETIME { + FILETIME { + dwLowDateTime: 0, + dwHighDateTime: 0, + } + } + + let mut start = empty(); + let mut exit = empty(); + let mut kernel = empty(); + let mut user = empty(); + + let res = + unsafe { GetProcessTimes(self.proc, &mut start, &mut exit, &mut kernel, &mut user) }; + if res == 0 { + return None; + } + + Some((start.dwHighDateTime as u64) << 32 | start.dwLowDateTime as u64) + } +} + +/// Parse a command line string into an argv array +fn cmd_line_to_argv(buf: &[u16]) -> Vec { + let mut argc = 0; + let argvp = unsafe { CommandLineToArgvW(buf.as_ptr(), &mut argc) }; + if argvp.is_null() { + return vec![]; + } + + let argv = unsafe { std::slice::from_raw_parts(argvp, argc as usize) }; + let mut args = vec![]; + for &arg in argv { + let len = unsafe { libc::wcslen(arg) }; + let arg = unsafe { std::slice::from_raw_parts(arg, len) }; + args.push(wstr_to_string(arg)); + } + unsafe { LocalFree(argvp as _) }; + args +} + +impl Drop for ProcHandle { + fn drop(&mut self) { + log::trace!("ProcHandle::drop(pid={} proc={:?})", self.pid, self.proc); + unsafe { CloseHandle(self.proc) }; + } +} + +impl LocalProcessInfo { + pub fn current_working_dir(pid: u32) -> Option { + log::trace!("current_working_dir({})", pid); + let proc = ProcHandle::new(pid)?; + let params = proc.get_params()?; + Some(params.cwd) + } + + pub fn executable_path(pid: u32) -> Option { + log::trace!("executable_path({})", pid); + let proc = ProcHandle::new(pid)?; + proc.executable() + } + + pub fn with_root_pid(pid: u32) -> Option { + log::trace!("LocalProcessInfo::with_root_pid({}), getting snapshot", pid); + let procs = Snapshot::entries(); + log::trace!("Got snapshot"); + + fn build_proc(info: &PROCESSENTRY32W, procs: &[PROCESSENTRY32W]) -> LocalProcessInfo { + let mut children = HashMap::new(); + + for kid in procs { + if kid.th32ParentProcessID == info.th32ProcessID { + children.insert(kid.th32ProcessID, build_proc(kid, procs)); + } + } + + let mut executable = None; + let mut start_time = 0; + let mut cwd = PathBuf::new(); + let mut argv = vec![]; + let mut console = 0; + + if let Some(proc) = ProcHandle::new(info.th32ProcessID) { + if let Some(exe) = proc.executable() { + executable.replace(exe); + } + if let Some(params) = proc.get_params() { + cwd = params.cwd; + argv = params.argv; + console = params.console as _; + } + if let Some(start) = proc.start_time() { + start_time = start; + } + } + + let executable = executable.unwrap_or_else(|| wstr_to_path(&info.szExeFile)); + let name = match executable.file_name() { + Some(name) => name.to_string_lossy().into_owned(), + None => String::new(), + }; + + LocalProcessInfo { + pid: info.th32ProcessID, + ppid: info.th32ParentProcessID, + name, + executable, + cwd, + argv, + start_time, + status: LocalProcessStatus::Run, + children, + console, + } + } + + if let Some(info) = procs.iter().find(|info| info.th32ProcessID == pid) { + Some(build_proc(info, &procs)) + } else { + None + } + } +} diff --git a/crates/terminal/Cargo.toml b/crates/terminal/Cargo.toml index 5910a2897ca5df9e73c21ee035aa605141978e59..6c49fb81bd306071a7a72438b3b13da50465a2dc 100644 --- a/crates/terminal/Cargo.toml +++ b/crates/terminal/Cargo.toml @@ -9,7 +9,7 @@ doctest = false [dependencies] alacritty_terminal = { git = "https://github.com/zed-industries/alacritty", rev = "4e1f0c6177975a040b37f942dfb0e723e46a9971" } -procinfo = { git = "https://github.com/zed-industries/wezterm", rev = "40a7dbf93542fbe4178c2e4b4bd438126a6432b9", default-features = false } +procinfo = { path = "../procinfo" } editor = { path = "../editor" } util = { path = "../util" } gpui = { path = "../gpui" } diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 7b504de4ae40aa326cb4b6a321aa46ffdccaf510..a6c2e6aa9ad0b92daa624202ae85e7eae9312bb7 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -3,7 +3,7 @@ pub mod modal; pub mod terminal_container_view; pub mod terminal_element; pub mod terminal_view; - +// procinfo = { git = "https://github.com/zed-industries/wezterm", rev = "40a7dbf93542fbe4178c2e4b4bd438126a6432b9", default-features = false } use alacritty_terminal::{ ansi::{ClearMode, Handler}, config::{Config, Program, PtyConfig, Scrolling}, @@ -237,28 +237,12 @@ impl TerminalError { self.shell .clone() .map(|shell| match shell { - Shell::System => { - let mut buf = [0; 1024]; - let pw = alacritty_unix::get_pw_entry(&mut buf).ok(); + Shell::System => "".to_string(), - match pw { - Some(pw) => format!(" {}", pw.shell), - None => "".to_string(), - } - } Shell::Program(s) => s, Shell::WithArguments { program, args } => format!("{} {}", program, args.join(" ")), }) - .unwrap_or_else(|| { - let mut buf = [0; 1024]; - let pw = alacritty_unix::get_pw_entry(&mut buf).ok(); - match pw { - Some(pw) => { - format!(" {}", pw.shell) - } - None => " {}".to_string(), - } - }) + .unwrap_or_else(|| "".to_string()) } } @@ -538,6 +522,7 @@ impl Terminal { AlacTermEvent::Wakeup => { cx.emit(Event::Wakeup); + dbg!("*********"); if self.update_process_info() { cx.emit(Event::TitleChanged) } @@ -1039,84 +1024,49 @@ fn make_search_matches<'a, T>( #[cfg(test)] mod tests { + use libc::c_int; + pub mod terminal_test_context; -} -//TODO Move this around and clean up the code -mod alacritty_unix { - use alacritty_terminal::config::Program; - use gpui::anyhow::{bail, Result}; + #[test] + pub fn wez_test() { + fn test() -> Option> { + let size = 28; - use std::ffi::CStr; - use std::mem::MaybeUninit; - use std::ptr; + //Test data pulled from running the code + let buf = [ + 2, 0, 0, 0, 47, 98, 105, 110, 47, 115, 108, 101, 101, 112, 0, 0, 0, 0, 0, 0, 115, + 108, 101, 101, 112, 0, 53, 0, + ]; - #[derive(Debug)] - pub struct Passwd<'a> { - _name: &'a str, - _dir: &'a str, - pub shell: &'a str, - } + let mut ptr = &buf[0..size]; - /// Return a Passwd struct with pointers into the provided buf. - /// - /// # Unsafety - /// - /// If `buf` is changed while `Passwd` is alive, bad thing will almost certainly happen. - pub fn get_pw_entry(buf: &mut [i8; 1024]) -> Result> { - // Create zeroed passwd struct. - let mut entry: MaybeUninit = MaybeUninit::uninit(); - - let mut res: *mut libc::passwd = ptr::null_mut(); - - // Try and read the pw file. - let uid = unsafe { libc::getuid() }; - let status = unsafe { - libc::getpwuid_r( - uid, - entry.as_mut_ptr(), - buf.as_mut_ptr() as *mut _, - buf.len(), - &mut res, - ) - }; - let entry = unsafe { entry.assume_init() }; + let argc: c_int = unsafe { std::ptr::read(ptr.as_ptr() as *const c_int) }; + ptr = &ptr[std::mem::size_of::()..]; - if status < 0 { - bail!("getpwuid_r failed"); - } - - if res.is_null() { - bail!("pw not found"); - } + fn consume_cstr(ptr: &mut &[u8]) -> Option { + let nul = ptr.iter().position(|&c| c == 0)?; + let s = String::from_utf8_lossy(&ptr[0..nul]).to_owned().to_string(); + *ptr = ptr.get(nul + 1..)?; + Some(s) + } - // Sanity check. - assert_eq!(entry.pw_uid, uid); + let _exe_path: Option = consume_cstr(&mut ptr)?.into(); - // Build a borrowed Passwd struct. - Ok(Passwd { - _name: unsafe { CStr::from_ptr(entry.pw_name).to_str().unwrap() }, - _dir: unsafe { CStr::from_ptr(entry.pw_dir).to_str().unwrap() }, - shell: unsafe { CStr::from_ptr(entry.pw_shell).to_str().unwrap() }, - }) - } + //Clear out the trailing null pointers + while ptr[0] == 0 { + ptr = ptr.get(1..)?; + } - #[cfg(target_os = "macos")] - pub fn _default_shell(pw: &Passwd<'_>) -> Program { - let shell_name = pw.shell.rsplit('/').next().unwrap(); - let argv = vec![ - String::from("-c"), - format!("exec -a -{} {}", shell_name, pw.shell), - ]; - - Program::WithArgs { - program: "/bin/bash".to_owned(), - args: argv, + let mut args = vec![]; + for _ in 0..argc { + args.push(consume_cstr(&mut ptr)?); + } + Some(args) } - } - #[cfg(not(target_os = "macos"))] - pub fn default_shell(pw: &Passwd<'_>) -> Program { - Program::Just(env::var("SHELL").unwrap_or_else(|_| pw.shell.to_owned())) + assert_eq!(test(), Some(vec!["sleep".to_string(), "5".to_string()])); } } + +mod wez_proc_info {} From a100956fbf5abdffb7f78df912e9bb2936a3953d Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Fri, 2 Sep 2022 23:15:12 -0700 Subject: [PATCH 07/12] removed test proc info, put fix in wezterm, just need to link them together now --- Cargo.lock | 1 + crates/procinfo/Cargo.toml | 23 - crates/procinfo/src/lib.rs | 93 ---- crates/procinfo/src/linux.rs | 139 ------ crates/procinfo/src/macos.rs | 231 ---------- crates/procinfo/src/windows.rs | 419 ------------------ crates/terminal/Cargo.toml | 2 +- crates/terminal/src/terminal.rs | 47 +- .../terminal/src/terminal_container_view.rs | 3 +- 9 files changed, 5 insertions(+), 953 deletions(-) delete mode 100644 crates/procinfo/Cargo.toml delete mode 100644 crates/procinfo/src/lib.rs delete mode 100644 crates/procinfo/src/linux.rs delete mode 100644 crates/procinfo/src/macos.rs delete mode 100644 crates/procinfo/src/windows.rs diff --git a/Cargo.lock b/Cargo.lock index 377d845f815b481ed5683873bc3ba94f89d9bf56..0a5d85d3b3b2af8d49278b21270a8609605b62fa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3824,6 +3824,7 @@ dependencies = [ [[package]] name = "procinfo" version = "0.1.0" +source = "git+https://github.com/zed-industries/wezterm?rev=40a7dbf93542fbe4178c2e4b4bd438126a6432b9#40a7dbf93542fbe4178c2e4b4bd438126a6432b9" dependencies = [ "libc", "log", diff --git a/crates/procinfo/Cargo.toml b/crates/procinfo/Cargo.toml deleted file mode 100644 index 567b390bf4eb91f1410896f4125a389ae2132da0..0000000000000000000000000000000000000000 --- a/crates/procinfo/Cargo.toml +++ /dev/null @@ -1,23 +0,0 @@ -[package] -name = "procinfo" -version = "0.1.0" -edition = "2021" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html -[features] - -[dependencies] -libc = "0.2" -log = "0.4" - -[target."cfg(windows)".dependencies] -ntapi = "0.3" -winapi = { version = "0.3", features = [ - "handleapi", - "memoryapi", - "psapi", - "processthreadsapi", - "shellapi", - "tlhelp32", -]} - diff --git a/crates/procinfo/src/lib.rs b/crates/procinfo/src/lib.rs deleted file mode 100644 index cfbec762b8bffca9140384d3fd3e377bdc8a60d9..0000000000000000000000000000000000000000 --- a/crates/procinfo/src/lib.rs +++ /dev/null @@ -1,93 +0,0 @@ -use std::collections::{HashMap, HashSet}; -use std::path::PathBuf; -mod linux; -mod macos; -mod windows; - -#[derive(Debug, Copy, Clone)] -pub enum LocalProcessStatus { - Idle, - Run, - Sleep, - Stop, - Zombie, - Tracing, - Dead, - Wakekill, - Waking, - Parked, - LockBlocked, - Unknown, -} - -#[derive(Debug, Clone)] -pub struct LocalProcessInfo { - /// The process identifier - pub pid: u32, - /// The parent process identifier - pub ppid: u32, - /// The COMM name of the process. May not bear any relation to - /// the executable image name. May be changed at runtime by - /// the process. - /// Many systems truncate this - /// field to 15-16 characters. - pub name: String, - /// Path to the executable image - pub executable: PathBuf, - /// The argument vector. - /// Some systems allow changing the argv block at runtime - /// eg: setproctitle(). - pub argv: Vec, - /// The current working directory for the process, or an empty - /// path if it was not accessible for some reason. - pub cwd: PathBuf, - /// The status of the process. Not all possible values are - /// portably supported on all systems. - pub status: LocalProcessStatus, - /// A clock value in unspecified system dependent units that - /// indicates the relative age of the process. - pub start_time: u64, - /// The console handle associated with the process, if any. - #[cfg(windows)] - pub console: u64, - /// Child processes, keyed by pid - pub children: HashMap, -} -#[cfg(feature = "lua")] -luahelper::impl_lua_conversion_dynamic!(LocalProcessInfo); - -impl LocalProcessInfo { - /// Walk this sub-tree of processes and return a unique set - /// of executable base names. eg: `foo/bar` and `woot/bar` - /// produce a set containing just `bar`. - pub fn flatten_to_exe_names(&self) -> HashSet { - let mut names = HashSet::new(); - - fn flatten(item: &LocalProcessInfo, names: &mut HashSet) { - if let Some(exe) = item.executable.file_name() { - names.insert(exe.to_string_lossy().into_owned()); - } - for proc in item.children.values() { - flatten(proc, names); - } - } - - flatten(self, &mut names); - names - } - - #[cfg(not(any(target_os = "macos", target_os = "linux", windows)))] - pub fn with_root_pid(_pid: u32) -> Option { - None - } - - #[cfg(not(any(target_os = "macos", target_os = "linux", windows)))] - pub fn current_working_dir(_pid: u32) -> Option { - None - } - - #[cfg(not(any(target_os = "macos", target_os = "linux", windows)))] - pub fn executable_path(_pid: u32) -> Option { - None - } -} diff --git a/crates/procinfo/src/linux.rs b/crates/procinfo/src/linux.rs deleted file mode 100644 index 9211f61551824758cda46aa47b0fcf906cac7dd6..0000000000000000000000000000000000000000 --- a/crates/procinfo/src/linux.rs +++ /dev/null @@ -1,139 +0,0 @@ -#![cfg(target_os = "linux")] -use super::*; - -impl From<&str> for LocalProcessStatus { - fn from(s: &str) -> Self { - match s { - "R" => Self::Run, - "S" => Self::Sleep, - "D" => Self::Idle, - "Z" => Self::Zombie, - "T" => Self::Stop, - "t" => Self::Tracing, - "X" | "x" => Self::Dead, - "K" => Self::Wakekill, - "W" => Self::Waking, - "P" => Self::Parked, - _ => Self::Unknown, - } - } -} - -impl LocalProcessInfo { - pub fn current_working_dir(pid: u32) -> Option { - std::fs::read_link(format!("/proc/{}/cwd", pid)).ok() - } - - pub fn executable_path(pid: u32) -> Option { - std::fs::read_link(format!("/proc/{}/exe", pid)).ok() - } - - pub fn with_root_pid(pid: u32) -> Option { - use libc::pid_t; - - let pid = pid as pid_t; - - fn all_pids() -> Vec { - let mut pids = vec![]; - if let Ok(dir) = std::fs::read_dir("/proc") { - for entry in dir { - if let Ok(entry) = entry { - if let Ok(file_type) = entry.file_type() { - if file_type.is_dir() { - if let Some(name) = entry.file_name().to_str() { - if let Ok(pid) = name.parse::() { - pids.push(pid); - } - } - } - } - } - } - } - pids - } - - struct LinuxStat { - pid: pid_t, - name: String, - status: String, - ppid: pid_t, - // Time process started after boot, measured in ticks - starttime: u64, - } - - fn info_for_pid(pid: pid_t) -> Option { - let data = std::fs::read_to_string(format!("/proc/{}/stat", pid)).ok()?; - let (_pid_space, name) = data.split_once('(')?; - let (name, fields) = name.rsplit_once(')')?; - let fields = fields.split_whitespace().collect::>(); - - Some(LinuxStat { - pid, - name: name.to_string(), - status: fields.get(0)?.to_string(), - ppid: fields.get(1)?.parse().ok()?, - starttime: fields.get(20)?.parse().ok()?, - }) - } - - fn exe_for_pid(pid: pid_t) -> PathBuf { - std::fs::read_link(format!("/proc/{}/exe", pid)).unwrap_or_else(|_| PathBuf::new()) - } - - fn cwd_for_pid(pid: pid_t) -> PathBuf { - LocalProcessInfo::current_working_dir(pid as u32).unwrap_or_else(|| PathBuf::new()) - } - - fn parse_cmdline(pid: pid_t) -> Vec { - let data = match std::fs::read(format!("/proc/{}/cmdline", pid)) { - Ok(data) => data, - Err(_) => return vec![], - }; - - let mut args = vec![]; - - let data = data.strip_suffix(&[0]).unwrap_or(&data); - - for arg in data.split(|&c| c == 0) { - args.push(String::from_utf8_lossy(arg).to_owned().to_string()); - } - - args - } - - let procs: Vec<_> = all_pids().into_iter().filter_map(info_for_pid).collect(); - - fn build_proc(info: &LinuxStat, procs: &[LinuxStat]) -> LocalProcessInfo { - let mut children = HashMap::new(); - - for kid in procs { - if kid.ppid == info.pid { - children.insert(kid.pid as u32, build_proc(kid, procs)); - } - } - - let executable = exe_for_pid(info.pid); - let name = info.name.clone(); - let argv = parse_cmdline(info.pid); - - LocalProcessInfo { - pid: info.pid as _, - ppid: info.ppid as _, - name, - executable, - cwd: cwd_for_pid(info.pid), - argv, - start_time: info.starttime, - status: info.status.as_str().into(), - children, - } - } - - if let Some(info) = procs.iter().find(|info| info.pid == pid) { - Some(build_proc(info, &procs)) - } else { - None - } - } -} diff --git a/crates/procinfo/src/macos.rs b/crates/procinfo/src/macos.rs deleted file mode 100644 index 5d2de1c399f9260bb5b7f05931852ae7722e30c2..0000000000000000000000000000000000000000 --- a/crates/procinfo/src/macos.rs +++ /dev/null @@ -1,231 +0,0 @@ -#![cfg(target_os = "macos")] -use super::*; -use std::ffi::{OsStr, OsString}; -use std::os::unix::ffi::{OsStrExt, OsStringExt}; - -impl From for LocalProcessStatus { - fn from(s: u32) -> Self { - match s { - 1 => Self::Idle, - 2 => Self::Run, - 3 => Self::Sleep, - 4 => Self::Stop, - 5 => Self::Zombie, - _ => Self::Unknown, - } - } -} - -impl LocalProcessInfo { - pub fn current_working_dir(pid: u32) -> Option { - let mut pathinfo: libc::proc_vnodepathinfo = unsafe { std::mem::zeroed() }; - let size = std::mem::size_of_val(&pathinfo) as libc::c_int; - let ret = unsafe { - libc::proc_pidinfo( - pid as _, - libc::PROC_PIDVNODEPATHINFO, - 0, - &mut pathinfo as *mut _ as *mut _, - size, - ) - }; - if ret != size { - return None; - } - - // Workaround a workaround for an old rustc version supported by libc; - // the type of vip_path should just be [c_char; MAXPATHLEN] but it - // is defined as a horrible nested array by the libc crate: - // `[[c_char; 32]; 32]`. - // Urgh. Let's re-cast it as the correct kind of slice. - let vip_path = unsafe { - std::slice::from_raw_parts( - pathinfo.pvi_cdir.vip_path.as_ptr() as *const u8, - libc::MAXPATHLEN as usize, - ) - }; - let nul = vip_path.iter().position(|&c| c == 0)?; - Some(OsStr::from_bytes(&vip_path[0..nul]).into()) - } - - pub fn executable_path(pid: u32) -> Option { - let mut buffer: Vec = Vec::with_capacity(libc::PROC_PIDPATHINFO_MAXSIZE as _); - let x = unsafe { - libc::proc_pidpath( - pid as _, - buffer.as_mut_ptr() as *mut _, - libc::PROC_PIDPATHINFO_MAXSIZE as _, - ) - }; - if x <= 0 { - return None; - } - - unsafe { buffer.set_len(x as usize) }; - Some(OsString::from_vec(buffer).into()) - } - - pub fn with_root_pid(pid: u32) -> Option { - /// Enumerate all current process identifiers - fn all_pids() -> Vec { - let num_pids = unsafe { libc::proc_listallpids(std::ptr::null_mut(), 0) }; - if num_pids < 1 { - return vec![]; - } - - // Give a bit of padding to avoid looping if processes are spawning - // rapidly while we're trying to collect this info - const PADDING: usize = 32; - let mut pids: Vec = Vec::with_capacity(num_pids as usize + PADDING); - loop { - let n = unsafe { - libc::proc_listallpids( - pids.as_mut_ptr() as *mut _, - (pids.capacity() * std::mem::size_of::()) as _, - ) - }; - - if n < 1 { - return vec![]; - } - - let n = n as usize; - - if n > pids.capacity() { - pids.reserve(n + PADDING); - continue; - } - - unsafe { pids.set_len(n) }; - return pids; - } - } - - /// Obtain info block for a pid. - /// Note that the process could have gone away since we first - /// observed the pid and the time we call this, so we must - /// be able to tolerate this failing. - fn info_for_pid(pid: libc::pid_t) -> Option { - let mut info: libc::proc_bsdinfo = unsafe { std::mem::zeroed() }; - let wanted_size = std::mem::size_of::() as _; - let res = unsafe { - libc::proc_pidinfo( - pid, - libc::PROC_PIDTBSDINFO, - 0, - &mut info as *mut _ as *mut _, - wanted_size, - ) - }; - - if res == wanted_size { - Some(info) - } else { - None - } - } - - fn cwd_for_pid(pid: libc::pid_t) -> PathBuf { - LocalProcessInfo::current_working_dir(pid as _).unwrap_or_else(PathBuf::new) - } - - fn exe_and_args_for_pid_sysctl(pid: libc::pid_t) -> Option<(PathBuf, Vec)> { - use libc::c_int; - let mut size = 64 * 1024; - let mut buf: Vec = Vec::with_capacity(size); - let mut mib = [libc::CTL_KERN, libc::KERN_PROCARGS2, pid as c_int]; - - let res = unsafe { - libc::sysctl( - mib.as_mut_ptr(), - mib.len() as _, - buf.as_mut_ptr() as *mut _, - &mut size, - std::ptr::null_mut(), - 0, - ) - }; - if res == -1 { - return None; - } - if size < (std::mem::size_of::() * 2) { - // Not big enough - return None; - } - unsafe { buf.set_len(size) }; - - // The data in our buffer is laid out like this: - // argc - c_int - // exe_path - NUL terminated string - // argv[0] - NUL terminated string - // argv[1] - NUL terminated string - // ... - // argv[n] - NUL terminated string - // envp[0] - NUL terminated string - // ... - - let mut ptr = &buf[0..size]; - - let argc: c_int = unsafe { std::ptr::read(ptr.as_ptr() as *const c_int) }; - ptr = &ptr[std::mem::size_of::()..]; - - fn consume_cstr(ptr: &mut &[u8]) -> Option { - let nul = ptr.iter().position(|&c| c == 0)?; - let s = String::from_utf8_lossy(&ptr[0..nul]).to_owned().to_string(); - *ptr = ptr.get(nul + 1..)?; - Some(s) - } - - let exe_path = consume_cstr(&mut ptr)?.into(); - - let mut args = vec![]; - for _ in 0..argc { - args.push(consume_cstr(&mut ptr)?); - } - - dbg!(&exe_path); - dbg!(&args); - Some((exe_path, args)) - } - - fn exe_for_pid(pid: libc::pid_t) -> PathBuf { - LocalProcessInfo::executable_path(pid as _).unwrap_or_else(PathBuf::new) - } - - let procs: Vec<_> = all_pids().into_iter().filter_map(info_for_pid).collect(); - - fn build_proc(info: &libc::proc_bsdinfo, procs: &[libc::proc_bsdinfo]) -> LocalProcessInfo { - let mut children = HashMap::new(); - - for kid in procs { - if kid.pbi_ppid == info.pbi_pid { - children.insert(kid.pbi_pid, build_proc(kid, procs)); - } - } - - let (executable, argv) = exe_and_args_for_pid_sysctl(info.pbi_pid as _) - .unwrap_or_else(|| (exe_for_pid(info.pbi_pid as _), vec![])); - - let name = unsafe { std::ffi::CStr::from_ptr(info.pbi_comm.as_ptr() as _) }; - let name = name.to_str().unwrap_or("").to_string(); - - LocalProcessInfo { - pid: info.pbi_pid, - ppid: info.pbi_ppid, - name, - executable, - cwd: cwd_for_pid(info.pbi_pid as _), - argv, - start_time: info.pbi_start_tvsec, - status: LocalProcessStatus::from(info.pbi_status), - children, - } - } - - if let Some(info) = procs.iter().find(|info| info.pbi_pid == pid) { - Some(build_proc(info, &procs)) - } else { - None - } - } -} diff --git a/crates/procinfo/src/windows.rs b/crates/procinfo/src/windows.rs deleted file mode 100644 index bebcd0157a089a4972348c4be06f00cfbbfb2edd..0000000000000000000000000000000000000000 --- a/crates/procinfo/src/windows.rs +++ /dev/null @@ -1,419 +0,0 @@ -#![cfg(windows)] -use super::*; -use ntapi::ntpebteb::PEB; -use ntapi::ntpsapi::{ - NtQueryInformationProcess, ProcessBasicInformation, ProcessWow64Information, - PROCESS_BASIC_INFORMATION, -}; -use ntapi::ntrtl::RTL_USER_PROCESS_PARAMETERS; -use ntapi::ntwow64::RTL_USER_PROCESS_PARAMETERS32; -use std::ffi::OsString; -use std::mem::MaybeUninit; -use std::os::windows::ffi::OsStringExt; -use winapi::shared::minwindef::{DWORD, FILETIME, LPVOID, MAX_PATH}; -use winapi::shared::ntdef::{FALSE, NT_SUCCESS}; -use winapi::um::handleapi::CloseHandle; -use winapi::um::memoryapi::ReadProcessMemory; -use winapi::um::processthreadsapi::{GetCurrentProcessId, GetProcessTimes, OpenProcess}; -use winapi::um::shellapi::CommandLineToArgvW; -use winapi::um::tlhelp32::*; -use winapi::um::winbase::{LocalFree, QueryFullProcessImageNameW}; -use winapi::um::winnt::{HANDLE, PROCESS_QUERY_INFORMATION, PROCESS_VM_READ}; - -/// Manages a Toolhelp32 snapshot handle -struct Snapshot(HANDLE); - -impl Snapshot { - pub fn new() -> Option { - let handle = unsafe { CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0) }; - if handle.is_null() { - None - } else { - Some(Self(handle)) - } - } - - pub fn iter(&self) -> ProcIter { - ProcIter { - snapshot: &self, - first: true, - } - } - - pub fn entries() -> Vec { - match Self::new() { - Some(snapshot) => snapshot.iter().collect(), - None => vec![], - } - } -} - -impl Drop for Snapshot { - fn drop(&mut self) { - unsafe { CloseHandle(self.0) }; - } -} - -struct ProcIter<'a> { - snapshot: &'a Snapshot, - first: bool, -} - -impl<'a> Iterator for ProcIter<'a> { - type Item = PROCESSENTRY32W; - - fn next(&mut self) -> Option { - let mut entry: PROCESSENTRY32W = unsafe { std::mem::zeroed() }; - entry.dwSize = std::mem::size_of::() as _; - let res = if self.first { - self.first = false; - unsafe { Process32FirstW(self.snapshot.0, &mut entry) } - } else { - unsafe { Process32NextW(self.snapshot.0, &mut entry) } - }; - if res == 0 { - None - } else { - Some(entry) - } - } -} - -fn wstr_to_path(slice: &[u16]) -> PathBuf { - match slice.iter().position(|&c| c == 0) { - Some(nul) => OsString::from_wide(&slice[..nul]), - None => OsString::from_wide(slice), - } - .into() -} - -fn wstr_to_string(slice: &[u16]) -> String { - wstr_to_path(slice).to_string_lossy().into_owned() -} - -struct ProcParams { - argv: Vec, - cwd: PathBuf, - console: HANDLE, -} - -/// A handle to an opened process -struct ProcHandle { - pid: u32, - proc: HANDLE, -} - -impl ProcHandle { - pub fn new(pid: u32) -> Option { - if pid == unsafe { GetCurrentProcessId() } { - // Avoid the potential for deadlock if we're examining ourselves - log::trace!("ProcHandle::new({}): skip because it is my own pid", pid); - return None; - } - let options = PROCESS_QUERY_INFORMATION | PROCESS_VM_READ; - log::trace!("ProcHandle::new({}): OpenProcess", pid); - let handle = unsafe { OpenProcess(options, FALSE as _, pid) }; - log::trace!("ProcHandle::new({}): OpenProcess -> {:?}", pid, handle); - if handle.is_null() { - return None; - } - Some(Self { pid, proc: handle }) - } - - /// Returns the executable image for the process - pub fn executable(&self) -> Option { - let mut buf = [0u16; MAX_PATH + 1]; - let mut len = buf.len() as DWORD; - let res = unsafe { QueryFullProcessImageNameW(self.proc, 0, buf.as_mut_ptr(), &mut len) }; - if res == 0 { - None - } else { - Some(wstr_to_path(&buf)) - } - } - - /// Wrapper around NtQueryInformationProcess that fetches `what` as `T` - fn query_proc(&self, what: u32) -> Option { - let mut data = MaybeUninit::::uninit(); - let res = unsafe { - NtQueryInformationProcess( - self.proc, - what, - data.as_mut_ptr() as _, - std::mem::size_of::() as _, - std::ptr::null_mut(), - ) - }; - if !NT_SUCCESS(res) { - return None; - } - let data = unsafe { data.assume_init() }; - Some(data) - } - - /// Read a `T` from the target process at the specified address - fn read_struct(&self, addr: LPVOID) -> Option { - let mut data = MaybeUninit::::uninit(); - let res = unsafe { - ReadProcessMemory( - self.proc, - addr as _, - data.as_mut_ptr() as _, - std::mem::size_of::() as _, - std::ptr::null_mut(), - ) - }; - if res == 0 { - return None; - } - let data = unsafe { data.assume_init() }; - Some(data) - } - - /// If the process is a 32-bit process running on Win64, return the address - /// of its process parameters. - /// Otherwise, return None to indicate a native win64 process. - fn get_peb32_addr(&self) -> Option { - let peb32_addr: LPVOID = self.query_proc(ProcessWow64Information)?; - if peb32_addr.is_null() { - None - } else { - Some(peb32_addr) - } - } - - /// Returns the cwd and args for the process - pub fn get_params(&self) -> Option { - match self.get_peb32_addr() { - Some(peb32) => self.get_params_32(peb32), - None => self.get_params_64(), - } - } - - fn get_basic_info(&self) -> Option { - self.query_proc(ProcessBasicInformation) - } - - fn get_peb(&self, info: &PROCESS_BASIC_INFORMATION) -> Option { - self.read_struct(info.PebBaseAddress as _) - } - - fn get_proc_params(&self, peb: &PEB) -> Option { - self.read_struct(peb.ProcessParameters as _) - } - - /// Returns the cwd and args for a 64 bit process - fn get_params_64(&self) -> Option { - let info = self.get_basic_info()?; - let peb = self.get_peb(&info)?; - let params = self.get_proc_params(&peb)?; - - let cmdline = self.read_process_wchar( - params.CommandLine.Buffer as _, - params.CommandLine.Length as _, - )?; - let cwd = self.read_process_wchar( - params.CurrentDirectory.DosPath.Buffer as _, - params.CurrentDirectory.DosPath.Length as _, - )?; - - Some(ProcParams { - argv: cmd_line_to_argv(&cmdline), - cwd: wstr_to_path(&cwd), - console: params.ConsoleHandle, - }) - } - - fn get_proc_params_32(&self, peb32: LPVOID) -> Option { - self.read_struct(peb32) - } - - /// Returns the cwd and args for a 32 bit process - fn get_params_32(&self, peb32: LPVOID) -> Option { - let params = self.get_proc_params_32(peb32)?; - - let cmdline = self.read_process_wchar( - params.CommandLine.Buffer as _, - params.CommandLine.Length as _, - )?; - let cwd = self.read_process_wchar( - params.CurrentDirectory.DosPath.Buffer as _, - params.CurrentDirectory.DosPath.Length as _, - )?; - - Some(ProcParams { - argv: cmd_line_to_argv(&cmdline), - cwd: wstr_to_path(&cwd), - console: params.ConsoleHandle as _, - }) - } - - /// Copies a sized WSTR from the address in the process - fn read_process_wchar(&self, ptr: LPVOID, byte_size: usize) -> Option> { - if byte_size > MAX_PATH * 4 { - // Defend against implausibly large paths, just in - // case we're reading the wrong offset into a kernel struct - return None; - } - - let mut buf = vec![0u16; byte_size / 2]; - let mut bytes_read = 0; - - let res = unsafe { - ReadProcessMemory( - self.proc, - ptr as _, - buf.as_mut_ptr() as _, - byte_size, - &mut bytes_read, - ) - }; - if res == 0 { - return None; - } - - // In the unlikely event that we have a short read, - // truncate the buffer to fit. - let wide_chars_read = bytes_read / 2; - buf.resize(wide_chars_read, 0); - - // Ensure that it is NUL terminated - match buf.iter().position(|&c| c == 0) { - Some(n) => { - // Truncate to include existing NUL but no later chars - buf.resize(n + 1, 0); - } - None => { - // Add a NUL - buf.push(0); - } - } - - Some(buf) - } - - /// Retrieves the start time of the process - fn start_time(&self) -> Option { - const fn empty() -> FILETIME { - FILETIME { - dwLowDateTime: 0, - dwHighDateTime: 0, - } - } - - let mut start = empty(); - let mut exit = empty(); - let mut kernel = empty(); - let mut user = empty(); - - let res = - unsafe { GetProcessTimes(self.proc, &mut start, &mut exit, &mut kernel, &mut user) }; - if res == 0 { - return None; - } - - Some((start.dwHighDateTime as u64) << 32 | start.dwLowDateTime as u64) - } -} - -/// Parse a command line string into an argv array -fn cmd_line_to_argv(buf: &[u16]) -> Vec { - let mut argc = 0; - let argvp = unsafe { CommandLineToArgvW(buf.as_ptr(), &mut argc) }; - if argvp.is_null() { - return vec![]; - } - - let argv = unsafe { std::slice::from_raw_parts(argvp, argc as usize) }; - let mut args = vec![]; - for &arg in argv { - let len = unsafe { libc::wcslen(arg) }; - let arg = unsafe { std::slice::from_raw_parts(arg, len) }; - args.push(wstr_to_string(arg)); - } - unsafe { LocalFree(argvp as _) }; - args -} - -impl Drop for ProcHandle { - fn drop(&mut self) { - log::trace!("ProcHandle::drop(pid={} proc={:?})", self.pid, self.proc); - unsafe { CloseHandle(self.proc) }; - } -} - -impl LocalProcessInfo { - pub fn current_working_dir(pid: u32) -> Option { - log::trace!("current_working_dir({})", pid); - let proc = ProcHandle::new(pid)?; - let params = proc.get_params()?; - Some(params.cwd) - } - - pub fn executable_path(pid: u32) -> Option { - log::trace!("executable_path({})", pid); - let proc = ProcHandle::new(pid)?; - proc.executable() - } - - pub fn with_root_pid(pid: u32) -> Option { - log::trace!("LocalProcessInfo::with_root_pid({}), getting snapshot", pid); - let procs = Snapshot::entries(); - log::trace!("Got snapshot"); - - fn build_proc(info: &PROCESSENTRY32W, procs: &[PROCESSENTRY32W]) -> LocalProcessInfo { - let mut children = HashMap::new(); - - for kid in procs { - if kid.th32ParentProcessID == info.th32ProcessID { - children.insert(kid.th32ProcessID, build_proc(kid, procs)); - } - } - - let mut executable = None; - let mut start_time = 0; - let mut cwd = PathBuf::new(); - let mut argv = vec![]; - let mut console = 0; - - if let Some(proc) = ProcHandle::new(info.th32ProcessID) { - if let Some(exe) = proc.executable() { - executable.replace(exe); - } - if let Some(params) = proc.get_params() { - cwd = params.cwd; - argv = params.argv; - console = params.console as _; - } - if let Some(start) = proc.start_time() { - start_time = start; - } - } - - let executable = executable.unwrap_or_else(|| wstr_to_path(&info.szExeFile)); - let name = match executable.file_name() { - Some(name) => name.to_string_lossy().into_owned(), - None => String::new(), - }; - - LocalProcessInfo { - pid: info.th32ProcessID, - ppid: info.th32ParentProcessID, - name, - executable, - cwd, - argv, - start_time, - status: LocalProcessStatus::Run, - children, - console, - } - } - - if let Some(info) = procs.iter().find(|info| info.th32ProcessID == pid) { - Some(build_proc(info, &procs)) - } else { - None - } - } -} diff --git a/crates/terminal/Cargo.toml b/crates/terminal/Cargo.toml index 6c49fb81bd306071a7a72438b3b13da50465a2dc..5910a2897ca5df9e73c21ee035aa605141978e59 100644 --- a/crates/terminal/Cargo.toml +++ b/crates/terminal/Cargo.toml @@ -9,7 +9,7 @@ doctest = false [dependencies] alacritty_terminal = { git = "https://github.com/zed-industries/alacritty", rev = "4e1f0c6177975a040b37f942dfb0e723e46a9971" } -procinfo = { path = "../procinfo" } +procinfo = { git = "https://github.com/zed-industries/wezterm", rev = "40a7dbf93542fbe4178c2e4b4bd438126a6432b9", default-features = false } editor = { path = "../editor" } util = { path = "../util" } gpui = { path = "../gpui" } diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index a6c2e6aa9ad0b92daa624202ae85e7eae9312bb7..639670fc15feed57e3eb11e55e7fd36f9ad9dc85 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -3,7 +3,7 @@ pub mod modal; pub mod terminal_container_view; pub mod terminal_element; pub mod terminal_view; -// procinfo = { git = "https://github.com/zed-industries/wezterm", rev = "40a7dbf93542fbe4178c2e4b4bd438126a6432b9", default-features = false } + use alacritty_terminal::{ ansi::{ClearMode, Handler}, config::{Config, Program, PtyConfig, Scrolling}, @@ -522,7 +522,6 @@ impl Terminal { AlacTermEvent::Wakeup => { cx.emit(Event::Wakeup); - dbg!("*********"); if self.update_process_info() { cx.emit(Event::TitleChanged) } @@ -1024,49 +1023,5 @@ fn make_search_matches<'a, T>( #[cfg(test)] mod tests { - use libc::c_int; - pub mod terminal_test_context; - - #[test] - pub fn wez_test() { - fn test() -> Option> { - let size = 28; - - //Test data pulled from running the code - let buf = [ - 2, 0, 0, 0, 47, 98, 105, 110, 47, 115, 108, 101, 101, 112, 0, 0, 0, 0, 0, 0, 115, - 108, 101, 101, 112, 0, 53, 0, - ]; - - let mut ptr = &buf[0..size]; - - let argc: c_int = unsafe { std::ptr::read(ptr.as_ptr() as *const c_int) }; - ptr = &ptr[std::mem::size_of::()..]; - - fn consume_cstr(ptr: &mut &[u8]) -> Option { - let nul = ptr.iter().position(|&c| c == 0)?; - let s = String::from_utf8_lossy(&ptr[0..nul]).to_owned().to_string(); - *ptr = ptr.get(nul + 1..)?; - Some(s) - } - - let _exe_path: Option = consume_cstr(&mut ptr)?.into(); - - //Clear out the trailing null pointers - while ptr[0] == 0 { - ptr = ptr.get(1..)?; - } - - let mut args = vec![]; - for _ in 0..argc { - args.push(consume_cstr(&mut ptr)?); - } - Some(args) - } - - assert_eq!(test(), Some(vec!["sleep".to_string(), "5".to_string()])); - } } - -mod wez_proc_info {} diff --git a/crates/terminal/src/terminal_container_view.rs b/crates/terminal/src/terminal_container_view.rs index 6e3d85f5f70c8ecf0655d49216c2663d312721b1..a9d2876e9845145c0c888c90920123a1b870ed58 100644 --- a/crates/terminal/src/terminal_container_view.rs +++ b/crates/terminal/src/terminal_container_view.rs @@ -246,12 +246,13 @@ impl Item for TerminalContainer { .as_ref() .map(|fpi| { format!( - "{} - {}", + "{} - {} {}", fpi.cwd .file_name() .map(|name| name.to_string_lossy().to_string()) .unwrap_or_default(), fpi.name, + (&fpi.argv[1..]).join(" ") ) }) .unwrap_or_else(|| "Terminal".to_string()), From 08b6fd09f949be89f106d1f45d92ac9cd9fc9a2b Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Fri, 2 Sep 2022 23:23:33 -0700 Subject: [PATCH 08/12] Changed bel to use dirty indicator, took away the 'has new content' indicator --- crates/terminal/src/terminal_container_view.rs | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/crates/terminal/src/terminal_container_view.rs b/crates/terminal/src/terminal_container_view.rs index a9d2876e9845145c0c888c90920123a1b870ed58..23f1b2ab168e1544fd1a8ad02ba3d9e2d7d59d17 100644 --- a/crates/terminal/src/terminal_container_view.rs +++ b/crates/terminal/src/terminal_container_view.rs @@ -325,18 +325,14 @@ impl Item for TerminalContainer { fn is_dirty(&self, cx: &gpui::AppContext) -> bool { if let TerminalContainerContent::Connected(connected) = &self.content { - connected.read(cx).has_new_content() + connected.read(cx).has_bell() } else { false } } - fn has_conflict(&self, cx: &AppContext) -> bool { - if let TerminalContainerContent::Connected(connected) = &self.content { - connected.read(cx).has_bell() - } else { - false - } + fn has_conflict(&self, _cx: &AppContext) -> bool { + false } fn should_update_tab_on_event(event: &Self::Event) -> bool { From ec8876bb40142c09bd72ebc022cc62832eaee2e7 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Sat, 3 Sep 2022 13:46:36 -0700 Subject: [PATCH 09/12] Fixed the easy search bugs --- .../terminal/src/terminal_container_view.rs | 51 +++++++++++++------ crates/terminal/src/terminal_view.rs | 2 +- 2 files changed, 37 insertions(+), 16 deletions(-) diff --git a/crates/terminal/src/terminal_container_view.rs b/crates/terminal/src/terminal_container_view.rs index 23f1b2ab168e1544fd1a8ad02ba3d9e2d7d59d17..5f5e1fb054afda62d2e45528942b490c17df2b6a 100644 --- a/crates/terminal/src/terminal_container_view.rs +++ b/crates/terminal/src/terminal_container_view.rs @@ -149,6 +149,13 @@ impl TerminalContainer { associated_directory: None, } } + + fn connected(&self) -> Option> { + match &self.content { + TerminalContainerContent::Connected(vh) => Some(vh.clone()), + TerminalContainerContent::Error(_) => None, + } + } } impl View for TerminalContainer { @@ -428,28 +435,42 @@ impl SearchableItem for TerminalContainer { matches: Vec, cx: &mut ViewContext, ) -> Option { - if let TerminalContainerContent::Connected(connected) = &self.content { - if let Some(selection_head) = connected.read(cx).terminal().read(cx).selection_head { + let connected = self.connected(); + // Selection head might have a value if there's a selection that isn't + // associated with a match. Therefore, if there are no matches, we should + // report None, no matter the state of the terminal + let res = if matches.len() > 0 && connected.is_some() { + if let Some(selection_head) = connected + .unwrap() + .read(cx) + .terminal() + .read(cx) + .selection_head + { // If selection head is contained in a match. Return that match - for (ix, search_match) in matches.iter().enumerate() { - if search_match.contains(&selection_head) { - return Some(ix); - } - - // If not contained, return the next match after the selection head - if search_match.start() > &selection_head { - return Some(ix); - } + if let Some(ix) = matches + .iter() + .enumerate() + .find(|(_, search_match)| { + search_match.contains(&selection_head) + || search_match.start() > &selection_head + }) + .map(|(ix, _)| ix) + { + Some(ix) + } else { + // If no selection after selection head, return the last match + Some(matches.len().saturating_sub(1)) } - - // If no selection after selection head, return the last match - return Some(matches.len().saturating_sub(1)); } else { + // Matches found but no active selection, return the first one Some(0) } } else { None - } + }; + + res } } diff --git a/crates/terminal/src/terminal_view.rs b/crates/terminal/src/terminal_view.rs index 1c49f8b3c2b30054f1cb90c732dfcf840f92931b..ec35ba0a5ce84a01994e788d6110152c96d37f80 100644 --- a/crates/terminal/src/terminal_view.rs +++ b/crates/terminal/src/terminal_view.rs @@ -91,8 +91,8 @@ impl TerminalView { if !cx.is_self_focused() { this.has_new_content = true; cx.notify(); - cx.emit(Event::Wakeup); } + cx.emit(Event::Wakeup); } Event::Bell => { this.has_bell = true; From 93d31e4152cc79ccd24c70f86801c8fb5efab96d Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Sat, 3 Sep 2022 13:54:18 -0700 Subject: [PATCH 10/12] Fixed search to read entire back buffer --- crates/terminal/src/terminal.rs | 33 +++++++++++++++++++++------------ 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 639670fc15feed57e3eb11e55e7fd36f9ad9dc85..633edb5ff689c799630bb526c5e7d2195cc33aa1 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -73,7 +73,7 @@ pub fn init(cx: &mut MutableAppContext) { ///Scroll multiplier that is set to 3 by default. This will be removed when I ///Implement scroll bars. const SCROLL_MULTIPLIER: f32 = 4.; -const MAX_SEARCH_LINES: usize = 100; +// const MAX_SEARCH_LINES: usize = 100; const DEBUG_TERMINAL_WIDTH: f32 = 500.; const DEBUG_TERMINAL_HEIGHT: f32 = 30.; const DEBUG_CELL_WIDTH: f32 = 5.; @@ -982,7 +982,7 @@ impl Terminal { let term = term.lock(); - make_search_matches(&term, &searcher).collect() + all_search_matches(&term, &searcher).collect() }) } } @@ -1005,20 +1005,29 @@ fn make_selection(range: &RangeInclusive) -> Selection { /// Copied from alacritty/src/display/hint.rs HintMatches::visible_regex_matches() /// Iterate over all visible regex matches. -fn make_search_matches<'a, T>( +// fn visible_search_matches<'a, T>( +// term: &'a Term, +// regex: &'a RegexSearch, +// ) -> impl Iterator + 'a { +// let viewport_start = Line(-(term.grid().display_offset() as i32)); +// let viewport_end = viewport_start + term.bottommost_line(); +// let mut start = term.line_search_left(Point::new(viewport_start, Column(0))); +// let mut end = term.line_search_right(Point::new(viewport_end, Column(0))); +// start.line = start.line.max(viewport_start - MAX_SEARCH_LINES); +// end.line = end.line.min(viewport_end + MAX_SEARCH_LINES); + +// RegexIter::new(start, end, AlacDirection::Right, term, regex) +// .skip_while(move |rm| rm.end().line < viewport_start) +// .take_while(move |rm| rm.start().line <= viewport_end) +// } + +fn all_search_matches<'a, T>( term: &'a Term, regex: &'a RegexSearch, ) -> impl Iterator + 'a { - let viewport_start = Line(-(term.grid().display_offset() as i32)); - let viewport_end = viewport_start + term.bottommost_line(); - let mut start = term.line_search_left(Point::new(viewport_start, Column(0))); - let mut end = term.line_search_right(Point::new(viewport_end, Column(0))); - start.line = start.line.max(viewport_start - MAX_SEARCH_LINES); - end.line = end.line.min(viewport_end + MAX_SEARCH_LINES); - + let start = Point::new(term.grid().topmost_line(), Column(0)); + let end = Point::new(term.grid().bottommost_line(), term.grid().last_column()); RegexIter::new(start, end, AlacDirection::Right, term, regex) - .skip_while(move |rm| rm.end().line < viewport_start) - .take_while(move |rm| rm.start().line <= viewport_end) } #[cfg(test)] From cefc6e8705ed9d6ee9e8e493e37436e40bffd999 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Sat, 3 Sep 2022 14:23:49 -0700 Subject: [PATCH 11/12] fixed tab content crash, discovered a giant nest of new issues re: resizing --- crates/terminal/src/terminal_container_view.rs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/crates/terminal/src/terminal_container_view.rs b/crates/terminal/src/terminal_container_view.rs index 5f5e1fb054afda62d2e45528942b490c17df2b6a..ac3695c53aa05b8000ff5cb028e26a8423e2a1be 100644 --- a/crates/terminal/src/terminal_container_view.rs +++ b/crates/terminal/src/terminal_container_view.rs @@ -253,13 +253,19 @@ impl Item for TerminalContainer { .as_ref() .map(|fpi| { format!( - "{} - {} {}", + "{} - {}{}", fpi.cwd .file_name() .map(|name| name.to_string_lossy().to_string()) .unwrap_or_default(), fpi.name, - (&fpi.argv[1..]).join(" ") + { + if fpi.argv.len() >= 1 { + format!(" {}", (&fpi.argv[1..]).join(" ")) + } else { + "".to_string() + } + } ) }) .unwrap_or_else(|| "Terminal".to_string()), @@ -463,8 +469,8 @@ impl SearchableItem for TerminalContainer { Some(matches.len().saturating_sub(1)) } } else { - // Matches found but no active selection, return the first one - Some(0) + // Matches found but no active selection, return the first last one (closest to cursor) + Some(matches.len().saturating_sub(1)) } } else { None From 7f7ec68427a16c76f86db043877db08ac2d40f34 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Sat, 3 Sep 2022 14:45:45 -0700 Subject: [PATCH 12/12] Improved formatting of tab title --- .../terminal/src/terminal_container_view.rs | 36 ++++++++++++------- crates/util/src/lib.rs | 17 +++++++++ 2 files changed, 40 insertions(+), 13 deletions(-) diff --git a/crates/terminal/src/terminal_container_view.rs b/crates/terminal/src/terminal_container_view.rs index ac3695c53aa05b8000ff5cb028e26a8423e2a1be..5a1d27fb7a8679c5bad2e7a2d3fe9233e1d08f0a 100644 --- a/crates/terminal/src/terminal_container_view.rs +++ b/crates/terminal/src/terminal_container_view.rs @@ -7,6 +7,7 @@ use gpui::{ actions, elements::*, AnyViewHandle, AppContext, Entity, ModelHandle, MutableAppContext, Task, View, ViewContext, ViewHandle, }; +use util::truncate_and_trailoff; use workspace::searchable::{SearchEvent, SearchOptions, SearchableItem, SearchableItemHandle}; use workspace::{Item, Workspace}; @@ -253,19 +254,28 @@ impl Item for TerminalContainer { .as_ref() .map(|fpi| { format!( - "{} - {}{}", - fpi.cwd - .file_name() - .map(|name| name.to_string_lossy().to_string()) - .unwrap_or_default(), - fpi.name, - { - if fpi.argv.len() >= 1 { - format!(" {}", (&fpi.argv[1..]).join(" ")) - } else { - "".to_string() - } - } + "{} — {}", + truncate_and_trailoff( + &fpi.cwd + .file_name() + .map(|name| name.to_string_lossy().to_string()) + .unwrap_or_default(), + 25 + ), + truncate_and_trailoff( + &{ + format!( + "{}{}", + fpi.name, + if fpi.argv.len() >= 1 { + format!(" {}", (&fpi.argv[1..]).join(" ")) + } else { + "".to_string() + } + ) + }, + 25 + ) ) }) .unwrap_or_else(|| "Terminal".to_string()), diff --git a/crates/util/src/lib.rs b/crates/util/src/lib.rs index 9b6507461576baf0068610b69743477b720e1fb1..97f409f410c36c2b2de7fb14e9cc7ef94f4996b1 100644 --- a/crates/util/src/lib.rs +++ b/crates/util/src/lib.rs @@ -9,6 +9,23 @@ use std::{ task::{Context, Poll}, }; +pub fn truncate(s: &str, max_chars: usize) -> &str { + match s.char_indices().nth(max_chars) { + None => s, + Some((idx, _)) => &s[..idx], + } +} + +pub fn truncate_and_trailoff(s: &str, max_chars: usize) -> String { + debug_assert!(max_chars >= 5); + + if s.len() > max_chars { + format!("{}…", truncate(&s, max_chars.saturating_sub(3))) + } else { + s.to_string() + } +} + pub fn post_inc + AddAssign + Copy>(value: &mut T) -> T { let prev = *value; *value += T::from(1);