Detect terminal color capabilities
This commit is contained in:
@@ -31,6 +31,8 @@ Rows are redgate TSV requirements: `ring<TAB>id<TAB>summary [tag]`.
|
|||||||
0 001 Narrow terminals SHALL use transient panels instead of permanent desktop splits. [mobile]
|
0 001 Narrow terminals SHALL use transient panels instead of permanent desktop splits. [mobile]
|
||||||
1 002 Files, git, diagnostics, LSP, build, terminal, and Pi surfaces SHALL share one panel navigation model. [panels]
|
1 002 Files, git, diagnostics, LSP, build, terminal, and Pi surfaces SHALL share one panel navigation model. [panels]
|
||||||
0 003 When the cursor moves beyond the visible horizontal or vertical viewport, the editor SHALL scroll enough to keep the cursor cell visible. [mobile]
|
0 003 When the cursor moves beyond the visible horizontal or vertical viewport, the editor SHALL scroll enough to keep the cursor cell visible. [mobile]
|
||||||
|
0 004 The default visual theme SHALL preserve semantic color identity for syntax, selections, diagnostics, job/tool status, hover/signature/context surfaces, and panel chrome on mobile SSH terminals. [mobile]
|
||||||
|
0 005 Terminal color adaptation SHALL choose truecolor, 256-color, 16-color, or mono output from local terminal environment signals with no runtime config files. [mobile]
|
||||||
|
|
||||||
## coding
|
## coding
|
||||||
|
|
||||||
|
|||||||
+113
@@ -39,6 +39,25 @@ pub const ColorMode = enum {
|
|||||||
mono,
|
mono,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
pub const Background = enum {
|
||||||
|
dark,
|
||||||
|
light,
|
||||||
|
unknown,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const TerminalEnv = struct {
|
||||||
|
term: ?[]const u8 = null,
|
||||||
|
colorterm: ?[]const u8 = null,
|
||||||
|
no_color: ?[]const u8 = null,
|
||||||
|
mim_color: ?[]const u8 = null,
|
||||||
|
colorfgbg: ?[]const u8 = null,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const TerminalCapabilities = struct {
|
||||||
|
color_mode: ColorMode,
|
||||||
|
background: Background,
|
||||||
|
};
|
||||||
|
|
||||||
pub const SgrKind = enum { fg, bg };
|
pub const SgrKind = enum { fg, bg };
|
||||||
|
|
||||||
pub const AdaptedColor = union(enum) {
|
pub const AdaptedColor = union(enum) {
|
||||||
@@ -98,6 +117,61 @@ pub fn rgb(r: u8, g: u8, b: u8) Color {
|
|||||||
return .{ .r = r, .g = g, .b = b };
|
return .{ .r = r, .g = g, .b = b };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn detectTerminal(env: TerminalEnv) TerminalCapabilities {
|
||||||
|
return .{
|
||||||
|
.color_mode = detectColorMode(env),
|
||||||
|
.background = detectBackground(env),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn detectColorMode(env: TerminalEnv) ColorMode {
|
||||||
|
if (env.no_color != null) return .mono;
|
||||||
|
if (env.mim_color) |forced| {
|
||||||
|
if (std.ascii.eqlIgnoreCase(forced, "mono")) return .mono;
|
||||||
|
if (std.ascii.eqlIgnoreCase(forced, "16")) return .color16;
|
||||||
|
if (std.ascii.eqlIgnoreCase(forced, "256")) return .color256;
|
||||||
|
if (std.ascii.eqlIgnoreCase(forced, "truecolor") or std.ascii.eqlIgnoreCase(forced, "24bit")) return .truecolor;
|
||||||
|
}
|
||||||
|
if (env.term) |term| {
|
||||||
|
if (std.mem.eql(u8, term, "dumb")) return .mono;
|
||||||
|
}
|
||||||
|
if (env.colorterm) |colorterm| {
|
||||||
|
if (containsIgnoreCase(colorterm, "truecolor") or containsIgnoreCase(colorterm, "24bit")) return .truecolor;
|
||||||
|
}
|
||||||
|
if (env.term) |term| {
|
||||||
|
if (containsIgnoreCase(term, "truecolor") or containsIgnoreCase(term, "24bit")) return .truecolor;
|
||||||
|
if (containsIgnoreCase(term, "256color")) return .color256;
|
||||||
|
if (containsIgnoreCase(term, "color")) return .color16;
|
||||||
|
}
|
||||||
|
return .color16;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn detectBackground(env: TerminalEnv) Background {
|
||||||
|
const value = env.colorfgbg orelse return .unknown;
|
||||||
|
var fields = std.mem.splitScalar(u8, value, ';');
|
||||||
|
var last: ?[]const u8 = null;
|
||||||
|
while (fields.next()) |field| last = field;
|
||||||
|
const bg = last orelse return .unknown;
|
||||||
|
const code = std.fmt.parseInt(u8, bg, 10) catch return .unknown;
|
||||||
|
return if (code >= 0 and code <= 6) .dark else .light;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn fromProcessEnv() TerminalCapabilities {
|
||||||
|
return detectTerminal(.{
|
||||||
|
.term = std.posix.getenv("TERM"),
|
||||||
|
.colorterm = std.posix.getenv("COLORTERM"),
|
||||||
|
.no_color = std.posix.getenv("NO_COLOR"),
|
||||||
|
.mim_color = std.posix.getenv("MIM_COLOR"),
|
||||||
|
.colorfgbg = std.posix.getenv("COLORFGBG"),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn hasAccessibleContrast(style: Style, fallback_bg: Color, minimum_ratio: f32) bool {
|
||||||
|
const fg = style.fg orelse return true;
|
||||||
|
const bg = style.bg orelse fallback_bg;
|
||||||
|
return contrastRatio(fg, bg) >= minimum_ratio;
|
||||||
|
}
|
||||||
|
|
||||||
pub fn adaptColor(color: Color, mode: ColorMode) AdaptedColor {
|
pub fn adaptColor(color: Color, mode: ColorMode) AdaptedColor {
|
||||||
return switch (mode) {
|
return switch (mode) {
|
||||||
.truecolor => .{ .rgb = color },
|
.truecolor => .{ .rgb = color },
|
||||||
@@ -156,6 +230,16 @@ fn sgrBase(kind: SgrKind) u8 {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn containsIgnoreCase(haystack: []const u8, needle: []const u8) bool {
|
||||||
|
if (needle.len == 0) return true;
|
||||||
|
if (needle.len > haystack.len) return false;
|
||||||
|
var i: usize = 0;
|
||||||
|
while (i + needle.len <= haystack.len) : (i += 1) {
|
||||||
|
if (std.ascii.eqlIgnoreCase(haystack[i .. i + needle.len], needle)) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
fn nearest16(color: Color) u8 {
|
fn nearest16(color: Color) u8 {
|
||||||
return nearestInTable(color, ansi16[0..]);
|
return nearestInTable(color, ansi16[0..]);
|
||||||
}
|
}
|
||||||
@@ -291,3 +375,32 @@ test "regular: 256 and 16 color SGR use indexed form" {
|
|||||||
defer std.testing.allocator.free(rendered16);
|
defer std.testing.allocator.free(rendered16);
|
||||||
try std.testing.expect(std.mem.startsWith(u8, rendered16, "\x1b[48;5;"));
|
try std.testing.expect(std.mem.startsWith(u8, rendered16, "\x1b[48;5;"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test "regular: terminal color-mode detection follows safe precedence" {
|
||||||
|
try std.testing.expectEqual(ColorMode.mono, detectColorMode(.{ .no_color = "1", .colorterm = "truecolor", .term = "xterm-256color" }));
|
||||||
|
try std.testing.expectEqual(ColorMode.mono, detectColorMode(.{ .term = "dumb" }));
|
||||||
|
try std.testing.expectEqual(ColorMode.truecolor, detectColorMode(.{ .colorterm = "truecolor", .term = "xterm-256color" }));
|
||||||
|
try std.testing.expectEqual(ColorMode.truecolor, detectColorMode(.{ .mim_color = "24bit", .term = "screen" }));
|
||||||
|
try std.testing.expectEqual(ColorMode.color256, detectColorMode(.{ .term = "xterm-256color" }));
|
||||||
|
try std.testing.expectEqual(ColorMode.color16, detectColorMode(.{ .term = "xterm-color" }));
|
||||||
|
try std.testing.expectEqual(ColorMode.color16, detectColorMode(.{}));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "regular: terminal background detection uses COLORFGBG final field" {
|
||||||
|
try std.testing.expectEqual(Background.dark, detectBackground(.{ .colorfgbg = "15;0" }));
|
||||||
|
try std.testing.expectEqual(Background.light, detectBackground(.{ .colorfgbg = "0;15" }));
|
||||||
|
try std.testing.expectEqual(Background.unknown, detectBackground(.{ .colorfgbg = "broken" }));
|
||||||
|
try std.testing.expectEqual(Background.unknown, detectBackground(.{}));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "regular: terminal capability detector combines color and background" {
|
||||||
|
const caps = detectTerminal(.{ .term = "xterm-256color", .colorfgbg = "15;0" });
|
||||||
|
try std.testing.expectEqual(ColorMode.color256, caps.color_mode);
|
||||||
|
try std.testing.expectEqual(Background.dark, caps.background);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "regular: accessibility guardrail checks style contrast" {
|
||||||
|
try std.testing.expect(hasAccessibleContrast(newed_night.normal, newed_night.normal.bg.?, 7.0));
|
||||||
|
try std.testing.expect(!hasAccessibleContrast(.{ .fg = rgb(30, 30, 35), .bg = rgb(25, 22, 33) }, newed_night.normal.bg.?, 4.5));
|
||||||
|
try std.testing.expect(hasAccessibleContrast(.{ .bg = rgb(25, 22, 33) }, newed_night.normal.bg.?, 4.5));
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user