╭─────────────╮ │scroll to top│ ╰─────────────╯ ╭ ui.zig ───────────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
1743 } 1744 }, 1745 else => {}, 1746 } 1747 } 1748 } 1749 }; 1750 1751 // a full-screen wrapper that renders ANSI art behind whatever page it wraps 1752 pub const AnsiBackground = struct { 1753 grid: ?Grid, 1754 child: *Widget, 1755 art: AnsiArt, 1756 session: *Session, 1757 1758 pub fn init(allocator: std.mem.Allocator, child_widget: Widget, art_content: []const u8, session: *Session) !AnsiBackground { 1759 var cw = child_widget; 1760 const child = allocator.create(Widget) catch |e| { 1761 cw.deinit(allocator); 1762 return e; 1763 }; 1764 child.* = cw; 1765 errdefer { 1766 child.deinit(allocator); 1767 allocator.destroy(child); 1768 } 1769 return .{ .grid = null, .child = child, .art = try AnsiArt.init(allocator, art_content), .session = session }; 1770 } 1771 1772 pub fn deinit(self: *AnsiBackground, allocator: std.mem.Allocator) void { 1773 if (self.grid) |*grid| grid.deinit(); 1774 self.art.deinit(allocator); 1775 self.child.deinit(allocator); 1776 allocator.destroy(self.child); 1777 } 1778 1779 pub fn build(self: *AnsiBackground, allocator: std.mem.Allocator, constraint: layout.Constraint, root_focus: *Focus) !void { 1780 self.clearGrid(); 1781 1782 // make the wrapped page fill the whole available area 1783 try self.child.build(allocator, .{ 1784 .min_size = .{ 1785 .width = constraint.max_size.width orelse constraint.min_size.width, 1786 .height = constraint.max_size.height orelse constraint.min_size.height, 1787 }, 1788 .max_size = constraint.max_size, 1789 }, root_focus); 1790 1791 if (self.session.data.enable_ansi) { 1792 if (self.child.getGrid()) |fg| { 1793 self.grid = try artBehind(allocator, fg, &self.art, .top_right, root_focus); 1794 } 1795 } 1796 } 1797 1798 pub fn input(self: *AnsiBackground, allocator: std.mem.Allocator, key: inp.Key, root_focus: *Focus) !void { 1799 try self.child.input(allocator, key, root_focus); 1800 } 1801 1802 pub fn clearGrid(self: *AnsiBackground) void { 1803 if (self.grid) |*grid| { 1804 grid.deinit(); 1805 self.grid = null; 1806 } 1807 self.child.clearGrid(); 1808 self.art.clearGrid(); 1809 } 1810 1811 pub fn getGrid(self: AnsiBackground) ?Grid { 1812 return self.grid orelse self.child.getGrid(); 1813 } 1814 1815 pub fn getFocus(self: *AnsiBackground) *Focus { 1816 return self.child.getFocus(); 1817 } 1818 1819 // which top corner the art hugs. 1820 const ArtAnchor = enum { top_left, top_right }; 1821 1822 // a foreground cell counts as blank if it's empty or a bare space without styling 1823 fn cellIsBlank(cell: Grid.Cell) bool { 1824 if (cell.rune) |rune| { 1825 return std.mem.eql(u8, rune, " ") and 1826 cell.style.fg == null and cell.style.bg == null and !cell.style.inverted; 1827 } 1828 return true; 1829 } 1830 1831 // the art is dimmed to this fraction of its brightness so foreground text 1832 // stays legible over it 1833 const art_brightness = 45; // percent 1834 1835 fn dimColor(color: ?Grid.Color) ?Grid.Color { 1836 const c = color orelse return null; 1837 return .{ 1838 .r = @intCast(@as(u16, c.r) * art_brightness / 100), 1839 .g = @intCast(@as(u16, c.g) * art_brightness / 100), 1840 .b = @intCast(@as(u16, c.b) * art_brightness / 100), 1841 }; 1842 } 1843 1844 // terminals that don't support truecolor misparse a "38;2;r;g;b"/"48;2;…" 1845 // SGR as a list of plain SGR codes, so any channel value in 1..9 turns on 1846 // a text attribute (5/6 = blink, 7 = inverse, 8 = conceal, …). 1847 // snap such channels clear of that range so the backdrop doesn't blink or 1848 // hide text there; the ≤9/255 shift is imperceptible on terminals that 1849 // actually render truecolor. 1850 fn sgrSafe(color: ?Grid.Color) ?Grid.Color { 1851 const c = color orelse return null; 1852 const snap = struct { 1853 fn f(v: u8) u8 { 1854 return if (v >= 1 and v <= 9) 10 else v; 1855 } 1856 }.f; 1857 return .{ .r = snap(c.r), .g = snap(c.g), .b = snap(c.b) }; 1858 } 1859 1860 // composites ANSI art behind a foreground grid 1861 fn artBehind(allocator: std.mem.Allocator, foreground: Grid, art: *AnsiArt, anchor: ArtAnchor, root_focus: *Focus) !Grid { 1862 art.clearGrid(); 1863 try art.build(allocator, .{ 1864 .min_size = .{ .width = null, .height = null }, 1865 .max_size = .{ .width = foreground.size.width, .height = foreground.size.height }, 1866 }, root_focus); 1867 1868 var out = try Grid.initFromGrid(allocator, foreground, foreground.size, 0, 0); 1869 errdefer out.deinit(); 1870 1871 if (art.getGrid()) |art_grid| { 1872 const anchor_x = switch (anchor) { 1873 .top_left => 0, 1874 .top_right => foreground.size.width -| art_grid.size.width, 1875 }; 1876 for (0..art_grid.size.height) |y| { 1877 for (0..art_grid.size.width) |x| { 1878 const src = art_grid.cells.items[try art_grid.cells.at(.{ y, x })]; 1879 if (src.rune == null) continue; 1880 const idx = out.cells.at(.{ y, anchor_x + x }) catch continue; 1881 const dst = &out.cells.items[idx]; 1882 // composite the dimmed art behind every cell so it shows 1883 // through the whole UI: a blank cell becomes the art, while a 1884 // cell with a glyph keeps the glyph but takes the dimmed art as 1885 // its background (replacing whatever background it had, which 1886 // would otherwise obscure the art). 1887 if (cellIsBlank(dst.*)) { 1888 dst.* = src; 1889 dst.style.fg = sgrSafe(dimColor(dst.style.fg)); 1890 dst.style.bg = sgrSafe(dimColor(dst.style.bg)); 1891 } else { 1892 dst.style.bg = sgrSafe(dimColor(src.style.bg orelse src.style.fg)); 1893 } 1894 } 1895 } 1896 } 1897 return out; 1898 } 1899 }; 1900