Skip to content

Commit 7bc4e69

Browse files
feat(style): add system theme using basic ANSI colors
Add a "system" theme that uses the terminal's own colors instead of overriding them with 256-color palette values. This respects users' custom terminal color schemes. Changes: - Add fg_ansi_code/bg_ansi_code helpers in Style: colors 0-15 render as basic ANSI codes (30-37/90-97 fg, 40-47/100-107 bg) which use the terminal's configured colors. Colors 16+ use 256-color codes as before. - Update to_ansi_prefix, Widgets.fg/bg, apply_bg_fill, and apply_themed_foreground to use the new helpers. - Add "system" builtin theme: no explicit text/background colors (transparent), basic ANSI accents, attributes for emphasis. - Add system theme to style_system demo (key 4).
1 parent a84d6cf commit 7bc4e69

File tree

6 files changed

+104
-18
lines changed

6 files changed

+104
-18
lines changed

example/demos/style_system/page.ml

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,17 @@ module Inner = struct
1717
module Theme = Miaou_style.Theme
1818
module Theme_loader = Miaou_style.Theme_loader
1919
module W = Miaou_widgets_display.Widgets
20+
module Builtin_themes = Miaou_style.Builtin_themes
2021

21-
type theme_choice = Dark | Light | High_contrast
22+
type theme_choice = Dark | Light | High_contrast | System
2223

2324
type loaded_theme = {theme : Theme.t; error : string option}
2425

2526
type themes = {
2627
dark : loaded_theme;
2728
light : loaded_theme;
2829
high_contrast : loaded_theme;
30+
system : loaded_theme;
2931
}
3032

3133
type state = {
@@ -39,13 +41,14 @@ module Inner = struct
3941

4042
let load_theme ~label blob =
4143
match Theme_loader.of_json_string blob with
42-
| Ok t -> {theme = t; error = None}
44+
| Ok t -> {theme = Theme.merge ~base:Theme.default ~overlay:t; error = None}
4345
| Error e -> {theme = Theme.default; error = Some (label ^ ": " ^ e)}
4446

4547
let load_theme_file ~label path ~fallback =
4648
if Sys.file_exists path then
4749
match Theme_loader.load_file path with
48-
| Ok t -> {theme = t; error = None}
50+
| Ok t ->
51+
{theme = Theme.merge ~base:Theme.default ~overlay:t; error = None}
4952
| Error e ->
5053
{
5154
theme = fallback;
@@ -65,22 +68,30 @@ module Inner = struct
6568
let high_contrast =
6669
load_theme ~label:"high-contrast" [%blob "themes/high-contrast.json"]
6770
in
68-
{dark; light; high_contrast}
71+
let system =
72+
match Builtin_themes.get_builtin "system" with
73+
| Some t -> {theme = t; error = None}
74+
| None ->
75+
{theme = Theme.default; error = Some "system: builtin not found"}
76+
in
77+
{dark; light; high_contrast; system}
6978

7079
let init () = {themes; choice = Dark; cursor = 0; next_page = None}
7180

7281
let theme_name = function
7382
| Dark -> "dark"
7483
| Light -> "light"
7584
| High_contrast -> "high-contrast"
85+
| System -> "system"
7686

7787
let current_theme s =
7888
match s.choice with
7989
| Dark -> s.themes.dark
8090
| Light -> s.themes.light
8191
| High_contrast -> s.themes.high_contrast
92+
| System -> s.themes.system
8293

83-
let clamp_idx i = if i < 0 then 0 else if i > 3 then 3 else i
94+
let clamp_idx i = if i < 0 then 0 else if i > 4 then 4 else i
8495

8596
let render_tile ~index ~cursor ~title ~size =
8697
let focused = index = cursor in
@@ -136,11 +147,14 @@ module Inner = struct
136147

137148
let view s ~focus:_ ~size =
138149
let current = current_theme s in
150+
(* Update the driver-level theme so apply_themed_background/foreground
151+
use the selected theme, not just the page content rendering. *)
152+
Style_context.set_theme current.theme ;
139153
Style_context.with_theme current.theme (fun () ->
140154
let header = W.themed_emphasis "Style System Demo" in
141155
let sub =
142156
W.themed_muted
143-
"1/2/3 switch theme · Left/Right move focus · Esc returns"
157+
"1/2/3/4 switch theme · Left/Right move focus · Esc returns"
144158
in
145159
let theme_line =
146160
W.themed_text
@@ -175,6 +189,7 @@ module Inner = struct
175189
| "1" -> {s with choice = Dark}
176190
| "2" -> {s with choice = Light}
177191
| "3" -> {s with choice = High_contrast}
192+
| "4" -> {s with choice = System}
178193
| "Left" | "h" -> {s with cursor = clamp_idx (s.cursor - 1)}
179194
| "Right" | "l" -> {s with cursor = clamp_idx (s.cursor + 1)}
180195
| _ -> (

src/miaou_driver_matrix/matrix_ansi_writer.ml

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,12 +48,20 @@ let style_to_sgr style =
4848
(* Reverse *)
4949
if style.reverse then Buffer.add_string buf ";7" ;
5050

51-
(* Foreground color *)
52-
if style.fg >= 0 && style.fg <= 255 then
51+
(* Foreground color: 0-15 use basic ANSI, 16-255 use 256-color *)
52+
if style.fg >= 0 && style.fg <= 7 then
53+
Buffer.add_string buf (Printf.sprintf ";%d" (30 + style.fg))
54+
else if style.fg >= 8 && style.fg <= 15 then
55+
Buffer.add_string buf (Printf.sprintf ";%d" (90 + style.fg - 8))
56+
else if style.fg >= 16 && style.fg <= 255 then
5357
Buffer.add_string buf (Printf.sprintf ";38;5;%d" style.fg) ;
5458

55-
(* Background color *)
56-
if style.bg >= 0 && style.bg <= 255 then
59+
(* Background color: same logic *)
60+
if style.bg >= 0 && style.bg <= 7 then
61+
Buffer.add_string buf (Printf.sprintf ";%d" (40 + style.bg))
62+
else if style.bg >= 8 && style.bg <= 15 then
63+
Buffer.add_string buf (Printf.sprintf ";%d" (100 + style.bg - 8))
64+
else if style.bg >= 16 && style.bg <= 255 then
5765
Buffer.add_string buf (Printf.sprintf ";48;5;%d" style.bg) ;
5866

5967
Buffer.add_char buf 'm' ;

src/miaou_style/builtin_themes.ml

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,14 @@ let all_themes =
107107
dark_mode = false;
108108
borderless = false;
109109
};
110+
(* System theme *)
111+
{
112+
id = "system";
113+
name = "System";
114+
description = "Uses terminal's own colors and background";
115+
dark_mode = true;
116+
borderless = false;
117+
};
110118
]
111119

112120
(** List all available built-in themes *)
@@ -386,6 +394,32 @@ let oled_json =
386394
"rules": {}
387395
}|}
388396

