mirror of
https://github.com/l0ng-ai/tty7.git
synced 2026-09-21 16:02:20 +00:00
feat(theme): support ANSI color overrides (#37)
This commit is contained in:
@@ -39,6 +39,10 @@ pub struct Config {
|
||||
pub theme_preset: String,
|
||||
/// Optional per-color overrides layered on top of the active preset.
|
||||
pub colors: Colors,
|
||||
/// Optional ANSI 0-15 overrides layered on top of the active preset's
|
||||
/// terminal palette. Each field maps to the familiar terminal `colorN`
|
||||
/// slot; `None` keeps the preset's value.
|
||||
pub ansi_colors: AnsiColors,
|
||||
/// Optional keybinding overrides: action name (e.g. "NewTab") → keystroke
|
||||
/// (e.g. "secondary-t", which is ⌘ on macOS and Ctrl elsewhere). Unknown
|
||||
/// actions and unparseable keystrokes are ignored (with a warning) so a bad
|
||||
@@ -233,6 +237,7 @@ impl Default for Config {
|
||||
// depend on ui). Unknown ids fall back to it anyway.
|
||||
theme_preset: "light".to_string(),
|
||||
colors: Colors::default(),
|
||||
ansi_colors: AnsiColors::default(),
|
||||
keybindings: HashMap::new(),
|
||||
// `None` → the platform default shell (login shell on Unix,
|
||||
// PowerShell 7 / Windows PowerShell on Windows), chosen by the
|
||||
@@ -284,6 +289,77 @@ pub struct Colors {
|
||||
pub selection: Option<String>,
|
||||
}
|
||||
|
||||
/// Optional overrides for terminal ANSI colors 0-15. These correspond to the
|
||||
/// standard `color0`…`color15` slots that command-line tools address with SGR
|
||||
/// foreground/background colors; unlike [`Colors`], they do not affect UI chrome
|
||||
/// or the terminal's default foreground/background.
|
||||
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
|
||||
#[serde(default)]
|
||||
pub struct AnsiColors {
|
||||
pub color0: Option<String>,
|
||||
pub color1: Option<String>,
|
||||
pub color2: Option<String>,
|
||||
pub color3: Option<String>,
|
||||
pub color4: Option<String>,
|
||||
pub color5: Option<String>,
|
||||
pub color6: Option<String>,
|
||||
pub color7: Option<String>,
|
||||
pub color8: Option<String>,
|
||||
pub color9: Option<String>,
|
||||
pub color10: Option<String>,
|
||||
pub color11: Option<String>,
|
||||
pub color12: Option<String>,
|
||||
pub color13: Option<String>,
|
||||
pub color14: Option<String>,
|
||||
pub color15: Option<String>,
|
||||
}
|
||||
|
||||
impl AnsiColors {
|
||||
pub fn get(&self, index: usize) -> Option<&Option<String>> {
|
||||
match index {
|
||||
0 => Some(&self.color0),
|
||||
1 => Some(&self.color1),
|
||||
2 => Some(&self.color2),
|
||||
3 => Some(&self.color3),
|
||||
4 => Some(&self.color4),
|
||||
5 => Some(&self.color5),
|
||||
6 => Some(&self.color6),
|
||||
7 => Some(&self.color7),
|
||||
8 => Some(&self.color8),
|
||||
9 => Some(&self.color9),
|
||||
10 => Some(&self.color10),
|
||||
11 => Some(&self.color11),
|
||||
12 => Some(&self.color12),
|
||||
13 => Some(&self.color13),
|
||||
14 => Some(&self.color14),
|
||||
15 => Some(&self.color15),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set(&mut self, index: usize, value: Option<String>) {
|
||||
match index {
|
||||
0 => self.color0 = value,
|
||||
1 => self.color1 = value,
|
||||
2 => self.color2 = value,
|
||||
3 => self.color3 = value,
|
||||
4 => self.color4 = value,
|
||||
5 => self.color5 = value,
|
||||
6 => self.color6 = value,
|
||||
7 => self.color7 = value,
|
||||
8 => self.color8 = value,
|
||||
9 => self.color9 = value,
|
||||
10 => self.color10 = value,
|
||||
11 => self.color11 = value,
|
||||
12 => self.color12 = value,
|
||||
13 => self.color13 = value,
|
||||
14 => self.color14 = value,
|
||||
15 => self.color15 = value,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Global for Config {}
|
||||
|
||||
impl Config {
|
||||
@@ -591,6 +667,31 @@ mod tests {
|
||||
assert_eq!(color_or(&Some("#ffffff".to_string()), 0x000000), white);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ansi_color_overrides_parse_and_default_independently() {
|
||||
let cfg: Config =
|
||||
serde_json::from_str(r##"{"ansi_colors":{"color0":"#575279","color15":"123456"}}"##)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
cfg.ansi_colors.get(0).and_then(|v| v.as_deref()),
|
||||
Some("#575279")
|
||||
);
|
||||
assert_eq!(
|
||||
cfg.ansi_colors.get(15).and_then(|v| v.as_deref()),
|
||||
Some("123456")
|
||||
);
|
||||
assert!(cfg.ansi_colors.get(1).is_some_and(Option::is_none));
|
||||
|
||||
let mut colors = AnsiColors::default();
|
||||
colors.set(0, Some("#111111".to_string()));
|
||||
colors.set(15, Some("#eeeeee".to_string()));
|
||||
assert_eq!(colors.get(0).and_then(|v| v.as_deref()), Some("#111111"));
|
||||
assert_eq!(colors.get(15).and_then(|v| v.as_deref()), Some("#eeeeee"));
|
||||
colors.set(0, None);
|
||||
assert!(colors.get(0).is_some_and(Option::is_none));
|
||||
assert!(colors.get(16).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sanitize_clamps_degenerate_font_metrics() {
|
||||
// A zero/negative/NaN font size or line height would round the row height
|
||||
|
||||
+104
-1
@@ -19,7 +19,7 @@ use crate::daemon::protocol::ShellSpec;
|
||||
use crate::terminal::view::{ChildExited, TerminalView};
|
||||
use crate::ui::palette::{Command, CommandKind, PaletteEvent, PaletteView};
|
||||
use crate::ui::pane::{CloseOutcome, Pane};
|
||||
use crate::ui::settings::{ColorKey, SettingsSection, SettingsState};
|
||||
use crate::ui::settings::{AnsiColorKey, ColorKey, SettingsSection, SettingsState};
|
||||
use crate::ui::theme::{apply_theme, set_menus};
|
||||
|
||||
/// Global font-size bounds and step for the live zoom actions.
|
||||
@@ -545,6 +545,61 @@ impl Tty7App {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
/// Apply a picked ANSI color override and re-paint the terminal palette live.
|
||||
/// `None` clears the slot back to the active preset's ANSI value.
|
||||
pub(crate) fn set_ansi_color_override(
|
||||
&mut self,
|
||||
key: AnsiColorKey,
|
||||
value: Option<gpui::Hsla>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let new = value.map(hsla_to_hex6);
|
||||
let cfg = cx.global_mut::<Config>();
|
||||
if key.get(&cfg.ansi_colors) == &new {
|
||||
return;
|
||||
}
|
||||
key.set(&mut cfg.ansi_colors, new);
|
||||
cfg.save();
|
||||
apply_theme(Some(window), cx);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
/// Clear one ANSI override back to the active preset default and sync the
|
||||
/// picker swatch to that effective color.
|
||||
pub(crate) fn reset_ansi_color_override(
|
||||
&mut self,
|
||||
key: AnsiColorKey,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
{
|
||||
let cfg = cx.global_mut::<Config>();
|
||||
if key.get(&cfg.ansi_colors).is_none() {
|
||||
return;
|
||||
}
|
||||
key.set(&mut cfg.ansi_colors, None);
|
||||
cfg.save();
|
||||
}
|
||||
apply_theme(Some(window), cx);
|
||||
let default: gpui::Hsla = {
|
||||
let cfg = cx.global::<Config>();
|
||||
let p = crate::ui::presets::by_id(&cfg.theme_preset);
|
||||
let (r, g, b) = p.ansi16[key.0];
|
||||
gpui::rgb((r as u32) << 16 | (g as u32) << 8 | b as u32).into()
|
||||
};
|
||||
let picker = self.active_settings().and_then(|s| {
|
||||
s.ansi_color_pickers
|
||||
.iter()
|
||||
.find(|(k, _)| *k == key)
|
||||
.map(|(_, state)| state.clone())
|
||||
});
|
||||
if let Some(state) = picker {
|
||||
state.update(cx, |s, cx| s.set_value(default, window, cx));
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
/// Switch the cursor shape and repaint. The element reads `cursor_style` from
|
||||
/// the global each frame, so we just persist and nudge every pane to redraw.
|
||||
pub(crate) fn set_cursor_style(
|
||||
@@ -1178,6 +1233,7 @@ impl Tty7App {
|
||||
let (shell_program_input, shell_args_input, wd_path_input) =
|
||||
self.build_shell_inputs(&mut subs, window, cx);
|
||||
let color_pickers = self.build_color_pickers(&mut subs, window, cx);
|
||||
let ansi_color_pickers = self.build_ansi_color_pickers(&mut subs, window, cx);
|
||||
let scroll_slider = self.build_scroll_slider(&mut subs, window, cx);
|
||||
|
||||
self.maximized = None;
|
||||
@@ -1194,7 +1250,9 @@ impl Tty7App {
|
||||
shell_args_input,
|
||||
wd_path_input,
|
||||
color_pickers,
|
||||
ansi_color_pickers,
|
||||
colors_expanded: false,
|
||||
ansi_colors_expanded: false,
|
||||
scroll_slider,
|
||||
_subs: subs,
|
||||
}),
|
||||
@@ -1395,6 +1453,38 @@ impl Tty7App {
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// One color picker per terminal ANSI color, seeded with the effective
|
||||
/// current value (override if set, else active preset default).
|
||||
fn build_ansi_color_pickers(
|
||||
&mut self,
|
||||
subs: &mut Vec<Subscription>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Vec<(AnsiColorKey, Entity<ColorPickerState>)> {
|
||||
let cfg = cx.global::<Config>();
|
||||
let theme_preset = cfg.theme_preset.clone();
|
||||
let ansi_colors = cfg.ansi_colors.clone();
|
||||
let preset = crate::ui::presets::by_id(&theme_preset);
|
||||
AnsiColorKey::ALL
|
||||
.iter()
|
||||
.map(|&key| {
|
||||
let (r, g, b) = preset.ansi16[key.0];
|
||||
let default = (r as u32) << 16 | (g as u32) << 8 | b as u32;
|
||||
let effective = color_or(key.get(&ansi_colors), default);
|
||||
let state = cx.new(|cx| ColorPickerState::new(window, cx).default_value(effective));
|
||||
subs.push(cx.subscribe_in(
|
||||
&state,
|
||||
window,
|
||||
move |this, _picker, ev: &ColorPickerEvent, window, cx| {
|
||||
let ColorPickerEvent::Change(value) = ev;
|
||||
this.set_ansi_color_override(key, *value, window, cx);
|
||||
},
|
||||
));
|
||||
(key, state)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Mouse-scroll multiplier slider (0.5×–5×). Emits `Change` continuously as
|
||||
/// the user drags; each writes + persists the multiplier.
|
||||
fn build_scroll_slider(
|
||||
@@ -1660,6 +1750,19 @@ impl Tty7App {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
/// Expand/collapse the ANSI Colors override group in the settings tab's
|
||||
/// Appearance section (no-op elsewhere).
|
||||
pub(crate) fn toggle_settings_ansi_colors(&mut self, cx: &mut Context<Self>) {
|
||||
if let Some(s) = self
|
||||
.tabs
|
||||
.get_mut(self.active)
|
||||
.and_then(|t| t.settings.as_mut())
|
||||
{
|
||||
s.ansi_colors_expanded = !s.ansi_colors_expanded;
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
/// Open `config.json` with the OS default handler (Settings → Keybindings).
|
||||
/// A fresh install may never have saved yet, so write the current config
|
||||
/// first — the button must not point at a missing file.
|
||||
|
||||
+163
-15
@@ -22,7 +22,7 @@ use std::sync::Arc;
|
||||
|
||||
use gpui_component::color_picker::{ColorPicker, ColorPickerState};
|
||||
|
||||
use crate::core::config::{Colors, Config, CursorStyle, NewTabPosition, NotifyMode};
|
||||
use crate::core::config::{AnsiColors, Colors, Config, CursorStyle, NewTabPosition, NotifyMode};
|
||||
use crate::ui::app::{FONT_SIZE_STEP, LINE_HEIGHT_STEP, Tty7App};
|
||||
use crate::ui::keymap::default_bindings;
|
||||
use crate::ui::presets;
|
||||
@@ -58,6 +58,49 @@ pub(crate) enum ColorKey {
|
||||
Selection,
|
||||
}
|
||||
|
||||
/// One terminal ANSI color slot (`ansi_colors.color0`…`color15`). These map to
|
||||
/// SGR colors that terminal programs request explicitly, separate from the
|
||||
/// default foreground/background.
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) struct AnsiColorKey(pub(crate) usize);
|
||||
|
||||
impl AnsiColorKey {
|
||||
pub(crate) const ALL: [AnsiColorKey; 16] = [
|
||||
AnsiColorKey(0),
|
||||
AnsiColorKey(1),
|
||||
AnsiColorKey(2),
|
||||
AnsiColorKey(3),
|
||||
AnsiColorKey(4),
|
||||
AnsiColorKey(5),
|
||||
AnsiColorKey(6),
|
||||
AnsiColorKey(7),
|
||||
AnsiColorKey(8),
|
||||
AnsiColorKey(9),
|
||||
AnsiColorKey(10),
|
||||
AnsiColorKey(11),
|
||||
AnsiColorKey(12),
|
||||
AnsiColorKey(13),
|
||||
AnsiColorKey(14),
|
||||
AnsiColorKey(15),
|
||||
];
|
||||
|
||||
fn label(self) -> String {
|
||||
format!("Color {}", self.0)
|
||||
}
|
||||
|
||||
fn id(self) -> String {
|
||||
format!("ansi-color{}", self.0)
|
||||
}
|
||||
|
||||
pub(crate) fn get(self, colors: &AnsiColors) -> &Option<String> {
|
||||
colors.get(self.0).unwrap_or(&colors.color0)
|
||||
}
|
||||
|
||||
pub(crate) fn set(self, colors: &mut AnsiColors, val: Option<String>) {
|
||||
colors.set(self.0, val);
|
||||
}
|
||||
}
|
||||
|
||||
impl ColorKey {
|
||||
/// The nine keys, in the order they appear in the panel.
|
||||
pub(crate) const ALL: [ColorKey; 9] = [
|
||||
@@ -169,10 +212,14 @@ pub(crate) struct SettingsState {
|
||||
/// order. Each is initialized to the effective color and emits a `Change` that
|
||||
/// writes the override + re-applies the theme.
|
||||
pub(crate) color_pickers: Vec<(ColorKey, Entity<ColorPickerState>)>,
|
||||
/// One color picker per overridable terminal ANSI color (`ansi_colors.*`).
|
||||
pub(crate) ansi_color_pickers: Vec<(AnsiColorKey, Entity<ColorPickerState>)>,
|
||||
/// Whether the Colors override group (Appearance) is expanded. Collapsed by
|
||||
/// default: its nine theme-internal slots are power-user surface, and open
|
||||
/// they would dwarf the theme/typography settings everyone else came for.
|
||||
pub(crate) colors_expanded: bool,
|
||||
/// Whether the ANSI Colors override group (Appearance) is expanded.
|
||||
pub(crate) ansi_colors_expanded: bool,
|
||||
/// Mouse-scroll multiplier slider (Terminal section).
|
||||
pub(crate) scroll_slider: Entity<SliderState>,
|
||||
pub(crate) _subs: Vec<Subscription>,
|
||||
@@ -434,20 +481,31 @@ impl Tty7App {
|
||||
let hover_bg = theme.secondary.opacity(0.6);
|
||||
let stepper_bg = theme.secondary.opacity(0.35);
|
||||
let font_size = self.font_size;
|
||||
let (font_select, font_bold_select, font_italic_select, color_pickers, colors_expanded) =
|
||||
match self.active_settings() {
|
||||
Some(s) => (
|
||||
s.font_select.clone(),
|
||||
s.font_bold_select.clone(),
|
||||
s.font_italic_select.clone(),
|
||||
s.color_pickers.clone(),
|
||||
s.colors_expanded,
|
||||
),
|
||||
None => return div().into_any_element(),
|
||||
};
|
||||
let cursor_style = cx.global::<Config>().cursor_style;
|
||||
let cursor_blink = cx.global::<Config>().cursor_blink;
|
||||
let colors = cx.global::<Config>().colors.clone();
|
||||
let (
|
||||
font_select,
|
||||
font_bold_select,
|
||||
font_italic_select,
|
||||
color_pickers,
|
||||
colors_expanded,
|
||||
ansi_color_pickers,
|
||||
ansi_colors_expanded,
|
||||
) = match self.active_settings() {
|
||||
Some(s) => (
|
||||
s.font_select.clone(),
|
||||
s.font_bold_select.clone(),
|
||||
s.font_italic_select.clone(),
|
||||
s.color_pickers.clone(),
|
||||
s.colors_expanded,
|
||||
s.ansi_color_pickers.clone(),
|
||||
s.ansi_colors_expanded,
|
||||
),
|
||||
None => return div().into_any_element(),
|
||||
};
|
||||
let cfg = cx.global::<Config>();
|
||||
let cursor_style = cfg.cursor_style;
|
||||
let cursor_blink = cfg.cursor_blink;
|
||||
let colors = cfg.colors.clone();
|
||||
let ansi_colors = cfg.ansi_colors.clone();
|
||||
|
||||
// Unified −/value/+ stepper plus a quiet Reset.
|
||||
let step = move |id: &'static str, glyph: &'static str, divider: bool| {
|
||||
@@ -638,6 +696,13 @@ impl Tty7App {
|
||||
))
|
||||
.child(self.section_rule(cx))
|
||||
.child(self.render_colors_group(colors_expanded, color_pickers, &colors, cx))
|
||||
.child(self.section_rule(cx))
|
||||
.child(self.render_ansi_colors_group(
|
||||
ansi_colors_expanded,
|
||||
ansi_color_pickers,
|
||||
&ansi_colors,
|
||||
cx,
|
||||
))
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
@@ -725,6 +790,89 @@ impl Tty7App {
|
||||
)
|
||||
}
|
||||
|
||||
/// Terminal ANSI color overrides (`color0`…`color15`), separate from the
|
||||
/// theme's default foreground/background. These are the slots CLI programs
|
||||
/// address explicitly with SGR colors.
|
||||
fn render_ansi_colors_group(
|
||||
&self,
|
||||
expanded: bool,
|
||||
color_pickers: Vec<(AnsiColorKey, Entity<ColorPickerState>)>,
|
||||
colors: &AnsiColors,
|
||||
cx: &mut Context<Self>,
|
||||
) -> AnyElement {
|
||||
let muted_fg = cx.theme().muted_foreground;
|
||||
let chevron = if expanded {
|
||||
IconName::ChevronDown
|
||||
} else {
|
||||
IconName::ChevronRight
|
||||
};
|
||||
let header = v_flex()
|
||||
.id("ansi-colors-toggle")
|
||||
.gap_1()
|
||||
.cursor_pointer()
|
||||
.on_click(cx.listener(|this, _, _w, cx| this.toggle_settings_ansi_colors(cx)))
|
||||
.child(
|
||||
h_flex()
|
||||
.items_center()
|
||||
.gap_1p5()
|
||||
.child(self.header_text("ANSI Colors", cx))
|
||||
.child(Icon::new(chevron).small().text_color(muted_fg)),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.text_xs()
|
||||
.text_color(muted_fg)
|
||||
.child("Advanced: override terminal color0-color15 slots used by CLI tools."),
|
||||
);
|
||||
Collapsible::new()
|
||||
.open(expanded)
|
||||
.child(header)
|
||||
.content(
|
||||
v_flex().mt_2().children(
|
||||
color_pickers
|
||||
.into_iter()
|
||||
.map(|(key, state)| self.render_ansi_color_row(key, state, colors, cx)),
|
||||
),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn render_ansi_color_row(
|
||||
&self,
|
||||
key: AnsiColorKey,
|
||||
state: Entity<ColorPickerState>,
|
||||
colors: &AnsiColors,
|
||||
cx: &mut Context<Self>,
|
||||
) -> impl IntoElement + use<> {
|
||||
let overridden = key.get(colors).is_some();
|
||||
let control = h_flex()
|
||||
.items_center()
|
||||
.gap_3()
|
||||
.w(px(240.))
|
||||
.child(ColorPicker::new(&state).small())
|
||||
.child(
|
||||
Button::new(SharedString::from(format!("{}-reset", key.id())))
|
||||
.label("Reset")
|
||||
.ghost()
|
||||
.small()
|
||||
.disabled(!overridden)
|
||||
.on_click(cx.listener(move |this, _, window, cx| {
|
||||
this.reset_ansi_color_override(key, window, cx);
|
||||
})),
|
||||
)
|
||||
.into_any_element();
|
||||
self.settings_row(
|
||||
key.label(),
|
||||
if overridden {
|
||||
"Custom"
|
||||
} else {
|
||||
"Theme default"
|
||||
},
|
||||
control,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
|
||||
/// Shell section: the program tty7 launches in each new terminal, plus its
|
||||
/// launch arguments. Both apply to *newly spawned* panes/tabs — existing
|
||||
/// shells keep running until closed. An empty program falls back to the
|
||||
|
||||
+52
-2
@@ -7,7 +7,8 @@ use gpui::{App, Hsla, Menu, MenuItem, Pixels, Point, Window, point, px, rgb};
|
||||
use gpui_component::{Theme, ThemeMode};
|
||||
|
||||
use crate::core::actions::*;
|
||||
use crate::core::config::{Config, color_or};
|
||||
use crate::core::config::{AnsiColors, Config, color_or, parse_hex_color};
|
||||
use crate::terminal::palette::ActivePalette;
|
||||
use crate::ui::presets;
|
||||
|
||||
/// The traffic-light origin, nudged down from the macOS default so the buttons
|
||||
@@ -78,7 +79,8 @@ pub(crate) fn apply_theme(mut window: Option<&mut Window>, cx: &mut App) {
|
||||
// User `colors.*` overrides apply on top of the derived neutrals; a `None`
|
||||
// field falls through to the theme.
|
||||
let c = cfg.colors.clone();
|
||||
let active = preset.active_palette();
|
||||
let mut active = preset.active_palette();
|
||||
apply_ansi_overrides(&mut active, &cfg.ansi_colors);
|
||||
|
||||
Theme::change(mode, window.as_deref_mut(), cx);
|
||||
// Publish the terminal palette before borrowing the theme mutably.
|
||||
@@ -167,6 +169,16 @@ pub(crate) fn apply_theme(mut window: Option<&mut Window>, cx: &mut App) {
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_ansi_overrides(active: &mut ActivePalette, overrides: &AnsiColors) {
|
||||
for i in 0..16 {
|
||||
if let Some(Some(hex)) = overrides.get(i) {
|
||||
if let Some(rgb) = parse_hex_color(hex) {
|
||||
active.ansi16[i] = crate::terminal::palette::hsla_to_rgb(rgb.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply `Config::mouse_hide_while_typing` to GPUI's cursor-hide policy: hide the
|
||||
/// pointer while typing when on, never when off. Called at startup and whenever
|
||||
/// the config changes (setter + hot-reload) so the switch takes effect live.
|
||||
@@ -216,3 +228,41 @@ fn sync_native_appearance(dark: bool) {
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
fn sync_native_appearance(_dark: bool) {}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn ansi_overrides_replace_only_valid_slots() {
|
||||
let mut active = presets::by_id("rose_pine_dawn").active_palette();
|
||||
let original0 = active.ansi16[0];
|
||||
let original1 = active.ansi16[1];
|
||||
let original15 = active.ansi16[15];
|
||||
let mut overrides = AnsiColors::default();
|
||||
overrides.color0 = Some("#575279".to_string());
|
||||
overrides.color1 = Some("not-a-color".to_string());
|
||||
overrides.color15 = Some("123456".to_string());
|
||||
|
||||
apply_ansi_overrides(&mut active, &overrides);
|
||||
|
||||
assert_eq!(
|
||||
(active.ansi16[0].r, active.ansi16[0].g, active.ansi16[0].b),
|
||||
(0x57, 0x52, 0x79)
|
||||
);
|
||||
assert_eq!(
|
||||
active.ansi16[1], original1,
|
||||
"malformed overrides are ignored"
|
||||
);
|
||||
assert_eq!(
|
||||
(
|
||||
active.ansi16[15].r,
|
||||
active.ansi16[15].g,
|
||||
active.ansi16[15].b
|
||||
),
|
||||
(0x12, 0x34, 0x56)
|
||||
);
|
||||
assert_ne!(active.ansi16[0], original0);
|
||||
assert_ne!(active.ansi16[15], original15);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user