diff --git a/Cargo.lock b/Cargo.lock index 8846277..062b896 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1313,6 +1313,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 72cb16f..1a9abb3 100644 --- a/i18n/en/cosmic_term.ftl +++ b/i18n/en/cosmic_term.ftl @@ -26,6 +26,8 @@ make-default = Make default working-directory = Working directory hold = Hold remain-open = Remain open after child process exits. +open-in-cwd = Use parent CWD +open-in-cwd-description = Open new terminals using the focused tab's working directory. ## Settings settings = Settings diff --git a/src/config.rs b/src/config.rs index 2652b6a..0f62d59 100644 --- a/src/config.rs +++ b/src/config.rs @@ -198,6 +198,9 @@ pub struct Profile { pub tab_title: String, #[serde(default)] pub working_directory: String, + /// Open new terminal with the current working directory of the focused term + #[serde(default = "cwd_default")] + pub open_in_cwd: bool, #[serde(default)] pub hold: bool, } @@ -212,10 +215,20 @@ impl Default for Profile { tab_title: String::new(), working_directory: String::new(), hold: false, + open_in_cwd: cwd_default(), } } } +#[cfg(not(windows))] +const fn cwd_default() -> bool { + true +} +#[cfg(windows)] +const fn cwd_default() -> bool { + false +} + #[derive(Clone, CosmicConfigEntry, Debug, Deserialize, Eq, PartialEq, Serialize)] pub struct Config { pub app_theme: AppTheme, diff --git a/src/main.rs b/src/main.rs index a770b78..0363260 100644 --- a/src/main.rs +++ b/src/main.rs @@ -334,6 +334,7 @@ pub enum Message { ProfileName(ProfileId, String), ProfileNew, ProfileOpen(ProfileId), + ProfileOpenInCWD(ProfileId, bool), ProfileRemove(ProfileId), ProfileSyntaxTheme(ProfileId, ColorSchemeKind, usize), ProfileTabTitle(ProfileId, String), @@ -994,6 +995,13 @@ impl App { ]) .align_items(Alignment::Center) .padding([0, space_s]), + ) + .add( + widget::settings::item::builder(fl!("open-in-cwd")) + .description(fl!("open-in-cwd-description")) + .toggler(profile.open_in_cwd, move |open_in_cwd| { + Message::ProfileOpenInCWD(profile_id, open_in_cwd) + }), ); let padding = Padding { @@ -1253,6 +1261,25 @@ impl App { shell = Some(tty::Shell::new(command, args)); } } + + #[cfg(not(windows))] + let working_directory = profile + .open_in_cwd + // Evaluate current working working directory based on + // selected tab/terminal + .then(|| { + tab_model.active_data::>().and_then( + |terminal| { + terminal + .lock() + .unwrap() + .current_working_directory() + }, + ) + }) + .flatten() + .or_else(|| Some(profile.working_directory.clone().into())); + #[cfg(windows)] let working_directory = (!profile.working_directory.is_empty()) .then(|| profile.working_directory.clone().into()); @@ -1269,7 +1296,20 @@ 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(); + #[cfg(not(windows))] + { + // Eval CWD since it's the default option + options.working_directory = tab_model + .active_data::>() + .and_then(|terminal| { + terminal.lock().unwrap().current_working_directory() + }); + } + (options, None) + } }; let entity = tab_model .insert() @@ -2153,6 +2193,13 @@ impl Application for App { Message::ProfileOpen(profile_id) => { return self.create_and_focus_new_terminal(self.pane_model.focus, Some(profile_id)); } + Message::ProfileOpenInCWD(profile_id, open_in_cwd) => { + #[cfg(not(windows))] + if let Some(profile) = self.config.profiles.get_mut(&profile_id) { + profile.open_in_cwd = open_in_cwd; + return self.save_profiles(); + } + } Message::ProfileRemove(profile_id) => { // Reset matching terminals to default profile for (_pane, tab_model) in self.pane_model.panes.iter() { 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 {