397+
(* System theme - uses terminal's own colors via basic ANSI codes (0-15).
398+
No explicit background or text color: inherits from the terminal. *)
399+
let system_json =
400+
{|{
401+
"name": "System",
402+
"dark_mode": true,
403+
"primary": { "fg": { "Fixed": 12 }, "bold": true },
404+
"secondary": { "fg": { "Fixed": 8 } },
405+
"accent": { "fg": { "Fixed": 13 } },
406+
"error": { "fg": { "Fixed": 1 }, "bold": true },
407+
"warning": { "fg": { "Fixed": 3 }, "bold": true },
408+
"success": { "fg": { "Fixed": 2 } },
409+
"info": { "fg": { "Fixed": 6 } },
410+
"text": { "fg": { "Fixed": -1 } },
411+
"text_muted": { "fg": { "Fixed": -1 }, "dim": true },
412+
"text_emphasized": { "fg": { "Fixed": -1 }, "bold": true },
413+
"background": { "bg": { "Fixed": -1 } },
414+
"background_secondary": { "bg": { "Fixed": -1 } },
415+
"border": { "fg": { "Fixed": -1 }, "dim": true },
416+
"border_focused": { "fg": { "Fixed": 12 }, "bold": true },
417+
"border_dim": { "fg": { "Fixed": -1 }, "dim": true },
418+
"selection": { "fg": { "Fixed": -1 }, "bg": { "Fixed": -1 }, "reverse": true },
419+
"default_border_style": "Rounded",
420+
"rules": {}
421+
}|}
422+
389423
(** Get the JSON string for a built-in theme *)
390424
let get_json id =
391425
match id with
@@ -400,6 +434,7 @@ let get_json id =
400434
| "tokyonight-day" -> Some tokyonight_day_json
401435
| "opencode" -> Some opencode_json
402436
| "oled" -> Some oled_json
437+
| "system" -> Some system_json
403438
| _ -> None
404439

405440
(** Load a built-in theme by ID *)

