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 {