╭─────────────╮ │scroll to top│ ╰─────────────╯ ╭ ui.zig ───────────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
1197 if (self.session.data.enable_ansi) { 1198 if (self.child.getGrid()) |fg| { 1199 self.grid = try artBehind(allocator, fg, &self.art, .top_right, root_focus); 1200 } 1201 } 1202 } 1203 1204 pub fn input(self: *AnsiBackground, allocator: std.mem.Allocator, key: inp.Key, root_focus: *Focus) !void { 1205 try self.child.input(allocator, key, root_focus); 1206 } 1207 1208 pub fn clearGrid(self: *AnsiBackground) void { 1209 if (self.grid) |*grid| { 1210 grid.deinit(); 1211 self.grid = null; 1212 } 1213 self.child.clearGrid(); 1214 self.art.clearGrid(); 1215 } 1216 1217 pub fn getGrid(self: AnsiBackground) ?Grid { 1218 return self.grid orelse self.child.getGrid(); 1219 } 1220 1221 pub fn getFocus(self: *AnsiBackground) *Focus { 1222 return self.child.getFocus(); 1223 } 1224 1225 // which top corner the art hugs. 1226 const ArtAnchor = enum { top_left, top_right }; 1227 1228 // a foreground cell counts as blank if it's empty or a bare space without styling 1229 fn cellIsBlank(cell: Grid.Cell) bool { 1230 if (cell.rune) |rune| { 1231 return std.mem.eql(u8, rune, " ") and 1232 cell.style.fg == null and cell.style.bg == null and !cell.style.inverted; 1233 } 1234 return true; 1235 } 1236 1237 // a single representative color for an art cell 1238 fn artCellColor(style: Grid.Style) ?Grid.Color { 1239 const f = style.fg orelse return style.bg; 1240 const b = style.bg orelse return f; 1241 return .{ 1242 .r = @intCast((@as(u16, f.r) + b.r) / 2), 1243 .g = @intCast((@as(u16, f.g) + b.g) / 2), 1244 .b = @intCast((@as(u16, f.b) + b.b) / 2), 1245 }; 1246 } 1247 1248 // behind a visible glyph the art color is dimmed to this fraction of its 1249 // brightness so light terminal text stays legible on top of it 1250 const glyph_bg_brightness = 45; // percent 1251 1252 // a glyph sitting over the art also gets its foreground pinned to this color 1253 // (unless it set its own), so the text stays legible 1254 const glyph_fg_over_art = Grid.Color{ .r = 235, .g = 235, .b = 235 }; 1255 1256 fn dimColor(color: ?Grid.Color) ?Grid.Color { 1257 const c = color orelse return null; 1258 return .{ 1259 .r = @intCast(@as(u16, c.r) * glyph_bg_brightness / 100), 1260 .g = @intCast(@as(u16, c.g) * glyph_bg_brightness / 100), 1261 .b = @intCast(@as(u16, c.b) * glyph_bg_brightness / 100), 1262 }; 1263 } 1264 1265 // terminals that don't support truecolor misparse a "38;2;r;g;b"/"48;2;…" 1266 // SGR as a list of plain SGR codes, so any channel value in 1..9 turns on 1267 // a text attribute (5/6 = blink, 7 = inverse, 8 = conceal, …). 1268 // snap such channels clear of that range so the backdrop doesn't blink or 1269 // hide text there; the ≤9/255 shift is imperceptible on terminals that 1270 // actually render truecolor. 1271 fn sgrSafe(color: ?Grid.Color) ?Grid.Color { 1272 const c = color orelse return null; 1273 const snap = struct { 1274 fn f(v: u8) u8 { 1275 return if (v >= 1 and v <= 9) 10 else v; 1276 } 1277 }.f; 1278 return .{ .r = snap(c.r), .g = snap(c.g), .b = snap(c.b) }; 1279 } 1280 1281 // composites ANSI art behind a foreground grid 1282 fn artBehind(allocator: std.mem.Allocator, foreground: Grid, art: *AnsiArt, anchor: ArtAnchor, root_focus: *Focus) !Grid { 1283 art.clearGrid(); 1284 try art.build(allocator, .{ 1285 .min_size = .{ .width = null, .height = null }, 1286 .max_size = .{ .width = foreground.size.width, .height = foreground.size.height }, 1287 }, root_focus); 1288 1289 var out = try Grid.initFromGrid(allocator, foreground, foreground.size, 0, 0); 1290 errdefer out.deinit(); 1291 1292 if (art.getGrid()) |art_grid| { 1293 const anchor_x = switch (anchor) { 1294 .top_left => 0, 1295 .top_right => foreground.size.width -| art_grid.size.width, 1296 }; 1297 for (0..art_grid.size.height) |y| { 1298 for (0..art_grid.size.width) |x| { 1299 const src = art_grid.cells.items[try art_grid.cells.at(.{ y, x })]; 1300 if (src.rune == null) continue; 1301 const idx = out.cells.at(.{ y, anchor_x + x }) catch continue; 1302 const dst = &out.cells.items[idx]; 1303 if (cellIsBlank(dst.*)) { 1304 dst.* = src; 1305 dst.style.fg = sgrSafe(dst.style.fg); 1306 dst.style.bg = sgrSafe(dst.style.bg); 1307 } else if (dst.style.bg == null) { 1308 dst.style.bg = sgrSafe(dimColor(artCellColor(src.style))); 1309 if (dst.style.fg == null) dst.style.fg = glyph_fg_over_art; 1310 } 1311 } 1312 } 1313 } 1314 return out; 1315 } 1316 }; 1317