╭──────────╮ │← previous│ ╰──────────╯ ╭ changed ─────────────╮ │src/ui/Auth/Logout.zig│ ╰──────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ 86 } │ │ 87 │ │ 88 pub fn atTop(self: View) bool { │ │130 - return self.center.child.box.focus.child_id == self.center.child.box.children.keys()[ansi_index];│ │ 89 + _ = self; │ │ 90 + return true; │ │ 91 } │ │ 92 }; │ │ 93 │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────╮ │134 -fn ansiLabel(session: *const ui.Session) []const u8 { │ │135 - return if (session.data.enable_ansi) "turn off ANSI art" else "turn on ANSI art";│ │136 -} │ │137 - │ ╰──────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ────────╮ │src/ui/widget.zig│ ╰─────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────╮ │1576 width = @max(width, row.items.len); │ │1577 try rows.append(allocator, row); │ │1578 row = .empty; │ │1579 + i += 1; │ │1580 + } else if (byte == '\r') { │ │1581 + // git may checkout the ansi art with crlf line endings on windows │ │1582 i += 1; │ │1583 } else if (byte == 0x1B and i + 1 < content.len and content[i + 1] == '[') {│ │1584 // CSI: scan to the final byte (0x40..0x7E); apply it if it's 'm' │ ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1733 .max_size = constraint.max_size, │ │1734 }, root_focus); │ │1735 │ │1733 - if (!self.session.data.enable_ansi) return; │ │1736 const foreground = self.child.getGrid() orelse return; │ │1737 + // terminals show the art only once they report a background to fade it toward │ │1738 + const backdrop: ?Grid.Color.Rgb = if (self.session.is_terminal) self.session.terminal_background orelse return else null;│ │1739 self.art.content = self.session.data.ansi_art; │ │1736 - try self.buildArt(allocator, foreground.size, root_focus); │ │1737 - if (!self.session.is_terminal) return; │ │1738 - const art_grid = self.art.getGrid() orelse return; │ │1739 - self.grid = try artBehind(allocator, foreground, art_grid); │ │1740 + try self.art.build(allocator, .{ │ │1741 + .min_size = .{ .width = null, .height = null }, │ │1742 + .max_size = .{ .width = foreground.size.width, .height = foreground.size.height }, │ │1743 + }, root_focus); │ │1744 + // the web fades the art with css opacity instead │ │1745 + const art_backdrop = backdrop orelse return; │ │1746 + if (self.art.grid) |*art_grid| { │ │1747 + for (art_grid.cells) |*cell| { │ │1748 + cell.style.fg = fadeColor(cell.style.fg, art_backdrop); │ │1749 + cell.style.bg = fadeColor(cell.style.bg, art_backdrop); │ │1750 + } │ │1751 + self.grid = try artBehind(allocator, foreground, art_grid.*); │ │1752 + } │ │1753 } │ │1754 │ │1755 pub fn input(self: *AnsiBackground, allocator: std.mem.Allocator, key: Key, root_focus: *Focus) !void { │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────╮ │1783 return true; │ │1784 } │ │1785 │ │1773 - // the art is dimmed to this fraction of its brightness so foreground text│ │1774 - // stays legible over it │ │1775 - const art_brightness = 35; // percent │ │1786 + // the art is faded toward the background at this opacity so foreground │ │1787 + // text stays legible over it │ │1788 + pub const art_opacity = 25; // percent │ │1789 │ │1790 // the art parser only emits truecolor, so a palette color never occurs │ │1791 fn artRgb(color: ?Grid.Color) ?Grid.Color.Rgb { │ ╰────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1796 }; │ │1797 } │ │1798 │ │1786 - fn dimColor(color: ?Grid.Color) ?Grid.Color { │ │1799 + fn fadeColor(color: ?Grid.Color, backdrop: Grid.Color.Rgb) ?Grid.Color { │ │1800 const v = artRgb(color) orelse return color; │ │1788 - return .{ .rgb = .{ │ │1789 - .r = @intCast(@as(u16, v.r) * art_brightness / 100), │ │1790 - .g = @intCast(@as(u16, v.g) * art_brightness / 100), │ │1791 - .b = @intCast(@as(u16, v.b) * art_brightness / 100), │ │1792 - } }; │ │1793 - } │ │1794 - │ │1795 - fn buildArt(self: *AnsiBackground, allocator: std.mem.Allocator, size: layout.Size, root_focus: *Focus) !void { │ │1796 - try self.art.build(allocator, .{ │ │1797 - .min_size = .{ .width = null, .height = null }, │ │1798 - .max_size = .{ .width = size.width, .height = size.height }, │ │1799 - }, root_focus); │ │1800 - if (self.art.grid) |*grid| { │ │1801 - for (grid.cells) |*cell| { │ │1802 - cell.style.fg = dimColor(cell.style.fg); │ │1803 - cell.style.bg = dimColor(cell.style.bg); │ │1801 + const blend = struct { │ │1802 + fn f(c: u8, b: u8) u8 { │ │1803 + return @intCast((@as(u16, c) * art_opacity + @as(u16, b) * (100 - art_opacity)) / 100); │ │1804 } │ │1805 - } │ │1805 + }.f; │ │1806 + return .{ .rgb = .{ .r = blend(v.r, backdrop.r), .g = blend(v.g, backdrop.g), .b = blend(v.b, backdrop.b) } };│ │1807 } │ │1808 │ │1809 // terminals that don't support truecolor misparse a "38;2;r;g;b"/"48;2;…" │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ──╮ │src/web.zig│ ╰───────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │144 if (std.mem.eql(u8, path, "/repo/new")) return handleRepoNew(io, request, allocator, path, server.admin_repo_path, server.users_dir, server.session_store); │ │145 // likewise the home page's new user form │ │146 if (std.mem.eql(u8, path, "/user/new")) return handleUserNew(io, request, allocator, path, server.admin_repo_path, server.users_dir, server.session_store); │ │147 - const PostRoute = enum { login, logout, ansi, @"new-repo", @"new-user", new, edit, remove, open, close, resolve, publish, merge, squash, attach, undo, clear };│ │147 + const PostRoute = enum { login, logout, @"new-repo", @"new-user", new, edit, remove, open, close, resolve, publish, merge, squash, attach, undo, clear }; │ │148 inline for (@typeInfo(PostRoute).@"enum".fields) |field| { │ │149 const suffix = "/" ++ field.name; │ │150 if (std.mem.endsWith(u8, path, suffix)) { │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │152 return switch (@field(PostRoute, field.name)) { │ │153 .login => handleLogin(io, request, allocator, base, server.admin_repo_path, server.session_store), │ │154 .logout => handleLogout(request, base, server.session_store), │ │155 - .ansi => handleAnsi(io, request, allocator, base, server.admin_repo_path, server.users_dir, server.session_store), │ │155 .@"new-repo" => handleRepoNew(io, request, allocator, path, server.admin_repo_path, server.users_dir, server.session_store),│ │156 .@"new-user" => handleUserNew(io, request, allocator, path, server.admin_repo_path, server.users_dir, server.session_store),│ │157 .new => handleNew(io, request, allocator, base, host), │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │471 }, │ │472 }); │ │473 } │ │475 - │ │476 -fn handleAnsi( │ │477 - io: std.Io, │ │478 - request: *std.http.Server.Request, │ │479 - allocator: std.mem.Allocator, │ │480 - base: []const u8, │ │481 - admin_repo_path: []const u8, │ │482 - users_dir: []const u8, │ │483 - session_store: SessionStore, │ │484 -) !void { │ │485 - // the toggle re-emits the user's own event, so it takes a logged-in user │ │486 - var user_id: [evt.event_id_size]u8 = undefined; │ │487 - const token = getCookieValue(request, cookie_name) orelse return respondLoginRequired(request);│ │488 - if (!session_store.lookup(token, &user_id)) return respondLoginRequired(request); │ │474 │ │490 - { │ │491 - const Repo = rp.Repo(.xit, evt.admin_repo_opts); │ │492 - var repo = try Repo.open(io, allocator, .{ .path = admin_repo_path }); │ │493 - defer repo.deinit(io, allocator); │ │494 - │ │495 - try evt.User.toggleAnsi(evt.admin_repo_opts, io, allocator, &repo, users_dir, &user_id); │ │496 - } │ │497 - │ │498 - // return to the auth tab the toggle came from so the change is visible. │ │499 - const location = try std.fmt.allocPrint(allocator, "{s}/auth", .{base}); │ │500 - defer allocator.free(location); │ │501 - │ │502 - // like logout, this is a bodyless POST, so close the connection rather than │ │503 - // letting the keep-alive path try to discard a body that isn't framed. │ │504 - try request.respond("", .{ │ │505 - .status = .see_other, │ │506 - .keep_alive = false, │ │507 - .extra_headers = &.{ │ │508 - .{ .name = "location", .value = location }, │ │509 - }, │ │510 - }); │ │511 -} │ │512 - │ │475 // the actor for a write to the repo `repo_base` names with at least `min_role`, │ │476 // or null once the refusal has been sent. local mode always writes. │ │477 fn authorizeWrite( │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1973 switch (root.*) { │ │1974 .background => |*background| { │ │1975 if (background.art.getGrid()) |art| { │ │2014 - var position_buf: [64]u8 = undefined; │ │2015 - try out.appendSlice(allocator, try std.fmt.bufPrint(&position_buf, "<div class=\"ansi-art\" aria-hidden=\"true\" style=\"left:{d}ch\">", .{grid.size.width -| art.size.width})); │ │1976 + try out.print(allocator, "<div class=\"ansi-art\" aria-hidden=\"true\" style=\"left:{d}ch;opacity:{d}%\">", .{ grid.size.width -| art.size.width, ui.widget.AnsiBackground.art_opacity });│ │1977 try renderPanel(allocator, &out, background.art.getFocus(), art, session, root_focus); │ │1978 try out.appendSlice(allocator, "</div>"); │ │1979 } │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