aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCan Celik <ogulcancelik@gmail.com>2026-09-11 00:55:19 +0300
committerGitHub <noreply@github.com>2026-09-11 00:55:19 +0300
commit58271459401fa461707c042f7f03867ddcfff3ea (patch)
treed8ae601d636858d9fafba62332870144059b2a3b
parent043b804c44665b07bd503fdad896bbe0a29311a0 (diff)
feat: support conditional sidebar token hiding (#3925)
-rw-r--r--docs/next/website/src/content/docs/configuration.mdx2
-rw-r--r--docs/next/website/src/content/docs/ja/configuration.mdx2
-rw-r--r--docs/next/website/src/content/docs/zh-cn/configuration.mdx2
-rw-r--r--src/app/mod.rs1
-rw-r--r--src/client/shell/config.rs5
-rw-r--r--src/config/sidebar.rs8
-rw-r--r--src/config/sidebar/rules.rs16
-rw-r--r--src/ui/sidebar/tokens.rs65
8 files changed, 87 insertions, 14 deletions
diff --git a/docs/next/website/src/content/docs/configuration.mdx b/docs/next/website/src/content/docs/configuration.mdx
index a8050d2b..34b810b5 100644
--- a/docs/next/website/src/content/docs/configuration.mdx
+++ b/docs/next/website/src/content/docs/configuration.mdx
@@ -415,7 +415,7 @@ rows = [
]
```
-The first matching rule wins. Its specified style fields override the occurrence's defaults; unspecified fields inherit them. If no rule matches, the defaults remain. Matching uses the full token value before display truncation and does not change its text, separators, or visibility. A rule with no style fields stops matching and keeps the defaults.
+The first matching rule wins. Its specified style fields override the occurrence's defaults; unspecified fields inherit them. If no rule matches, the defaults remain. Matching uses the full token value before display truncation. Set `hide = true` on a rule to remove the matching token and its separator; rows with no remaining tokens also disappear. For example, `{ token = "machine", fg = "#61afef", rules = [{ equals = "Local", hide = true }] }` hides the machine token when its label is exactly `Local`. This compares the label, not the connection type. With `hide = false` or omitted, the token stays visible. A rule with no overrides still stops matching and keeps the defaults.
`equals`, `contains`, and `starts_with` take strings and are case-sensitive. Add `ignore_case = true` for ASCII case-insensitive matching; non-ASCII characters remain case-sensitive. Empty strings follow ordinary string matching: empty `equals` matches only an empty value, while empty `contains` or `starts_with` matches any present value.
diff --git a/docs/next/website/src/content/docs/ja/configuration.mdx b/docs/next/website/src/content/docs/ja/configuration.mdx
index e4c722ff..7867e8c4 100644
--- a/docs/next/website/src/content/docs/ja/configuration.mdx
+++ b/docs/next/website/src/content/docs/ja/configuration.mdx
@@ -411,7 +411,7 @@ rows = [
]
```
-最初に一致したルールだけを適用します。指定されたスタイルはその出現箇所のデフォルトを上書きし、省略したフィールドは継承します。一致しなければデフォルトを保ちます。判定には表示幅で切り詰める前の値全体を使い、テキスト、区切り、表示の有無は変更しません。スタイルを指定しないルールに一致すると、そこで判定を終了してデフォルトを保ちます。
+最初に一致したルールだけを適用します。指定されたスタイルはその出現箇所のデフォルトを上書きし、省略したフィールドは継承します。一致しなければデフォルトを保ちます。判定には表示幅で切り詰める前の値全体を使います。ルールに `hide = true` を指定すると、一致したトークンとその区切りを非表示にします。残るトークンがない行も非表示になります。たとえば `{ token = "machine", fg = "#61afef", rules = [{ equals = "Local", hide = true }] }` は、ラベルが正確に `Local` の場合にマシントークンを非表示にします。接続の種類ではなくラベルを比較します。`hide = false` または省略時は表示を維持します。上書きを指定しないルールでも、一致するとそこで判定を終了してデフォルトを保ちます。
`equals`、`contains`、`starts_with` は文字列を受け取り、大文字と小文字を区別します。`ignore_case = true` は ASCII だけを区別しなくなり、非 ASCII 文字は引き続き区別します。空文字列の `equals` は空の値だけに一致し、空の `contains` と `starts_with` は存在するすべての値に一致します。
diff --git a/docs/next/website/src/content/docs/zh-cn/configuration.mdx b/docs/next/website/src/content/docs/zh-cn/configuration.mdx
index 0a87b36a..1bb7d541 100644
--- a/docs/next/website/src/content/docs/zh-cn/configuration.mdx
+++ b/docs/next/website/src/content/docs/zh-cn/configuration.mdx
@@ -411,7 +411,7 @@ rows = [
]
```
-只有第一条匹配的规则生效。指定的样式字段覆盖当前位置的默认值,未指定的字段保持继承。没有规则匹配时保留默认样式。匹配使用显示截断前的完整值,不改变文本、分隔符或可见性。没有样式字段的规则一旦匹配,就会停止后续匹配并保留默认样式。
+只有第一条匹配的规则生效。指定的样式字段覆盖当前位置的默认值,未指定的字段保持继承。没有规则匹配时保留默认样式。匹配使用显示截断前的完整值。在规则中设置 `hide = true` 可移除匹配的 token 及其分隔符;没有剩余 token 的行也会消失。例如,`{ token = "machine", fg = "#61afef", rules = [{ equals = "Local", hide = true }] }` 会在机器标签恰好为 `Local` 时隐藏机器 token。它比较的是标签,而不是连接类型。设置 `hide = false` 或省略时,token 保持可见。没有覆盖字段的规则一旦匹配,仍会停止后续匹配并保留默认值。
`equals`、`contains` 和 `starts_with` 接受字符串,默认区分大小写。添加 `ignore_case = true` 可忽略 ASCII 大小写,非 ASCII 字符仍区分大小写。空字符串遵循普通字符串匹配:空的 `equals` 只匹配空值,空的 `contains` 或 `starts_with` 匹配任何已存在的值。
diff --git a/src/app/mod.rs b/src/app/mod.rs
index b327b5bb..4ac109c7 100644
--- a/src/app/mod.rs
+++ b/src/app/mod.rs
@@ -1848,6 +1848,7 @@ mod tests {
assert_eq!(
app.state.sidebar_agents.rows[0][0]
.style_for_value("90")
+ .unwrap()
.bold,
Some(true)
);
diff --git a/src/client/shell/config.rs b/src/client/shell/config.rs
index 36e1051f..222e027e 100644
--- a/src/client/shell/config.rs
+++ b/src/client/shell/config.rs
@@ -476,7 +476,10 @@ mod tests {
);
assert_eq!(shell.agents.row_gap, 2);
assert_eq!(
- shell.agents.rows[0][0].style_for_value("Local").bold,
+ shell.agents.rows[0][0]
+ .style_for_value("Local")
+ .unwrap()
+ .bold,
Some(true)
);
let previous = shell.agents.clone();
diff --git a/src/config/sidebar.rs b/src/config/sidebar.rs
index 0b489765..9a4cb5e0 100644
--- a/src/config/sidebar.rs
+++ b/src/config/sidebar.rs
@@ -139,10 +139,10 @@ pub enum SpaceSidebarToken {
}
impl AgentSidebarToken {
- pub(crate) fn style_for_value(&self, value: &str) -> SidebarTokenStyle {
+ pub(crate) fn style_for_value(&self, value: &str) -> Option<SidebarTokenStyle> {
match self {
Self::Styled { style, rules, .. } => rules::matching_style(rules, *style, value),
- _ => SidebarTokenStyle::default(),
+ _ => Some(SidebarTokenStyle::default()),
}
}
@@ -155,10 +155,10 @@ impl AgentSidebarToken {
}
impl SpaceSidebarToken {
- pub(crate) fn style_for_value(&self, value: &str) -> SidebarTokenStyle {
+ pub(crate) fn style_for_value(&self, value: &str) -> Option<SidebarTokenStyle> {
match self {
Self::Styled { style, rules, .. } => rules::matching_style(rules, *style, value),
- _ => SidebarTokenStyle::default(),
+ _ => Some(SidebarTokenStyle::default()),
}
}
diff --git a/src/config/sidebar/rules.rs b/src/config/sidebar/rules.rs
index 3ee72e56..049e0979 100644
--- a/src/config/sidebar/rules.rs
+++ b/src/config/sidebar/rules.rs
@@ -8,6 +8,7 @@ pub struct SidebarTokenRule {
condition: Condition,
ignore_case: bool,
style: SidebarTokenStyle,
+ hide: Option<bool>,
}
// Deserialization rejects non-finite thresholds, so equality is reflexive.
@@ -43,6 +44,8 @@ struct RawRule {
bold: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
dim: Option<bool>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ hide: Option<bool>,
}
impl TryFrom<RawRule> for SidebarTokenRule {
@@ -91,6 +94,7 @@ impl TryFrom<RawRule> for SidebarTokenRule {
Ok(Self {
condition,
ignore_case: raw.ignore_case.unwrap_or(false),
+ hide: raw.hide,
style: SidebarTokenStyle {
fg: raw.fg,
bold: raw.bold,
@@ -107,6 +111,7 @@ impl From<SidebarTokenRule> for RawRule {
fg: rule.style.fg,
bold: rule.style.bold,
dim: rule.style.dim,
+ hide: rule.hide,
..Self::default()
};
match rule.condition {
@@ -171,18 +176,21 @@ pub(super) fn matching_style(
rules: &[SidebarTokenRule],
base: SidebarTokenStyle,
value: &str,
-) -> SidebarTokenStyle {
+) -> Option<SidebarTokenStyle> {
let mut numeric = None;
for rule in rules {
if rule.matches(value, &mut numeric) {
- return SidebarTokenStyle {
+ if rule.hide == Some(true) {
+ return None;
+ }
+ return Some(SidebarTokenStyle {
fg: rule.style.fg.or(base.fg),
bold: rule.style.bold.or(base.bold),
dim: rule.style.dim.or(base.dim),
- };
+ });
}
}
- base
+ Some(base)
}
#[cfg(test)]
diff --git a/src/ui/sidebar/tokens.rs b/src/ui/sidebar/tokens.rs
index d9cefe99..c1e38aa9 100644
--- a/src/ui/sidebar/tokens.rs
+++ b/src/ui/sidebar/tokens.rs
@@ -112,7 +112,7 @@ pub(crate) fn agent_rows(
}?;
let style = kind
.text_value()
- .map_or(style, |value| configured.style_for_value(value));
+ .map_or(Some(style), |value| configured.style_for_value(value))?;
Some(ResolvedToken::new(kind, style))
})
.collect::<Vec<_>>();
@@ -168,7 +168,7 @@ pub(crate) fn space_rows(
}?;
let style = kind
.text_value()
- .map_or(style, |value| configured.style_for_value(value));
+ .map_or(Some(style), |value| configured.style_for_value(value))?;
Some(ResolvedToken::new(kind, style))
})
.collect::<Vec<_>>();
@@ -339,6 +339,67 @@ rows = [[{ token = "$load", rules = [{ lt = 50, dim = true }] }]]
}
#[test]
+ fn conditional_hide_removes_tokens_and_empty_rows() {
+ let config: crate::config::SidebarConfig = toml::from_str(
+ r##"
+[agents]
+rows = [[{ token = "machine", fg = "#61afef", rules = [{ equals = "Local", hide = true }] }, "agent"]]
+[agents.rows_by_agent]
+pi = [[{ token = "$load", rules = [{ lt = 50, hide = true }] }], ["agent"]]
+[spaces]
+rows = [[{ token = "$load", rules = [{ lt = 50, hide = true }] }], ["workspace"]]
+"##,
+ ).unwrap();
+ let encoded = toml::to_string(&config).unwrap();
+ assert!(encoded.contains("hide = true"));
+ let config: crate::config::SidebarConfig = toml::from_str(&encoded).unwrap();
+ let mut entry = entry();
+ entry.canonical_agent = None;
+ for (machine, count) in [("Local", 1), ("Remote", 2)] {
+ let mut ctx = context(&entry);
+ ctx.machine = Some(machine);
+ let rows = agent_rows(&config.agents, ctx, "working");
+ assert_eq!(rows[0].len(), count);
+ assert_eq!(
+ rows[0].last().unwrap().kind,
+ ResolvedTokenKind::Agent("pi".into())
+ );
+ }
+ entry.canonical_agent = Some(crate::detect::Agent::Pi);
+ for (value, count) in [("20", 1), ("90", 2)] {
+ entry.tokens.insert("load".into(), value.into());
+ assert_eq!(
+ agent_rows(&config.agents, context(&entry), "working").len(),
+ count
+ );
+ let rows = space_rows(
+ &config.spaces,
+ SpaceTokenContext {
+ workspace: "repo",
+ branch: None,
+ state_text: "working",
+ ahead_behind: None,
+ suppress_git_details: false,
+ tokens: &entry.tokens,
+ },
+ );
+ assert_eq!(rows.len(), count);
+ }
+ }
+
+ #[test]
+ fn conditional_hide_preserves_first_match_wins() {
+ for first in ["hide = false", "bold = true"] {
+ let config: AgentsSidebarConfig = toml::from_str(&format!(
+ "rows = [[{{ token = 'agent', rules = [{{ equals = 'pi', {first} }}, {{ contains = '', hide = true }}] }}]]"
+ )).unwrap();
+ let entry = entry();
+ let rows = agent_rows(&config, context(&entry), "working");
+ assert_eq!(rows[0][0].kind, ResolvedTokenKind::Agent("pi".into()));
+ }
+ }
+
+ #[test]
fn missing_custom_tokens_elide_rows_and_separators() {
let entry = entry();
let config = AgentsSidebarConfig {