Skip to content

Commit c261fb2

Browse files
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
1 parent 0652001 commit c261fb2

File tree

6 files changed

+92
-8
lines changed

6 files changed

+92
-8
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ ron = "0.8"
2323
serde = { version = "1", features = ["serde_derive"] }
2424
shlex = "1"
2525
tokio = { version = "1", features = ["sync"] }
26+
rustix = { version = "0.38", features = ["termios"] }
2627
# Internationalization
2728
i18n-embed = { version = "0.14", features = [
2829
"fluent-system",

i18n/en/cosmic_term.ftl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@ focus-follow-mouse = Typing focus follows mouse
6060
advanced = Advanced
6161
show-headerbar = Show header
6262
show-header-description = Reveal the header from the right-click menu.
63+
open-in-cwd = Use parent CWD
64+
open-in-cwd-description = Start new terms using the focused tab's working directory.
6365
6466
# Find
6567
find-placeholder = Find...

src/config.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,8 @@ pub struct Config {
229229
pub font_stretch: u16,
230230
pub font_size_zoom_step_mul_100: u16,
231231
pub opacity: u8,
232+
/// Open new terminal with the current working directory of the focused term
233+
pub open_in_cwd: bool,
232234
pub profiles: BTreeMap<ProfileId, Profile>,
233235
pub show_headerbar: bool,
234236
pub use_bright_bold: bool,
@@ -253,6 +255,7 @@ impl Default for Config {
253255
font_stretch: Stretch::Normal.to_number(),
254256
font_weight: Weight::NORMAL.0,
255257
opacity: 100,
258+
open_in_cwd: true,
256259
profiles: BTreeMap::new(),
257260
show_headerbar: true,
258261
syntax_theme_dark: COSMIC_THEME_DARK.to_string(),

src/main.rs

Lines changed: 42 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,7 @@ pub enum Message {
325325
ProfileSyntaxTheme(ProfileId, ColorSchemeKind, usize),
326326
ProfileTabTitle(ProfileId, String),
327327
SelectAll(Option<segmented_button::Entity>),
328+
SetOpenInCWD(bool),
328329
ShowAdvancedFontSettings(bool),
329330
ShowHeaderBar(bool),
330331
SyntaxTheme(ColorSchemeKind, usize),
@@ -1160,11 +1161,17 @@ impl App {
11601161
.toggler(self.config.focus_follow_mouse, Message::FocusFollowMouse),
11611162
);
11621163

1163-
let advanced_section = widget::settings::view_section(fl!("advanced")).add(
1164-
widget::settings::item::builder(fl!("show-headerbar"))
1165-
.description(fl!("show-header-description"))
1166-
.toggler(self.config.show_headerbar, Message::ShowHeaderBar),
1167-
);
1164+
let advanced_section = widget::settings::view_section(fl!("advanced"))
1165+
.add(
1166+
widget::settings::item::builder(fl!("show-headerbar"))
1167+
.description(fl!("show-header-description"))
1168+
.toggler(self.config.show_headerbar, Message::ShowHeaderBar),
1169+
)
1170+
.add(
1171+
widget::settings::item::builder(fl!("open-in-cwd"))
1172+
.description(fl!("open-in-cwd-description"))
1173+
.toggler(self.config.open_in_cwd, Message::SetOpenInCWD),
1174+
);
11681175

11691176
widget::settings::view_column(vec![
11701177
appearance_section.into(),
@@ -1202,6 +1209,22 @@ impl App {
12021209
Some(colors) => {
12031210
let current_pane = self.pane_model.focus;
12041211
if let Some(tab_model) = self.pane_model.active_mut() {
1212+
// Current working directory of the selected tab/terminal
1213+
#[cfg(not(windows))]
1214+
let cwd = self
1215+
.config
1216+
.open_in_cwd
1217+
.then(|| {
1218+
tab_model.active_data::<Mutex<Terminal>>().and_then(
1219+
|terminal| {
1220+
terminal.lock().unwrap().current_working_directory()
1221+
},
1222+
)
1223+
})
1224+
.flatten();
1225+
#[cfg(windows)]
1226+
let cwd: Option<std::path::PathBuf> = None;
1227+
12051228
// Use the profile options, startup options, or defaults
12061229
let (options, tab_title_override) = match profile_id_opt
12071230
.and_then(|profile_id| self.config.profiles.get(&profile_id))
@@ -1214,8 +1237,8 @@ impl App {
12141237
shell = Some(tty::Shell::new(command, args));
12151238
}
12161239
}
1217-
let working_directory = (!profile.working_directory.is_empty())
1218-
.then(|| profile.working_directory.clone().into());
1240+
let working_directory = cwd
1241+
.or_else(|| Some(profile.working_directory.clone().into()));
12191242

12201243
let options = tty::Options {
12211244
shell,
@@ -1230,7 +1253,12 @@ impl App {
12301253
};
12311254
(options, tab_title_override)
12321255
}
1233-
None => (self.startup_options.take().unwrap_or_default(), None),
1256+
None => {
1257+
let mut options =
1258+
self.startup_options.take().unwrap_or_default();
1259+
options.working_directory = cwd;
1260+
(options, None)
1261+
}
12341262
};
12351263
let entity = tab_model
12361264
.insert()
@@ -2125,6 +2153,12 @@ impl Application for App {
21252153
}
21262154
return self.update_focus();
21272155
}
2156+
Message::SetOpenInCWD(open_in_cwd) => {
2157+
if open_in_cwd != self.config.open_in_cwd {
2158+
self.config.open_in_cwd = open_in_cwd;
2159+
return self.save_config();
2160+
}
2161+
}
21282162
Message::ShowHeaderBar(show_headerbar) => {
21292163
if show_headerbar != self.config.show_headerbar {
21302164
self.config.show_headerbar = show_headerbar;

src/terminal.rs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ use cosmic_text::{
2525
Weight, Wrap,
2626
};
2727
use indexmap::IndexSet;
28+
#[cfg(not(windows))]
29+
use rustix::fd::AsFd;
2830
use std::{
2931
borrow::Cow,
3032
collections::HashMap,
@@ -35,6 +37,8 @@ use std::{
3537
},
3638
time::Instant,
3739
};
40+
#[cfg(not(windows))]
41+
use std::{fs, path::PathBuf};
3842
use tokio::sync::mpsc;
3943

4044
pub use alacritty_terminal::grid::Scroll as TerminalScroll;
@@ -212,6 +216,10 @@ pub struct Terminal {
212216
search_value: String,
213217
size: Size,
214218
use_bright_bold: bool,
219+
#[cfg(not(windows))]
220+
master_fd: Option<rustix::fd::OwnedFd>,
221+
#[cfg(not(windows))]
222+
shell_pid: rustix::process::Pid,
215223
}
216224

217225
impl Terminal {
@@ -281,6 +289,11 @@ impl Terminal {
281289
let window_id = 0;
282290
let pty = tty::new(&options, size.into(), window_id)?;
283291

292+
#[cfg(not(windows))]
293+
let master_fd = pty.file().as_fd().try_clone_to_owned().ok();
294+
#[cfg(not(windows))]
295+
let shell_pid = rustix::process::Pid::from_child(pty.child());
296+
284297
let pty_event_loop = EventLoop::new(term.clone(), event_proxy, pty, options.hold, false)?;
285298
let notifier = Notifier(pty_event_loop.channel());
286299
let _pty_join_handle = pty_event_loop.spawn();
@@ -303,6 +316,10 @@ impl Terminal {
303316
tab_title_override,
304317
term,
305318
use_bright_bold,
319+
#[cfg(not(windows))]
320+
master_fd,
321+
#[cfg(not(windows))]
322+
shell_pid,
306323
})
307324
}
308325

@@ -915,6 +932,32 @@ impl Terminal {
915932
);
916933
}
917934
}
935+
936+
/// Current working directory
937+
#[cfg(not(windows))]
938+
pub fn current_working_directory(&self) -> Option<PathBuf> {
939+
// Largely based off of Alacritty
940+
// https://github.com/alacritty/alacritty/blob/6bd1674bd80e73df0d41e4342ad4e34bb7d04f84/alacritty/src/daemon.rs#L85-L108
941+
let pid = self
942+
.master_fd
943+
.as_ref()
944+
.and_then(|pid| rustix::termios::tcgetpgrp(pid).ok())
945+
.or(Some(self.shell_pid))?;
946+
947+
#[cfg(not(any(target_os = "freebsd", target_os = "macos")))]
948+
let link_path = format!("/proc/{}/cwd", pid.as_raw_nonzero());
949+
#[cfg(target_os = "freebsd")]
950+
let link_path = format!("/compat/linux/proc/{}/cwd", pid.as_raw_nonzero());
951+
952+
#[cfg(not(target_os = "macos"))]
953+
let cwd = fs::read_link(link_path).ok();
954+
955+
// TODO: macOS support
956+
#[cfg(target_os = "macos")]
957+
let cwd = None;
958+
959+
cwd
960+
}
918961
}
919962

920963
impl Drop for Terminal {

0 commit comments

Comments
 (0)