New tab opens on current directory

This patch add a new feature, now tab behavior now inherits the
active tab’s current directory on Linux controlled by a settings
toggle, and default is off
This commit is contained in:
Nicolas Danelon 2026-03-06 13:53:46 +01:00
parent 7ce2974273
commit 75714dedee
4 changed files with 81 additions and 15 deletions

View file

@ -63,6 +63,8 @@ focus-follow-mouse = Typing focus follows mouse
advanced = Advanced advanced = Advanced
show-headerbar = Show header show-headerbar = Show header
show-header-description = Reveal the header from the right-click menu show-header-description = Reveal the header from the right-click menu
tab-new-inherit-working-directory = New tabs use current directory
tab-new-inherit-working-directory-description = Open new tabs in the active tab's working directory
### Keyboard shortcuts ### Keyboard shortcuts
add-another-keybinding = Add another keybinding add-another-keybinding = Add another keybinding

View file

@ -235,6 +235,8 @@ pub struct Config {
pub syntax_theme_dark: String, pub syntax_theme_dark: String,
pub syntax_theme_light: String, pub syntax_theme_light: String,
pub focus_follow_mouse: bool, pub focus_follow_mouse: bool,
#[serde(default)]
pub tab_new_inherit_working_directory: bool,
pub default_profile: Option<ProfileId>, pub default_profile: Option<ProfileId>,
#[serde(default)] #[serde(default)]
pub shortcuts_custom: Shortcuts, pub shortcuts_custom: Shortcuts,
@ -249,6 +251,7 @@ impl Default for Config {
color_schemes_light: BTreeMap::new(), color_schemes_light: BTreeMap::new(),
dim_font_weight: Weight::NORMAL.0, dim_font_weight: Weight::NORMAL.0,
focus_follow_mouse: false, focus_follow_mouse: false,
tab_new_inherit_working_directory: false,
font_name: "Noto Sans Mono".to_string(), font_name: "Noto Sans Mono".to_string(),
font_size: 14, font_size: 14,
font_size_zoom_step_mul_100: 100, font_size_zoom_step_mul_100: 100,

View file

@ -431,6 +431,7 @@ pub enum Message {
ShowHeaderBar(bool), ShowHeaderBar(bool),
SyntaxTheme(ColorSchemeKind, usize), SyntaxTheme(ColorSchemeKind, usize),
SystemThemeChange, SystemThemeChange,
TabNewInheritWorkingDirectory(bool),
TabActivate(segmented_button::Entity), TabActivate(segmented_button::Entity),
TabActivateJump(usize), TabActivateJump(usize),
TabClose(Option<segmented_button::Entity>), TabClose(Option<segmented_button::Entity>),
@ -1483,11 +1484,21 @@ impl App {
.toggler(self.config.focus_follow_mouse, Message::FocusFollowMouse), .toggler(self.config.focus_follow_mouse, Message::FocusFollowMouse),
); );
let advanced_section = widget::settings::section().title(fl!("advanced")).add( let advanced_section = widget::settings::section()
widget::settings::item::builder(fl!("show-headerbar")) .title(fl!("advanced"))
.description(fl!("show-header-description")) .add(
.toggler(self.config.show_headerbar, Message::ShowHeaderBar), 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!("tab-new-inherit-working-directory"))
.description(fl!("tab-new-inherit-working-directory-description"))
.toggler(
self.config.tab_new_inherit_working_directory,
Message::TabNewInheritWorkingDirectory,
),
);
widget::settings::view_column(vec![ widget::settings::view_column(vec![
appearance_section.into(), appearance_section.into(),
@ -1501,11 +1512,23 @@ impl App {
self.config.default_profile self.config.default_profile
} }
fn active_terminal_working_directory(&self) -> Option<PathBuf> {
let tab_model = self.pane_model.active()?;
let entity = tab_model.active();
let terminal = tab_model.data::<Mutex<Terminal>>(entity)?;
let terminal = terminal.lock().unwrap();
terminal.working_directory()
}
fn create_and_focus_new_terminal( fn create_and_focus_new_terminal(
&mut self, &mut self,
pane: pane_grid::Pane, pane: pane_grid::Pane,
profile_id_opt: Option<ProfileId>, profile_id_opt: Option<ProfileId>,
inherit_working_directory: bool,
) -> Task<Message> { ) -> Task<Message> {
let inherited_working_directory = inherit_working_directory
.then(|| self.active_terminal_working_directory())
.flatten();
self.pane_model.set_focus(pane); self.pane_model.set_focus(pane);
match &self.term_event_tx_opt { match &self.term_event_tx_opt {
Some(term_event_tx) => { Some(term_event_tx) => {
@ -1542,12 +1565,13 @@ impl App {
} }
return None; return None;
}), }),
working_directory: startup_options.working_directory.or_else( working_directory: startup_options
|| { .working_directory
.or_else(|| inherited_working_directory.clone())
.or_else(|| {
(!profile.working_directory.is_empty()) (!profile.working_directory.is_empty())
.then(|| profile.working_directory.clone().into()) .then(|| profile.working_directory.clone().into())
}, }),
),
drain_on_exit: startup_options.drain_on_exit drain_on_exit: startup_options.drain_on_exit
|| profile.drain_on_exit, || profile.drain_on_exit,
..startup_options ..startup_options
@ -1559,7 +1583,11 @@ impl App {
}; };
(options, tab_title_override) (options, tab_title_override)
} else { } else {
(self.startup_options.take().unwrap_or_default(), None) let mut options = self.startup_options.take().unwrap_or_default();
if options.working_directory.is_none() {
options.working_directory = inherited_working_directory.clone();
}
(options, None)
}; };
let entity = tab_model let entity = tab_model
@ -2541,7 +2569,7 @@ impl Application for App {
if let Some((pane, _)) = result { if let Some((pane, _)) = result {
self.terminal_ids.insert(pane, widget::Id::unique()); self.terminal_ids.insert(pane, widget::Id::unique());
let command = let command =
self.create_and_focus_new_terminal(pane, self.get_default_profile()); self.create_and_focus_new_terminal(pane, self.get_default_profile(), false);
self.pane_model.panes_created += 1; self.pane_model.panes_created += 1;
return command; return command;
} }
@ -2653,8 +2681,11 @@ impl Application for App {
return self.save_profiles(); return self.save_profiles();
} }
Message::ProfileOpen(profile_id) => { Message::ProfileOpen(profile_id) => {
return self return self.create_and_focus_new_terminal(
.create_and_focus_new_terminal(self.pane_model.focused(), Some(profile_id)); self.pane_model.focused(),
Some(profile_id),
false,
);
} }
Message::ProfileRemove(profile_id) => { Message::ProfileRemove(profile_id) => {
// Reset matching terminals to default profile // Reset matching terminals to default profile
@ -2720,6 +2751,12 @@ impl Application for App {
return self.update_config(); return self.update_config();
} }
} }
Message::TabNewInheritWorkingDirectory(tab_new_inherit_working_directory) => {
config_set!(
tab_new_inherit_working_directory,
tab_new_inherit_working_directory
);
}
Message::UseBrightBold(use_bright_bold) => { Message::UseBrightBold(use_bright_bold) => {
if use_bright_bold != self.config.use_bright_bold { if use_bright_bold != self.config.use_bright_bold {
config_set!(use_bright_bold, use_bright_bold); config_set!(use_bright_bold, use_bright_bold);
@ -2863,10 +2900,15 @@ impl Application for App {
return self.create_and_focus_new_terminal( return self.create_and_focus_new_terminal(
self.pane_model.focused(), self.pane_model.focused(),
self.get_default_profile(), self.get_default_profile(),
self.config.tab_new_inherit_working_directory,
); );
} }
Message::TabNewNoProfile => { Message::TabNewNoProfile => {
return self.create_and_focus_new_terminal(self.pane_model.focused(), None); return self.create_and_focus_new_terminal(
self.pane_model.focused(),
None,
self.config.tab_new_inherit_working_directory,
);
} }
Message::TabNext => { Message::TabNext => {
if let Some(tab_model) = self.pane_model.active() { if let Some(tab_model) = self.pane_model.active() {

View file

@ -28,7 +28,8 @@ use indexmap::IndexSet;
use std::{ use std::{
borrow::Cow, borrow::Cow,
collections::HashMap, collections::HashMap,
io, mem, fs, io, mem,
path::PathBuf,
sync::{ sync::{
Arc, Mutex, Weak, Arc, Mutex, Weak,
atomic::{AtomicU32, Ordering}, atomic::{AtomicU32, Ordering},
@ -257,6 +258,7 @@ pub struct Terminal {
notifier: Notifier, notifier: Notifier,
search_regex_opt: Option<RegexSearch>, search_regex_opt: Option<RegexSearch>,
search_value: String, search_value: String,
shell_pid: Option<u32>,
size: Size, size: Size,
use_bright_bold: bool, use_bright_bold: bool,
zoom_adj: i8, zoom_adj: i8,
@ -328,6 +330,10 @@ impl Terminal {
let window_id = 0; let window_id = 0;
let pty = tty::new(&options, size.into(), window_id)?; let pty = tty::new(&options, size.into(), window_id)?;
#[cfg(not(windows))]
let shell_pid = Some(pty.child().id());
#[cfg(windows)]
let shell_pid = pty.child_watcher().pid().map(|pid| pid.get());
let pty_event_loop = let pty_event_loop =
EventLoop::new(term.clone(), event_proxy, pty, options.drain_on_exit, false)?; EventLoop::new(term.clone(), event_proxy, pty, options.drain_on_exit, false)?;
@ -352,6 +358,7 @@ impl Terminal {
profile_id_opt, profile_id_opt,
search_regex_opt: None, search_regex_opt: None,
search_value: String::new(), search_value: String::new(),
shell_pid,
size, size,
tab_title_override, tab_title_override,
term, term,
@ -407,6 +414,18 @@ impl Terminal {
self.notifier.notify(input); self.notifier.notify(input);
} }
pub fn working_directory(&self) -> Option<PathBuf> {
#[cfg(not(windows))]
{
let shell_pid = self.shell_pid?;
fs::read_link(format!("/proc/{shell_pid}/cwd")).ok()
}
#[cfg(windows)]
{
None
}
}
pub fn input_scroll<I: Into<Cow<'static, [u8]>>>(&self, input: I) { pub fn input_scroll<I: Into<Cow<'static, [u8]>>>(&self, input: I) {
self.input_no_scroll(input); self.input_no_scroll(input);
self.scroll(TerminalScroll::Bottom); self.scroll(TerminalScroll::Bottom);