1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
//! Per-character style computation.
//!
//! Given a description of a single cell (byte position, syntax highlight,
//! overlays, cursor / selection state, theme) this module returns the final
//! ratatui `Style` together with theme-key provenance used by the theme
//! inspector. The input and output structs are private to this module so they
//! never leak to callers outside `split_rendering`.
use crate::view::overlay::{Overlay, OverlayFace};
use crate::view::theme::{Theme, TokenColorExt};
use fresh_core::api::ViewTokenStyle;
use ratatui::style::{Color, Modifier, Style};
/// Context for computing the style of a single character.
pub(super) struct CharStyleContext<'a> {
pub byte_pos: Option<usize>,
pub token_style: Option<&'a ViewTokenStyle>,
pub ansi_style: Style,
pub is_cursor: bool,
pub is_selected: bool,
pub theme: &'a Theme,
/// Pre-resolved syntax highlight color for this byte position.
pub highlight_color: Option<Color>,
/// Theme key for the syntax highlight category (e.g. "syntax.keyword").
pub highlight_theme_key: Option<&'static str>,
/// Pre-resolved syntax highlight background colour for this byte
/// position. `Some(..)` only for diff categories (Inserted /
/// Deleted / Changed); `None` keeps the existing fg-only path.
pub highlight_bg: Option<Color>,
/// Theme key for the bg above, when set. Surfaced to the theme
/// inspector.
pub highlight_bg_theme_key: Option<&'static str>,
/// Pre-resolved semantic token color for this byte position.
pub semantic_token_color: Option<Color>,
/// Overlays currently active at `byte_pos`, already in priority-ascending
/// order ("last write wins"). Empty when `byte_pos` is `None`.
pub active_overlays: &'a [&'a Overlay],
pub primary_cursor_position: usize,
pub is_active: bool,
/// Skip REVERSED style on the primary cursor cell. True when a hardware
/// cursor is available (not software_cursor_only), or in session mode.
pub skip_primary_cursor_reverse: bool,
/// Whether this character is on the cursor line and current-line
/// highlighting is enabled.
pub is_cursor_line_highlighted: bool,
/// Background color for the current line.
pub current_line_bg: Color,
}
/// Output from [`compute_char_style`].
pub(super) struct CharStyleOutput {
pub style: Style,
pub is_secondary_cursor: bool,
/// Theme key for the foreground color used on this cell.
pub fg_theme_key: Option<&'static str>,
/// Theme key for the background color used on this cell.
pub bg_theme_key: Option<&'static str>,
/// Region label for this cell.
pub region: &'static str,
}
/// Compute the style for a character by layering:
/// token -> ANSI -> syntax -> semantic -> overlays -> selection -> cursor.
/// Also tracks which theme keys produced the final fg/bg colors.
pub(super) fn compute_char_style(ctx: &CharStyleContext) -> CharStyleOutput {
let highlight_color = ctx.highlight_color;
// Track theme key provenance alongside style
let mut fg_theme_key: Option<&'static str> = None;
let mut bg_theme_key: Option<&'static str> = Some("editor.bg");
let mut region: &'static str = "Editor Content";
// Start with token style if present (for injected content like annotation headers)
// Otherwise use ANSI/syntax/theme default
let mut style = if let Some(ts) = ctx.token_style {
let mut s = Style::default();
if let Some(ref fg) = ts.fg {
s = s.fg(fg.to_ratatui(ctx.theme));
} else {
s = s.fg(ctx.theme.editor_fg);
fg_theme_key = Some("editor.fg");
}
if let Some(ref bg) = ts.bg {
s = s.bg(bg.to_ratatui(ctx.theme));
}
if ts.bold {
s = s.add_modifier(Modifier::BOLD);
}
if ts.italic {
s = s.add_modifier(Modifier::ITALIC);
}
if ts.underline {
s = s.add_modifier(Modifier::UNDERLINED);
}
region = "Plugin Token";
s
} else if ctx.ansi_style.fg.is_some()
|| ctx.ansi_style.bg.is_some()
|| !ctx.ansi_style.add_modifier.is_empty()
{
// Apply ANSI styling from escape codes
let mut s = Style::default();
if let Some(fg) = ctx.ansi_style.fg {
s = s.fg(fg);
} else {
s = s.fg(ctx.theme.editor_fg);
fg_theme_key = Some("editor.fg");
}
if let Some(bg) = ctx.ansi_style.bg {
s = s.bg(bg);
bg_theme_key = None; // ANSI bg, not from theme
}
s = s.add_modifier(ctx.ansi_style.add_modifier);
region = "ANSI Escape";
s
} else if let Some(color) = highlight_color {
// Apply syntax highlighting
fg_theme_key = ctx.highlight_theme_key;
Style::default().fg(color)
} else {
// Default color from theme
fg_theme_key = Some("editor.fg");
Style::default().fg(ctx.theme.editor_fg)
};
// If we have ANSI style but also syntax highlighting, syntax takes precedence for color
// (unless ANSI has explicit color which we already applied above)
if let Some(color) = highlight_color {
if ctx.ansi_style.fg.is_none()
&& (ctx.ansi_style.bg.is_some() || !ctx.ansi_style.add_modifier.is_empty())
{
style = style.fg(color);
fg_theme_key = ctx.highlight_theme_key;
}
}
// Syntax-driven background: diff categories (markup.inserted /
// markup.deleted / meta.diff.range) carry a bg the renderer
// applies as a row wash. Slots BELOW overlays (so an
// `addOverlay` from a plugin can still paint over the diff
// colour) but ABOVE ANSI bg (so the syntax intent wins).
if let Some(bg) = ctx.highlight_bg {
style = style.bg(bg);
if let Some(key) = ctx.highlight_bg_theme_key {
bg_theme_key = Some(key);
}
}
// Apply LSP semantic token foreground color when no custom token style is set.
if ctx.token_style.is_none() {
if let Some(color) = ctx.semantic_token_color {
style = style.fg(color);
// Semantic tokens don't have a single static key; leave fg_theme_key as-is
// (the syntax highlight key is a reasonable approximation)
}
}
// Apply overlay styles — last overlay wins for each attribute
for overlay in ctx.active_overlays {
match &overlay.face {
OverlayFace::Underline {
color,
style: _underline_style,
} => {
style = style.add_modifier(Modifier::UNDERLINED).fg(*color);
if let Some(key) = overlay.theme_key {
fg_theme_key = Some(key);
}
}
OverlayFace::Background { color } => {
style = style.bg(*color);
if let Some(key) = overlay.theme_key {
bg_theme_key = Some(key);
// Pick up any SGR modifier the theme associates with
// this bg slot (e.g. terminal-adaptive themes ship
// `Reversed` for `ui.semantic_highlight_bg`).
let m = ctx.theme.modifier_for_bg_key(key);
if !m.is_empty() {
style = style.add_modifier(m);
}
}
}
OverlayFace::Foreground { color } => {
style = style.fg(*color);
if let Some(key) = overlay.theme_key {
fg_theme_key = Some(key);
}
}
OverlayFace::Style {
style: overlay_style,
} => {
style = style.patch(*overlay_style);
if let Some(key) = overlay.theme_key {
if overlay_style.bg.is_some() {
bg_theme_key = Some(key);
}
if overlay_style.fg.is_some() {
fg_theme_key = Some(key);
}
}
}
OverlayFace::ThemedStyle {
fallback_style,
fg_theme,
bg_theme,
} => {
let mut themed_style = *fallback_style;
if let Some(fg_key) = fg_theme {
if let Some(color) = ctx.theme.resolve_theme_key(fg_key) {
themed_style = themed_style.fg(color);
}
}
if let Some(bg_key) = bg_theme {
if let Some(color) = ctx.theme.resolve_theme_key(bg_key) {
themed_style = themed_style.bg(color);
}
let m = ctx.theme.modifier_for_bg_key(bg_key);
if !m.is_empty() {
themed_style = themed_style.add_modifier(m);
}
}
style = style.patch(themed_style);
}
}
}
// Apply current line background highlight (before selection, so selection overrides it)
if ctx.is_cursor_line_highlighted && !ctx.is_selected && style.bg.is_none() {
style = style.bg(ctx.current_line_bg);
}
// Apply selection highlighting (preserve fg/syntax colors, only change bg).
// Themes may also opt into SGR text attributes here (e.g. `Reversed`)
// so a native-palette theme can swap fg/bg via the terminal instead
// of relying on a fixed bg color — see `Theme::selection_modifier`.
if ctx.is_selected {
style = style.bg(ctx.theme.selection_bg);
if !ctx.theme.selection_modifier.is_empty() {
style = style.add_modifier(ctx.theme.selection_modifier);
}
bg_theme_key = Some("editor.selection_bg");
region = "Selection";
}
// Apply cursor styling.
let is_secondary_cursor = ctx.is_cursor && ctx.byte_pos != Some(ctx.primary_cursor_position);
if ctx.is_active {
if ctx.is_cursor {
if ctx.skip_primary_cursor_reverse {
if is_secondary_cursor {
style = style.add_modifier(Modifier::REVERSED);
}
} else {
style = style.add_modifier(Modifier::REVERSED);
}
region = "Cursor";
}
} else if ctx.is_cursor {
style = style.fg(ctx.theme.editor_fg).bg(ctx.theme.inactive_cursor);
fg_theme_key = Some("editor.fg");
bg_theme_key = Some("editor.inactive_cursor");
region = "Inactive Cursor";
}
CharStyleOutput {
style,
is_secondary_cursor,
fg_theme_key,
bg_theme_key,
region,
}
}