diff options
| author | Dave Willett <willett@hey.com> | 2026-07-08 09:47:15 -0700 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-07-08 18:47:15 +0200 |
| commit | 01412c2e0999de5b40b68a9d1a731eea52844338 (patch) | |
| tree | 8e57345f32224f9c087648f62e67436fe65b5d0a /src/widgets | |
| parent | chore(deps): bump cached from 1.1.0 to 2.0.2 (#269) (diff) | |
| download | sl0pstatus-01412c2e0999de5b40b68a9d1a731eea52844338.tar.gz sl0pstatus-01412c2e0999de5b40b68a9d1a731eea52844338.tar.xz sl0pstatus-01412c2e0999de5b40b68a9d1a731eea52844338.zip | |
feat(command): add {focused_pane_cwd} to run commands in focused cwd (#273)
* feat(command): add {focused_pane_cwd} to run commands in the focused pane's directory
Command widgets configured with command_<name>_cwd "{focused_pane_cwd}"
now execute in the current working directory of the focused terminal pane,
following it as the user changes directories.
This relies on zellij's CwdChanged event and get_pane_cwd API. The focused
terminal pane of the active tab is tracked via PaneUpdate (seeding the cwd
on focus change) and updated on CwdChanged. Affected command results are
invalidated so they re-run, and stale results from a previous cwd are
discarded to avoid showing the wrong directory.
Closes #92
* fix(command): release focused cwd command locks
Focused-cwd commands can be invalidated while their first run is still in flight. If that result is later discarded as stale, no command result is stored but the per-command lock remains, preventing future reruns.
Release the command lock when invalidating focused-cwd commands so the branch badge can recover on subsequent renders or cwd updates.
Add a regression test for rerunning a command with no stored result after its lock is released.
Diffstat (limited to 'src/widgets')
| -rw-r--r-- | src/widgets/command.rs | 116 |
1 files changed, 108 insertions, 8 deletions
diff --git a/src/widgets/command.rs b/src/widgets/command.rs index 0a8d019..304bf23 100644 --- a/src/widgets/command.rs +++ b/src/widgets/command.rs @@ -22,6 +22,8 @@ lazy_static! { static ref COMMAND_REGEX: Regex = Regex::new("_[a-zA-Z0-9]+$").unwrap(); } +const FOCUSED_PANE_CWD: &str = "{focused_pane_cwd}"; + #[derive(Clone, Debug, PartialEq)] enum RenderMode { Static, @@ -35,6 +37,7 @@ struct CommandConfig { format: Vec<FormattedPart>, env: Option<BTreeMap<String, String>>, cwd: Option<PathBuf>, + follow_focus_cwd: bool, interval: i64, render_mode: RenderMode, click_action: String, @@ -183,6 +186,15 @@ fn run_command_if_needed(command_config: CommandConfig, name: &str, state: &Zell let last_run = get_timestamp_from_event_or_default(name, state, command_config.interval); if ts.timestamp() - last_run.timestamp() >= command_config.interval { + let cwd = if command_config.follow_focus_cwd { + match &state.focused_pane_cwd { + Some(cwd) => Some(cwd.clone()), + None => return false, + } + } else { + command_config.cwd.clone() + }; + let mut context = BTreeMap::new(); context.insert("name".to_owned(), name.to_owned()); context.insert( @@ -190,16 +202,20 @@ fn run_command_if_needed(command_config: CommandConfig, name: &str, state: &Zell ts.format(TIMESTAMP_FORMAT).to_string(), ); + if let Some(cwd) = &cwd { + context.insert("cwd".to_owned(), cwd.to_string_lossy().into_owned()); + } + #[allow(unused_variables)] let command = commandline_parser(&command_config.command); tracing::debug!("Running command: {:?}", command); - if command_config.env.is_some() || command_config.cwd.is_some() { + if command_config.env.is_some() || cwd.is_some() { #[cfg(all(not(feature = "bench"), not(test)))] run_command_with_env_variables_and_cwd( &command.iter().map(|x| x.as_str()).collect::<Vec<&str>>(), - command_config.env.unwrap(), - command_config.cwd.unwrap(), + command_config.env.unwrap_or_default(), + cwd.unwrap_or_default(), context, ); @@ -218,6 +234,14 @@ fn run_command_if_needed(command_config: CommandConfig, name: &str, state: &Zell false } +pub fn focus_cwd_command_names(zj_conf: &BTreeMap<String, String>) -> Vec<String> { + parse_config(zj_conf) + .into_iter() + .filter(|(_, conf)| conf.follow_focus_cwd) + .map(|(name, _)| name) + .collect() +} + fn parse_config(zj_conf: &BTreeMap<String, String>) -> BTreeMap<String, CommandConfig> { let mut keys: Vec<String> = zj_conf .keys() @@ -235,6 +259,7 @@ fn parse_config(zj_conf: &BTreeMap<String, String>) -> BTreeMap<String, CommandC command: "".to_owned(), format: Vec::new(), cwd: None, + follow_focus_cwd: false, env: None, interval: 1, render_mode: RenderMode::Static, @@ -267,10 +292,18 @@ fn parse_config(zj_conf: &BTreeMap<String, String>) -> BTreeMap<String, CommandC } if key.ends_with("cwd") { - let mut cwd = PathBuf::new(); - cwd.push(zj_conf.get(&key).unwrap().to_owned().clone()); + let value = zj_conf.get(&key).unwrap().to_owned(); + + if value == FOCUSED_PANE_CWD { + command_conf.follow_focus_cwd = true; + command_conf.cwd = None; + } else { + let mut cwd = PathBuf::new(); + cwd.push(value); - command_conf.cwd = Some(cwd); + command_conf.cwd = Some(cwd); + command_conf.follow_focus_cwd = false; + } } if key.ends_with("format") { @@ -349,7 +382,7 @@ fn get_timestamp_from_event_or_default( let ts_context = ts_context.unwrap(); if Local::now().timestamp() - state.start_time.timestamp() < interval { - release(name, state.clone()); + release_command_lock(state, name); } match DateTime::parse_from_str(ts_context, TIMESTAMP_FORMAT) { @@ -370,7 +403,7 @@ fn lock(name: &str, state: ZellijState) -> bool { true } -fn release(name: &str, state: ZellijState) { +pub fn release_command_lock(state: &ZellijState, name: &str) { let path = format!("/tmp/{}.{}.lock", state.plugin_uuid, name); if Path::new(&path).exists() { @@ -436,6 +469,30 @@ mod test { use rstest::rstest; #[test] + pub fn test_focused_pane_cwd_parsing() { + let conf = BTreeMap::from([ + ("command_branch_command".to_owned(), "git branch".to_owned()), + ( + "command_branch_cwd".to_owned(), + "{focused_pane_cwd}".to_owned(), + ), + ("command_static_command".to_owned(), "ls".to_owned()), + ("command_static_cwd".to_owned(), "/tmp".to_owned()), + ]); + + let config = parse_config(&conf); + assert!(config.get("command_branch").unwrap().follow_focus_cwd); + assert_eq!(config.get("command_branch").unwrap().cwd, None); + assert!(!config.get("command_static").unwrap().follow_focus_cwd); + assert_eq!( + config.get("command_static").unwrap().cwd, + Some(PathBuf::from("/tmp")) + ); + + assert_eq!(focus_cwd_command_names(&conf), vec!["command_branch"]); + } + + #[test] pub fn test_commandline_parser() { let input = "pwd"; let result = commandline_parser(input); @@ -453,6 +510,48 @@ mod test { assert_eq!(result, expected); } + #[test] + pub fn test_release_command_lock_allows_rerun_without_result() { + let state = ZellijState { + plugin_uuid: "release_command_lock_test".to_owned(), + ..ZellijState::default() + }; + let command_config = CommandConfig { + command: "echo test".to_owned(), + format: Vec::new(), + env: None, + cwd: None, + follow_focus_cwd: false, + interval: 1, + render_mode: RenderMode::Static, + click_action: "".to_owned(), + hide_on_empty_stdout: false, + }; + + release_command_lock(&state, "test_release"); + + assert!(run_command_if_needed( + command_config.clone(), + "test_release", + &state + )); + assert!(!run_command_if_needed( + command_config.clone(), + "test_release", + &state + )); + + release_command_lock(&state, "test_release"); + + assert!(run_command_if_needed( + command_config, + "test_release", + &state + )); + + release_command_lock(&state, "test_release"); + } + #[rstest] // no result, interval 1 second #[case(1, &ZellijState::default(), true)] @@ -499,6 +598,7 @@ mod test { format: Vec::new(), env: None, cwd: None, + follow_focus_cwd: false, interval, render_mode: RenderMode::Static, click_action: "".to_owned(), |
