Detect terminal color capabilities
This commit is contained in:
+113
@@ -39,6 +39,25 @@ pub const ColorMode = enum {
|
||||
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 AdaptedColor = union(enum) {
|
||||
@@ -98,6 +117,61 @@ pub fn rgb(r: u8, g: u8, b: u8) Color {
|
||||
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 {
|
||||
return switch (mode) {
|
||||
.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 {
|
||||
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);
|
||||
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