fix: allow setting an explicit theme hue rather than guessing (#5536)

* fix: allow setting an explicit theme hue rather than guessing

* add pr
This commit is contained in:
Aram Drevekenin
2026-08-25 15:24:04 +02:00
committed by GitHub
parent 4845e07fe5
commit e4b7a39931
25 changed files with 603 additions and 15 deletions
+1
View File
@@ -13,6 +13,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
* feat: allow opting-out of scroll mode sync (https://github.com/zellij-org/zellij/pull/5532)
* fix: save space for title row in fixed-layout panes in titles only mode (https://github.com/zellij-org/zellij/pull/5533)
* fix: properly fire visibility event to plugins when their tab/floating-layer is hidden/shown (https://github.com/zellij-org/zellij/pull/5518 and https://github.com/zellij-org/zellij/pull/5534)
* feat: allow setting explicit_theme_hue (light/dark) (https://github.com/zellij-org/zellij/pull/5536)
## [0.45.0] - 2026-08-20
* feat: allow tabs to have different sizes if clients aren't focused on the same one (https://github.com/zellij-org/zellij/pull/5133)
+1
View File
@@ -454,6 +454,7 @@ impl SessionMetaData {
.unwrap_or_else(|| default_palette().into()),
host_theme_dark,
host_theme_light,
explicit_theme_hue: new_config.options.explicit_theme_hue,
simplified_ui: new_config.options.simplified_ui.unwrap_or(false),
default_shell: new_config.options.default_shell,
pane_frame_style,
+3
View File
@@ -762,6 +762,9 @@ pub(crate) fn plugin_thread_main(
if events.contains(&EventType::InitialKeybinds) {
wasm_bridge.send_initial_keybinds_to_plugin(plugin_id, client_id);
}
if events.contains(&EventType::HostTerminalThemeChanged) {
wasm_bridge.send_host_terminal_theme_mode_to_plugin(plugin_id, client_id);
}
if events.contains(&EventType::HintText) {
let _ = bus
.senders
+27 -2
View File
@@ -23,8 +23,9 @@ use url::Url;
use wasmi::{Engine, Module};
use zellij_utils::consts::{ZELLIJ_CACHE_DIR, ZELLIJ_SESSION_CACHE_DIR, ZELLIJ_TMP_DIR};
use zellij_utils::data::{
FloatingPaneCoordinates, InputMode, KeybindsVec, LayoutInfo, LayoutWithError, PaneContents,
PaneRenderReport, PermissionStatus, PermissionType, PipeMessage, PipeSource,
FloatingPaneCoordinates, HostTerminalThemeMode, InputMode, KeybindsVec, LayoutInfo,
LayoutWithError, PaneContents, PaneRenderReport, PermissionStatus, PermissionType, PipeMessage,
PipeSource,
};
use zellij_utils::downloader::Downloader;
use zellij_utils::input::keybinds::Keybinds;
@@ -200,6 +201,7 @@ pub struct WasmBridge {
base_modes: HashMap<ClientId, InputMode>,
downloader: Downloader,
previous_pane_render_report: Option<PaneRenderReport>,
last_host_terminal_theme_mode: Option<HostTerminalThemeMode>,
pub last_session_save_time: Arc<Mutex<Option<u64>>>, // milliseconds since UNIX epoch
}
@@ -262,6 +264,7 @@ impl WasmBridge {
base_modes: HashMap::new(),
downloader,
previous_pane_render_report: None,
last_host_terminal_theme_mode: None,
last_session_save_time: Arc::new(Mutex::new(None)),
}
}
@@ -881,6 +884,13 @@ impl WasmBridge {
mut updates: Vec<(Option<PluginId>, Option<ClientId>, Event)>,
shutdown_sender: Sender<()>,
) -> Result<()> {
for (plugin_id, client_id, event) in updates.iter() {
if plugin_id.is_none() && client_id.is_none() {
if let Event::HostTerminalThemeChanged(mode) = event {
self.last_host_terminal_theme_mode = Some(*mode);
}
}
}
let plugins_to_update: Vec<(
PluginId,
ClientId,
@@ -1354,6 +1364,21 @@ impl WasmBridge {
}
}
pub fn send_host_terminal_theme_mode_to_plugin(
&self,
plugin_id: PluginId,
client_id: ClientId,
) {
let Some(mode) = self.last_host_terminal_theme_mode else {
return;
};
let _ = self.senders.send_to_plugin(PluginInstruction::Update(vec![(
Some(plugin_id),
Some(client_id),
Event::HostTerminalThemeChanged(mode),
)]));
}
fn send_keybinds_payload_to_plugin(
&self,
plugin_id: PluginId,
+85 -10
View File
@@ -802,6 +802,7 @@ pub enum ScreenInstruction {
host_theme_dark: Option<Styling>,
/// Resolved styling for `theme_light`. See `host_theme_dark`.
host_theme_light: Option<Styling>,
explicit_theme_hue: Option<ThemeHue>,
simplified_ui: bool,
default_shell: Option<PathBuf>,
pane_frame_style: PaneFrameStyle,
@@ -1617,6 +1618,9 @@ pub(crate) struct Screen {
host_descend_keys: Vec<KeyWithModifier>,
host_descended: bool,
host_terminal_theme_mode: Option<HostTerminalThemeMode>,
last_host_reported_theme_mode: Option<HostTerminalThemeMode>,
theme_policy: ThemePolicy,
configured_explicit_theme_hue: Option<ThemeHue>,
host_theme_dark_styling: Option<Styling>,
host_theme_light_styling: Option<Styling>,
nested_session_handling: NestedSessionHandling,
@@ -1629,6 +1633,12 @@ pub(crate) struct Screen {
client_host_terminal_env: HashMap<ClientId, BTreeMap<String, String>>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ThemePolicy {
Auto,
Pinned(HostTerminalThemeMode),
}
/// A pending forward waiting to be dispatched once the current in-flight
/// forward's barrier reply (or timeout) arrives.
#[derive(Debug, Clone)]
@@ -1815,6 +1825,9 @@ impl Screen {
host_descend_keys: vec![],
host_descended: false,
host_terminal_theme_mode: None,
last_host_reported_theme_mode: None,
theme_policy: ThemePolicy::Auto,
configured_explicit_theme_hue: None,
host_theme_dark_styling: None,
host_theme_light_styling: None,
nested_session_handling,
@@ -6828,6 +6841,7 @@ impl Screen {
self.arrow_fonts = should_support_arrow_fonts;
// global configuration
self.style.colors = theme;
self.default_mode_info.update_theme(theme);
self.default_mode_info
.update_rounded_corners(rounded_corners);
@@ -6919,16 +6933,14 @@ impl Screen {
self.broadcast_nested_shortcuts();
Ok(())
}
/// Apply a host-reported color-palette theme mode (CSI 2031 / DSR 997).
///
/// This is the Phase 2 entry-point: it
/// 1. de-duplicates against the last-known mode,
/// 2. swaps the active palette to the configured `theme_dark` / `theme_light`
/// (when both are configured) by reusing the reconfigure propagation,
/// 3. fans out an `Event::HostTerminalThemeChanged` plugin event,
/// 4. forwards a `CSI ?997;{1|2}n` DSR onto the pty of every terminal pane
/// whose app opted in via `CSI ? 2031 h`.
pub fn update_host_terminal_theme_mode(&mut self, mode: HostTerminalThemeMode) -> Result<()> {
self.last_host_reported_theme_mode = Some(mode);
if matches!(self.theme_policy, ThemePolicy::Pinned(_)) {
return Ok(());
}
self.apply_theme_mode(mode)
}
fn apply_theme_mode(&mut self, mode: HostTerminalThemeMode) -> Result<()> {
let err_context = || "Failed to update host terminal theme mode".to_string();
// dedupe
@@ -6950,6 +6962,7 @@ impl Screen {
self.host_theme_dark_styling.is_some() && self.host_theme_light_styling.is_some();
if auto_switch_enabled {
if let Some(theme) = resolved {
self.style.colors = theme;
self.default_mode_info.update_theme(theme);
for tab in self.tabs.values_mut() {
tab.update_theme(theme);
@@ -7047,7 +7060,51 @@ impl Screen {
}
return Ok(());
}
self.update_host_terminal_theme_mode(mode)
self.theme_policy = ThemePolicy::Pinned(mode);
self.apply_theme_mode(mode)
}
fn resolve_default_theme_mode(&mut self) -> Result<()> {
if self.host_terminal_theme_mode.is_some() {
return Ok(());
}
if !matches!(self.theme_policy, ThemePolicy::Auto) {
return Ok(());
}
if self.host_theme_dark_styling.is_some() && self.host_theme_light_styling.is_some() {
self.apply_theme_mode(HostTerminalThemeMode::Dark)?;
}
Ok(())
}
fn reapply_effective_theme_mode(&mut self) -> Result<()> {
let known_mode = match self.theme_policy {
ThemePolicy::Pinned(mode) => Some(mode),
ThemePolicy::Auto => self.host_terminal_theme_mode,
};
if let Some(mode) = known_mode {
self.host_terminal_theme_mode = None;
self.apply_theme_mode(mode)?;
}
Ok(())
}
fn apply_configured_explicit_theme_hue(
&mut self,
explicit_theme_hue: Option<ThemeHue>,
) -> Result<()> {
self.configured_explicit_theme_hue = explicit_theme_hue;
match explicit_theme_hue {
Some(hue) => {
let mode = HostTerminalThemeMode::from(hue);
self.theme_policy = ThemePolicy::Pinned(mode);
self.apply_theme_mode(mode)
},
None => {
self.theme_policy = ThemePolicy::Auto;
match self.last_host_reported_theme_mode {
Some(mode) => self.apply_theme_mode(mode),
None => Ok(()),
}
},
}
}
pub fn toggle_pane_pinned(&mut self, client_id: ClientId) {
active_tab_and_connected_client_id!(
@@ -7975,6 +8032,8 @@ pub(crate) fn screen_thread_main(
);
}
let explicit_theme_hue = config.options.explicit_theme_hue;
let config_options = config.options;
let host_notification_protocol = config_options
.host_notification_protocol
@@ -8105,6 +8164,13 @@ pub(crate) fn screen_thread_main(
screen.host_theme_light_styling = host_theme_light_styling;
screen.paste_buffer_read_enabled = dangerously_enable_paste_buffer_read;
screen.set_host_notification_protocol(host_notification_protocol);
if explicit_theme_hue.is_some() {
screen
.apply_configured_explicit_theme_hue(explicit_theme_hue)
.non_fatal();
} else {
screen.resolve_default_theme_mode().non_fatal();
}
let mut pending_tab_ids: HashSet<usize> = HashSet::new();
let mut pending_tab_switches: HashSet<(usize, ClientId)> = HashSet::new(); // usize is the
@@ -11465,6 +11531,7 @@ pub(crate) fn screen_thread_main(
theme,
host_theme_dark,
host_theme_light,
explicit_theme_hue,
simplified_ui,
default_shell,
pane_frame_style,
@@ -11526,6 +11593,14 @@ pub(crate) fn screen_thread_main(
client_id,
)
.non_fatal();
if explicit_theme_hue != screen.configured_explicit_theme_hue {
screen
.apply_configured_explicit_theme_hue(explicit_theme_hue)
.non_fatal();
} else {
screen.reapply_effective_theme_mode().non_fatal();
}
screen.resolve_default_theme_mode().non_fatal();
},
ScreenInstruction::RerunCommandPane(terminal_pane_id, completion_tx) => {
screen.rerun_command_pane_with_id(terminal_pane_id, completion_tx)
+246
View File
@@ -9994,6 +9994,252 @@ fn color_palette_mode_query_skips_plugin_panes() {
);
}
fn styling_with_background(color: (u8, u8, u8)) -> zellij_utils::data::Styling {
let mut styling = zellij_utils::data::Styling::default();
styling.text_unselected.background = zellij_utils::data::PaletteColor::Rgb(color);
styling
}
const TEST_DARK_BG: (u8, u8, u8) = (17, 17, 17);
const TEST_LIGHT_BG: (u8, u8, u8) = (238, 238, 238);
fn create_new_screen_with_dark_and_light_themes(size: Size) -> (Screen, ThemeCapture) {
let (mut screen, capture) = create_new_screen_with_theme_capture(size);
screen.host_theme_dark_styling = Some(styling_with_background(TEST_DARK_BG));
screen.host_theme_light_styling = Some(styling_with_background(TEST_LIGHT_BG));
(screen, capture)
}
#[test]
fn explicit_theme_hue_resolves_the_session_appearance() {
let size = Size { cols: 80, rows: 20 };
let (mut screen, capture) = create_new_screen_with_dark_and_light_themes(size);
screen
.apply_configured_explicit_theme_hue(Some(zellij_utils::data::ThemeHue::Light))
.expect("explicit hue applied");
assert_eq!(
screen.host_terminal_theme_mode,
Some(zellij_utils::data::HostTerminalThemeMode::Light),
"an explicit hue must become the session's effective mode"
);
assert_eq!(
screen.style.colors.text_unselected.background,
zellij_utils::data::PaletteColor::Rgb(TEST_LIGHT_BG),
"Screen's own style must track the swap so later panes inherit it"
);
let events = capture.drain_plugin_events();
assert!(
events.iter().any(|e| matches!(
e,
Event::HostTerminalThemeChanged(zellij_utils::data::HostTerminalThemeMode::Light)
)),
"plugins must learn the resolved mode, got: {:?}",
events
);
}
#[test]
fn dark_and_light_themes_without_an_explicit_hue_default_to_dark() {
let size = Size { cols: 80, rows: 20 };
let (mut screen, capture) = create_new_screen_with_dark_and_light_themes(size);
screen
.resolve_default_theme_mode()
.expect("default resolved");
assert_eq!(
screen.host_terminal_theme_mode,
Some(zellij_utils::data::HostTerminalThemeMode::Dark),
);
assert_eq!(
screen.style.colors.text_unselected.background,
zellij_utils::data::PaletteColor::Rgb(TEST_DARK_BG),
"a configured dark/light pair must resolve to the dark theme rather than \
to the unrelated static theme"
);
assert!(
capture.drain_plugin_events().iter().any(|e| matches!(
e,
Event::HostTerminalThemeChanged(zellij_utils::data::HostTerminalThemeMode::Dark)
)),
"the resolved mode is real state and must reach plugins"
);
}
#[test]
fn the_default_dark_mode_still_yields_to_the_host_terminal() {
let size = Size { cols: 80, rows: 20 };
let (mut screen, _capture) = create_new_screen_with_dark_and_light_themes(size);
screen
.resolve_default_theme_mode()
.expect("default resolved");
screen
.update_host_terminal_theme_mode(zellij_utils::data::HostTerminalThemeMode::Light)
.expect("host report applied");
assert_eq!(
screen.host_terminal_theme_mode,
Some(zellij_utils::data::HostTerminalThemeMode::Light),
"the default is not a pin - the host terminal remains authoritative"
);
assert_eq!(
screen.style.colors.text_unselected.background,
zellij_utils::data::PaletteColor::Rgb(TEST_LIGHT_BG),
);
}
#[test]
fn no_default_mode_without_both_themes_configured() {
let size = Size { cols: 80, rows: 20 };
let (mut screen, capture) = create_new_screen_with_theme_capture(size);
screen.host_theme_dark_styling = Some(styling_with_background(TEST_DARK_BG));
screen
.resolve_default_theme_mode()
.expect("nothing to resolve");
assert_eq!(
screen.host_terminal_theme_mode, None,
"a lone theme_dark leaves the static theme authoritative, so the mode \
stays genuinely unknown"
);
assert!(
capture.drain_plugin_events().is_empty(),
"no synthetic event may be emitted while the mode is unknown"
);
}
#[test]
fn the_default_does_not_override_an_explicit_hue() {
let size = Size { cols: 80, rows: 20 };
let (mut screen, _capture) = create_new_screen_with_dark_and_light_themes(size);
screen
.apply_configured_explicit_theme_hue(Some(zellij_utils::data::ThemeHue::Light))
.expect("explicit hue applied");
screen
.resolve_default_theme_mode()
.expect("default is a no-op here");
assert_eq!(
screen.host_terminal_theme_mode,
Some(zellij_utils::data::HostTerminalThemeMode::Light),
);
}
#[test]
fn explicit_theme_hue_outranks_ambient_host_reports() {
let size = Size { cols: 80, rows: 20 };
let (mut screen, capture) = create_new_screen_with_dark_and_light_themes(size);
screen
.apply_configured_explicit_theme_hue(Some(zellij_utils::data::ThemeHue::Light))
.expect("explicit hue applied");
let _ = capture.drain_plugin_events();
screen
.update_host_terminal_theme_mode(zellij_utils::data::HostTerminalThemeMode::Dark)
.expect("ambient report absorbed");
assert_eq!(
screen.host_terminal_theme_mode,
Some(zellij_utils::data::HostTerminalThemeMode::Light),
"the pinned mode must survive an ambient report to the contrary"
);
assert_eq!(
screen.style.colors.text_unselected.background,
zellij_utils::data::PaletteColor::Rgb(TEST_LIGHT_BG),
"the palette must not be swapped while pinned"
);
assert!(
capture.drain_plugin_events().is_empty(),
"a suppressed report is not a mode change and must not reach plugins"
);
}
#[test]
fn manual_theme_action_pins_the_session() {
let size = Size { cols: 80, rows: 20 };
let (mut screen, capture) = create_new_screen_with_dark_and_light_themes(size);
let mut completion_tx = None;
screen
.apply_manual_host_terminal_theme_mode(
zellij_utils::data::HostTerminalThemeMode::Dark,
&mut completion_tx,
)
.expect("manual switch ok");
let _ = capture.drain_plugin_events();
screen
.update_host_terminal_theme_mode(zellij_utils::data::HostTerminalThemeMode::Light)
.expect("ambient report absorbed");
assert_eq!(
screen.host_terminal_theme_mode,
Some(zellij_utils::data::HostTerminalThemeMode::Dark),
"a deliberate choice must not be reverted by the host terminal"
);
assert!(
capture.drain_plugin_events().is_empty(),
"no mode change occurred, so no plugin event may be emitted"
);
}
#[test]
fn removing_explicit_theme_hue_hands_authority_back_to_the_host() {
let size = Size { cols: 80, rows: 20 };
let (mut screen, capture) = create_new_screen_with_dark_and_light_themes(size);
screen
.apply_configured_explicit_theme_hue(Some(zellij_utils::data::ThemeHue::Light))
.expect("explicit hue applied");
screen
.update_host_terminal_theme_mode(zellij_utils::data::HostTerminalThemeMode::Dark)
.expect("ambient report recorded while pinned");
let _ = capture.drain_plugin_events();
screen
.apply_configured_explicit_theme_hue(None)
.expect("unpinned");
assert_eq!(
screen.host_terminal_theme_mode,
Some(zellij_utils::data::HostTerminalThemeMode::Dark),
"unpinning must restore the ambient report that was suppressed"
);
assert_eq!(
screen.style.colors.text_unselected.background,
zellij_utils::data::PaletteColor::Rgb(TEST_DARK_BG),
"the palette must follow the restored mode"
);
}
#[test]
fn effective_theme_mode_is_reasserted_after_a_theme_definition_change() {
let size = Size { cols: 80, rows: 20 };
let (mut screen, _capture) = create_new_screen_with_dark_and_light_themes(size);
screen
.update_host_terminal_theme_mode(zellij_utils::data::HostTerminalThemeMode::Light)
.expect("host report applied");
let new_light = styling_with_background((250, 250, 250));
screen.host_theme_light_styling = Some(new_light);
screen.style.colors = styling_with_background((1, 2, 3));
screen
.reapply_effective_theme_mode()
.expect("mode reasserted");
assert_eq!(
screen.style.colors.text_unselected.background,
zellij_utils::data::PaletteColor::Rgb((250, 250, 250)),
"the session's mode must be re-resolved against the new definitions \
instead of falling back to the static theme"
);
}
#[test]
fn host_theme_no_pty_writes_when_no_panes_subscribed() {
let size = Size { cols: 80, rows: 20 };
+15
View File
@@ -367,6 +367,21 @@ load_plugins {
//
// theme "default"
// Themes to use when the host terminal reports a dark or light color palette.
// Both must be set for automatic switching to take place, otherwise `theme`
// remains authoritative. Until the host terminal reports (or if it never does),
// the dark theme is used.
//
// theme_dark "nightfox"
// theme_light "dayfox"
// Pin the session to a dark or light appearance, ignoring what the host
// terminal reports. Resolved before the first render. When unset, the host
// terminal decides.
// Options: dark, light
//
// explicit_theme_hue "dark"
// The name of the default layout to load on startup
// Default: "default"
// (Requires restart)
@@ -2064,6 +2064,8 @@ pub struct Options {
pub host_notification_protocol: ::core::option::Option<::prost::alloc::string::String>,
#[prost(bool, optional, tag="68")]
pub scroll_mode_sync: ::core::option::Option<bool>,
#[prost(enumeration="ThemeHue", optional, tag="69")]
pub explicit_theme_hue: ::core::option::Option<i32>,
}
/// Pane-targeting action messages
#[allow(clippy::derive_partial_eq_without_eq)]
@@ -3032,6 +3034,35 @@ impl NestedSessionHandling {
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)]
#[repr(i32)]
pub enum ThemeHue {
Unspecified = 0,
Dark = 1,
Light = 2,
}
impl ThemeHue {
/// String value of the enum field names used in the ProtoBuf definition.
///
/// The values are not transformed in any way and thus are considered stable
/// (if the ProtoBuf definition does not change) and safe for programmatic use.
pub fn as_str_name(&self) -> &'static str {
match self {
ThemeHue::Unspecified => "THEME_HUE_UNSPECIFIED",
ThemeHue::Dark => "THEME_HUE_DARK",
ThemeHue::Light => "THEME_HUE_LIGHT",
}
}
/// Creates an enum from field names used in the ProtoBuf definition.
pub fn from_str_name(value: &str) -> ::core::option::Option<Self> {
match value {
"THEME_HUE_UNSPECIFIED" => Some(Self::Unspecified),
"THEME_HUE_DARK" => Some(Self::Dark),
"THEME_HUE_LIGHT" => Some(Self::Light),
_ => None,
}
}
}
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct ClientToServerMsg {
@@ -1224,6 +1224,7 @@ message Options {
optional bool mouse_hover_tips = 59;
optional string host_notification_protocol = 67;
optional bool scroll_mode_sync = 68;
optional ThemeHue explicit_theme_hue = 69;
}
enum OnForceClose {
@@ -1259,6 +1260,12 @@ enum NestedSessionHandling {
NESTED_SESSION_HANDLING_NEVER = 4;
}
enum ThemeHue {
THEME_HUE_UNSPECIFIED = 0;
THEME_HUE_DARK = 1;
THEME_HUE_LIGHT = 2;
}
// Pane-targeting action messages
message ScrollUpByPaneIdAction { PaneId pane_id = 1; }
+44 -1
View File
@@ -1208,9 +1208,11 @@ impl Default for InputMode {
}
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, Hash, ValueEnum)]
pub enum ThemeHue {
#[serde(alias = "light")]
Light,
#[serde(alias = "dark")]
Dark,
}
impl Default for ThemeHue {
@@ -1219,6 +1221,47 @@ impl Default for ThemeHue {
}
}
impl FromStr for ThemeHue {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.trim().to_lowercase().as_str() {
"light" => Ok(ThemeHue::Light),
"dark" => Ok(ThemeHue::Dark),
e => Err(format!(
"Unknown theme hue: '{}' (expected 'dark' or 'light')",
e
)),
}
}
}
impl fmt::Display for ThemeHue {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ThemeHue::Light => write!(f, "light"),
ThemeHue::Dark => write!(f, "dark"),
}
}
}
impl From<ThemeHue> for HostTerminalThemeMode {
fn from(hue: ThemeHue) -> Self {
match hue {
ThemeHue::Light => HostTerminalThemeMode::Light,
ThemeHue::Dark => HostTerminalThemeMode::Dark,
}
}
}
impl From<HostTerminalThemeMode> for ThemeHue {
fn from(mode: HostTerminalThemeMode) -> Self {
match mode {
HostTerminalThemeMode::Light => ThemeHue::Light,
HostTerminalThemeMode::Dark => ThemeHue::Dark,
}
}
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub enum PaletteColor {
Rgb((u8, u8, u8)),
+11 -1
View File
@@ -1,6 +1,6 @@
//! Handles cli and configuration options
use crate::cli::Command;
use crate::data::{InputMode, WebSharing};
use crate::data::{InputMode, ThemeHue, WebSharing};
use clap::{Args, ValueEnum};
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
@@ -141,6 +141,12 @@ pub struct Options {
/// is missing the static `theme` remains authoritative.
#[clap(long, value_parser)]
pub theme_light: Option<String>,
/// Pin the session to a dark or light appearance ("dark" or "light"),
/// resolved before the first render and kept authoritative over ambient
/// host terminal reports (CSI 2031 / DSR 997). When unset, the session
/// follows the host terminal.
#[clap(long, value_enum, hide_possible_values = true, value_parser)]
pub explicit_theme_hue: Option<ThemeHue>,
/// Set the default mode
#[clap(long, value_enum, hide_possible_values = true, value_parser)]
pub default_mode: Option<InputMode>,
@@ -520,6 +526,7 @@ impl Options {
let theme = other.theme.or_else(|| self.theme.clone());
let theme_dark = other.theme_dark.or_else(|| self.theme_dark.clone());
let theme_light = other.theme_light.or_else(|| self.theme_light.clone());
let explicit_theme_hue = other.explicit_theme_hue.or(self.explicit_theme_hue);
let on_force_close = other.on_force_close.or(self.on_force_close);
let scroll_buffer_size = other.scroll_buffer_size.or(self.scroll_buffer_size);
let copy_command = other.copy_command.or_else(|| self.copy_command.clone());
@@ -601,6 +608,7 @@ impl Options {
theme,
theme_dark,
theme_light,
explicit_theme_hue,
default_mode,
default_shell,
default_cwd,
@@ -693,6 +701,7 @@ impl Options {
let theme = other.theme.or_else(|| self.theme.clone());
let theme_dark = other.theme_dark.or_else(|| self.theme_dark.clone());
let theme_light = other.theme_light.or_else(|| self.theme_light.clone());
let explicit_theme_hue = other.explicit_theme_hue.or(self.explicit_theme_hue);
let on_force_close = other.on_force_close.or(self.on_force_close);
let scroll_buffer_size = other.scroll_buffer_size.or(self.scroll_buffer_size);
let copy_command = other.copy_command.or_else(|| self.copy_command.clone());
@@ -770,6 +779,7 @@ impl Options {
theme,
theme_dark,
theme_light,
explicit_theme_hue,
default_mode,
default_shell,
default_cwd,
@@ -984,6 +984,13 @@ impl From<crate::input::options::Options>
NestedSessionHandling::Never => ProtoNestedSessionHandling::Never as i32,
}
}),
explicit_theme_hue: options.explicit_theme_hue.map(|hue| {
use crate::client_server_contract::client_server_contract::ThemeHue as ProtoThemeHue;
match hue {
crate::data::ThemeHue::Dark => ProtoThemeHue::Dark as i32,
crate::data::ThemeHue::Light => ProtoThemeHue::Light as i32,
}
}),
}
}
}
@@ -1120,6 +1127,17 @@ impl TryFrom<crate::client_server_contract::client_server_contract::Options>
_ => Err(anyhow!("Invalid NestedSessionHandling value: {}", n)),
})
.transpose()?,
explicit_theme_hue: options
.explicit_theme_hue
.map(|hue| {
use crate::client_server_contract::client_server_contract::ThemeHue as ProtoThemeHue;
match ProtoThemeHue::try_from(hue).ok() {
Some(ProtoThemeHue::Dark) => Ok(crate::data::ThemeHue::Dark),
Some(ProtoThemeHue::Light) => Ok(crate::data::ThemeHue::Light),
_ => Err(anyhow!("Invalid ThemeHue value: {}", hue)),
}
})
.transpose()?,
})
}
}
@@ -456,6 +456,7 @@ fn test_client_messages() {
theme: Some("theme".to_owned()),
theme_dark: Some("theme_dark".to_owned()),
theme_light: Some("theme_light".to_owned()),
explicit_theme_hue: Some(crate::data::ThemeHue::Light),
default_mode: Some(InputMode::Normal),
default_shell: Some(PathBuf::from("default_shell")),
default_cwd: Some(PathBuf::from("default_cwd")),
+92 -1
View File
@@ -2,7 +2,7 @@ mod kdl_layout_parser;
use crate::data::{
BareKey, Direction, FloatingPaneCoordinates, InputMode, KeyWithModifier, LayoutInfo,
LayoutMetadata, MultiplayerColors, Palette, PaletteColor, PaneId, PaneInfo, PaneManifest,
PermissionType, Resize, SessionInfo, StyleDeclaration, Styling, TabInfo, WebSharing,
PermissionType, Resize, SessionInfo, StyleDeclaration, Styling, TabInfo, ThemeHue, WebSharing,
DEFAULT_STYLES,
};
use crate::envs::EnvironmentVariables;
@@ -2783,6 +2783,16 @@ impl Options {
.map(|(theme, _entry)| theme.to_string());
let theme_light = kdl_property_first_arg_as_string_or_error!(kdl_options, "theme_light")
.map(|(theme, _entry)| theme.to_string());
let explicit_theme_hue =
match kdl_property_first_arg_as_string_or_error!(kdl_options, "explicit_theme_hue") {
Some((string, entry)) => Some(ThemeHue::from_str(string).map_err(|_| {
kdl_parsing_error!(
format!("Invalid value for explicit_theme_hue: '{}'", string),
entry
)
})?),
None => None,
};
let default_mode =
match kdl_property_first_arg_as_string_or_error!(kdl_options, "default_mode") {
Some((string, entry)) => Some(InputMode::from_str(string).map_err(|_| {
@@ -2986,6 +2996,7 @@ impl Options {
theme,
theme_dark,
theme_light,
explicit_theme_hue,
default_mode,
default_shell,
default_cwd,
@@ -3190,6 +3201,35 @@ impl Options {
None
}
}
fn explicit_theme_hue_to_kdl(&self, add_comments: bool) -> Option<KdlNode> {
let comment_text = format!(
"{}\n{}\n{}\n{}\n{}",
" ",
"// Pin the session to a dark or light appearance, ignoring what the",
"// host terminal reports. When unset, the host terminal decides.",
"// Options: dark, light",
"// ",
);
let create_node = |hue: &ThemeHue| -> KdlNode {
let mut node = KdlNode::new("explicit_theme_hue");
node.push(format!("{}", hue));
node
};
if let Some(explicit_theme_hue) = &self.explicit_theme_hue {
let mut node = create_node(explicit_theme_hue);
if add_comments {
node.set_leading(format!("{}\n", comment_text));
}
Some(node)
} else if add_comments {
let mut node = create_node(&ThemeHue::Dark);
node.set_leading(format!("{}\n// ", comment_text));
Some(node)
} else {
None
}
}
fn default_mode_to_kdl(&self, add_comments: bool) -> Option<KdlNode> {
let comment_text = format!(
"{}\n{}\n{}\n{}",
@@ -4758,6 +4798,9 @@ impl Options {
if let Some(theme_light_node) = self.theme_light_to_kdl(add_comments) {
nodes.push(theme_light_node);
}
if let Some(explicit_theme_hue_node) = self.explicit_theme_hue_to_kdl(add_comments) {
nodes.push(explicit_theme_hue_node);
}
if let Some(default_mode) = self.default_mode_to_kdl(add_comments) {
nodes.push(default_mode);
}
@@ -7727,6 +7770,54 @@ fn scroll_mode_sync_round_trips_through_kdl() {
);
}
#[test]
fn explicit_theme_hue_from_kdl() {
let fake_config = r##"
explicit_theme_hue "light"
"##;
let document: KdlDocument = fake_config.parse().unwrap();
let deserialized = Options::from_kdl(&document).unwrap();
assert_eq!(deserialized.explicit_theme_hue, Some(ThemeHue::Light));
let empty_document: KdlDocument = "".parse().unwrap();
let deserialized_empty = Options::from_kdl(&empty_document).unwrap();
assert_eq!(
deserialized_empty.explicit_theme_hue, None,
"an unspecified explicit_theme_hue leaves the host terminal in charge"
);
}
#[test]
fn explicit_theme_hue_rejects_unknown_values() {
let fake_config = r##"
explicit_theme_hue "sepia"
"##;
let document: KdlDocument = fake_config.parse().unwrap();
assert!(
Options::from_kdl(&document).is_err(),
"only 'dark' and 'light' are accepted"
);
}
#[test]
fn explicit_theme_hue_round_trips_through_kdl() {
let fake_config = r##"
explicit_theme_hue "dark"
"##;
let document: KdlDocument = fake_config.parse().unwrap();
let deserialized = Options::from_kdl(&document).unwrap();
let mut serialized = Options::to_kdl(&deserialized, false);
let mut fake_document = KdlDocument::new();
fake_document.nodes_mut().append(&mut serialized);
let deserialized_from_serialized =
Options::from_kdl(&fake_document.to_string().parse::<KdlDocument>().unwrap()).unwrap();
assert_eq!(
deserialized_from_serialized.explicit_theme_hue,
Some(ThemeHue::Dark),
"explicit_theme_hue survives a serialize/parse round trip"
);
}
#[test]
fn config_options_to_string() {
let fake_config = r##"
@@ -322,6 +322,12 @@ web_client {
//
// theme_light "solarized-light"
// Pin the session to a dark or light appearance, ignoring what the
// host terminal reports. When unset, the host terminal decides.
// Options: dark, light
//
// explicit_theme_hue "dark"
// Choose the base input mode of zellij.
// Default: normal
//
@@ -32,6 +32,12 @@ theme "dracula"
//
// theme_light "solarized-light"
// Pin the session to a dark or light appearance, ignoring what the
// host terminal reports. When unset, the host terminal decides.
// Options: dark, light
//
// explicit_theme_hue "dark"
// Choose the base input mode of zellij.
// Default: normal
//
@@ -9,6 +9,7 @@ Options {
theme: None,
theme_dark: None,
theme_light: None,
explicit_theme_hue: None,
default_mode: None,
default_shell: None,
default_cwd: None,
@@ -7,6 +7,7 @@ Options {
theme: None,
theme_dark: None,
theme_light: None,
explicit_theme_hue: None,
default_mode: None,
default_shell: None,
default_cwd: None,
@@ -7,6 +7,7 @@ Options {
theme: None,
theme_dark: None,
theme_light: None,
explicit_theme_hue: None,
default_mode: None,
default_shell: None,
default_cwd: None,
@@ -6099,6 +6099,7 @@ Config {
theme: None,
theme_dark: None,
theme_light: None,
explicit_theme_hue: None,
default_mode: None,
default_shell: None,
default_cwd: None,
@@ -6099,6 +6099,7 @@ Config {
theme: None,
theme_dark: None,
theme_light: None,
explicit_theme_hue: None,
default_mode: None,
default_shell: None,
default_cwd: None,
@@ -94,6 +94,7 @@ Config {
theme: None,
theme_dark: None,
theme_light: None,
explicit_theme_hue: None,
default_mode: None,
default_shell: None,
default_cwd: None,
@@ -7,6 +7,7 @@ Options {
theme: None,
theme_dark: None,
theme_light: None,
explicit_theme_hue: None,
default_mode: None,
default_shell: None,
default_cwd: None,
@@ -6099,6 +6099,7 @@ Config {
theme: None,
theme_dark: None,
theme_light: None,
explicit_theme_hue: None,
default_mode: None,
default_shell: None,
default_cwd: None,
@@ -6099,6 +6099,7 @@ Config {
theme: None,
theme_dark: None,
theme_light: None,
explicit_theme_hue: None,
default_mode: None,
default_shell: None,
default_cwd: None,