╭─────────────╮ │scroll to top│ ╰─────────────╯ ╭ ui.zig ───────────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
1068 _ = key; 1069 _ = root_focus; 1070 } 1071 1072 pub fn clearGrid(self: *FlowBox) void { 1073 if (self.grid) |*grid| { 1074 grid.deinit(); 1075 self.grid = null; 1076 } 1077 for (self.text_boxes.items) |*tb| tb.clearGrid(); 1078 } 1079 1080 pub fn getGrid(self: FlowBox) ?Grid { 1081 return self.grid; 1082 } 1083 1084 pub fn getFocus(self: *FlowBox) *Focus { 1085 return self.focus; 1086 } 1087 1088 pub fn cellRect(self: FlowBox, index: usize) ?layout.IRect { 1089 if (self.last_cols == 0 or index >= self.text_boxes.items.len) return null; 1090 const slot_height = self.options.cell_height + border_rows; 1091 const col = index % self.last_cols; 1092 const row = index / self.last_cols; 1093 return .{ 1094 .x = @intCast(col * self.options.cell_width), 1095 .y = @intCast(row * slot_height), 1096 .size = .{ .width = self.options.cell_width, .height = slot_height }, 1097 }; 1098 } 1099 1100 pub fn indexOfFocusId(self: FlowBox, focus_id: usize) ?usize { 1101 for (self.text_boxes.items, 0..) |tb, i| { 1102 if (tb.box.focus.id == focus_id) return i; 1103 } 1104 return null; 1105 } 1106 1107 pub const Scroll = struct { 1108 scroll: wgt.Scroll(Widget), 1109 1110 pub fn init(allocator: std.mem.Allocator, options: FlowBox.Options, web_native: bool) !Scroll { 1111 var layout_inner = try FlowBox.init(allocator, options); 1112 errdefer layout_inner.deinit(allocator); 1113 var scroll = try wgt.Scroll(Widget).init(allocator, .{ .flow_box = layout_inner }, .{ .direction = .vert, .web_native = web_native }); 1114 errdefer scroll.deinit(allocator); 1115 return .{ .scroll = scroll }; 1116 } 1117 1118 pub fn deinit(self: *Scroll, allocator: std.mem.Allocator) void { 1119 self.scroll.deinit(allocator); 1120 } 1121 1122 pub fn setItems(self: *Scroll, allocator: std.mem.Allocator, items: []const Item) !void { 1123 self.scroll.x = 0; 1124 self.scroll.y = 0; 1125 try self.inner().setItems(allocator, items); 1126 } 1127 1128 pub fn build(self: *Scroll, allocator: std.mem.Allocator, constraint: layout.Constraint, root_focus: *Focus) !void { 1129 try self.scroll.build(allocator, constraint, root_focus); 1130 } 1131 1132 pub fn input(self: *Scroll, allocator: std.mem.Allocator, key: inp.Key, root_focus: *Focus) !void { 1133 _ = allocator; 1134 const in = self.inner(); 1135 const child_id = in.focus.child_id orelse return; 1136 const current_index = in.indexOfFocusId(child_id) orelse return; 1137 const count = in.text_boxes.items.len; 1138 if (count == 0) return; 1139 const cols = in.last_cols; 1140 const slot_height = in.options.cell_height + FlowBox.border_rows; 1141 1142 var index = current_index; 1143 switch (key) { 1144 .arrow_up => index -|= cols, 1145 .arrow_down => if (index + cols < count) { 1146 index += cols; 1147 }, 1148 .arrow_left => index -|= 1, 1149 .arrow_right => if (index + 1 < count) { 1150 index += 1; 1151 }, 1152 .home => index = 0, 1153 .end => index = count - 1, 1154 .page_up => { 1155 if (self.scroll.grid) |grid| if (slot_height > 0) { 1156 const rows_per_page = grid.size.height / slot_height; 1157 index -|= rows_per_page * cols; 1158 }; 1159 }, 1160 .page_down => { 1161 if (self.scroll.grid) |grid| if (slot_height > 0) { 1162 const rows_per_page = grid.size.height / slot_height; 1163 index = @min(index + rows_per_page * cols, count - 1); 1164 }; 1165 }, 1166 // scroll wheel moves the focused cell by a full row so the 1167 // viewport (via scrollToRect) follows in row-sized steps, 1168 // matching how a scroll wheel feels in a grid view. 1169 .mouse => |mouse| switch (mouse.action) { 1170 .scroll => |dir| switch (dir) { 1171 .up => index -|= cols, 1172 .down => if (index + cols < count) { 1173 index += cols; 1174 }, 1175 }, 1176 else => {}, 1177 }, 1178 else => {}, 1179 } 1180 1181 if (index != current_index) { 1182 try root_focus.setFocus(in.text_boxes.items[index].getFocus().id); 1183 if (in.cellRect(index)) |rect| { 1184 self.scroll.scrollToRect(rect); 1185 } 1186 } 1187 } 1188 1189 pub fn clearGrid(self: *Scroll) void { 1190 self.scroll.clearGrid(); 1191 } 1192 1193 pub fn getGrid(self: Scroll) ?Grid { 1194 return self.scroll.getGrid(); 1195 } 1196 1197 pub fn getFocus(self: *Scroll) *Focus { 1198 return self.scroll.getFocus(); 1199 } 1200 1201 pub fn getSelectedIndex(self: Scroll) ?usize { 1202 const in = self.scroll.child.flow_box; 1203 const child_id = in.focus.child_id orelse return null; 1204 return in.indexOfFocusId(child_id); 1205 } 1206 1207 fn inner(self: *Scroll) *FlowBox { 1208 return &self.scroll.child.flow_box; 1209 } 1210 }; 1211 }; 1212 1213 // an invisible widget that fills the horizontal space granted by its parent. 1214 // used inside Box(horiz) with a min_size so the box reserves space for the 1215 // children that follow, pushing them to the right. 1216 pub const Spacer = struct { 1217 focus: *Focus, 1218 grid: ?Grid, 1219 1220 pub fn init(allocator: std.mem.Allocator) !Spacer { 1221 return .{ 1222 .focus = try Focus.create(allocator, .container), 1223 .grid = null, 1224 }; 1225 } 1226 1227 pub fn deinit(self: *Spacer, allocator: std.mem.Allocator) void { 1228 self.focus.destroy(allocator); 1229 if (self.grid) |*grid| { 1230 grid.deinit(); 1231 self.grid = null; 1232 } 1233 } 1234 1235 pub fn build(self: *Spacer, allocator: std.mem.Allocator, constraint: layout.Constraint, root_focus: *Focus) !void { 1236 _ = root_focus; 1237 self.clearGrid(); 1238 const width = constraint.max_size.width orelse return; 1239 if (width == 0) return; 1240 self.grid = try Grid.init(allocator, .{ .width = width, .height = 1 }); 1241 } 1242 1243 pub fn input(self: *Spacer, allocator: std.mem.Allocator, key: inp.Key, root_focus: *Focus) !void { 1244 _ = self; 1245 _ = allocator; 1246 _ = key; 1247 _ = root_focus; 1248 } 1249 1250 pub fn clearGrid(self: *Spacer) void { 1251 if (self.grid) |*grid| { 1252 grid.deinit(); 1253 self.grid = null; 1254 } 1255 } 1256 1257 pub fn getGrid(self: Spacer) ?Grid { 1258 return self.grid; 1259 } 1260 1261 pub fn getFocus(self: *Spacer) *Focus { 1262 return self.focus; 1263 } 1264 }; 1265 1266 // a one-row, non-focusable status bar showing the current page's url 1267 pub const Footer = struct { 1268 focus: *Focus, 1269 grid: ?Grid, 1270 session: *Session, 1271 arena: std.heap.ArenaAllocator, 1272 1273 pub fn init(allocator: std.mem.Allocator, session: *Session) !Footer { 1274 return .{ 1275 .focus = try Focus.create(allocator, .container), 1276 .grid = null, 1277 .session = session, 1278 .arena = std.heap.ArenaAllocator.init(allocator), 1279 }; 1280 } 1281 1282 pub fn deinit(self: *Footer, allocator: std.mem.Allocator) void { 1283 self.focus.destroy(allocator); 1284 if (self.grid) |*grid| grid.deinit(); 1285 self.arena.deinit(); 1286 } 1287 1288 pub fn build(self: *Footer, allocator: std.mem.Allocator, constraint: layout.Constraint, root_focus: *Focus) !void { 1289 _ = root_focus; 1290 self.clearGrid(); 1291 const width = constraint.max_size.width orelse return; 1292 if (width == 0) return; 1293 1294 _ = self.arena.reset(.retain_capacity); 1295 const aa = self.arena.allocator(); 1296 const path = self.session.data.current_page.urlAlloc(&self.arena) catch return; 1297 const text = if (self.session.web_port) |port| 1298 std.fmt.allocPrint(aa, "http://localhost:{d}{s}", .{ port, path }) catch return 1299 else 1300 path; 1301 1302 var grid = try Grid.init(allocator, .{ .width = width, .height = 1 }); 1303 errdefer grid.deinit(); 1304 var utf8 = (std.unicode.Utf8View.init(text) catch return).iterator(); 1305 var i: usize = 0; 1306 while (utf8.nextCodepointSlice()) |ch| { 1307 if (i == width) break; 1308 grid.cells.items[try grid.cells.at(.{ 0, i })].rune = ch; 1309 i += 1; 1310 } 1311 self.grid = grid; 1312 } 1313 1314 pub fn input(self: *Footer, allocator: std.mem.Allocator, key: inp.Key, root_focus: *Focus) !void { 1315 _ = self; 1316 _ = allocator; 1317 _ = key; 1318 _ = root_focus; 1319 } 1320 1321 pub fn clearGrid(self: *Footer) void { 1322 if (self.grid) |*grid| { 1323 grid.deinit(); 1324 self.grid = null; 1325 } 1326 } 1327 1328 pub fn getGrid(self: Footer) ?Grid { 1329 return self.grid; 1330 } 1331 1332 pub fn getFocus(self: *Footer) *Focus { 1333 return self.focus; 1334 } 1335 }; 1336 1337 // a single-child wrapper that builds the child at its natural size and 1338 // positions its grid in the middle of the area granted by the parent 1339 pub const Center = struct { 1340 focus: *Focus, 1341 grid: ?Grid, 1342 child: *Widget, 1343 direction: Direction, 1344 1345 pub const Direction = enum { both, horiz, vert }; 1346 1347 pub fn init(allocator: std.mem.Allocator, child_widget: Widget, direction: Direction) !Center { 1348 const child = try allocator.create(Widget); 1349 errdefer allocator.destroy(child); 1350 child.* = child_widget; 1351 return .{ 1352 .focus = try Focus.create(allocator, .container), 1353 .grid = null, 1354 .child = child, 1355 .direction = direction, 1356 }; 1357 } 1358 1359 pub fn deinit(self: *Center, allocator: std.mem.Allocator) void { 1360 self.focus.destroy(allocator); 1361 if (self.grid) |*grid| { 1362 grid.deinit(); 1363 self.grid = null; 1364 } 1365 self.child.deinit(allocator); 1366 allocator.destroy(self.child); 1367 } 1368 1369 pub fn build(self: *Center, allocator: std.mem.Allocator, constraint: layout.Constraint, root_focus: *Focus) !void { 1370 self.clearGrid(); 1371 self.getFocus().clear(); 1372 1373 // build the child without forcing it to fill min_size; it sizes 1374 // itself to its content within the available max. 1375 try self.child.build(allocator, .{ 1376 .min_size = .{ .width = null, .height = null }, 1377 .max_size = constraint.max_size, 1378 }, root_focus); 1379 1380 const child_grid = self.child.getGrid() orelse return; 1381 1382 // prefer max when given; otherwise grow to max(min, child) so that 1383 // when only a min is set (e.g. the wasm path passes viewport rows 1384 // as min_height with no max), we can still vertically center while 1385 // letting taller content extend past the min. 1386 const width = if (constraint.max_size.width) |w| w else if (constraint.min_size.width) |min_w| @max(min_w, child_grid.size.width) else child_grid.size.width; 1387 const height = if (constraint.max_size.height) |h| h else if (constraint.min_size.height) |min_h| @max(min_h, child_grid.size.height) else child_grid.size.height; 1388 if (width == 0 or height == 0) return; 1389 1390 const offset_x: usize = if (self.direction == .horiz or self.direction == .both) 1391 (width -| child_grid.size.width) / 2 1392 else 1393 0; 1394 const offset_y: usize = if (self.direction == .vert or self.direction == .both) 1395 (height -| child_grid.size.height) / 2 1396 else 1397 0; 1398 1399 var grid = try Grid.init(allocator, .{ .width = width, .height = height }); 1400 errdefer grid.deinit(); 1401 try grid.drawGrid(child_grid, offset_x, offset_y); 1402 try self.getFocus().addChild(allocator, self.child.getFocus(), child_grid.size, offset_x, offset_y); 1403 self.getFocus().child_id = self.child.getFocus().id; 1404 1405 self.grid = grid; 1406 } 1407 1408 pub fn input(self: *Center, allocator: std.mem.Allocator, key: inp.Key, root_focus: *Focus) !void { 1409 try self.child.input(allocator, key, root_focus); 1410 } 1411 1412 pub fn clearGrid(self: *Center) void { 1413 if (self.grid) |*grid| { 1414 grid.deinit(); 1415 self.grid = null; 1416 } 1417 self.child.clearGrid(); 1418 } 1419 1420 pub fn getGrid(self: Center) ?Grid { 1421 return self.grid; 1422 } 1423 1424 pub fn getFocus(self: *Center) *Focus { 1425 return self.focus; 1426 } 1427 }; 1428 1429 // renders truecolor ANSI art 1430 pub const AnsiArt = struct { 1431 focus: *Focus, 1432 grid: ?Grid, 1433 content: []const u8, 1434 1435 pub fn init(allocator: std.mem.Allocator, content: []const u8) !AnsiArt { 1436 return .{ 1437 .focus = try Focus.create(allocator, .container), 1438 .grid = null, 1439 .content = content, 1440 }; 1441 } 1442 1443 pub fn deinit(self: *AnsiArt, allocator: std.mem.Allocator) void { 1444 self.focus.destroy(allocator); 1445 if (self.grid) |*grid| { 1446 grid.deinit(); 1447 self.grid = null; 1448 } 1449 } 1450 1451 pub fn build(self: *AnsiArt, allocator: std.mem.Allocator, constraint: layout.Constraint, root_focus: *Focus) !void { 1452 _ = root_focus; 1453 self.clearGrid(); 1454 1455 // parse into rows of cells, then copy into a rectangular grid 1456 var rows: std.ArrayList(std.ArrayList(Grid.Cell)) = .empty; 1457 defer { 1458 for (rows.items) |*row| row.deinit(allocator); 1459 rows.deinit(allocator); 1460 } 1461 var row: std.ArrayList(Grid.Cell) = .empty; 1462 errdefer row.deinit(allocator); 1463 1464 var style: Grid.Style = .{}; 1465 var width: usize = 0; 1466 const content = self.content; 1467 var i: usize = 0; 1468 while (i < content.len) { 1469 const byte = content[i]; 1470 if (byte == '\n') { 1471 width = @max(width, row.items.len); 1472 try rows.append(allocator, row); 1473 row = .empty; 1474 i += 1; 1475 } else if (byte == 0x1B and i + 1 < content.len and content[i + 1] == '[') { 1476 // CSI: scan to the final byte (0x40..0x7E); apply it if it's 'm' 1477 var j = i + 2; 1478 while (j < content.len and !(content[j] >= 0x40 and content[j] <= 0x7E)) j += 1; 1479 if (j >= content.len) { 1480 i = content.len; // malformed trailing escape; stop 1481 } else { 1482 if (content[j] == 'm') applySgr(&style, content[i + 2 .. j]); 1483 i = j + 1; 1484 } 1485 } else { 1486 const len = std.unicode.utf8ByteSequenceLength(byte) catch 1; 1487 const end = @min(content.len, i + len); 1488 const rune = content[i..end]; 1489 const transparent = end - i == 1 and rune[0] == ' ' and 1490 style.fg == null and style.bg == null and !style.inverted; 1491 try row.append(allocator, .{ 1492 .rune = if (transparent) null else rune, 1493 .style = style, 1494 }); 1495 i = end; 1496 } 1497 } 1498 // a trailing row with content but no closing newline 1499 if (row.items.len > 0) { 1500 width = @max(width, row.items.len); 1501 try rows.append(allocator, row); 1502 } else { 1503 row.deinit(allocator); 1504 } 1505 row = .empty; // ownership moved into rows (or freed); keep errdefer safe 1506 1507 const height = rows.items.len; 1508 if (width == 0 or height == 0) return; 1509 1510 const clamped_w = @min(width, constraint.max_size.width orelse width); 1511 const clamped_h = @min(height, constraint.max_size.height orelse height); 1512 1513 var grid = try Grid.init(allocator, .{ .width = clamped_w, .height = clamped_h }); 1514 errdefer grid.deinit(); 1515 for (rows.items[0..clamped_h], 0..) |r, y| { 1516 const n = @min(r.items.len, clamped_w); 1517 for (r.items[0..n], 0..) |cell, x| { 1518 grid.cells.items[try grid.cells.at(.{ y, x })] = cell; 1519 } 1520 } 1521 self.grid = grid; 1522 } 1523 1524 pub fn input(self: *AnsiArt, allocator: std.mem.Allocator, key: inp.Key, root_focus: *Focus) !void { 1525 _ = self; 1526 _ = allocator; 1527 _ = key; 1528 _ = root_focus; 1529 } 1530 1531 pub fn clearGrid(self: *AnsiArt) void { 1532 if (self.grid) |*grid| { 1533 grid.deinit(); 1534 self.grid = null; 1535 } 1536 } 1537 1538 pub fn getGrid(self: AnsiArt) ?Grid { 1539 return self.grid; 1540 } 1541 1542 pub fn getFocus(self: *AnsiArt) *Focus { 1543 return self.focus; 1544 } 1545 1546 fn applySgr(style: *Grid.Style, params: []const u8) void { 1547 var nums: [16]u32 = undefined; 1548 var n: usize = 0; 1549 var it = std.mem.splitScalar(u8, params, ';'); 1550 while (it.next()) |tok| { 1551 if (n >= nums.len) break; 1552 // an empty parameter (e.g. bare "\x1b[m") means 0 1553 nums[n] = std.fmt.parseInt(u32, tok, 10) catch 0; 1554 n += 1; 1555 } 1556 if (n == 0) { 1557 style.* = .{}; 1558 return; 1559 } 1560 var i: usize = 0; 1561 while (i < n) : (i += 1) { 1562 switch (nums[i]) { 1563 0 => style.* = .{}, 1564 7 => style.inverted = true, 1565 27 => style.inverted = false, 1566 39 => style.fg = null, 1567 49 => style.bg = null, 1568 38, 48 => { 1569 // truecolor form: 38;2;r;g;b — anything else is ignored 1570 if (i + 4 < n and nums[i + 1] == 2) { 1571 const c = Grid.Color{ 1572 .r = @truncate(nums[i + 2]), 1573 .g = @truncate(nums[i + 3]), 1574 .b = @truncate(nums[i + 4]), 1575 }; 1576 if (nums[i] == 38) style.fg = c else style.bg = c; 1577 i += 4; 1578 } 1579 }, 1580 else => {}, 1581 } 1582 } 1583 } 1584 }; 1585 1586 // a full-screen wrapper that renders ANSI art behind whatever page it wraps 1587 pub const AnsiBackground = struct { 1588 grid: ?Grid, 1589 child: *Widget, 1590 art: AnsiArt, 1591 session: *Session, 1592 1593 pub fn init(allocator: std.mem.Allocator, child_widget: Widget, art_content: []const u8, session: *Session) !AnsiBackground { 1594 var cw = child_widget; 1595 const child = allocator.create(Widget) catch |e| { 1596 cw.deinit(allocator); 1597 return e; 1598 }; 1599 child.* = cw; 1600 errdefer { 1601 child.deinit(allocator); 1602 allocator.destroy(child); 1603 } 1604 return .{ .grid = null, .child = child, .art = try AnsiArt.init(allocator, art_content), .session = session }; 1605 } 1606 1607 pub fn deinit(self: *AnsiBackground, allocator: std.mem.Allocator) void { 1608 if (self.grid) |*grid| grid.deinit(); 1609 self.art.deinit(allocator); 1610 self.child.deinit(allocator); 1611 allocator.destroy(self.child); 1612 } 1613 1614 pub fn build(self: *AnsiBackground, allocator: std.mem.Allocator, constraint: layout.Constraint, root_focus: *Focus) !void { 1615 self.clearGrid(); 1616 1617 // make the wrapped page fill the whole available area 1618 try self.child.build(allocator, .{ 1619 .min_size = .{ 1620 .width = constraint.max_size.width orelse constraint.min_size.width, 1621 .height = constraint.max_size.height orelse constraint.min_size.height, 1622 }, 1623 .max_size = constraint.max_size, 1624 }, root_focus); 1625 1626 if (self.session.data.enable_ansi) { 1627 if (self.child.getGrid()) |fg| { 1628 self.grid = try artBehind(allocator, fg, &self.art, .top_right, root_focus); 1629 } 1630 } 1631 } 1632 1633 pub fn input(self: *AnsiBackground, allocator: std.mem.Allocator, key: inp.Key, root_focus: *Focus) !void { 1634 try self.child.input(allocator, key, root_focus); 1635 } 1636 1637 pub fn clearGrid(self: *AnsiBackground) void { 1638 if (self.grid) |*grid| { 1639 grid.deinit(); 1640 self.grid = null; 1641 } 1642 self.child.clearGrid(); 1643 self.art.clearGrid(); 1644 } 1645 1646 pub fn getGrid(self: AnsiBackground) ?Grid { 1647 return self.grid orelse self.child.getGrid(); 1648 } 1649 1650 pub fn getFocus(self: *AnsiBackground) *Focus { 1651 return self.child.getFocus(); 1652 } 1653 1654 // which top corner the art hugs. 1655 const ArtAnchor = enum { top_left, top_right }; 1656 1657 // a foreground cell counts as blank if it's empty or a bare space without styling 1658 fn cellIsBlank(cell: Grid.Cell) bool { 1659 if (cell.rune) |rune| { 1660 return std.mem.eql(u8, rune, " ") and 1661 cell.style.fg == null and cell.style.bg == null and !cell.style.inverted; 1662 } 1663 return true; 1664 } 1665 1666 // the art is dimmed to this fraction of its brightness so foreground text 1667 // stays legible over it 1668 const art_brightness = 45; // percent 1669 1670 fn dimColor(color: ?Grid.Color) ?Grid.Color { 1671 const c = color orelse return null; 1672 return .{ 1673 .r = @intCast(@as(u16, c.r) * art_brightness / 100), 1674 .g = @intCast(@as(u16, c.g) * art_brightness / 100), 1675 .b = @intCast(@as(u16, c.b) * art_brightness / 100), 1676 }; 1677 } 1678 1679 // terminals that don't support truecolor misparse a "38;2;r;g;b"/"48;2;…" 1680 // SGR as a list of plain SGR codes, so any channel value in 1..9 turns on 1681 // a text attribute (5/6 = blink, 7 = inverse, 8 = conceal, …). 1682 // snap such channels clear of that range so the backdrop doesn't blink or 1683 // hide text there; the ≤9/255 shift is imperceptible on terminals that 1684 // actually render truecolor. 1685 fn sgrSafe(color: ?Grid.Color) ?Grid.Color { 1686 const c = color orelse return null; 1687 const snap = struct { 1688 fn f(v: u8) u8 { 1689 return if (v >= 1 and v <= 9) 10 else v; 1690 } 1691 }.f; 1692 return .{ .r = snap(c.r), .g = snap(c.g), .b = snap(c.b) }; 1693 } 1694 1695 // composites ANSI art behind a foreground grid 1696 fn artBehind(allocator: std.mem.Allocator, foreground: Grid, art: *AnsiArt, anchor: ArtAnchor, root_focus: *Focus) !Grid { 1697 art.clearGrid(); 1698 try art.build(allocator, .{ 1699 .min_size = .{ .width = null, .height = null }, 1700 .max_size = .{ .width = foreground.size.width, .height = foreground.size.height }, 1701 }, root_focus); 1702 1703 var out = try Grid.initFromGrid(allocator, foreground, foreground.size, 0, 0); 1704 errdefer out.deinit(); 1705 1706 if (art.getGrid()) |art_grid| { 1707 const anchor_x = switch (anchor) { 1708 .top_left => 0, 1709 .top_right => foreground.size.width -| art_grid.size.width, 1710 }; 1711 for (0..art_grid.size.height) |y| { 1712 for (0..art_grid.size.width) |x| { 1713 const src = art_grid.cells.items[try art_grid.cells.at(.{ y, x })]; 1714 if (src.rune == null) continue; 1715 const idx = out.cells.at(.{ y, anchor_x + x }) catch continue; 1716 const dst = &out.cells.items[idx]; 1717 // composite the dimmed art behind every cell so it shows 1718 // through the whole UI: a blank cell becomes the art, while a 1719 // cell with a glyph keeps the glyph but takes the dimmed art as 1720 // its background (replacing whatever background it had, which 1721 // would otherwise obscure the art). 1722 if (cellIsBlank(dst.*)) { 1723 dst.* = src; 1724 dst.style.fg = sgrSafe(dimColor(dst.style.fg)); 1725 dst.style.bg = sgrSafe(dimColor(dst.style.bg)); 1726 } else { 1727 dst.style.bg = sgrSafe(dimColor(src.style.bg orelse src.style.fg)); 1728 } 1729 } 1730 } 1731 } 1732 return out; 1733 } 1734 }; 1735