diff options
| author | user <user@clank> | 2026-08-08 22:36:30 +0200 |
|---|---|---|
| committer | user <user@clank> | 2026-08-08 22:36:30 +0200 |
| commit | cfae103a7afae1541a46cbfe21b011ffe4d09f2d (patch) | |
| tree | 35db8eac79e73d6cf2a9cb9ccdbf885b815072cc /zellij-utils | |
| parent | chore: stop tracking transient bgrun job artifacts (diff) | |
| download | sl0ppij-cfae103a7afae1541a46cbfe21b011ffe4d09f2d.tar.gz sl0ppij-cfae103a7afae1541a46cbfe21b011ffe4d09f2d.tar.xz sl0ppij-cfae103a7afae1541a46cbfe21b011ffe4d09f2d.zip | |
sl0p: grayscale the contents of unfocused panes
`ui { pane_frames { inactive_grayscale 0-100 } }`, default 0 (off).
Real colour transformation, not the SGR 'faint' attribute zellij already uses
for frames: faint is terminal-dependent, several terminals only apply it to
the 16 ANSI colours, and it does not remove colour. Each cell's colour is
resolved to RGB and blended toward its own Rec.709 luminance, so an unfocused
pane keeps its brightness but drains of colour.
Colour resolution covers the cases that actually occur: RgbCode directly,
ColorIndex via eightbit_to_rgb, and NamedColor by mapping to its 0-15 index.
Cells with NO explicit colour are deliberately left alone - they render in the
terminal default, which is already unsaturated, and rewriting them would mean
emitting an explicit colour for nearly every cell for no visible gain.
Two non-obvious pieces were needed to make it actually work:
- The output buffer only emits CHANGED cells, so an idle pane would keep its
old colours indefinitely after losing focus. Pane::refresh_for_focus_change
forces a full repaint, called from ActivePanes::insert (the single place
focus changes). It is a no-op when the feature is off, so it costs nothing
by default.
- Terminal panes render through the SHARED multi-client path. Focus is per
client, so once unfocused panes are desaturated two clients may need
different output for the same pane. Both tiled and floating panes now fall
back to per-client rendering when grayscale is enabled - the reason this is
opt-in.
Known limitation: sixel/kitty image chunks are not colour-transformed and stay
in colour.
Hot-reloads like the other sl0p options. Tests cover the transform: full
strength yields a true neutral grey, partial sits between original and grey,
indexed and named colours resolve rather than being skipped, luminance is
perceptual (green brighter than blue) not a flat average, already-grey is
unchanged, and non-colour codes are untouched.
Baselines held: zellij-server 1436/30, zellij-utils 436/5.
Diffstat (limited to 'zellij-utils')
| -rw-r--r-- | zellij-utils/src/data.rs | 3 | ||||
| -rw-r--r-- | zellij-utils/src/input/config.rs | 1 | ||||
| -rw-r--r-- | zellij-utils/src/input/theme.rs | 5 | ||||
| -rw-r--r-- | zellij-utils/src/kdl/mod.rs | 11 | ||||
| -rw-r--r-- | zellij-utils/src/plugin_api/event.rs | 1 | ||||
| -rw-r--r-- | zellij-utils/src/plugin_api/style.rs | 1 |
6 files changed, 22 insertions, 0 deletions
diff --git a/zellij-utils/src/data.rs b/zellij-utils/src/data.rs index f424a3fc..9f9205db 100644 --- a/zellij-utils/src/data.rs +++ b/zellij-utils/src/data.rs @@ -1465,6 +1465,9 @@ pub struct Style { /// INNER padding: cells between a pane's frame and its content, per side. /// Purely cosmetic breathing room inside the pane; does not move the frame. pub pane_padding: PanePadding, + /// Desaturation strength (0-100) applied to the CONTENT of panes this + /// client is not focused on. 0 = off. + pub inactive_pane_grayscale: u8, } #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Deserialize, Serialize)] diff --git a/zellij-utils/src/input/config.rs b/zellij-utils/src/input/config.rs index db86de54..77260ff8 100644 --- a/zellij-utils/src/input/config.rs +++ b/zellij-utils/src/input/config.rs @@ -1456,6 +1456,7 @@ mod config_test { gap: Default::default(), padding: Default::default(), gap_aspect: 2, // parser default; see FrameConfig::default() + inactive_grayscale: 0, }, }; assert_eq!(config.ui, expected_ui_config, "Ui config defined in config"); diff --git a/zellij-utils/src/input/theme.rs b/zellij-utils/src/input/theme.rs index f1e0adf1..e1371f85 100644 --- a/zellij-utils/src/input/theme.rs +++ b/zellij-utils/src/input/theme.rs @@ -37,6 +37,9 @@ pub struct FrameConfig { /// wide, so 1 row of gap needs ~2 columns to look like the same distance. /// 0 is treated as 1 (no correction). pub gap_aspect: usize, + /// How strongly to desaturate the CONTENT of unfocused panes, 0-100. + /// 0 disables it entirely (the default). + pub inactive_grayscale: u8, } /// Hand-written rather than derived: `gap_aspect` must default to 2 to match @@ -52,6 +55,7 @@ impl Default for FrameConfig { gap: 0, padding: Default::default(), gap_aspect: 2, + inactive_grayscale: 0, } } } @@ -78,6 +82,7 @@ impl FrameConfig { merged.gap = other.gap; merged.padding = other.padding; merged.gap_aspect = other.gap_aspect; + merged.inactive_grayscale = other.inactive_grayscale; merged } } diff --git a/zellij-utils/src/kdl/mod.rs b/zellij-utils/src/kdl/mod.rs index b5670195..cae59ff6 100644 --- a/zellij-utils/src/kdl/mod.rs +++ b/zellij-utils/src/kdl/mod.rs @@ -5595,6 +5595,10 @@ impl UiConfig { }, None => Default::default(), }; + let inactive_grayscale = + kdl_children_property_first_arg_as_i64!(pane_frames, "inactive_grayscale") + .unwrap_or(0) + .clamp(0, 100) as u8; let frame_config = FrameConfig { rounded_corners, hide_session_name, @@ -5602,6 +5606,7 @@ impl UiConfig { gap, padding, gap_aspect, + inactive_grayscale, }; ui_config.pane_frames = frame_config; } @@ -5659,6 +5664,12 @@ impl UiConfig { } frame_config_children.nodes_mut().push(padding); } + if self.pane_frames.inactive_grayscale > 0 { + has_ui_config = true; + let mut node = KdlNode::new("inactive_grayscale"); + node.push(KdlValue::Base10(self.pane_frames.inactive_grayscale as i64)); + frame_config_children.nodes_mut().push(node); + } if self.pane_frames.gap_aspect != 2 { has_ui_config = true; let mut gap_aspect = KdlNode::new("gap_aspect"); diff --git a/zellij-utils/src/plugin_api/event.rs b/zellij-utils/src/plugin_api/event.rs index 7bb1c7ac..0303ec1f 100644 --- a/zellij-utils/src/plugin_api/event.rs +++ b/zellij-utils/src/plugin_api/event.rs @@ -2496,6 +2496,7 @@ fn serialize_mode_update_event_with_non_default_values() { pane_gap_x: Default::default(), pane_gap_y: Default::default(), pane_padding: Default::default(), + inactive_pane_grayscale: Default::default(), }, capabilities: PluginCapabilities { arrow_fonts: false }, session_name: Some("my awesome test session".to_owned()), diff --git a/zellij-utils/src/plugin_api/style.rs b/zellij-utils/src/plugin_api/style.rs index 527f9841..cb896c76 100644 --- a/zellij-utils/src/plugin_api/style.rs +++ b/zellij-utils/src/plugin_api/style.rs @@ -29,6 +29,7 @@ impl TryFrom<ProtobufStyle> for Style { pane_gap_x: Default::default(), pane_gap_y: Default::default(), pane_padding: Default::default(), + inactive_pane_grayscale: Default::default(), }) } } |
