aboutsummaryrefslogtreecommitdiffstats
path: root/src/widgets
diff options
context:
space:
mode:
authorDave Willett <willett@hey.com>2026-07-08 09:47:15 -0700
committerGitHub <noreply@github.com>2026-07-08 18:47:15 +0200
commit01412c2e0999de5b40b68a9d1a731eea52844338 (patch)
tree8e57345f32224f9c087648f62e67436fe65b5d0a /src/widgets
parentchore(deps): bump cached from 1.1.0 to 2.0.2 (#269) (diff)
downloadsl0pstatus-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.rs116
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(),