aboutsummaryrefslogtreecommitdiff
path: root/src/app
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-05-23 00:32:46 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-05-23 00:32:46 +0300
commit3de6ea799965a7d2aad0f7db2f23f54cd24cb039 (patch)
treec9233a7f3d432aed7761c9db9c0072fee4ff5de2 /src/app
parent0fec1199671becba511e03d98a4921e375a46fe3 (diff)
feat: configure new terminal cwd
Diffstat (limited to 'src/app')
-rw-r--r--src/app/api/panes.rs5
-rw-r--r--src/app/api/tabs.rs16
-rw-r--r--src/app/api/workspaces.rs11
-rw-r--r--src/app/creation.rs47
-rw-r--r--src/app/input/mod.rs6
-rw-r--r--src/app/mod.rs28
-rw-r--r--src/app/state.rs4
7 files changed, 86 insertions, 31 deletions
diff --git a/src/app/api/panes.rs b/src/app/api/panes.rs
index 2b1741d9..e0df6845 100644
--- a/src/app/api/panes.rs
+++ b/src/app/api/panes.rs
@@ -21,14 +21,15 @@ impl App {
};
let (rows, cols) = self.state.estimate_pane_size();
let split_cwd = params.cwd.map(std::path::PathBuf::from).or_else(|| {
- self.state.workspaces.get(ws_idx).and_then(|ws| {
+ let follow_cwd = self.state.workspaces.get(ws_idx).and_then(|ws| {
let tab_idx = ws.find_tab_index_for_pane(target_pane_id)?;
ws.tabs.get(tab_idx)?.cwd_for_pane(
target_pane_id,
&self.state.terminals,
&self.terminal_runtimes,
)
- })
+ });
+ Some(self.resolve_new_terminal_cwd(follow_cwd))
});
let default_shell = self.state.default_shell.clone();
let scrollback_limit_bytes = self.state.pane_scrollback_limit_bytes;
diff --git a/src/app/api/tabs.rs b/src/app/api/tabs.rs
index 7978a17f..06d59fe3 100644
--- a/src/app/api/tabs.rs
+++ b/src/app/api/tabs.rs
@@ -63,15 +63,13 @@ impl App {
} else {
return encode_error(id, "workspace_not_found", "no active workspace");
};
- let cwd = cwd
- .map(PathBuf::from)
- .or_else(|| {
- self.state
- .focused_runtime_in_workspace(&self.terminal_runtimes, ws_idx)
- .and_then(|rt| rt.cwd())
- })
- .or_else(|| std::env::current_dir().ok())
- .unwrap_or_else(|| PathBuf::from("/"));
+ let cwd = cwd.map(PathBuf::from).unwrap_or_else(|| {
+ let follow_cwd = self
+ .state
+ .focused_runtime_in_workspace(&self.terminal_runtimes, ws_idx)
+ .and_then(|rt| rt.cwd());
+ self.resolve_new_terminal_cwd(follow_cwd)
+ });
let (rows, cols) = self.state.estimate_pane_size();
let default_shell = self.state.default_shell.clone();
let scrollback_limit_bytes = self.state.pane_scrollback_limit_bytes;
diff --git a/src/app/api/workspaces.rs b/src/app/api/workspaces.rs
index f5e9a563..92b41486 100644
--- a/src/app/api/workspaces.rs
+++ b/src/app/api/workspaces.rs
@@ -45,11 +45,12 @@ impl App {
id: String,
params: WorkspaceCreateParams,
) -> String {
- let cwd = params
- .cwd
- .map(PathBuf::from)
- .or_else(|| std::env::current_dir().ok())
- .unwrap_or_else(|| PathBuf::from("/"));
+ let cwd = params.cwd.map(PathBuf::from).unwrap_or_else(|| {
+ let follow_cwd = self
+ .workspace_creation_source()
+ .and_then(|ws_idx| self.seed_cwd_from_workspace(ws_idx));
+ self.resolve_new_terminal_cwd(follow_cwd)
+ });
match self.create_workspace_with_options(cwd, params.focus) {
Ok(index) => {
if let Some(label) = params.label {
diff --git a/src/app/creation.rs b/src/app/creation.rs
index f6166a85..2bfe389b 100644
--- a/src/app/creation.rs
+++ b/src/app/creation.rs
@@ -1,19 +1,44 @@
+use std::path::PathBuf;
+
use tracing::error;
use super::{
api_helpers::{pane_agent_status, tab_attention_priority},
App, Mode,
};
-use crate::workspace::Workspace;
+use crate::{config::NewTerminalCwdConfig, workspace::Workspace};
+
+pub(crate) fn resolve_new_terminal_cwd(
+ policy: &NewTerminalCwdConfig,
+ follow_cwd: Option<PathBuf>,
+) -> PathBuf {
+ match policy {
+ NewTerminalCwdConfig::Follow => follow_cwd
+ .or_else(|| std::env::current_dir().ok())
+ .unwrap_or_else(|| PathBuf::from("/")),
+ NewTerminalCwdConfig::Home => std::env::var_os("HOME")
+ .map(PathBuf::from)
+ .or_else(|| std::env::current_dir().ok())
+ .unwrap_or_else(|| PathBuf::from("/")),
+ NewTerminalCwdConfig::Current => {
+ std::env::current_dir().unwrap_or_else(|_| PathBuf::from("/"))
+ }
+ NewTerminalCwdConfig::Path(path) => crate::worktree::expand_tilde_path(path),
+ }
+}
impl App {
- pub(super) fn seed_cwd_from_workspace(&self, ws_idx: usize) -> Option<std::path::PathBuf> {
+ pub(super) fn seed_cwd_from_workspace(&self, ws_idx: usize) -> Option<PathBuf> {
self.state
.workspaces
.get(ws_idx)?
.resolved_identity_cwd_from(&self.state.terminals, &self.terminal_runtimes)
}
+ pub(super) fn resolve_new_terminal_cwd(&self, follow_cwd: Option<PathBuf>) -> PathBuf {
+ resolve_new_terminal_cwd(&self.state.new_terminal_cwd, follow_cwd)
+ }
+
pub(super) fn workspace_creation_source(&self) -> Option<usize> {
if self.state.mode == Mode::Navigate
&& self.state.workspaces.get(self.state.selected).is_some()
@@ -31,11 +56,10 @@ impl App {
/// Create a workspace with a real PTY (needs event_tx).
pub(crate) fn create_workspace(&mut self) {
- let initial_cwd = self
+ let follow_cwd = self
.workspace_creation_source()
- .and_then(|ws_idx| self.seed_cwd_from_workspace(ws_idx))
- .or_else(|| std::env::current_dir().ok())
- .unwrap_or_else(|| std::path::PathBuf::from("/"));
+ .and_then(|ws_idx| self.seed_cwd_from_workspace(ws_idx));
+ let initial_cwd = self.resolve_new_terminal_cwd(follow_cwd);
if let Err(e) = self.create_workspace_with_options(initial_cwd, true) {
error!(err = %e, "failed to create workspace");
self.state.mode = Mode::Navigate;
@@ -44,12 +68,11 @@ impl App {
pub(crate) fn create_tab(&mut self) {
let custom_name = self.state.requested_new_tab_name.take();
- let initial_cwd = self
+ let follow_cwd = self
.state
.active
- .and_then(|ws_idx| self.seed_cwd_from_workspace(ws_idx))
- .or_else(|| std::env::current_dir().ok())
- .unwrap_or_else(|| std::path::PathBuf::from("/"));
+ .and_then(|ws_idx| self.seed_cwd_from_workspace(ws_idx));
+ let initial_cwd = self.resolve_new_terminal_cwd(follow_cwd);
match self.create_tab_with_options(initial_cwd, true) {
Ok(tab_idx) => {
if let Some(name) = custom_name {
@@ -73,7 +96,7 @@ impl App {
pub(super) fn create_tab_with_options(
&mut self,
- initial_cwd: std::path::PathBuf,
+ initial_cwd: PathBuf,
focus: bool,
) -> std::io::Result<usize> {
let Some(ws_idx) = self.state.active else {
@@ -107,7 +130,7 @@ impl App {
pub(crate) fn create_workspace_with_options(
&mut self,
- initial_cwd: std::path::PathBuf,
+ initial_cwd: PathBuf,
focus: bool,
) -> std::io::Result<usize> {
let (rows, cols) = self.state.estimate_pane_size();
diff --git a/src/app/input/mod.rs b/src/app/input/mod.rs
index e357b33d..ef8b93f3 100644
--- a/src/app/input/mod.rs
+++ b/src/app/input/mod.rs
@@ -256,13 +256,17 @@ impl AppState {
let new_rows = (rows / 2).max(4);
let new_cols = (cols / 2).max(10);
- let cwd = self
+ let follow_cwd = self
.active
.and_then(|i| self.workspaces.get(i))
.and_then(|ws| {
let tab = ws.active_tab()?;
tab.cwd_for_pane(tab.layout.focused(), &self.terminals, terminal_runtimes)
});
+ let cwd = Some(super::creation::resolve_new_terminal_cwd(
+ &self.new_terminal_cwd,
+ follow_cwd,
+ ));
if let Some(ws) = self.active.and_then(|i| self.workspaces.get_mut(i)) {
if let Ok(new_pane) = ws.split_focused(
diff --git a/src/app/mod.rs b/src/app/mod.rs
index 1eeba25b..4bcbada7 100644
--- a/src/app/mod.rs
+++ b/src/app/mod.rs
@@ -454,6 +454,7 @@ impl App {
cjk_ime_cursor_shape: config.experimental.cjk_ime_cursor_shape.to_decscusr(),
kitty_graphics_enabled: config.experimental.kitty_graphics,
default_shell: config.terminal.default_shell.clone(),
+ new_terminal_cwd: config.terminal.new_cwd.clone(),
pane_scrollback_limit_bytes: config.advanced.scrollback_limit_bytes,
accent: crate::config::parse_color(&config.ui.accent),
sound: config.ui.sound.clone(),
@@ -1004,6 +1005,7 @@ impl App {
if !invalid_section("terminal") {
self.state.default_shell = config.terminal.default_shell.clone();
+ self.state.new_terminal_cwd = config.terminal.new_cwd.clone();
}
if !invalid_section("worktrees") {
@@ -1461,7 +1463,7 @@ mod tests {
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
std::fs::write(
&path,
- "[terminal]\ndefault_shell = \"nu\"\n[keys]\nnew_workspace = \"prefix+g\"\nprefix = \"ctrl+a\"\n[ui]\nagent_panel_scope = \"current\"\n[ui.toast]\ndelivery = \"herdr\"\n",
+ "[terminal]\ndefault_shell = \"nu\"\nnew_cwd = \"home\"\n[keys]\nnew_workspace = \"prefix+g\"\nprefix = \"ctrl+a\"\n[ui]\nagent_panel_scope = \"current\"\n[ui.toast]\ndelivery = \"herdr\"\n",
)
.unwrap();
std::env::set_var(crate::config::CONFIG_PATH_ENV_VAR, &path);
@@ -1486,6 +1488,10 @@ mod tests {
state::AgentPanelScope::CurrentWorkspace
);
assert_eq!(app.state.default_shell, "nu");
+ assert_eq!(
+ app.state.new_terminal_cwd,
+ crate::config::NewTerminalCwdConfig::Home
+ );
assert!(app.state.config_diagnostic.is_none());
let toast = app.state.toast.as_ref().unwrap();
assert_eq!(toast.kind, crate::app::state::ToastKind::UpdateInstalled);
@@ -2055,6 +2061,26 @@ mod tests {
}
#[test]
+ fn new_terminal_cwd_follow_uses_source_cwd() {
+ let cwd = creation::resolve_new_terminal_cwd(
+ &crate::config::NewTerminalCwdConfig::Follow,
+ Some(std::path::PathBuf::from("/tmp/herdr-source")),
+ );
+
+ assert_eq!(cwd, std::path::PathBuf::from("/tmp/herdr-source"));
+ }
+
+ #[test]
+ fn new_terminal_cwd_path_uses_configured_path() {
+ let cwd = creation::resolve_new_terminal_cwd(
+ &crate::config::NewTerminalCwdConfig::Path("/tmp/herdr-fixed".into()),
+ Some(std::path::PathBuf::from("/tmp/herdr-source")),
+ );
+
+ assert_eq!(cwd, std::path::PathBuf::from("/tmp/herdr-fixed"));
+ }
+
+ #[test]
fn server_stop_request_sets_should_quit_flag() {
let mut app = test_app();
diff --git a/src/app/state.rs b/src/app/state.rs
index ff039c14..5d4fb67d 100644
--- a/src/app/state.rs
+++ b/src/app/state.rs
@@ -1,4 +1,4 @@
-use crate::config::{Keybinds, SoundConfig, ToastConfig, ToastDelivery};
+use crate::config::{Keybinds, NewTerminalCwdConfig, SoundConfig, ToastConfig, ToastDelivery};
use crossterm::event::{KeyCode, KeyModifiers};
use ratatui::layout::{Direction, Rect};
use ratatui::style::Color;
@@ -1075,6 +1075,7 @@ pub struct AppState {
pub cjk_ime_cursor_shape: u8,
pub kitty_graphics_enabled: bool,
pub default_shell: String,
+ pub new_terminal_cwd: NewTerminalCwdConfig,
pub pane_scrollback_limit_bytes: usize,
#[allow(dead_code)] // kept for backward compat; palette.accent is the source of truth
pub accent: Color,
@@ -1362,6 +1363,7 @@ impl AppState {
cjk_ime_cursor_shape: 2, // steady_block
kitty_graphics_enabled: false,
default_shell: String::new(),
+ new_terminal_cwd: NewTerminalCwdConfig::Follow,
pane_scrollback_limit_bytes: crate::config::DEFAULT_SCROLLBACK_LIMIT_BYTES,
accent: Color::Cyan,
sound: SoundConfig {