//! Phone-friendly TUI: human-readable sizes, compact paths, clean layout. const std = @import("std"); const posix = std.posix; const scan_mod = @import("scan.zig"); const classify = @import("classify.zig"); const plan_mod = @import("plan.zig"); const runlog_mod = @import("runlog.zig"); pub const terminal_input_buffer_bytes: u32 = @intCast(@import("spec_data").limit_value.terminal_input_buffer_bytes); pub const Screen = enum { checklist, help, filters, plan, confirmation, executing, report, }; // ── ANSI colors ─────────────────────────────────────────────────────── const Color = struct { const reset = "\x1b[0m"; const bold = "\x1b[1m"; const dim = "\x1b[2m"; const fg = [_][]const u8{ "", "\x1b[31m", "\x1b[32m", "\x1b[33m", "\x1b[34m", "\x1b[35m", "\x1b[36m", "\x1b[37m", "\x1b[90m", "\x1b[91m", "\x1b[92m", "\x1b[93m", "\x1b[94m", "\x1b[95m", "\x1b[96m", "\x1b[97m", }; const bg = [_][]const u8{ "", "\x1b[41m", "\x1b[42m", "\x1b[43m", "\x1b[44m", "\x1b[45m", "\x1b[46m", "\x1b[47m", }; const highlight = "\x1b[37m\x1b[44m"; const sel_on = "\x1b[32m[x]\x1b[0m"; const sel_off = "\x1b[9m[ ]\x1b[0m"; pub fn fgColor(n: usize) []const u8 { return if (n < fg.len) fg[n] else ""; } pub fn bgColor(n: usize) []const u8 { return if (n < bg.len) bg[n] else ""; } }; // ── Human-readable size ─────────────────────────────────────────────── fn formatSize(buf: []u8, bytes: u64) []const u8 { if (bytes < 1024) return std.fmt.bufPrint(buf, "{d}B", .{bytes}) catch "???B"; if (bytes < 1024 * 1024) return std.fmt.bufPrint(buf, "{d:.1}KB", .{@as(f64, @floatFromInt(bytes)) / 1024.0}) catch "???KB"; if (bytes < 1024 * 1024 * 1024) return std.fmt.bufPrint(buf, "{d:.1}MB", .{@as(f64, @floatFromInt(bytes)) / (1024.0 * 1024.0)}) catch "???MB"; return std.fmt.bufPrint(buf, "{d:.1}GB", .{@as(f64, @floatFromInt(bytes)) / (1024.0 * 1024.0 * 1024.0)}) catch "???GB"; } // ── Path truncation: last 2-3 components ────────────────────────────── fn truncatePath(buf: []u8, path: []const u8, max_len: usize) []const u8 { if (path.len <= max_len) { @memcpy(buf[0..path.len], path); return buf[0..path.len]; } var s1: usize = path.len; var s2: usize = path.len; var s3: usize = path.len; var i: usize = path.len; while (i > 0) : (i -= 1) { if (path[i - 1] == '/') { if (s1 == path.len) { s1 = i - 1; } else if (s2 == path.len) { s2 = i - 1; } else if (s3 == path.len) { s3 = i - 1; break; } } } const ellipsis = "..."; const elen = ellipsis.len; if (elen + (path.len - s3) <= max_len) { var w: usize = 0; @memcpy(buf[w..][0..elen], ellipsis); w += elen; @memcpy(buf[w..][0 .. path.len - s3], path[s3..]); w += path.len - s3; return buf[0..w]; } if (elen + (path.len - s2) <= max_len) { var w: usize = 0; @memcpy(buf[w..][0..elen], ellipsis); w += elen; @memcpy(buf[w..][0 .. path.len - s2], path[s2..]); w += path.len - s2; return buf[0..w]; } if (elen + (path.len - s1) <= max_len) { var w: usize = 0; @memcpy(buf[w..][0..elen], ellipsis); w += elen; @memcpy(buf[w..][0 .. path.len - s1], path[s1..]); w += path.len - s1; return buf[0..w]; } if (max_len > 3) { @memcpy(buf[0..3], ellipsis); return buf[0..max_len]; } return buf[0..@min(max_len, path.len)]; } // ── Constants ───────────────────────────────────────────────────────── const MAX_LIST_ROWS: u32 = 12; const PATH_MAX_LEN: usize = 30; // ── Termios / raw mode ──────────────────────────────────────────────── pub const Termios = struct { original: posix.termios, saved: bool = false, pub fn enterRaw(fd: posix.fd_t) !Termios { const state: Termios = .{ .original = try posix.tcgetattr(fd), .saved = true }; var raw = state.original; raw.lflag.ECHO = false; raw.lflag.ICANON = false; raw.lflag.ISIG = false; raw.iflag.IXON = false; raw.iflag.ICRNL = false; raw.cc[@intFromEnum(std.os.linux.V.MIN)] = 1; raw.cc[@intFromEnum(std.os.linux.V.TIME)] = 0; try posix.tcsetattr(fd, .NOW, raw); return state; } pub fn restore(self: *Termios, fd: posix.fd_t) void { if (!self.saved) return; posix.tcsetattr(fd, .NOW, self.original) catch {}; self.saved = false; } }; // ── TUI state ───────────────────────────────────────────────────────── pub const Tui = struct { stdin_fd: posix.fd_t, stdout_fd: posix.fd_t, termios: Termios = .{ .original = undefined }, screen: Screen = .checklist, focus: u32 = 0, confirm: plan_mod.Confirm = .{}, message: []const u8 = "", ascii: bool = false, log_broken: bool = false, plan: ?*const plan_mod.Plan = null, report_ok: u32 = 0, report_failed: u32 = 0, search_open: bool = false, search_query: [64]u8 = undefined, search_len: u32 = 0, visible: [scan_mod.retained_findings_max]u32 = undefined, visible_len: u32 = 0, category_filter: u32 = 0, filters_draft: u32 = 0, selection_filter_selected: bool = false, selection_filter_unselected: bool = false, selection_draft_selected: bool = false, selection_draft_unselected: bool = false, filter_focus: u32 = 0, filter_summary_buffer: [96]u8 = undefined, package_focus: u32 = 0, io: std.Io, pub fn init(io: std.Io, stdin_fd: posix.fd_t, stdout_fd: posix.fd_t) !Tui { return .{ .stdin_fd = stdin_fd, .stdout_fd = stdout_fd, .io = io }; } pub fn enterAlternateScreen(self: *Tui) void { _ = self; writeAll("\x1b[?1049h\x1b[H\x1b[2J"); } pub fn leaveAlternateScreen(self: *Tui) void { _ = self; writeAll("\x1b[?1049l"); } pub fn writeAll(text: []const u8) void { var stdout = std.Io.File.stdout(); stdout.writeStreamingAll(std.Io.Threaded.global_single_threaded.io(), text) catch {}; } pub fn readKey(self: *Tui) ?Key { var byte_buffer: [1]u8 = undefined; const stdin = std.Io.File{ .handle = self.stdin_fd, .flags = .{ .nonblocking = false } }; const read = stdin.readStreaming(self.io, &.{&byte_buffer}) catch return null; if (read == 0) return null; const byte = byte_buffer[0]; if (byte != 0x1B) return .{ .byte = byte }; var escape_probe = [_]posix.pollfd{.{ .fd = self.stdin_fd, .events = posix.POLL.IN, .revents = 0 }}; const pending = posix.poll(&escape_probe, 25) catch return .{ .byte = 0x1B }; if (pending == 0) return .{ .byte = 0x1B }; const sequence = stdin.readStreaming(self.io, &.{&byte_buffer}) catch return null; if (sequence == 0) return .{ .byte = 0x1B }; if (byte_buffer[0] != '[') return .{ .byte = byte_buffer[0] }; const final = stdin.readStreaming(self.io, &.{&byte_buffer}) catch return null; if (final == 0) return null; return switch (byte_buffer[0]) { 'A' => .up, 'B' => .down, 'C' => .right, 'D' => .left, else => .{ .byte = byte_buffer[0] }, }; } pub fn render(self: *Tui, scan: *const scan_mod.Scan, paths: [][]const u8) void { var out: [16384]u8 = undefined; var writer = std.Io.Writer.fixed(&out); writer.writeAll("\x1b[H\x1b[2J") catch return; switch (self.screen) { .checklist => self.renderChecklist(scan, paths, &writer), .help => self.renderHelp(scan, &writer), .filters => self.renderFilters(&writer), .confirmation => self.renderConfirmation(scan, paths, &writer), .report => self.renderReport(&writer), .executing, .plan => self.renderChecklist(scan, paths, &writer), } self.writeFrame(out[0..writer.end]); } fn renderChecklist(self: *Tui, scan: *const scan_mod.Scan, paths: [][]const u8, writer: *std.Io.Writer) void { self.rebuildVisibility(scan, paths); if (self.visible_len == 0) self.focus = 0; if (self.visible_len > 0 and self.focus >= self.visible_len) self.focus = self.visible_len - 1; var selected_count: u32 = 0; var unavailable_count: u32 = 0; var hidden_count: u32 = 0; var counted: u32 = 0; while (counted < scan.findings_len) : (counted += 1) { const f = scan.findings[counted]; if (!f.selected) continue; selected_count += 1; const ineligible = f.kind != .file and f.kind != .symlink and f.kind != .directory; if (self.log_broken or ineligible) unavailable_count += 1; if (!self.visibleContains(counted)) hidden_count += 1; } var pkg_selected: u32 = 0; counted = 0; while (counted < scan.package_records_len) : (counted += 1) { if (scan.package_records[counted].available) pkg_selected += 1; } // Header writer.writeAll(Color.bold) catch return; writer.writeAll(" Termux Janitor") catch return; writer.writeAll(Color.reset) catch return; writer.writeAll("\n") catch return; // Status writer.writeAll(Color.dim) catch return; var status_buf: [120]u8 = undefined; const status = std.fmt.bufPrint(&status_buf, " {d} findings {d} selected {d} hidden {d} unavail", .{ scan.findings_len, selected_count, hidden_count, unavailable_count }) catch " ---"; writer.writeAll(status) catch return; if (scan.package_records_len > 0) { writer.writeAll(" ") catch return; writer.print("{d} pkgs", .{pkg_selected}) catch return; } if (!scan.complete) { writer.writeAll(" ") catch return; writer.writeAll(Color.fgColor(8)) catch return; writer.writeAll("incomplete") catch return; writer.writeAll(Color.reset) catch return; } writer.writeAll(Color.reset) catch return; writer.writeAll("\n") catch return; // List var rows: u32 = 2; if (self.visible_len == 0) { writer.writeAll(Color.dim) catch return; writer.writeAll(" ( no matches )") catch return; writer.writeAll(Color.reset) catch return; writer.writeAll("\n") catch return; rows += 1; } var position: u32 = self.focus; var shown: u32 = 0; while (shown < MAX_LIST_ROWS and position < self.visible_len) : (shown += 1) { const idx = self.visible[position]; const finding = scan.findings[idx]; const path = paths[idx]; const is_selected = finding.selected; const is_focus = (position == self.focus); const cursor: []const u8 = if (is_focus) ">" else " "; const cat: classify.Category = @enumFromInt(finding.category); const cat_label = classify.Category.label(cat); const col = categoryColor(cat); var path_buf: [64]u8 = undefined; const disp_path = truncatePath(&path_buf, path, PATH_MAX_LEN); var size_buf: [16]u8 = undefined; const size_str = formatSize(&size_buf, if (finding.blocks_known) finding.allocated_bytes else 0); writer.writeAll(" ") catch break; if (is_focus) { writer.writeAll(Color.highlight) catch break; writer.writeAll(cursor) catch break; writer.writeAll(Color.reset) catch break; } else { writer.writeAll(cursor) catch break; } if (is_selected) { writer.writeAll(Color.sel_on) catch break; } else { writer.writeAll(Color.sel_off) catch break; } writer.writeAll(" ") catch break; writer.writeAll(Color.fgColor(col)) catch break; writer.writeAll(cat_label) catch break; writer.writeAll(Color.reset) catch break; writer.writeAll(" ") catch break; writer.writeAll(disp_path) catch break; writer.writeAll(" ") catch break; writer.writeAll(Color.fgColor(col)) catch break; writer.writeAll(size_str) catch break; writer.writeAll(Color.reset) catch break; writer.writeAll("\n") catch break; rows += 1; position += 1; } // Packages if (rows < 2 + MAX_LIST_ROWS and scan.package_records_len > 0 and self.visible_len == 0) { writer.writeAll(Color.dim) catch return; writer.writeAll(" Packages:\n") catch return; writer.writeAll(Color.reset) catch return; var pkg_pos: u32 = 0; while (pkg_pos < scan.package_records_len and rows < 2 + MAX_LIST_ROWS) : (pkg_pos += 1) { const pkg = scan.package_records[pkg_pos]; const name = pkg.name(scan.package_name_pool[0..]); const is_focus = (pkg_pos == self.package_focus); const cursor: []const u8 = if (is_focus) ">" else " "; const state_label: []const u8 = switch (pkg.state) { .active => "active", .orphan => "orphan", .obsolete => "obsolete", .cache_removable => "cache", else => "???", }; const col: usize = switch (pkg.state) { .active => 2, .orphan => 1, .obsolete => 8, .cache_removable => 3, else => 8, }; writer.writeAll(" ") catch break; if (is_focus) { writer.writeAll(Color.highlight) catch break; writer.writeAll(cursor) catch break; writer.writeAll(Color.reset) catch break; } else { writer.writeAll(cursor) catch break; } writer.writeAll(Color.fgColor(col)) catch break; writer.writeAll(state_label) catch break; writer.writeAll(Color.reset) catch break; writer.writeAll(" ") catch break; writer.writeAll(name) catch break; writer.writeAll("\n") catch break; rows += 1; } } // Footer writer.writeAll(Color.dim) catch return; writer.writeAll(" j/k:nav space:select /:search c:confirm q:quit f:filters") catch return; writer.writeAll(Color.reset) catch return; writer.writeAll("\n") catch return; if (self.search_open) { const query = self.search_query[0..self.search_len]; writer.writeAll(Color.fgColor(5)) catch return; writer.print(" search: {s}_", .{query}) catch return; writer.writeAll(Color.reset) catch return; } else if (self.search_len > 0 or self.category_filter != 0 or self.selection_filter_selected or self.selection_filter_unselected) { const parts = self.filterSummary(); writer.writeAll(Color.fgColor(6)) catch return; writer.print(" filter: {s}", .{parts}) catch return; writer.writeAll(Color.reset) catch return; } else { writer.writeAll(Color.dim) catch return; writer.writeAll(" ? help") catch return; writer.writeAll(Color.reset) catch return; } writer.writeAll("\n") catch return; } fn renderHelp(_: *Tui, scan: *const scan_mod.Scan, writer: *std.Io.Writer) void { writer.writeAll(Color.bold) catch return; writer.writeAll(" Help\n") catch return; writer.writeAll(Color.reset) catch return; writer.writeAll("\n") catch return; writer.writeAll(" Navigation:\n") catch return; writer.writeAll(Color.fgColor(6)) catch return; writer.writeAll(" j / k / up / down Move cursor\n") catch return; writer.writeAll(Color.reset) catch return; writer.writeAll("\n Actions:\n") catch return; writer.writeAll(Color.fgColor(6)) catch return; writer.writeAll(" space Toggle selection\n") catch return; writer.writeAll(" / Start search\n") catch return; writer.writeAll(" c Confirm & execute\n") catch return; writer.writeAll(" q Quit\n") catch return; writer.writeAll(Color.reset) catch return; writer.writeAll("\n Filters:\n") catch return; writer.writeAll(Color.fgColor(6)) catch return; writer.writeAll(" f Open filter overlay\n") catch return; writer.writeAll(" + Select visible only\n") catch return; writer.writeAll(" - Clear filters\n") catch return; writer.writeAll(Color.reset) catch return; writer.writeAll("\n Categories:\n") catch return; writer.writeAll(" green = cache/temp/download\n") catch return; writer.writeAll(" red = backup/log/lock/broken\n") catch return; writer.writeAll(" blue = vcs/source\n") catch return; writer.writeAll(" magenta = large files\n") catch return; writer.writeAll(" cyan = config/document\n") catch return; writer.writeAll("\n") catch return; writer.writeAll(Color.dim) catch return; writer.print(" {d} findings {d} warnings", .{ scan.findings_len, scan.warnings_len }) catch return; writer.writeAll(Color.reset) catch return; writer.writeAll("\n ? close q quit\n") catch return; } fn renderFilters(self: *Tui, writer: *std.Io.Writer) void { writer.writeAll(Color.bold) catch return; writer.writeAll(" Filters\n") catch return; writer.writeAll(Color.reset) catch return; writer.writeAll("\n") catch return; const categories = @typeInfo(classify.Category).@"enum".fields; var rows: u32 = 0; inline for (categories, 0..) |field, index| { if (rows >= MAX_LIST_ROWS) break; const bit = @as(u32, 1) << @intCast(index); const active = self.filters_draft & bit != 0; const cursor: []const u8 = if (self.filter_focus == index) ">" else " "; const col = categoryColor(@enumFromInt(index)); writer.writeAll(" ") catch break; if (self.filter_focus == index) { writer.writeAll(Color.highlight) catch break; writer.writeAll(cursor) catch break; writer.writeAll(Color.reset) catch break; } else { writer.writeAll(cursor) catch break; } if (active) { writer.writeAll(Color.fgColor(1)) catch break; writer.writeAll("[x]") catch break; } else { writer.writeAll(Color.fgColor(8)) catch break; writer.writeAll("[ ]") catch break; } writer.writeAll(Color.reset) catch break; writer.writeAll(" ") catch break; writer.writeAll(Color.fgColor(col)) catch break; writer.writeAll(field.name) catch break; writer.writeAll(Color.reset) catch break; writer.writeAll("\n") catch break; rows += 1; } const sel_base: u32 = categories.len; writer.writeAll("\n") catch return; const cursor_sel: []const u8 = if (self.filter_focus == sel_base) ">" else " "; const cursor_unsel: []const u8 = if (self.filter_focus == sel_base + 1) ">" else " "; writer.writeAll(" ") catch return; if (self.filter_focus == sel_base) { writer.writeAll(Color.highlight) catch return; writer.writeAll(cursor_sel) catch return; writer.writeAll(Color.reset) catch return; } else { writer.writeAll(cursor_sel) catch return; } if (self.selection_draft_selected) { writer.writeAll(Color.fgColor(1)) catch return; writer.writeAll("[x]") catch return; } else { writer.writeAll(Color.fgColor(8)) catch return; writer.writeAll("[ ]") catch return; } writer.writeAll(Color.reset) catch return; writer.writeAll(" selected\n") catch return; writer.writeAll(" ") catch return; if (self.filter_focus == sel_base + 1) { writer.writeAll(Color.highlight) catch return; writer.writeAll(cursor_unsel) catch return; writer.writeAll(Color.reset) catch return; } else { writer.writeAll(cursor_unsel) catch return; } if (self.selection_draft_unselected) { writer.writeAll(Color.fgColor(1)) catch return; writer.writeAll("[x]") catch return; } else { writer.writeAll(Color.fgColor(8)) catch return; writer.writeAll("[ ]") catch return; } writer.writeAll(Color.reset) catch return; writer.writeAll(" unselected\n") catch return; writer.writeAll("\n") catch return; writer.writeAll(Color.dim) catch return; writer.writeAll(" space:toggle Enter:apply Esc:cancel") catch return; writer.writeAll(Color.reset) catch return; } fn renderConfirmation(self: *Tui, scan: *const scan_mod.Scan, paths: [][]const u8, writer: *std.Io.Writer) void { writer.writeAll(Color.bold) catch return; writer.writeAll(" Confirm Plan\n") catch return; writer.writeAll(Color.reset) catch return; writer.writeAll("\n") catch return; var selected_count: u32 = 0; var unavailable_count: u32 = 0; var counted: u32 = 0; while (counted < scan.findings_len) : (counted += 1) { const f = scan.findings[counted]; if (!f.selected) continue; selected_count += 1; const ineligible = f.kind != .file and f.kind != .symlink and f.kind != .directory; if (self.log_broken or ineligible) unavailable_count += 1; } writer.writeAll(" Actions: ") catch return; writer.writeAll(Color.fgColor(15)) catch return; writer.print("{d}", .{selected_count}) catch return; writer.writeAll(Color.reset) catch return; if (unavailable_count > 0) { writer.writeAll(" (") catch return; writer.writeAll(Color.fgColor(1)) catch return; writer.print("{d} unavailable", .{unavailable_count}) catch return; writer.writeAll(Color.reset) catch return; writer.writeAll(")") catch return; } writer.writeAll("\n\n") catch return; if (self.plan) |plan| { var index: u32 = 0; var rows: u32 = 0; while (index < plan.actions_len and rows < MAX_LIST_ROWS) : (index += 1) { const action = plan.actions[index]; const is_complete = action.complete; const status: []const u8 = if (is_complete) "x" else "!"; const col: usize = if (is_complete) 2 else 1; const kind_label: []const u8 = switch (action.kind) { .file => "file", .symlink => "link", .directory => "dir", .package_purge => "purge", .package_clean => "clean", }; var label_buf: [128]u8 = undefined; const label: []const u8 = switch (action.kind) { .file, .symlink, .directory => blk: { break :blk truncatePath(&label_buf, paths[action.finding], 40); }, .package_purge => blk: { if (action.finding < scan.package_records_len) { const pkg = scan.package_records[action.finding]; const name = pkg.name(scan.package_name_pool[0..]); @memcpy(label_buf[0..name.len], name); break :blk label_buf[0..name.len]; } break :blk "?"; }, .package_clean => "apt clean (cache)", }; writer.writeAll(" ") catch break; writer.writeAll(Color.fgColor(col)) catch break; writer.writeAll(status) catch break; writer.writeAll(Color.reset) catch break; writer.writeAll(" [") catch break; writer.writeAll(Color.fgColor(6)) catch break; writer.writeAll(kind_label) catch break; writer.writeAll(Color.reset) catch break; writer.writeAll("] ") catch break; writer.writeAll(label) catch break; writer.writeAll("\n") catch break; rows += 1; } } writer.writeAll("\n") catch return; writer.writeAll(Color.dim) catch return; writer.writeAll(" This will delete selected items permanently.\n") catch return; writer.writeAll(Color.reset) catch return; writer.writeAll("\n confirm: ") catch return; writer.writeAll(Color.fgColor(5)) catch return; writer.writeAll(self.confirm.text()) catch return; writer.writeAll(Color.reset) catch return; writer.writeAll(" ") catch return; writer.writeAll(Color.dim) catch return; writer.writeAll("Enter:accept Esc:cancel") catch return; writer.writeAll(Color.reset) catch return; } fn renderReport(self: *Tui, writer: *std.Io.Writer) void { writer.writeAll(Color.bold) catch return; writer.writeAll(" Results\n") catch return; writer.writeAll(Color.reset) catch return; writer.writeAll("\n") catch return; writer.writeAll(" ") catch return; writer.writeAll(Color.fgColor(2)) catch return; writer.writeAll("x ") catch return; writer.writeAll(Color.reset) catch return; writer.writeAll(" Success: ") catch return; writer.writeAll(Color.fgColor(2)) catch return; writer.print("{d}", .{self.report_ok}) catch return; writer.writeAll(Color.reset) catch return; writer.writeAll("\n") catch return; writer.writeAll(" ") catch return; writer.writeAll(Color.fgColor(1)) catch return; writer.writeAll("! ") catch return; writer.writeAll(Color.reset) catch return; writer.writeAll(" Failed: ") catch return; writer.writeAll(Color.fgColor(1)) catch return; writer.print("{d}", .{self.report_failed}) catch return; writer.writeAll(Color.reset) catch return; writer.writeAll("\n") catch return; if (self.message.len > 0) { writer.writeAll(" ") catch return; writer.writeAll(Color.fgColor(3)) catch return; writer.writeAll(self.message) catch return; writer.writeAll(Color.reset) catch return; writer.writeAll("\n") catch return; } writer.writeAll("\n") catch return; writer.writeAll(Color.dim) catch return; writer.writeAll(" q quit") catch return; writer.writeAll(Color.reset) catch return; } pub fn rebuildVisibility(self: *Tui, scan: *const scan_mod.Scan, paths: [][]const u8) void { const query = self.search_query[0..self.search_len]; const category_active = self.category_filter != 0; const selection_active = self.selection_filter_selected or self.selection_filter_unselected; self.visible_len = 0; var index: u32 = 0; while (index < scan.findings_len) : (index += 1) { if (category_active and self.category_filter & (@as(u32, 1) << @intCast(scan.findings[index].category)) == 0) continue; if (selection_active) { const want_selected = if (scan.findings[index].selected) self.selection_filter_selected else self.selection_filter_unselected; if (!want_selected) continue; } if (query.len > 0 and std.mem.indexOf(u8, scan.findings[index].name, query) == null) continue; if (self.visible_len >= self.visible.len) break; self.visible[self.visible_len] = index; self.visible_len += 1; } const Order = struct { scan: *const scan_mod.Scan, paths: [][]const u8, fn sizeKnown(finding: scan_mod.Finding) bool { return finding.blocks_known and finding.allocated_bytes != scan_mod.unknown_size; } fn lessThan(ctx: @This(), a: u32, b: u32) bool { const first = ctx.scan.findings[a]; const second = ctx.scan.findings[b]; const first_known = sizeKnown(first); const second_known = sizeKnown(second); if (first_known != second_known) return first_known; if (first_known and first.allocated_bytes != second.allocated_bytes) { return first.allocated_bytes > second.allocated_bytes; } if (first.category != second.category) return first.category < second.category; return std.mem.order(u8, ctx.paths[a], ctx.paths[b]) == .lt; } }; std.mem.sort(u32, self.visible[0..self.visible_len], Order{ .scan = scan, .paths = paths }, Order.lessThan); } fn filterSummary(self: *Tui) []const u8 { var buffer: [96]u8 = undefined; var writer = std.Io.Writer.fixed(&buffer); if (self.category_filter != 0) { writer.print("category#{x:0>8}", .{self.category_filter}) catch {}; } if (self.selection_filter_selected) { if (writer.end > 0) writer.writeAll("+") catch {}; writer.writeAll("sel") catch {}; } if (self.selection_filter_unselected) { if (writer.end > 0) writer.writeAll("+") catch {}; writer.writeAll("unsel") catch {}; } const written = buffer[0..writer.end]; const stored = self.filter_summary_buffer[0..written.len]; @memcpy(stored, written); return stored; } fn visibleContains(self: *Tui, finding: u32) bool { var position: u32 = 0; while (position < self.visible_len) : (position += 1) { if (self.visible[position] == finding) return true; } return false; } fn writeFrame(self: *Tui, bytes: []const u8) void { _ = self; var stdout = std.Io.File.stdout(); stdout.writeStreamingAll(std.Io.Threaded.global_single_threaded.io(), bytes) catch {}; } }; fn categoryColor(cat: classify.Category) usize { return switch (cat) { .stale_temporary, .heuristic_temporary => 2, .broken_symlink => 1, .rotated_log, .live_log => 1, .lock_file => 1, .other => 8, .download => 2, .workspace => 5, .source => 4, .document => 6, .trash => 8, }; } pub const Key = union(enum) { byte: u8, up, down, left, right, };