From 43fbe1b81c4693b329ab817e95db0f1d3d96b28d Mon Sep 17 00:00:00 2001 From: Josh Megnauth Date: Wed, 24 Jul 2024 02:48:21 -0400 Subject: [PATCH] feat: Open new terms in focused term's CWD Closes: #251 This patch implements an optional (but enabled by default) feature for opening new terminals using the focused terminal's working directory. The code to retrieve the CWD is largely based on Alacritty's implementation of the same feature. I added Rustix as a new direct dependency for the working directory logic. Both libc and Rustix are transitive dependencies of COSMIC Term. I opted for Rustix over libc to avoid an `unsafe` block as well as for its stronger type guarantees. References: * https://github.com/alacritty/alacritty/blob/6bd1674bd80e73df0d41e4342ad4e34bb7d04f84/alacritty/src/daemon.rs#L85-L108 --- Cargo.lock | 1 + Cargo.toml | 1 + i18n/en/cosmic_term.ftl | 2 ++ src/config.rs | 3 +++ src/main.rs | 50 ++++++++++++++++++++++++++++++++++------- src/terminal.rs | 43 +++++++++++++++++++++++++++++++++++ 6 files changed, 92 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8ec01eb..b52e36b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1298,6 +1298,7 @@ dependencies = [ "paste", "ron", "rust-embed", + "rustix 0.38.34", "serde", "shlex", "smol_str", diff --git a/Cargo.toml b/Cargo.toml index eaa3846..2f9b2a8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,6 +23,7 @@ ron = "0.8" serde = { version = "1", features = ["serde_derive"] } shlex = "1" tokio = { version = "1", features = ["sync"] } +rustix = { version = "0.38", features = ["termios"] } # Internationalization i18n-embed = { version = "0.14", features = [ "fluent-system", diff --git a/i18n/en/cosmic_term.ftl b/i18n/en/cosmic_term.ftl index b7146b6..7bc3b55 100644 --- a/i18n/en/cosmic_term.ftl +++ b/i18n/en/cosmic_term.ftl @@ -60,6 +60,8 @@ focus-follow-mouse = Typing focus follows mouse advanced = Advanced show-headerbar = Show header show-header-description = Reveal the header from the right-click menu. +open-in-cwd = Use parent CWD +open-in-cwd-description = Start new terms using the focused tab's working directory. # Find find-placeholder = Find... diff --git a/src/config.rs b/src/config.rs index 2652b6a..0832dcd 100644 --- a/src/config.rs +++ b/src/config.rs @@ -229,6 +229,8 @@ pub struct Config { pub font_stretch: u16, pub font_size_zoom_step_mul_100: u16, pub opacity: u8, + /// Open new terminal with the current working directory of the focused term + pub open_in_cwd: bool, pub profiles: BTreeMap, pub show_headerbar: bool, pub use_bright_bold: bool, @@ -253,6 +255,7 @@ impl Default for Config { font_stretch: Stretch::Normal.to_number(), font_weight: Weight::NORMAL.0, opacity: 100, + open_in_cwd: true, profiles: BTreeMap::new(), show_headerbar: true, syntax_theme_dark: COSMIC_THEME_DARK.to_string(), diff --git a/src/main.rs b/src/main.rs index 0b8f5cd..bb20357 100644 --- a/src/main.rs +++ b/src/main.rs @@ -335,6 +335,7 @@ pub enum Message { ProfileSyntaxTheme(ProfileId, ColorSchemeKind, usize), ProfileTabTitle(ProfileId, String), SelectAll(Option), + SetOpenInCWD(bool), ShowAdvancedFontSettings(bool), ShowHeaderBar(bool), SyntaxTheme(ColorSchemeKind, usize), @@ -1196,11 +1197,17 @@ impl App { .toggler(self.config.focus_follow_mouse, Message::FocusFollowMouse), ); - let advanced_section = widget::settings::view_section(fl!("advanced")).add( - widget::settings::item::builder(fl!("show-headerbar")) - .description(fl!("show-header-description")) - .toggler(self.config.show_headerbar, Message::ShowHeaderBar), - ); + let advanced_section = widget::settings::view_section(fl!("advanced")) + .add( + widget::settings::item::builder(fl!("show-headerbar")) + .description(fl!("show-header-description")) + .toggler(self.config.show_headerbar, Message::ShowHeaderBar), + ) + .add( + widget::settings::item::builder(fl!("open-in-cwd")) + .description(fl!("open-in-cwd-description")) + .toggler(self.config.open_in_cwd, Message::SetOpenInCWD), + ); widget::settings::view_column(vec![ appearance_section.into(), @@ -1238,6 +1245,22 @@ impl App { Some(colors) => { let current_pane = self.pane_model.focus; if let Some(tab_model) = self.pane_model.active_mut() { + // Current working directory of the selected tab/terminal + #[cfg(not(windows))] + let cwd = self + .config + .open_in_cwd + .then(|| { + tab_model.active_data::>().and_then( + |terminal| { + terminal.lock().unwrap().current_working_directory() + }, + ) + }) + .flatten(); + #[cfg(windows)] + let cwd: Option = None; + // Use the profile options, startup options, or defaults let (options, tab_title_override) = match profile_id_opt .and_then(|profile_id| self.config.profiles.get(&profile_id)) @@ -1250,8 +1273,8 @@ impl App { shell = Some(tty::Shell::new(command, args)); } } - let working_directory = (!profile.working_directory.is_empty()) - .then(|| profile.working_directory.clone().into()); + let working_directory = cwd + .or_else(|| Some(profile.working_directory.clone().into())); let options = tty::Options { shell, @@ -1266,7 +1289,12 @@ impl App { }; (options, tab_title_override) } - None => (self.startup_options.take().unwrap_or_default(), None), + None => { + let mut options = + self.startup_options.take().unwrap_or_default(); + options.working_directory = cwd; + (options, None) + } }; let entity = tab_model .insert() @@ -2198,6 +2226,12 @@ impl Application for App { } return self.update_focus(); } + Message::SetOpenInCWD(open_in_cwd) => { + if open_in_cwd != self.config.open_in_cwd { + self.config.open_in_cwd = open_in_cwd; + return self.update_config(); + } + } Message::ShowHeaderBar(show_headerbar) => { if show_headerbar != self.config.show_headerbar { config_set!(show_headerbar, show_headerbar); diff --git a/src/terminal.rs b/src/terminal.rs index 87541cd..7b807f4 100644 --- a/src/terminal.rs +++ b/src/terminal.rs @@ -25,6 +25,8 @@ use cosmic_text::{ Weight, Wrap, }; use indexmap::IndexSet; +#[cfg(not(windows))] +use rustix::fd::AsFd; use std::{ borrow::Cow, collections::HashMap, @@ -35,6 +37,8 @@ use std::{ }, time::Instant, }; +#[cfg(not(windows))] +use std::{fs, path::PathBuf}; use tokio::sync::mpsc; pub use alacritty_terminal::grid::Scroll as TerminalScroll; @@ -214,6 +218,10 @@ pub struct Terminal { size: Size, use_bright_bold: bool, zoom_adj: i8, + #[cfg(not(windows))] + master_fd: Option, + #[cfg(not(windows))] + shell_pid: rustix::process::Pid, } impl Terminal { @@ -283,6 +291,11 @@ impl Terminal { let window_id = 0; let pty = tty::new(&options, size.into(), window_id)?; + #[cfg(not(windows))] + let master_fd = pty.file().as_fd().try_clone_to_owned().ok(); + #[cfg(not(windows))] + let shell_pid = rustix::process::Pid::from_child(pty.child()); + let pty_event_loop = EventLoop::new(term.clone(), event_proxy, pty, options.hold, false)?; let notifier = Notifier(pty_event_loop.channel()); let _pty_join_handle = pty_event_loop.spawn(); @@ -306,6 +319,10 @@ impl Terminal { term, use_bright_bold, zoom_adj: Default::default(), + #[cfg(not(windows))] + master_fd, + #[cfg(not(windows))] + shell_pid, }) } @@ -924,6 +941,32 @@ impl Terminal { ); } } + + /// Current working directory + #[cfg(not(windows))] + pub fn current_working_directory(&self) -> Option { + // Largely based off of Alacritty + // https://github.com/alacritty/alacritty/blob/6bd1674bd80e73df0d41e4342ad4e34bb7d04f84/alacritty/src/daemon.rs#L85-L108 + let pid = self + .master_fd + .as_ref() + .and_then(|pid| rustix::termios::tcgetpgrp(pid).ok()) + .or(Some(self.shell_pid))?; + + #[cfg(not(any(target_os = "freebsd", target_os = "macos")))] + let link_path = format!("/proc/{}/cwd", pid.as_raw_nonzero()); + #[cfg(target_os = "freebsd")] + let link_path = format!("/compat/linux/proc/{}/cwd", pid.as_raw_nonzero()); + + #[cfg(not(target_os = "macos"))] + let cwd = fs::read_link(link_path).ok(); + + // TODO: macOS support + #[cfg(target_os = "macos")] + let cwd = None; + + cwd + } } impl Drop for Terminal {