src/miaou_style/style.ml

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,18 @@ let to_resolved ?(dark_mode = true) style =
171171
r_strikethrough = Option.value ~default:false resolved.strikethrough;
172172
}
173173

174+
let fg_ansi_code n =
175+
if n >= 0 && n <= 7 then string_of_int (30 + n)
176+
else if n >= 8 && n <= 15 then string_of_int (90 + n - 8)
177+
else if n >= 16 then "38;5;" ^ string_of_int n
178+
else ""
179+
180+
let bg_ansi_code n =
181+
if n >= 0 && n <= 7 then string_of_int (40 + n)
182+
else if n >= 8 && n <= 15 then string_of_int (100 + n - 8)
183+
else if n >= 16 then "48;5;" ^ string_of_int n
184+
else ""
185+
174186
let to_ansi_prefix r =
175187
let buf = Buffer.create 32 in
176188
let add_code code =
@@ -184,10 +196,13 @@ let to_ansi_prefix r =
184196
if r.r_underline then add_code "4" ;
185197
if r.r_reverse then add_code "7" ;
186198
if r.r_strikethrough then add_code "9" ;
187-
(* Foreground color *)
188-
if r.r_fg >= 0 then add_code ("38;5;" ^ string_of_int r.r_fg) ;
189-
(* Background color *)
190-
if r.r_bg >= 0 then add_code ("48;5;" ^ string_of_int r.r_bg) ;
199+
(* Foreground color: 0-15 use basic ANSI codes (terminal-configurable),
200+
16-255 use 256-color extended codes *)
201+
let fg_code = fg_ansi_code r.r_fg in
202+
if fg_code <> "" then add_code fg_code ;
203+
(* Background color: same logic *)
204+
let bg_code = bg_ansi_code r.r_bg in
205+
if bg_code <> "" then add_code bg_code ;
191206
(* Build escape sequence *)
192207
if Buffer.length buf > 0 then "\027[" ^ Buffer.contents buf ^ "m" else ""
193208

src/miaou_style/style.mli

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,15 @@ val resolve_color : ?dark_mode:bool -> color -> int
128128
(** Resolve style to concrete values *)
129129
val to_resolved : ?dark_mode:bool -> t -> resolved
130130

131+
(** ANSI escape code fragment for a foreground color index.
132+
Colors 0-15 use basic ANSI codes (respects terminal color scheme).
133+
Colors 16-255 use 256-color extended codes.
134+
Returns [""] for negative values (no color). *)
135+
val fg_ansi_code : int -> string
136+
137+
(** Same as {!fg_ansi_code} but for background colors. *)
138+
val bg_ansi_code : int -> string
139+
131140
(** Convert resolved style to ANSI escape sequence prefix *)
132141
val to_ansi_prefix : resolved -> string
133142

src/miaou_widgets_display/widgets.ml

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,13 @@ let bold s = ansi "1" s
3535

3636
let dim s = ansi "2" s
3737

38-
let fg n s = ansi ("38;5;" ^ string_of_int n) s
38+
let fg n s =
39+
let code = Miaou_style.Style.fg_ansi_code n in
40+
if code = "" then s else ansi code s
3941

40-
let bg n s = ansi ("48;5;" ^ string_of_int n) s
42+
let bg n s =
43+
let code = Miaou_style.Style.bg_ansi_code n in
44+
if code = "" then s else ansi code s
4145

4246
let green s = ansi "32" s
4347

@@ -132,7 +136,7 @@ let themed_background_alt s = styled (Style_context.background_secondary ()) s
132136
let themed_contextual s = Style_context.styled s
133137

134138
let apply_bg_fill ~bg s =
135-
let prefix = "\027[48;5;" ^ string_of_int bg ^ "m" in
139+
let prefix = "\027[" ^ Miaou_style.Style.bg_ansi_code bg ^ "m" in
136140
let reset = Style.ansi_reset in
137141
let len_s = String.length s in
138142
let len_reset = String.length reset in
@@ -173,7 +177,7 @@ let apply_themed_foreground content =
173177
let fg = resolved.Style.r_fg in
174178
if fg < 0 then content (* No text color in theme *)
175179
else
176-
let fg_prefix = Printf.sprintf "\027[38;5;%dm" fg in
180+
let fg_prefix = "\027[" ^ Style.fg_ansi_code fg ^ "m" in
177181
let reset_fg = "\027[39m" in
178182
(* Regex to match ANSI escape sequences *)
179183
let ansi_re = Str.regexp "\027\\[[0-9;]*m" in

0 commit comments

Comments
 (0)