diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-21 22:37:36 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-10-01 00:12:14 -0300 |
| commit | 297e14cfc36e4613a8c1cb3b995597d0a9c2873b (patch) | |
| tree | ad2ff7bd7a5869b2e45fe6098825a8ac35598703 | |
| parent | 9070942b29bd10dddcdecdb0e88ba0fb40608467 (diff) | |
| parent | 44ce573e15c773cf5bb0d42941a143b63e44d900 (diff) | |
| download | pardes-297e14cfc36e4613a8c1cb3b995597d0a9c2873b.tar.gz pardes-297e14cfc36e4613a8c1cb3b995597d0a9c2873b.zip | |
Merge the macOS first-class-host work
33 files changed, 1160 insertions, 377 deletions
@@ -301,6 +301,9 @@ pub fn build(b: *std.Build) void { const esp32p4_cols = b.option(u16, "esp32p4-cols", "the board's grid width in cells (-Dplatform=esp32p4 only)") orelse 56; const esp32p4_rows = b.option(u16, "esp32p4-rows", "the board's grid height in cells (-Dplatform=esp32p4 only)") orelse 14; const theme_animation = b.option(bool, "theme-animation", "animate chrome colors across a theme change (default: off for esp32p4)"); + // The macOS shell carries the workspace tag row's commands in its native + // menu bar; the row itself goes away. `-Dworkspace-tag=true` puts it back. + const workspace_tag = b.option(bool, "workspace-tag", "draw the workspace tag row (default: on except on macOS, whose menu bar carries its commands)"); const shell_cfg: ShellConfig = .{ .tree_sitter_grammars = tree_sitter_grammars, .tracy = tracy != null, @@ -309,6 +312,7 @@ pub fn build(b: *std.Build) void { .esp32p4_cols = esp32p4_cols, .esp32p4_rows = esp32p4_rows, .theme_animation = theme_animation, + .workspace_tag = workspace_tag, .prebuilt_shaders = prebuilt_shaders, .embed_sources = embed_sources, .zig_lib_dir = b.graph.zig_lib_directory.path orelse "", @@ -1434,6 +1438,9 @@ const ShellConfig = struct { /// null = "the platform's own default", resolved per shell below, because /// the default is off for the board and on everywhere else. theme_animation: ?bool, + /// null = "the platform's own default": the macOS shell hands the row to + /// its native menu bar, every other shell keeps the acme topbar. + workspace_tag: ?bool, prebuilt_shaders: bool, embed_sources: bool, zig_lib_dir: []const u8, @@ -1452,6 +1459,7 @@ fn shellOptions(b: *std.Build, cfg: ShellConfig, platform: Platform) *std.Build. o.addOption(bool, "mupdf", cfg.mupdf); o.addOption(u16, "esp32p4_cols", cfg.esp32p4_cols); o.addOption(u16, "esp32p4_rows", cfg.esp32p4_rows); + o.addOption(bool, "workspace_tag", cfg.workspace_tag orelse (platform != .macos)); o.addOption(bool, "theme_animation", cfg.theme_animation orelse (platform != .esp32p4)); // Meaningful only for the SDL shell. Keeping the platform condition here // prevents Config/EffectCode from describing tty/macOS/web as "prebuilt". diff --git a/docs/config.md b/docs/config.md index 04b0fd82..e2b95e21 100644 --- a/docs/config.md +++ b/docs/config.md @@ -548,6 +548,7 @@ including the standard `-Dtarget` and `-Doptimize` options. | `-Djpx` | bool | `true` — JPEG 2000, and with it scanned PDFs | | `-Dtree-sitter` | `disabled`, `zig`, `minimal`, `full` | `full` natively, `zig` for web, `disabled` for esp32p4 | | `-Dtheme-animation` | bool | on everywhere except `-Dplatform=esp32p4` | +| `-Dworkspace-tag` | bool; draw the workspace tag row — the macOS shell hands its commands to the native menu bar instead | on except on `-Dplatform=macos` | | `-Dprebuilt-shaders` | bool | on for a bare `zig build`, off when `-Dplatform` names a shell | | `-Dtracy` | path to a Tracy source checkout | off | | `-Dmacos-identity` | codesigning identity for `pardes.app` | `-` (ad-hoc) | diff --git a/docs/macos.md b/docs/macos.md index 9190ca67..e104d3a6 100644 --- a/docs/macos.md +++ b/docs/macos.md @@ -369,6 +369,14 @@ second spelling of builtins — `AppDelegate` calls `run("New")`, `run("Save")`, `run("Help")`, `run("Tutor")` — and the items with no builtin behind them are left out rather than stubbed. +The workspace tag row — the topmost tagline, the one carrying `Newcol Joincol +Find Grep …` — is not drawn on this shell. `-Dworkspace-tag` (default off for +`-Dplatform=macos`, on everywhere else) hands the row to the menu bar: the +core stops reserving the row (`Pardes.topBarHeight`), the grid starts at the +column tags, and the row's commands live in a **Builtins** menu instead. The +items are the tag's own words, spelled exactly as a tag Exec would type them, +and nothing in that menu is editable — it is the tag's buttons, not the tag: + | menu | item | chord | what it does | |---|---|---|---| | pardes | About pardes | — | AppKit's panel | @@ -378,6 +386,7 @@ left out rather than stubbed. | | New | ⌘N | the `New` builtin | | | Save | ⌘S | the `Save` builtin | | | Close Window | ⌘W | AppKit; the app terminates after the last one | +| Builtins | Newcol / Joincol / Find / Grep / Changelog / Dump / NextColor / Debug / Kill | — | the workspace tag's own builtins, `run(word)` | | Edit | Paste | ⌘V | the view's own paste path, not a second one | | View | Zoom In / Zoom Out / Actual Size | ⌘= / ⌘- / ⌘0 | point size, host-side | | Window | Minimize / Zoom | ⌘M / — | AppKit | @@ -455,6 +464,32 @@ Nested launches use the same inherited 9P address and pane serial as other native hosts. They do not inspect ancestor processes or executable names. See [the control filesystem](fs.md) for paths and transports. +Two more things this host cannot inherit from its launcher, because a `.app` +has none: + +- **The terminal's identity.** `host_io.ChildEnv` writes `TERM`, `COLORTERM` + and `TERM_PROGRAM` over whatever the environment carried and the child is + `execve`'d with that array — built before the fork, because a `setenv` + between fork and exec can deadlock on the heap a pty reader thread was + holding. Every pane is emulated by the bundled VT, so the value describes + pardes and never the terminal pardes was started from; `-Dplatform=gui` and + the tty shell take the same path, where the difference is only that their + launcher usually happened to set something. The names come from + `config.child_term` / `child_colorterm` / `child_term_program`, and `TERM` + is deliberately `xterm-256color` rather than a name with no installed + terminfo entry — `clear`, colour and cursor addressing all resolve through + that lookup. +- **Whether a program has the terminal.** `host_io.ttyTaken` is what + `Pardes.takesCommandLine` asks before deciding that Escape is the `Last` + builtin rather than a keystroke for the child, and what `Exec` asks before + believing a pane is at its prompt. Linux descends `/proc/<pid>/task/<pid>/ + children`; darwin has neither that nor a children list, so it enumerates the + tty's foreground process group with `proc_listpids(PROC_PGRP_ONLY)` and + compares each member's `proc_pidpath` against the shell's own. A nested + interactive shell is therefore still a prompt, and `bash -c 'sleep 30'` is + still taken — the wrapper wears the shell's binary, but `sleep` shares its + group. The occupancy suite in `src/host_io.zig` runs on both platforms. + ## Fonts and zoom The face is the shell's business and the size is the window's, so the two are @@ -1109,9 +1144,6 @@ used for the table and is not folded into the direct-path claim. says nothing either. Wiring it up means a unix-socket frontend loop beside the AppKit one, which is `src/detached/client.zig`'s job in the tty and SDL shells; see `docs/detached.md`. -- **`tty_taken` on darwin.** The pull is wired (`ttyTaken`, `src/macos.zig:1838`) - but `look.ttyTaken` has no libproc implementation and answers `false`, so an - `Exec` here always believes the pane is still at the prompt it forked. - **IME and marked text.** Only finished characters reach `pardes_key`, so a dead key composes nothing and Option is Alt rather than a compose modifier. Real composition means implementing `NSTextInputClient` *and* giving the core diff --git a/src/CHANGELOG.md b/src/CHANGELOG.md index 18bcceb9..e518f97a 100644 --- a/src/CHANGELOG.md +++ b/src/CHANGELOG.md @@ -2,6 +2,18 @@ ## 0.0.3 +- Give pty children their own terminal identity. `TERM`, `COLORTERM` and + `TERM_PROGRAM` are now written by pardes rather than inherited from whatever + launched it, and the child is exec'd with that environment. A macOS `.app` + launch supplies no `TERM` at all, so until now `clear` failed, nothing + painted a colour, and every cursor-addressing program fell back to printing + lines. +- Answer tty occupancy on macOS. `ttyTaken` had no darwin implementation and + said `false` for every pane, which made `Pardes.takesCommandLine` permanently + true: a bare Escape in a raw terminal ran the `Last` builtin instead of + reaching the child, and `Exec` always believed the pane sat at its prompt. + libproc answers it from the tty's foreground process group, and the + occupancy suite now runs on both platforms instead of only Linux. - Flatten the 9P control tree: `self/` is gone, so `/index`, `/pane/<n>/`, `/screen` and `/os` sit at the root. `/new` is one clone file (reading it makes a pane and answers its serial), `/ctl` takes `look TEXT` and diff --git a/src/config.zig b/src/config.zig index 3169b20c..81fefd2c 100644 --- a/src/config.zig +++ b/src/config.zig @@ -223,6 +223,17 @@ pub const tty_blank: enum { prompt, prompt_and_input } = .prompt; // Projected colors below this contrast use the theme's more legible anchor. pub const tty_filter_min_contrast: f64 = 1.5; +// What a pty child is told it is talking to. Every pane is emulated by the +// bundled VT, so this describes pardes and never the terminal pardes itself +// was launched from — the child inherits the launcher's TERM otherwise, which +// under a macOS .app launch is no TERM at all, and `clear`, colour and every +// cursor-addressing program fail at the terminfo lookup. There is no installed +// `xterm-ghostty` entry to name, and a name with no entry is worse than a +// conservative one that every system ships. +pub const child_term = "xterm-256color"; +pub const child_colorterm = "truecolor"; +pub const child_term_program = "pardes"; + pub const topbar_str = "Newcol Joincol Find Grep Help Changelog Tutor Dump NextColor Debug Kill"; pub const pane_builtins_str = "Tty Collapse Del"; @@ -539,6 +550,10 @@ pub const Runtime = struct { tree_context_tag_style: bool = true, debug: bool = false, focus_tint: bool = true, + /// The workspace tag row. Native shells with their own chrome (the macOS + /// menu bar) turn it off and carry its commands there instead; the core + /// keeps the row as the acme tradition everywhere else. + workspace_tag: bool = true, column_tags: bool = true, syntax_bold: bool = false, /// Builtins announce themselves on the message row. diff --git a/src/gui/gui.zig b/src/gui/gui.zig index 27f3346c..d871e55e 100644 --- a/src/gui/gui.zig +++ b/src/gui/gui.zig @@ -5290,10 +5290,12 @@ test "tagline bands face the topbar rule and Tagbottom faces the window edge" { const canvas_h: f32 = 200; const border = topbarPaneBorderPixels(cell_h, tagline_h); try std.testing.expectEqual(@as(u32, 1), border); - try std.testing.expectEqual(@as(u32, 4), taglineBandOffset(0, canvas_h, cell_h, tagline_h)); - try std.testing.expectEqual(@as(u32, 1), taglineBandOffset(1, canvas_h, cell_h, tagline_h)); - try std.testing.expectEqual(@as(u32, 2), taglineBandOffset(5, canvas_h, cell_h, tagline_h)); - try std.testing.expectEqual(@as(u32, 4), taglineBandOffset(9, canvas_h, cell_h, tagline_h)); + try std.testing.expectEqual(@as(u32, 4), taglineBandOffset(0, canvas_h, cell_h, tagline_h, true)); + try std.testing.expectEqual(@as(u32, 2), taglineBandOffset(0, canvas_h, cell_h, tagline_h, false)); + try std.testing.expectEqual(@as(u32, 1), taglineBandOffset(1, canvas_h, cell_h, tagline_h, true)); + try std.testing.expectEqual(@as(u32, 2), taglineBandOffset(1, canvas_h, cell_h, tagline_h, false)); + try std.testing.expectEqual(@as(u32, 2), taglineBandOffset(5, canvas_h, cell_h, tagline_h, true)); + try std.testing.expectEqual(@as(u32, 4), taglineBandOffset(9, canvas_h, cell_h, tagline_h, true)); } fn resolveCell(g: *Gui, cell: *const pardes.Cell, role: pardes.FontRole, is_cursor: bool, page: Ground) ResolvedCell { @@ -5486,7 +5488,7 @@ fn emitBodyLayer(g: *Gui, instances: [*]CellInstance, next: *u32, layer: *const if (left + @as(f32, @floatFromInt(col)) * width >= right) break; const cell = &layer.cells[index * layer.cols + col]; const cursor = if (layer.cursor) |cu| show_cursor and !cu.bar and cu.x == col and cu.y == row else false; - const glyph_top = top - (if (header) @as(f32, @floatFromInt(taglineBandOffset(0, win_h, g.cell_h, g.tagline_height))) else 0); + const glyph_top = top - (if (header) @as(f32, @floatFromInt(taglineBandOffset(0, win_h, g.cell_h, g.tagline_height, true))) else 0); emitInstance(g, instances, next.*, 0, 0, .{ .w = width, .h = height, .x_off = left + @as(f32, @floatFromInt(col)) * width, .y_off = glyph_top }, win_w, win_h, track, role, cell, old_layer, cursor, page); clipBodyInstance(&instances[next.*], bottom, win_h); clipInstanceRight(&instances[next.*], right, win_w); @@ -5638,7 +5640,7 @@ fn emitInstance( else layout.h; const band_offset: f32 = if (resolved.role == .tagline) - @floatFromInt(taglineBandOffset(row, win_h, g.cell_h, g.tagline_height)) + @floatFromInt(taglineBandOffset(row, win_h, g.cell_h, g.tagline_height, true)) else 0; const py0 = layout.y_off + @as(f32, @floatFromInt(row)) * layout.h + band_offset; @@ -6448,7 +6450,7 @@ fn addCursorBar( const x0 = layout.x_off + @as(f32, @floatFromInt(x)) * layout.w; const height = std.math.clamp(visual_height, 1.0, layout.h); const y0 = layout.y_off + @as(f32, @floatFromInt(y)) * layout.h + - @as(f32, @floatFromInt(taglineBandOffset(y, builder.win_h, @intFromFloat(layout.h), @intFromFloat(height)))); + @as(f32, @floatFromInt(taglineBandOffset(y, builder.win_h, @intFromFloat(layout.h), @intFromFloat(height), true))); builder.addRect( x0, y0, @@ -6548,7 +6550,7 @@ fn buildOverlay( const right = left + @as(f32, @floatFromInt(layer.viewport.w)) * layout.w; const x = left + @as(f32, @floatFromInt(cursor.col)) * width; if (x >= right) continue; - const top = @as(f32, @floatFromInt(layer.viewport.y)) * layout.h + @as(f32, @floatFromInt(taglineBandOffset(layer.viewport.y, builder.win_h, g.cell_h, g.tagline_height))); + const top = @as(f32, @floatFromInt(layer.viewport.y)) * layout.h + @as(f32, @floatFromInt(taglineBandOffset(layer.viewport.y, builder.win_h, g.cell_h, g.tagline_height, true))); builder.addRect(x, top, @min(right, x + @max(1, width / 8)), top + @as(f32, @floatFromInt(g.tagline_height)), overlayRgb(cursorColor(core, &layer.cells[cursor.col], .tagline))); }; if (transient_on) appendTouchOverlay(g, &builder, debug_on); @@ -6569,7 +6571,7 @@ fn appendWorkspacePet(g: *Gui, core: ?*const pardes.Pardes, surface: *const pard if (cellCodepoint(cell) != ' ') occupied = i + 1; } const editing = p.topbar_col != null and p.header_column == null; - const band_top: f32 = @floatFromInt(taglineBandOffset(0, builder.win_h, g.cell_h, g.tagline_height)); + const band_top: f32 = @floatFromInt(taglineBandOffset(0, builder.win_h, g.cell_h, g.tagline_height, true)); const region = if (occupied >= surface.cols) null else pet.lane( occupied, @floatFromInt(g.tagline_width), @@ -6741,7 +6743,7 @@ fn appendTagGrips(g: *const Gui, surface: *const pardes.Surface, layout: CellLay else => continue, }; const center_x = (@as(f32, @floatFromInt(x)) + @as(f32, @floatFromInt(config.GUTTER)) / 2) * layout.w; - const center_y = @as(f32, @floatFromInt(y)) * layout.h + @as(f32, @floatFromInt(taglineBandOffset(y, builder.win_h, g.cell_h, g.tagline_height))) + @as(f32, @floatFromInt(g.tagline_height)) / 2; + const center_y = @as(f32, @floatFromInt(y)) * layout.h + @as(f32, @floatFromInt(taglineBandOffset(y, builder.win_h, g.cell_h, g.tagline_height, true))) + @as(f32, @floatFromInt(g.tagline_height)) / 2; const half_w = @max(1, @min(3, layout.w - 2)); builder.addRect(@floor(center_x - half_w), @floor(center_y - 2), @floor(center_x + half_w), @floor(center_y - 1), overlayRgb(rgb)); builder.addRect(@floor(center_x - half_w), @floor(center_y + 1), @floor(center_x + half_w), @floor(center_y + 2), overlayRgb(rgb)); diff --git a/src/host_io.zig b/src/host_io.zig index fb09fcc7..a7babc74 100644 --- a/src/host_io.zig +++ b/src/host_io.zig @@ -837,6 +837,7 @@ pub const Shell = struct { extern "c" fn forkpty(amaster: *c_int, name: ?[*:0]u8, termp: ?*const anyopaque, winp: ?*const posix.winsize) c_int; extern "c" fn execv(path: [*:0]const u8, argv: [*:null]const ?[*:0]const u8) c_int; +extern "c" fn execve(path: [*:0]const u8, argv: [*:null]const ?[*:0]const u8, envp: [*:null]const ?[*:0]const u8) c_int; extern "c" fn chdir(path: [*:0]const u8) c_int; extern "c" fn _exit(status: c_int) noreturn; @@ -845,6 +846,100 @@ pub const Child = struct { pid: posix.pid_t, }; +/// The environment a pty child is handed: everything this process carries, +/// with the terminal's own identity written over whatever the launcher left +/// behind. The child is talking to the bundled VT, never to the terminal +/// pardes itself was started from, and a macOS `.app` launch supplies no +/// `TERM` at all — which is exactly why `clear` fails there, why nothing +/// paints a colour, and why every cursor-addressing program falls back to a +/// dumb line-at-a-time mode. +pub const ChildEnv = struct { + /// Entries pardes owns. The name of each is read back out of its own + /// spelling, so there is one place to edit and no second list to drift. + const own = [_][*:0]const u8{ + "TERM=" ++ pardes.config.child_term, + "COLORTERM=" ++ pardes.config.child_colorterm, + "TERM_PROGRAM=" ++ pardes.config.child_term_program, + }; + + /// Wide enough for any real environment; `build` answers null rather than + /// truncate, because a child missing half its variables is a worse bug + /// than one missing TERM, and the caller then execs with `environ` intact. + pub const Slots = [1024]?[*:0]const u8; + + fn name(entry: [*:0]const u8) []const u8 { + const line = std.mem.span(entry); + return line[0 .. std.mem.indexOfScalar(u8, line, '=') orelse line.len]; + } + + pub fn build(slots: *Slots) ?[*:null]const ?[*:0]const u8 { + var n: usize = 0; + var i: usize = 0; + while (libc.environ[i]) |entry| : (i += 1) { + const inherited = name(entry); + const shadowed = for (own) |mine| { + if (std.mem.eql(u8, inherited, name(mine))) break true; + } else false; + if (shadowed) continue; + if (n + own.len + 1 > slots.len) return null; + slots[n] = entry; + n += 1; + } + for (own) |mine| { + slots[n] = mine; + n += 1; + } + slots[n] = null; + return @ptrCast(slots); + } +}; + +test "the child environment replaces the launcher's terminal identity exactly once" { + const saved: ?[]const u8 = if (libc.getenv("TERM")) |t| std.mem.span(t) else null; + var saved_buf: [256]u8 = undefined; + const restore: ?[:0]const u8 = if (saved) |t| blk: { + if (t.len >= saved_buf.len) break :blk null; + @memcpy(saved_buf[0..t.len], t); + saved_buf[t.len] = 0; + break :blk saved_buf[0..t.len :0]; + } else null; + defer if (restore) |t| { + _ = Shell.setenv("TERM", t.ptr, 1); + } else { + _ = unsetenv("TERM"); + }; + + // A launcher TERM must be shadowed rather than duplicated, and the rest of + // the environment must arrive untouched. + try std.testing.expectEqual(@as(c_int, 0), Shell.setenv("TERM", "dumb", 1)); + try std.testing.expectEqual(@as(c_int, 0), Shell.setenv("PARDES_CHILD_ENV_PROBE", "1", 1)); + defer _ = unsetenv("PARDES_CHILD_ENV_PROBE"); + + var slots: ChildEnv.Slots = undefined; + const envp = ChildEnv.build(&slots) orelse return error.EnvironmentTooLarge; + var terms: usize = 0; + var colorterms: usize = 0; + var probes: usize = 0; + var dumb = false; + var i: usize = 0; + while (envp[i]) |entry| : (i += 1) { + const line = std.mem.span(entry); + if (std.mem.startsWith(u8, line, "TERM=")) terms += 1; + if (std.mem.startsWith(u8, line, "COLORTERM=")) colorterms += 1; + if (std.mem.eql(u8, line, "TERM=dumb")) dumb = true; + if (std.mem.eql(u8, line, "PARDES_CHILD_ENV_PROBE=1")) probes += 1; + } + try std.testing.expectEqual(@as(usize, 1), terms); + try std.testing.expectEqual(@as(usize, 1), colorterms); + try std.testing.expectEqual(@as(usize, 1), probes); + try std.testing.expect(!dumb); + // TERM_PROGRAM shares the prefix "TERM"; the match is on the name, so it + // is a separate entry and never a second TERM. + try std.testing.expectEqualStrings("TERM=" ++ pardes.config.child_term, std.mem.span(envp[i - ChildEnv.own.len].?)); +} + +extern "c" fn unsetenv(name: [*:0]const u8) c_int; + pub fn forkShell( core: ?*pardes.Pardes, pane: usize, @@ -891,6 +986,11 @@ pub fn forkShell( try @import("linux/v9fs.zig").writeLaunchCommand(&command.writer, path, std.mem.span(socket), &spawn.argv); if (!try pardes.panes.Terminal.queuePendingCommand(pn, command.written())) return error.ShellAlreadyStarted; } + // Built here and not after the fork: between fork and exec the child may + // not call setenv, whose malloc can deadlock against a pty reader thread + // that held the heap when the fork took its snapshot. + var env_slots: ChildEnv.Slots = undefined; + const envp = ChildEnv.build(&env_slots); const ws = posix.winsize{ .row = rows, .col = cols, .xpixel = 0, .ypixel = 0 }; const pid = forkpty(&master, null, null, &ws); if (pid < 0) return error.ForkFailed; @@ -899,6 +999,7 @@ pub fn forkShell( posix.sigaddset(&set, posix.SIG.WINCH); posix.sigprocmask(posix.SIG.UNBLOCK, &set, null); if (cwd_z) |path| if (chdir(path.ptr) != 0) _exit(126); + if (envp) |env| _ = execve(spawn.path, &spawn.argv, env); _ = execv(spawn.path, &spawn.argv); _exit(127); } @@ -928,6 +1029,42 @@ test "shell spawn rejects invalid directories before creating a child" { try std.testing.expectError(error.FileNotFound, forkShell(null, 0, &rcs, "/bin/sh", missing, 24, 80, null)); } +test "a forked shell is told which terminal it is talking to" { + if (!haveFile("/bin/sh")) return error.SkipZigTest; + // Whatever launched the test is the wrong answer, so make it a loud one. + const saved = libc.getenv("TERM"); + var saved_buf: [256]u8 = undefined; + const restore: ?[:0]const u8 = if (saved) |t| blk: { + const span = std.mem.span(t); + if (span.len >= saved_buf.len) break :blk null; + @memcpy(saved_buf[0..span.len], span); + saved_buf[span.len] = 0; + break :blk saved_buf[0..span.len :0]; + } else null; + defer if (restore) |t| { + _ = Shell.setenv("TERM", t.ptr, 1); + } else { + _ = unsetenv("TERM"); + }; + _ = Shell.setenv("TERM", "dumb", 1); + + const rcs: Shell.PromptFiles = .{}; + const child = try forkShell(null, 0, &rcs, "/bin/sh", "", 24, 80, null); + defer { + _ = libc.kill(child.pid, libc.SIG.KILL); + _ = libc.waitpid(child.pid, null, 0); + _ = libc.close(child.file.handle); + } + var sh: TestShell = .{ .master = child.file.handle, .pid = child.pid }; + // The echoed command carries the unexpanded `$TERM`, so only the shell's + // own expansion can match this. + try std.testing.expect(writeFd(child.file.handle, "printf '\nPARDES-TERM:%s:%s\n' \"$TERM\" \"$COLORTERM\"; exit\n")); + try std.testing.expect(sh.waitText( + "PARDES-TERM:" ++ pardes.config.child_term ++ ":" ++ pardes.config.child_colorterm, + 5_000, + )); +} + test "shell spawn uses an explicit OS directory longer than 256 bytes" { if (!haveFile("/bin/sh")) return error.SkipZigTest; var tmp = std.testing.tmpDir(.{}); @@ -1034,6 +1171,14 @@ extern "c" fn tcgetpgrp(fd: c_int) libc.pid_t; const occ_max_depth: u8 = 8; const occ_max_visited: usize = 32; +/// Where `ttyTaken` answers from evidence rather than from `false`. The two +/// implementations are different walks over the same question, so they share +/// one suite rather than each asserting half of it. +const tty_probe_platform = switch (builtin.os.tag) { + .linux, .macos => true, + else => false, +}; + const TtyProbe = struct { self_exe: [std.fs.max_path_bytes]u8 = undefined, exe: [std.fs.max_path_bytes]u8 = undefined, @@ -1088,10 +1233,94 @@ pub fn ttyTaken(shell_pid: libc.pid_t, master_fd: c_int) bool { } return !saw_fg; }, + .macos => { + const fg = tcgetpgrp(master_fd); + if (fg <= 0) return false; + if (fg == shell_pid) return false; + return darwin.taken(shell_pid, fg); + }, else => return false, } } +/// The same question the Linux walk asks, answered the way macOS can answer +/// it. There is no /proc to descend and no children list, but the tty's +/// foreground process group can be enumerated in one call — and "is anything +/// but the shell itself holding this terminal?" is exactly a statement about +/// the executables in that group. The test is the shell's own executable and +/// never its pid, which is what keeps a nested interactive shell a prompt, +/// and what still catches `bash -c 'sleep 30'`: the group holds `sleep` too. +/// +/// Until this existed, `ttyTaken` answered `false` on darwin for every pane, +/// so `Pardes.takesCommandLine` was permanently true and every bare Escape in +/// a raw terminal ran the `Last` builtin instead of reaching the child. +const darwin = struct { + const proc_pgrp_only: u32 = 2; + const proc_pidt_shortbsdinfo: c_int = 13; + const zombie_status: u32 = 5; // SZOMB + /// PROC_PIDPATHINFO_MAXSIZE. + const path_max = 4 * 1024; + /// A foreground group is a pipeline; anything larger is not a prompt. + const group_max = 64; + + const ShortBsdInfo = extern struct { + pid: u32, + ppid: u32, + pgid: u32, + status: u32, + comm: [16]u8, + flags: u32, + uid: u32, + gid: u32, + ruid: u32, + rgid: u32, + svuid: u32, + svgid: u32, + rfu: u32, + }; + + extern "c" fn proc_listpids(kind: u32, typeinfo: u32, buffer: ?*anyopaque, buffersize: c_int) c_int; + extern "c" fn proc_pidpath(pid: c_int, buffer: [*]u8, buffersize: u32) c_int; + + fn exe(pid: libc.pid_t, buf: *[path_max]u8) ?[]const u8 { + const got = proc_pidpath(pid, buf, buf.len); + if (got <= 0) return null; + return buf[0..@intCast(got)]; + } + + /// A pid with no readable path is either gone or not ours. Only a zombie + /// is provably harmless — it holds no tty — so everything else is taken. + fn zombie(pid: libc.pid_t) bool { + var info: ShortBsdInfo = undefined; + const got = proc_pidinfo(pid, proc_pidt_shortbsdinfo, 0, &info, @sizeOf(ShortBsdInfo)); + if (got != @sizeOf(ShortBsdInfo)) return false; + return info.status == zombie_status; + } + + fn taken(shell_pid: libc.pid_t, fg: libc.pid_t) bool { + var shell_buf: [path_max]u8 = undefined; + const shell_exe = exe(shell_pid, &shell_buf) orelse return false; + var group: [group_max]libc.pid_t = undefined; + const bytes = proc_listpids(proc_pgrp_only, @intCast(fg), &group, @sizeOf(@TypeOf(group))); + // An empty group is a group whose last member just exited: the shell + // is about to have its terminal back, and calling that taken would + // blink the prompt heuristic off for a frame every time a job ends. + if (bytes <= 0) return false; + const n = @as(usize, @intCast(bytes)) / @sizeOf(libc.pid_t); + if (n >= group.len) return true; + var exe_buf: [path_max]u8 = undefined; + for (group[0..n]) |pid| { + if (pid <= 0 or pid == shell_pid) continue; + const path = exe(pid, &exe_buf) orelse { + if (zombie(pid)) continue; + return true; + }; + if (!std.mem.eql(u8, path, shell_exe)) return true; + } + return false; + } +}; + pub fn signalTty(shell_pid: libc.pid_t, master_fd: c_int, which: pardes.PtySignal) void { const sig = switch (which) { .int => libc.SIG.INT, @@ -1332,23 +1561,33 @@ const TestShell = struct { } fn stop(sh: *TestShell) void { - var probe: TtyProbe = undefined; - var pending: usize = 0; - var doomed: [occ_max_visited]libc.pid_t = undefined; - var n: usize = 0; - _ = pushChildren(&probe, &pending, sh.pid, 1); - while (pending > 0) { - pending -= 1; - const node = probe.pending[pending]; - if (n == doomed.len) break; - doomed[n] = node.pid; - n += 1; - if (node.depth < occ_max_depth) _ = pushChildren(&probe, &pending, node.pid, node.depth + 1); - } - _ = libc.kill(sh.pid, libc.SIG.KILL); - for (doomed[0..n]) |kid| { - _ = libc.kill(kid, libc.SIG.KILL); - _ = libc.kill(-kid, libc.SIG.KILL); + if (comptime builtin.os.tag == .linux) { + var probe: TtyProbe = undefined; + var pending: usize = 0; + var doomed: [occ_max_visited]libc.pid_t = undefined; + var n: usize = 0; + _ = pushChildren(&probe, &pending, sh.pid, 1); + while (pending > 0) { + pending -= 1; + const node = probe.pending[pending]; + if (n == doomed.len) break; + doomed[n] = node.pid; + n += 1; + if (node.depth < occ_max_depth) _ = pushChildren(&probe, &pending, node.pid, node.depth + 1); + } + _ = libc.kill(sh.pid, libc.SIG.KILL); + for (doomed[0..n]) |kid| { + _ = libc.kill(kid, libc.SIG.KILL); + _ = libc.kill(-kid, libc.SIG.KILL); + } + } else { + // No children list to descend. forkpty made the shell a session + // leader, so its own group plus whichever group currently holds + // the tty covers the job the test left running. + const fg = tcgetpgrp(sh.master); + if (fg > 0 and fg != sh.pid) _ = libc.kill(-fg, libc.SIG.KILL); + _ = libc.kill(-sh.pid, libc.SIG.KILL); + _ = libc.kill(sh.pid, libc.SIG.KILL); } _ = libc.waitpid(sh.pid, null, 0); _ = libc.close(sh.master); @@ -1377,7 +1616,7 @@ fn sleepMs(ms: i64) void { } test "an idle prompt is free, a foreground job takes the tty, and Ctrl-C hands it back" { - if (comptime builtin.os.tag != .linux) return error.SkipZigTest; + if (comptime !tty_probe_platform) return error.SkipZigTest; var sh = TestShell.start() orelse return error.SkipZigTest; defer sh.stop(); @@ -1393,7 +1632,7 @@ test "an idle prompt is free, a foreground job takes the tty, and Ctrl-C hands i } test "a background job is not the tty's owner" { - if (comptime builtin.os.tag != .linux) return error.SkipZigTest; + if (comptime !tty_probe_platform) return error.SkipZigTest; var sh = TestShell.start() orelse return error.SkipZigTest; defer sh.stop(); @@ -1406,7 +1645,7 @@ test "a background job is not the tty's owner" { } test "a nested interactive shell is still a prompt" { - if (comptime builtin.os.tag != .linux) return error.SkipZigTest; + if (comptime !tty_probe_platform) return error.SkipZigTest; var sh = TestShell.start() orelse return error.SkipZigTest; defer sh.stop(); @@ -1427,7 +1666,7 @@ test "a nested interactive shell is still a prompt" { } test "the walk reaches the leaf: bash -c 'sleep 30' takes the tty" { - if (comptime builtin.os.tag != .linux) return error.SkipZigTest; + if (comptime !tty_probe_platform) return error.SkipZigTest; var sh = TestShell.start() orelse return error.SkipZigTest; defer sh.stop(); @@ -1439,23 +1678,40 @@ test "the walk reaches the leaf: bash -c 'sleep 30' takes the tty" { sh.send("bash --norc -c 'sleep 30; :'\n"); try std.testing.expect(sh.waitTaken(true, 10_000)); - var probe: TtyProbe = undefined; - var pending: usize = 0; - try std.testing.expectEqual(Pushed.pushed, pushChildren(&probe, &pending, sh.pid, 1)); - try std.testing.expectEqual(@as(usize, 1), pending); - var wrapper_buf: [std.fs.max_path_bytes]u8 = undefined; - var shell_buf: [std.fs.max_path_bytes]u8 = undefined; - try std.testing.expectEqualStrings( - procExe(sh.pid, &shell_buf).?, - procExe(probe.pending[0].pid, &wrapper_buf).?, - ); + // Why the shallow answer is the wrong one, in each walk's own evidence: + // the wrapper between the shell and `sleep` wears the shell's executable, + // so a probe that stopped at it would report a prompt. + if (comptime builtin.os.tag == .linux) { + var probe: TtyProbe = undefined; + var pending: usize = 0; + try std.testing.expectEqual(Pushed.pushed, pushChildren(&probe, &pending, sh.pid, 1)); + try std.testing.expectEqual(@as(usize, 1), pending); + var wrapper_buf: [std.fs.max_path_bytes]u8 = undefined; + var shell_buf: [std.fs.max_path_bytes]u8 = undefined; + try std.testing.expectEqualStrings( + procExe(sh.pid, &shell_buf).?, + procExe(probe.pending[0].pid, &wrapper_buf).?, + ); + } else { + const fg = tcgetpgrp(sh.master); + try std.testing.expect(fg > 0 and fg != sh.pid); + var leader_buf: [darwin.path_max]u8 = undefined; + var shell_buf: [darwin.path_max]u8 = undefined; + // The group's leader IS that wrapper, and it is the shell's binary. + try std.testing.expectEqualStrings( + darwin.exe(sh.pid, &shell_buf).?, + darwin.exe(fg, &leader_buf).?, + ); + // Taken all the same, because the rest of the group is not. + try std.testing.expect(darwin.taken(sh.pid, fg)); + } sh.send("\x03"); try std.testing.expect(sh.waitTaken(false, 10_000)); } test "a full-screen program takes the tty until it quits" { - if (comptime builtin.os.tag != .linux) return error.SkipZigTest; + if (comptime !tty_probe_platform) return error.SkipZigTest; const cases = [_]struct { bin: [*:0]const u8, run: []const u8, quit: []const u8 }{ .{ .bin = "/usr/bin/vim", .run = "vim -u NONE -i NONE\n", .quit = "\x1b:q!\r" }, .{ .bin = "/usr/bin/less", .run = "env LESS= less /etc/hosts\n", .quit = "q" }, diff --git a/src/layout.zig b/src/layout.zig index 47b2c1a4..c6cb7503 100644 --- a/src/layout.zig +++ b/src/layout.zig @@ -8,7 +8,7 @@ pub const Rect = struct { x: u16 = 0, y: u16 = 0, w: u16 = 0, h: u16 = 0 }; pub const Snapshot = struct { serial: u32, box: Box }; const MAX_COLS = pardes.MAX_COLS; const MAX_PANES = pardes.MAX_PANES; -const TOPBAR_H = pardes.TOPBAR_H; + const BOX_H = pardes.BOX_H; pub const Presentation = struct { @@ -795,7 +795,7 @@ pub fn movePlacement(p: *Pardes, id: usize, cur_x: u16, cur_y: u16) ?MovePlaceme const sib = if (src.idx > 0) p.col_panes[src.col][src.idx - 1] else p.col_panes[src.col][src.idx + 1]; heights[sib] +|= p.rects[id].h; } - var y: u16 = TOPBAR_H + p.columnBarHeight(); + var y: u16 = p.topBarHeight() + p.columnBarHeight(); var last: ?MovePlacement = null; for (0..p.col_n[tc]) |k| { const pid = p.col_panes[tc][k]; @@ -1123,7 +1123,7 @@ fn collapseNeighbor(p: *Pardes, col: usize, index: usize) ?usize { } fn computeCollapsedColumn(p: *Pardes, c: usize, x: u16, width: u16, folded: usize) void { - var y: u16 = TOPBAR_H + p.columnBarHeight(); + var y: u16 = p.topBarHeight() + p.columnBarHeight(); const available = p.screen_h -| y; var expanded_left: usize = 0; var weight: f32 = 0; @@ -1196,7 +1196,7 @@ pub fn compute(p: *Pardes) void { } if (vsum <= 0) vsum = 1; - var y: u16 = TOPBAR_H + p.columnBarHeight(); + var y: u16 = p.topBarHeight() + p.columnBarHeight(); const avail_h = p.screen_h -| y; for (0..p.col_n[c]) |k| { const id = p.col_panes[c][k]; diff --git a/src/macos.zig b/src/macos.zig index 79036c3e..d263cfc6 100644 --- a/src/macos.zig +++ b/src/macos.zig @@ -106,6 +106,7 @@ const color_default: u32 = 0x01000000; const color_indexed: u32 = 0x02000000; const cell_flag_default: u8 = 1; const cell_flag_tagline: u8 = 2; +const cell_flag_hover: u8 = 4; const scene_flag_crt: u32 = 1 << 0; const scene_flag_ripple: u32 = 1 << 1; const scene_flag_glitch: u32 = 1 << 2; @@ -730,6 +731,16 @@ fn initCore(runtime: ?*const Runtime, cols_arg: u16, rows_arg: u16) !void { const core = try pardes.Pardes.init(allocs.pardes, opts); errdefer core.deinit(); + // The workspace tag row is the macOS menu bar's business here (see + // docs/macos.md): the build option says whether this shell keeps the + // acme topbar or hands its commands to native chrome. The e2e harness + // opts back in with PARDES_WORKSPACE_TAG=1 — its scripts speak to a + // grid that has the row. Applied before the first resize so the layout + // has never seen it the other way. + core.settings.workspace_tag = if (std.c.getenv("PARDES_WORKSPACE_TAG")) |v| + std.mem.eql(u8, std.mem.span(v), "1") + else + @import("pardes_config").workspace_tag; core.native_images = true; var prompt_rcs = host_io.Shell.prepare(); @@ -917,7 +928,7 @@ export fn pardes_gui_tagline_font_percent() u8 { } export fn pardes_tagline_band_offset(row: u16, canvas_h: f32, cell_h: u32, tagline_h: u32) u32 { - return pardes.taglineBandOffset(row, canvas_h, cell_h, tagline_h); + return pardes.taglineBandOffset(row, canvas_h, cell_h, tagline_h, if (state) |*st| st.core.settings.workspace_tag else true); } export fn pardes_topbar_pane_border_px(cell_h: u32, tagline_h: u32) u32 { @@ -942,6 +953,11 @@ export fn pardes_topbar_pane_border_rgb() u32 { return @as(u32, rgb[0]) << 16 | @as(u32, rgb[1]) << 8 | rgb[2]; } +export fn pardes_tag_active_bg() u32 { + const st = state orelse return color_default; + const rgb = st.core.chromeTheme().tag_active_bg; + return @as(u32, rgb[0]) << 16 | @as(u32, rgb[1]) << 8 | rgb[2]; +} export fn pardes_tagline_bg() u32 { const st = state orelse return color_default; const rgb = st.core.chromeTheme().tag_bg; @@ -1756,12 +1772,18 @@ fn clearFrame(st: *State) void { st.panel_diff_len = 0; } -fn blankTagCells(cells: []Cell, cols: u16, rows: u16, layers: []const pardes.TagLayer) void { +fn blankTagCells(cells: []Cell, cols: u16, rows: u16, source: []const pardes.Cell, layers: []const pardes.TagLayer) void { for (layers) |layer| { if (layer.cols == 0 or layer.viewport.y >= rows or layer.viewport.x >= cols) continue; const start = @as(usize, layer.viewport.y) * cols + layer.viewport.x; const count = @min(layer.viewport.w, cols - layer.viewport.x); - for (cells[start..][0..count]) |*cell| cell.* = encodeCell(.{}); + for (cells[start..][0..count], source[start..][0..count]) |*cell, was| { + // The tag text is the layer's to draw, but a look-hover on a tag + // word is a canonical fact: carry the hover bit through so the + // glass overlay still finds the affordance rect. + cell.* = encodeCell(.{}); + cell.flags |= @as(u8, @intFromBool(was.hover)) * cell_flag_hover; + } } } @@ -1800,7 +1822,7 @@ fn presentFrame(ctx: ?*anyopaque, surface: *const pardes.Surface) void { defer tz_cells.end(); for (surface.cells, st.cells[0..count]) |cell, *out| out.* = encodeCell(cell); } - blankTagCells(st.cells[0..count], surface.cols, surface.rows, &surface.tag_layers); + blankTagCells(st.cells[0..count], surface.cols, surface.rows, surface.cells, &surface.tag_layers); collectPanelDiff(st, surface, count); collectImages(st, surface); collectPanelTracks(st, surface); @@ -1831,7 +1853,7 @@ fn collectPanelDiff(st: *State, surface: *const pardes.Surface, count: usize) vo } for (surface.previous_cells, st.previous_cells[0..count]) |cell, *out| out.* = encodeCell(cell); - blankTagCells(st.previous_cells[0..count], surface.cols, surface.rows, surface.previous_tag_layers); + blankTagCells(st.previous_cells[0..count], surface.cols, surface.rows, surface.previous_cells, surface.previous_tag_layers); for (surface.cell_diffs, st.changed_cells[0..count]) |diff, *out| out.* = encodeChanged(diff); st.panel_diff_len = count; @@ -2395,8 +2417,8 @@ fn encodeCell(cell: pardes.Cell) Cell { .fg = encodeColor(cell.style.fg), .bg = encodeColor(cell.style.bg), .attrs = encodeAttrs(cell.style), + .flags = encodeCellFlags(cell.default, cell.style.font_role, cell.hover), .len = if (cell.default) 1 else cell.len, - .flags = encodeCellFlags(cell.default, cell.style.font_role), }; if (cell.default) out.text[0] = ' ' @@ -2418,9 +2440,10 @@ fn encodeAttrs(style: pardes.CellStyle) u16 { return attrs; } -fn encodeCellFlags(default: bool, role: pardes.FontRole) u8 { +fn encodeCellFlags(default: bool, role: pardes.FontRole, hover: bool) u8 { return @as(u8, @intFromBool(default)) * cell_flag_default | - @as(u8, @intFromBool(role == .tagline)) * cell_flag_tagline; + @as(u8, @intFromBool(role == .tagline)) * cell_flag_tagline | + @as(u8, @intFromBool(hover)) * cell_flag_hover; } fn noteScrollVelocity(st: *State, delta_rows: f32, delta_cols: f32) void { @@ -2566,7 +2589,8 @@ test "pardes.h declares every export the way it is defined" { try expectSameAbi(@TypeOf(c.pardes_animating), @TypeOf(pardes_animating)); try expectSameAbi(@TypeOf(c.pardes_gui_tagline_font_percent), @TypeOf(pardes_gui_tagline_font_percent)); try expectSameAbi(@TypeOf(c.pardes_tagline_band_offset), @TypeOf(pardes_tagline_band_offset)); - try expectSameAbi(@TypeOf(c.pardes_topbar_pane_border_px), @TypeOf(pardes_topbar_pane_border_px)); + try expectSameAbi(@TypeOf(c.pardes_topbar_pane_border_rgb), @TypeOf(pardes_topbar_pane_border_rgb)); + try expectSameAbi(@TypeOf(c.pardes_tag_active_bg), @TypeOf(pardes_tag_active_bg)); try expectSameAbi(@TypeOf(c.pardes_tagline_origin_col), @TypeOf(pardes_tagline_origin_col)); try expectSameAbi(@TypeOf(c.pardes_grid_col_at), @TypeOf(pardes_grid_col_at)); try expectSameAbi(@TypeOf(c.pardes_topbar_pane_border_rgb), @TypeOf(pardes_topbar_pane_border_rgb)); @@ -2658,6 +2682,7 @@ test "pardes.h matches the Zig boundary" { try expectEqual(@as(u32, c.PARDES_COLOR_INDEXED), color_indexed); try expectEqual(@as(u8, c.PARDES_CELL_DEFAULT), cell_flag_default); try expectEqual(@as(u8, c.PARDES_CELL_TAGLINE), cell_flag_tagline); + try expectEqual(@as(u8, c.PARDES_CELL_HOVER), cell_flag_hover); try expectEqual(@as(u16, c.PARDES_TOPBAR_H), pardes.TOPBAR_H); try expectEqual(@as(u32, c.PARDES_SCENE_CRT), scene_flag_crt); try expectEqual(@as(u32, c.PARDES_SCENE_RIPPLE), scene_flag_ripple); @@ -2723,9 +2748,10 @@ test "pardes.h matches the Zig boundary" { encodeAttrs(.{ .ul = .curly }), ); - try expectEqual(@as(u8, 0), encodeCellFlags(false, .body)); - try expectEqual(cell_flag_tagline, encodeCellFlags(false, .tagline)); - try expectEqual(cell_flag_default | cell_flag_tagline, encodeCellFlags(true, .tagline)); + try expectEqual(@as(u8, 0), encodeCellFlags(false, .body, false)); + try expectEqual(cell_flag_hover, encodeCellFlags(false, .body, true)); + try expectEqual(cell_flag_tagline, encodeCellFlags(false, .tagline, false)); + try expectEqual(cell_flag_default | cell_flag_tagline, encodeCellFlags(true, .tagline, false)); try expectEqual(pardes.config.gui_tagline_font_percent, pardes_gui_tagline_font_percent()); } @@ -2947,10 +2973,16 @@ test "mac tag layer ABI preserves logical capacity and physical grip" { try std.testing.expectEqual(@as(u32, 11), pardes_tag_layer_value(0, 8)); try std.testing.expectEqual(@as(u8, 'Z'), pardes_tag_layer_cells(0).?[11].text[0]); var canonical: [24]Cell = @splat(encodeCell(.{ .text = .{'G'} ++ .{0} ** 6, .len = 1, .default = false })); - blankTagCells(&canonical, 8, 3, &core.surface.tag_layers); + // A look-hover under the layer is a canonical fact and survives the blank; + // the glyph it marks does not, because the layer redraws that text itself. + var source: [24]pardes.Cell = @splat(.{}); + source[11].hover = true; + blankTagCells(&canonical, 8, 3, &source, &core.surface.tag_layers); try std.testing.expectEqual(@as(u8, 'G'), canonical[9].text[0]); try std.testing.expectEqual(@as(u8, 'G'), canonical[18].text[0]); try std.testing.expectEqual(encodeCell(.{}).len, canonical[10].len); + try std.testing.expectEqual(@as(u8, 0), canonical[10].flags & cell_flag_hover); + try std.testing.expectEqual(cell_flag_hover, canonical[11].flags & cell_flag_hover); } test "mac window blur builtin reaches the ABI without changing opacity" { diff --git a/src/macos/Sources/AppDelegate.swift b/src/macos/Sources/AppDelegate.swift index cbe9cdc9..3cce7592 100644 --- a/src/macos/Sources/AppDelegate.swift +++ b/src/macos/Sources/AppDelegate.swift @@ -310,6 +310,16 @@ final class AppDelegate: NSObject, NSApplicationDelegate, PardesViewDelegate { file.addItem(withTitle: "Close Window", action: #selector(NSWindow.performClose(_:)), keyEquivalent: "w") + // The workspace tag row's commands, in the place macOS keeps window + // commands. The build hands that row to this menu bar (see + // docs/macos.md): the words are the same builtins the tag carried, + // spelled exactly as a tag Exec would type them, and nothing here is + // editable — the menu is the tag's buttons, not the tag. + let builtins = submenu("Builtins", of: main) + for word in ["Newcol", "Joincol", "Find", "Grep", "Changelog", + "Dump", "NextColor", "Debug", "Kill"] { + builtins.addItem(command(word, #selector(menuBuiltin(_:)), "")) + } let edit = submenu("Edit", of: main) // Paste and nothing else. Copy, Cut, Undo and Select All have no // builtin behind them — the core yanks into the clipboard on its own @@ -379,13 +389,17 @@ final class AppDelegate: NSObject, NSApplicationDelegate, PardesViewDelegate { // here would hide half of what Look does. panel.canChooseDirectories = true panel.allowsMultipleSelection = true - panel.resolvesAliases = true - guard panel.runModal() == .OK else { return } for url in panel.urls where url.isFileURL { look(url.path) } } + /// Any workspace-tag word. The title is the builtin's own spelling, so + /// this is the same `run` a tag Exec performs and there is no second + /// vocabulary to keep in step. + @objc private func menuBuiltin(_ sender: NSMenuItem) { run(sender.title) } + + @objc private func menuNew(_ sender: Any?) { run("New") } @objc private func menuSave(_ sender: Any?) { run("Save") } @objc private func menuHelp(_ sender: Any?) { run("Help") } diff --git a/src/macos/Sources/PardesView.swift b/src/macos/Sources/PardesView.swift index 7981cf46..5ad09c38 100644 --- a/src/macos/Sources/PardesView.swift +++ b/src/macos/Sources/PardesView.swift @@ -433,7 +433,26 @@ private struct Metrics { let symbolic = (traits?[kCTFontSymbolicTrait] as? UInt32) ?? 0 return symbolic & UInt32(CTFontSymbolicTraits.traitBold.rawValue | CTFontSymbolicTraits.traitItalic.rawValue) == 0 } - let face = CTFontCreateWithFontDescriptor(plain ?? descriptors[0], size, nil) + let chosen = CTFontCreateWithFontDescriptor(plain ?? descriptors[0], size, nil) + // A variable face's descriptor carries no variation, and CoreText + // picks whichever named instance it likes for it — Maple Mono's + // variable file comes up Thin. Pin every axis to its own default: + // the face the file itself calls regular. + var face = chosen + if let axes = CTFontCopyVariationAxes(face) as? [[CFString: Any]] { + var defaults: [CFNumber: Any] = [:] + for axis in axes { + guard let tag = axis[kCTFontVariationAxisIdentifierKey], + let value = axis[kCTFontVariationAxisDefaultValueKey] else { continue } + defaults[tag as! CFNumber] = value + } + if !defaults.isEmpty { + let descriptor = CTFontDescriptorCreateCopyWithAttributes( + CTFontCopyFontDescriptor(face), + [kCTFontVariationAttribute: defaults] as CFDictionary) + face = CTFontCreateWithFontDescriptor(descriptor, size, nil) + } + } // The core already filtered for fixed pitch by reading the file's own // advances. This is the same question asked of the face CoreText // actually built, which is the one that will be drawn with. @@ -577,7 +596,14 @@ final class PardesView: NSView { // full redraw does not allocate 24 times. private var runGlyphs: [CGGlyph] = [] private var runPositions: [CGPoint] = [] - + /// Ligature shaping, keyed by face and text. A sequence ligates when + /// shaping it produces fewer glyphs than it has characters — the only + /// answer CoreText gives without interrogating the font's own features. + /// Cleared with the glyph caches, because it belongs to one face. + private var ligatureShaped: [String: Bool] = [:] + /// Columns the current row has already consumed as part of a ligature + /// drawn further left. One row's worth at a time. + private var ligatureCovered: Set<Int> = [] /// Constructed lazily because the ordinary path is still a direct /// CoreText/CoreGraphics frame. No scene bit and no panel track means no /// bitmap, no Core Image context and no Metal work. @@ -748,6 +774,7 @@ final class PardesView: NSView { bodyGlyphCache.removeAll(keepingCapacity: true) taglineGlyphCache.removeAll(keepingCapacity: true) gripGlyphCache.removeAll(keepingCapacity: true) + ligatureShaped.removeAll(keepingCapacity: true) metrics = next taglines = TaglineMetrics(body: next, percent: taglines.percent, scale: scale) metricsScale = scale @@ -844,12 +871,12 @@ final class PardesView: NSView { // whatever we do, so clipping would save fills and nothing else. Narrow // the row loops to the dirty band if that ever shows up in a profile. let count = pardes_frame() - let nextLinkPointer = pardes_pointer_shape() == 1 - if nextLinkPointer != linkPointer { - linkPointer = nextLinkPointer + let nextPointer = pointerShape() + if nextPointer != pointer { + pointer = nextPointer window?.invalidateCursorRects(for: self) if let window, bounds.contains(convert(window.mouseLocationOutsideOfEventStream, from: nil)) { - (linkPointer ? NSCursor.pointingHand : NSCursor.iBeam).set() + cursorFor(pointer).set() } } let scene = pardes_scene() @@ -1178,10 +1205,13 @@ final class PardesView: NSView { drawTagLayers(ctx, hideCursor:hideCursor, canonical:cells, cols:cols, rows:rows) } else { for layer in presentedTagLayers { - drawTagGrip(ctx, values:layer.values, canonical:cells, cols:cols, rows:rows) - layer.cells.withUnsafeBufferPointer { cells in - if let base = cells.baseAddress { drawTagLayer(ctx, values:layer.values, cells:base, hideCursor:true) } + layer.cells.withUnsafeBufferPointer { tagCells in + if let base = tagCells.baseAddress { + drawTagLayer(ctx, values: layer.values, cells: base, hideCursor: true, + canonical: cells, cols: cols) + } } + drawTagGrip(ctx, values: layer.values, canonical: cells, cols: cols, rows: rows) } for layer in presentedBodyLayers { layer.cells.withUnsafeBufferPointer { cells in @@ -1190,6 +1220,7 @@ final class PardesView: NSView { } drawTagRules(ctx, layers: presentedTagLayers.map { $0.values }) } + drawHoverGlass(ctx, cells: cells, cols: cols, rows: rows) } private var presentedTagLayers: [FrozenBodyLayer] = [] @@ -1212,16 +1243,22 @@ final class PardesView: NSView { let values = (0...12).map { Int(pardes_tag_layer_value(index, UInt32($0))) } if values[0] == 0 { continue } layers.append(values) - drawTagGrip(ctx, values:values, canonical:canonical, cols:cols, rows:rows) - if let cells = pardes_tag_layer_cells(index) { drawTagLayer(ctx, values: values, cells: cells, hideCursor: hideCursor) } + // The layer's full-row fill must land before the grip: the row + // background now spans the anchor columns, and the grip glyph + // and the anchor fills have to come back on top of it. + if let cells = pardes_tag_layer_cells(index) { + drawTagLayer(ctx, values: values, cells: cells, hideCursor: hideCursor, + canonical: canonical, cols: cols) + } + drawTagGrip(ctx, values: values, canonical: canonical, cols: cols, rows: rows) } drawTagRules(ctx, layers: layers) } /// SDL's chrome overlay is drawn after its compact layers. Keep these - /// one-pixel rules above the full-row tag backgrounds as well. + /// one-point rules above the full-row tag backgrounds as well. private func drawTagRules(_ ctx: CGContext, layers: [[Int]]) { - let pixel = 1 / metricsScale + let pixel = CGFloat(1) for values in layers where values[1] != 0 { let kind = values[1] let bottom = kind == 2 && values[11] != 0 @@ -1252,15 +1289,17 @@ final class PardesView: NSView { } private func drawTagGrip(_ ctx: CGContext, values: [Int], canonical: UnsafePointer<pardes_cell_s>, cols: Int, rows: Int) { - let width = Int(pardes_grip_columns()) let x = values[4] - Int(pardes_tag_text_inset()), y = values[5] if values[1] != 2 || x < 0 || x >= cols || y < 0 || y >= rows { return } var cell = canonical[y * cols + x] let text = withUnsafeBytes(of: &cell.text) { String(decoding: $0.prefix(Int(cell.len)), as: UTF8.self) } if text.isEmpty { return } let style = resolve(cell, block:false, ground:themeBG ?? pardesDefaultBG, clearGround:false) - let centerX = (CGFloat(x) + CGFloat(width) / 2) * cellWidth - let centerY = CGFloat(y) * cellHeight + taglines.top(row:y, canvasHeight:bounds.height) + taglines.height / 2 + // The box drawTagLayer filled is this mark's ground, so the mark + // centres on that same square rather than on the grip columns: one + // rect measures both and the two cannot drift apart. + guard let square = anchorSquare(values: values) else { return } + let centerX = square.midX, centerY = square.midY if text != " " { let face = Face(bold: cell.attrs & UInt16(PARDES_ATTR_BOLD) != 0, italic: cell.attrs & UInt16(PARDES_ATTR_ITALIC) != 0) @@ -1268,9 +1307,7 @@ final class PardesView: NSView { let originX = ((centerX - measured.ink.midX) * metricsScale).rounded() / metricsScale let baseline = ((bounds.height - centerY - measured.ink.midY) * metricsScale).rounded() / metricsScale ctx.saveGState() - ctx.clip(to: CGRect(x: CGFloat(x) * cellWidth, - y: CGFloat(y) * cellHeight + taglines.top(row:y, canvasHeight:bounds.height), - width: CGFloat(width) * cellWidth, height: taglines.height)) + ctx.clip(to: square) ctx.translateBy(x: 0, y: bounds.height) ctx.scaleBy(x: 1, y: -1) ctx.textMatrix = .identity @@ -1286,16 +1323,36 @@ final class PardesView: NSView { fill(ctx, CGRect(x:floor(centerX-halfWidth), y:floor(centerY+1), width:2*halfWidth, height:1), style.fg, 1) } - private func drawTagLayer(_ ctx: CGContext, values: [Int], cells: UnsafePointer<pardes_cell_s>, hideCursor: Bool) { + private func drawTagLayer(_ ctx: CGContext, values: [Int], cells: UnsafePointer<pardes_cell_s>, + hideCursor: Bool, canonical: UnsafePointer<pardes_cell_s>, cols: Int) { let left = CGFloat(values[4]) * cellWidth let y = CGFloat(values[5]) * cellHeight + taglines.top(row: values[5], canvasHeight: bounds.height) // SDL gives the tag a full body-row background; only its glyphs - // and per-cell highlights use the compact band. - let viewport = CGRect(x: left, y: CGFloat(values[5]) * cellHeight, - width: CGFloat(values[6]) * cellWidth, height: cellHeight) + // and per-cell highlights use the compact band. The band spans the + // row's whole extent, not just its text: the grip and the mode box + // live in the inset columns to the left of `values[4]`, and their + // own fills cover only the compact band — a row background that + // stops at the text leaves holes of ground under those anchors. + // The rule in drawTagRules spans exactly this left-to-right range. + let rowLeft = CGFloat(max(0, values[4] - Int(pardes_tag_text_inset()))) * cellWidth + let rowRight = CGFloat(values[4] + values[6]) * cellWidth + let viewport = CGRect(x: rowLeft, y: CGFloat(values[5]) * cellHeight, + width: rowRight - rowLeft, height: cellHeight) ctx.saveGState() ctx.clip(to: viewport) fillBackground(ctx, viewport, UInt32(values[10]), backgroundAlpha) + // The anchor (grip, mode box) lives in the inset columns that fill + // just covered, so its own box goes back on top: one solid fill in + // the box colour the anchor cell wears, with drawTagGrip's mark + // drawn over it afterwards. + if let square = anchorSquare(values: values) { + let anchorX = max(0, values[4] - Int(pardes_tag_text_inset())) + let cell = canonical[values[5] * cols + anchorX] + let style = resolve(cell, block: false, + ground: UInt32(values[10]), clearGround: false) + if style.visible { fillBackground(ctx, square.integral, style.bg, backgroundAlpha) } + } + for pass in 0...1 { for col in 0..<values[0] { var cell = cells[col] @@ -1331,6 +1388,184 @@ final class PardesView: NSView { ctx.restoreGState() } + /// The anchor square: one box drawn from the tag band's height — square, + /// at the row's left edge, inset by the same amount on both axes — + /// shared by the fill in drawTagLayer and the glyph in drawTagGrip so + /// the two can never disagree. + private func anchorSquare(values: [Int]) -> CGRect? { + let anchorX = max(0, values[4] - Int(pardes_tag_text_inset())) + guard anchorX < values[4] else { return nil } + let pad = (cellHeight - taglines.height) / 2 + return CGRect(x: CGFloat(anchorX) * cellWidth + pad, + y: CGFloat(values[5]) * cellHeight + taglines.top(row: values[5], canvasHeight: bounds.height), + width: taglines.height, height: taglines.height) + } + + /// The look-hover affordance as liquid glass: the core marks the word's + /// cells (PARDES_CELL_HOVER), and this composes a real material over the + /// run — the drawn frame snapshot refracted through CoreImage (a small + /// blur, then a lens scale about the pill's centre, which is what makes + /// the edges swim), the theme's selection colour as a faint tint, a + /// specular sheen from the top edge, and a light rim — with the word's + /// own glyphs drawn back on top, crisp, the way text sits on a real + /// material rather than behind one. + private func drawHoverGlass(_ ctx: CGContext, cells: UnsafePointer<pardes_cell_s>, cols: Int, rows: Int) { + var drew = false + for row in 0..<rows { + var start = -1 + var tint: UInt32 = 0 + for col in 0...cols { + let hover = col < cols + && cells[row * cols + col].flags & UInt8(PARDES_CELL_HOVER) != 0 + if hover, start < 0 { + start = col + tint = cells[row * cols + col].bg + } + if !hover, start >= 0 { + let rect = CGRect(x: CGFloat(start) * cellWidth, y: CGFloat(row) * cellHeight, + width: CGFloat(col - start) * cellWidth, height: cellHeight) + drawLiquidGlass(ctx, rect, tint: tint) + redrawHoverRun(ctx, cells, cols: cols, row: row, start: start, end: col) + drew = true + start = -1 + } + } + } + // No affordance this frame: the held snapshot describes a frame + // that no longer exists, and the next hover must take a fresh one. + if !drew { glassShot = nil } + } + + /// A CoreImage context for the glass refraction, created once. + private lazy var glassContext = CIContext() + /// The snapshot behind the current pill. Refraction reads the already + /// drawn frame, and re-reading it every pump would blur a moving pill + /// out of its own budget, so the shot is taken when the rect changes. + private var glassShot: (rect: CGRect, image: CGImage)? + + private func drawLiquidGlass(_ ctx: CGContext, _ rect: CGRect, tint: UInt32) { + let radius = min(rect.height / 4, 6) + let pill = CGPath(roundedRect: rect.insetBy(dx: 1, dy: 1.5), + cornerWidth: radius, cornerHeight: radius, transform: nil) + // The refraction: the frame as drawn, blurred a little and scaled + // about the pill's centre, so the rim bends what surrounds it. The + // shot extends past the pill for exactly this — the lens needs + // pixels from outside the glass to pull in. + let lens = rect.insetBy(dx: -10, dy: -10) + if refract(ctx, pill: pill, rect: rect, lens: lens, tint: tint) { + // Specular: light caught at the top edge, fading as it falls. + ctx.saveGState() + ctx.addPath(pill) + ctx.clip() + let shine = [CGColor(srgbRed: 1, green: 1, blue: 1, alpha: 0.22), + CGColor(srgbRed: 1, green: 1, blue: 1, alpha: 0.0)] as CFArray + if let gradient = CGGradient(colorsSpace: CGColorSpace(name: CGColorSpace.sRGB), + colors: shine, locations: [0, 0.55]) { + ctx.drawLinearGradient(gradient, + start: CGPoint(x: rect.minX, y: rect.minY), + end: CGPoint(x: rect.minX, y: rect.maxY), options: []) + } + ctx.restoreGState() + } else { + // No refraction without a snapshot: the tint alone, faint. + ctx.saveGState() + ctx.addPath(pill) + ctx.clip() + ctx.setFillColor(CGColor(srgbRed: CGFloat((tint >> 16) & 0xFF) / 255, + green: CGFloat((tint >> 8) & 0xFF) / 255, + blue: CGFloat(tint & 0xFF) / 255, + alpha: 0.18)) + ctx.fill(rect) + ctx.restoreGState() + } + // The lens edge: a light rim and a darker bottom hairline, the way a + // real material catches its own edge. + ctx.addPath(pill) + ctx.setStrokeColor(CGColor(srgbRed: 1, green: 1, blue: 1, alpha: 0.35)) + ctx.setLineWidth(1) + ctx.strokePath() + ctx.addPath(pill) + ctx.setStrokeColor(CGColor(srgbRed: 0, green: 0, blue: 0, alpha: 0.25)) + ctx.setLineWidth(0.5) + ctx.clip() + ctx.stroke(CGRect(x: rect.minX, y: rect.maxY - 2, width: rect.width, height: 1.5)) + } + + /// True when the refraction composited. The snapshot is taken when the + /// pill moves and reused while it sits still. + private func refract(_ ctx: CGContext, pill: CGPath, rect: CGRect, lens: CGRect, tint: UInt32) -> Bool { + guard let shot = snapshotFor(ctx, lens: lens) else { return false } + var image = CIImage(cgImage: shot) + // The blur is grown slightly past the crop and the whole thing is + // pulled toward the centre by a few percent: the rim bends what + // surrounds it, which is the refraction the eye reads as glass. + let blur = image.applyingGaussianBlur(sigma: 2 * metricsScale) + let grown = blur.extent.insetBy(dx: -8 * metricsScale, dy: -8 * metricsScale) + let centre = CGPoint(x: grown.midX, y: grown.midY) + let scale = 1.06 + let lensTransform = CGAffineTransform(translationX: centre.x * (1 - scale), + y: centre.y * (1 - scale)).scaledBy(x: scale, y: scale) + image = blur.transformed(by: lensTransform) + guard let filtered = glassContext.createCGImage(image, from: grown) else { return false } + ctx.saveGState() + ctx.addPath(pill) + ctx.clip() + ctx.draw(filtered, in: rect) + // The glass's own body colour: the theme's selection, faint — the + // refraction and the sheen carry the material, not opacity. + ctx.setFillColor(CGColor(srgbRed: CGFloat((tint >> 16) & 0xFF) / 255, + green: CGFloat((tint >> 8) & 0xFF) / 255, + blue: CGFloat(tint & 0xFF) / 255, + alpha: 0.15)) + ctx.fill(rect) + ctx.restoreGState() + return true + } + + private func snapshotFor(_ ctx: CGContext, lens: CGRect) -> CGImage? { + if let held = glassShot, held.rect == lens { return held.image } + guard let whole = ctx.makeImage() else { return nil } + // makeImage hands back backing pixels; the lens is in view points. + let region = CGRect(x: lens.minX * metricsScale, y: lens.minY * metricsScale, + width: lens.width * metricsScale, height: lens.height * metricsScale) + guard let cropped = whole.cropping(to: region) else { return nil } + glassShot = (lens, cropped) + return cropped + } + + /// The hovered word's glyphs, back on top of the material, crisp: text + /// sits on liquid glass, not behind it. + private func redrawHoverRun(_ ctx: CGContext, _ cells: UnsafePointer<pardes_cell_s>, + cols: Int, row: Int, start: Int, end: Int) { + ctx.saveGState() + ctx.textMatrix = .identity + ctx.translateBy(x: 0, y: bounds.height) + ctx.scaleBy(x: 1, y: -1) + let baseline = bounds.height - (CGFloat(row) * cellHeight + metrics.ascent) + for col in start..<end { + let cell = cells[row * cols + col] + guard cell.flags & UInt8(PARDES_CELL_DEFAULT) == 0 else { continue } + let text = withUnsafeBytes(of: cell.text) { raw in + String(decoding: raw.prefix(Int(cell.len)), as: UTF8.self) + } + guard !text.isEmpty, text != " " else { continue } + let style = resolve(cell, block: false, + ground: themeBG ?? pardesDefaultBG, clearGround: false) + guard style.visible else { continue } + setFill(ctx, style.fg, style.alpha) + ctx.textPosition = CGPoint(x: CGFloat(col) * cellWidth, y: baseline) + let attributed = NSAttributedString(string: text, attributes: [ + fontAttribute: metrics.fonts[Face(bold: cell.attrs & UInt16(PARDES_ATTR_BOLD) != 0, + italic: cell.attrs & UInt16(PARDES_ATTR_ITALIC) != 0).rawValue], + NSAttributedString.Key(kCTForegroundColorFromContextAttributeName as String): true]) + CTLineDraw(CTLineCreateWithAttributedString(attributed as CFAttributedString), ctx) + } + ctx.textMatrix = .identity + ctx.restoreGState() + } + + + private struct FrozenBodyLayer { let values: [Int] let cells: [pardes_cell_s] @@ -1626,6 +1861,7 @@ final class PardesView: NSView { var runAlpha: CGFloat = 1 runGlyphs.removeAll(keepingCapacity: true) runPositions.removeAll(keepingCapacity: true) + ligatureCovered.removeAll(keepingCapacity: true) func flush() { guard !runGlyphs.isEmpty else { return } @@ -1640,6 +1876,7 @@ final class PardesView: NSView { for col in 0..<cols { let cell = cells[base + col] + if ligatureCovered.contains(col) { continue } if cell.flags & UInt8(PARDES_CELL_DEFAULT) != 0 { continue } let tagline = cell.flags & UInt8(PARDES_CELL_TAGLINE) != 0 let cellBaseline = tagline @@ -1687,6 +1924,24 @@ final class PardesView: NSView { let units = text.utf16 let known = units.count == 1 ? glyph(face, units.first!, tagline: tagline) : 0 if known != 0 { + // Ligatures: consecutive cells shaped together when the face + // collapses them into fewer glyphs. A monospace coding + // ligature advances by the same whole cells its characters + // did, so the grid cannot drift and only the ink changes. + if !tagline, let ligature = ligatureSpan(cells, base: base, cols: cols, col: col, + face: face, fg: style.fg, alpha: style.alpha, + blockCol: blockCol, paneGripCells: paneGripCells) { + flush() + setFill(ctx, style.fg, style.alpha) + let attributed = NSAttributedString(string: ligature.text, + attributes: [fontAttribute: metrics.fonts[face.rawValue], + NSAttributedString.Key(kCTForegroundColorFromContextAttributeName as String): true]) + ctx.textPosition = CGPoint(x: x, y: cellBaseline) + CTLineDraw(CTLineCreateWithAttributedString(attributed as CFAttributedString), ctx) + ctx.textMatrix = .identity + for covered in 1..<ligature.covered { ligatureCovered.insert(col + covered) } + continue + } if !runGlyphs.isEmpty && (face != runFace || tagline != runTagline || style.fg != runColor || style.alpha != runAlpha) { @@ -1747,6 +2002,73 @@ final class PardesView: NSView { return found } + /// Ligature-prone character heads: a coding ligature always begins with + /// one of these, and refusing everything else keeps the per-frame cost of + /// ligature detection a set lookup rather than a shape. + private static let ligatureHeads: Set<Character> = [ + "<", ">", "=", "!", "&", "|", "-", "/", ":", ".", "#", "*", "+", "~", "^", "%", + ] + + private func ligates(_ face: Face, _ text: String) -> Bool { + let key = "\(face.rawValue):\(text)" + if let cached = ligatureShaped[key] { return cached } + let attributed = NSAttributedString(string: text, attributes: [fontAttribute: metrics.fonts[face.rawValue], + NSAttributedString.Key(kCTForegroundColorFromContextAttributeName as String): true]) + let shaped = CTLineCreateWithAttributedString(attributed as CFAttributedString) + var glyphs: [CGGlyph] = [] + for run in (CTLineGetGlyphRuns(shaped) as? [CTRun]) ?? [] { + let count = CTRunGetGlyphCount(run) + var runGlyphs = [CGGlyph](repeating: 0, count: count) + CTRunGetGlyphs(run, CFRange(location: 0, length: count), &runGlyphs) + glyphs.append(contentsOf: runGlyphs) + } + // A ligature is any divergence from per-character mapping. A font may + // collapse the pair into one glyph (JetBrains Mono) or swap each + // character for a joining half kept at the same count (Maple Mono) — + // either way the sequence must be shaped, not mapped. + let singles = text.utf16.map { glyph(face, $0, tagline: false) } + let result = glyphs.count != singles.count + || zip(glyphs, singles).contains { $0 != $1 } + ligatureShaped[key] = result + return result + } + + /// When the cells at `col` ligate under `face`, the text to shape and how + /// many cells the ligature covers. Nil when they do not. Ligature cells + /// must carry the same face, colour and alpha and no rules of their own — + /// a ligature is one glyph of one style, not a style transition. + private func ligatureSpan(_ cells: UnsafePointer<pardes_cell_s>, base: Int, cols: Int, col: Int, + face: Face, fg: UInt32, alpha: CGFloat, + blockCol: Int, paneGripCells: Set<Int>) -> (text: String, covered: Int)? { + func eligible(_ index: Int) -> String? { + let cell = cells[base + index] + if cell.flags & UInt8(PARDES_CELL_DEFAULT) != 0 { return nil } + if cell.flags & UInt8(PARDES_CELL_TAGLINE) != 0 { return nil } + if cell.attrs >> UInt16(PARDES_ATTR_UL_SHIFT) != 0 + || cell.attrs & UInt16(PARDES_ATTR_STRIKETHROUGH) != 0 { return nil } + if paneGripCells.contains(base + index) { return nil } + let text = withUnsafeBytes(of: cell.text) { raw in + String(decoding: raw.prefix(Int(cell.len)), as: UTF8.self) + } + guard text.utf16.count == 1 else { return nil } + let other = resolve(cell, block: blockCol == index, + ground: themeBG ?? pardesDefaultBG, clearGround: false) + guard other.visible, other.fg == fg, other.alpha == alpha, + Face(bold: cell.attrs & UInt16(PARDES_ATTR_BOLD) != 0, + italic: cell.attrs & UInt16(PARDES_ATTR_ITALIC) != 0) == face else { return nil } + return text + } + guard let head = eligible(col), PardesView.ligatureHeads.contains(head.first!) else { return nil } + guard col + 1 < cols, let second = eligible(col + 1) else { return nil } + let pair = head + second + if col + 2 < cols, let third = eligible(col + 2) { + let triple = pair + third + if ligates(face, triple) { return (triple, 3) } + } + if ligates(face, pair) { return (pair, 2) } + return nil + } + private func drawRules( _ ctx: CGContext, _ cell: pardes_cell_s, @@ -1921,6 +2243,13 @@ final class PardesView: NSView { override func keyDown(with event: NSEvent) { let flags = event.modifierFlags + // Same reason as the trackpad trace below: whether AppKit delivered a + // keystroke at all is not something this process can otherwise answer, + // and "Escape does nothing" has two entirely different causes + // depending on whether this line prints. + trace("key: code=\(event.keyCode) mods=\(flags.rawValue) chars=" + + (event.charactersIgnoringModifiers?.unicodeScalars + .map { String($0.value, radix: 16) }.joined(separator: ",") ?? "-")) // The ABI has no super bit, so a Command chord cannot be expressed at all. // Anything the main menu claims never reaches here; the rest is swallowed // rather than delivered as the bare keystroke the core would insert. @@ -2328,12 +2657,35 @@ final class PardesView: NSView { userInfo: nil)) } - private var linkPointer = false + private enum Pointer: Equatable { case text, link, target } + private var pointer: Pointer = .text + + /// The core's pointer_shape, in this shell's cursor vocabulary: a link + /// is a PDF hyperlink (the hand), a target is a word the core's own + /// Look-hover plumbing validated as a real Look or Exec operand, and + /// anything else is text — the I-beam. + private func pointerShape() -> Pointer { + switch pardes_pointer_shape() { + case 1: return .link + case 2: return .target + default: return .text + } + } + + private func cursorFor(_ shape: Pointer) -> NSCursor { + switch shape { + case .link: return .pointingHand + case .target: return .arrow + case .text: return .iBeam + } + } override func resetCursorRects() { - // Every cell in this view is text, including the tags. An arrow over a - // grid you can sweep and click words in is the wrong affordance. - addCursorRect(bounds, cursor: linkPointer ? .pointingHand : .iBeam) + // Every cell in this view is text, including the tags. An arrow over + // plain text is the wrong affordance — but a word the core has + // validated as a Look or Exec target earns it: middle- and + // right-clicking there acts, and the cursor says so before it does. + addCursorRect(bounds, cursor: cursorFor(pointer)) } override func setFrameSize(_ newSize: NSSize) { diff --git a/src/macos/pardes.h b/src/macos/pardes.h index df3ba04f..f74330e5 100644 --- a/src/macos/pardes.h +++ b/src/macos/pardes.h @@ -76,6 +76,11 @@ typedef struct { #define PARDES_CELL_DEFAULT 0x01u #define PARDES_CELL_TAGLINE 0x02u +// The look-hover affordance: the word under the pointer is a real Look or +// Exec operand. Hosts that draw the affordance as more than the quiet tint +// (macOS composes a liquid-glass overlay) read this instead of guessing +// from the background colour, which a real selection shares. +#define PARDES_CELL_HOVER 0x04u // One coherent snapshot for the full-window shader pass. The bitmask lets the // host run CRT, ripple and glitch in one kernel instead of stacking filters; @@ -323,6 +328,10 @@ uint32_t pardes_topbar_pane_border_rgb(void); // pardes.TOPBAR_H, and src/macos.zig asserts the two agree. #define PARDES_TOPBAR_H 1u +// The active pane's tag row background, for hosts that afford the active +// tagline more than a tint — macOS raises it like an old-school bevel. +uint32_t pardes_tag_active_bg(void); + // Persistent full-window effect switches and their display-clock time. A zero // flags word means draw the CoreText frame directly; any nonzero combination // is one postprocess pass. Snapshot by value, with no borrowed storage. @@ -510,7 +519,8 @@ bool pardes_active_dirty(void); int32_t pardes_cursor_x(void); int32_t pardes_cursor_y(void); bool pardes_cursor_bar(void); -// Mouse pointer affordance: 0 = default text pointer, 1 = link hand. +// Mouse pointer affordance: 0 = default text pointer, 1 = link hand, +// 2 = a validated Look/Exec target under the pointer (the acme arrow). uint32_t pardes_pointer_shape(void); // The Look or Exec the core performed since this was last asked, and clears diff --git a/src/pardes.zig b/src/pardes.zig index c66688f5..7e18db59 100644 --- a/src/pardes.zig +++ b/src/pardes.zig @@ -88,12 +88,19 @@ pub const BOX_H: u16 = 1; pub const TAG_GAP: u16 = 1; pub const TAG_TEXT_INSET: u16 = config.GUTTER + TAG_GAP; -pub fn taglineBandOffset(row: u16, canvas_h: f32, cell_h: u32, tagline_h: u32) u32 { +pub fn taglineBandOffset(row: u16, canvas_h: f32, cell_h: u32, tagline_h: u32, workspace: bool) u32 { const spare = cell_h -| tagline_h; const border = topbarPaneBorderPixels(cell_h, tagline_h); + const rows: u32 = @max(1, @as(u32, @intFromFloat(@floor(canvas_h / @as(f32, @floatFromInt(cell_h)))))); + if (!workspace) { + // No workspace row: nothing above row 0 to share a rule with, so + // every band centers in its cell the way the middle rows always + // have. The anchor boxes this lets the hosts draw stay symmetric. + if (@as(u32, row) + 1 >= rows) return spare; + return spare / 2; + } if (row == 0) return spare - border / 2; if (row == TOPBAR_H) return border - border / 2; - const rows: u32 = @max(1, @as(u32, @intFromFloat(@floor(canvas_h / @as(f32, @floatFromInt(cell_h)))))); if (@as(u32, row) + 1 >= rows) return spare; return spare / 2; } @@ -104,8 +111,8 @@ pub fn topbarPaneBorderPixels(cell_h: u32, tagline_h: u32) u32 { } pub fn taglineOriginCol(p: *const Pardes, col: u16, row: u16, track: ?layout.Track) f32 { - if (row < TOPBAR_H) return 0; - if (row < TOPBAR_H + p.columnBarHeight()) { + if (row < p.topBarHeight()) return 0; + if (row < p.topBarHeight() + p.columnBarHeight()) { for (p.col_x[0..p.ncol], p.col_w[0..p.ncol]) |x, w| if (col >= x and col - x < w) return @floatFromInt(x); } @@ -164,8 +171,8 @@ test "paint order is moving, then opening, then closing tombstones on top" { pub fn gridColAt(p: ?*const Pardes, x: f32, row: u16, body_w: f32, tagline_w: f32) u16 { const body = @max(body_w, 1); const tag = @max(tagline_w, 1); - if (row < TOPBAR_H) return colFromSpan(x, tag); - if (p) |core| if (row < TOPBAR_H + core.columnBarHeight()) { + if (row < (if (p) |core| core.topBarHeight() else TOPBAR_H)) return colFromSpan(x, tag); + if (p) |core| if (row < core.topBarHeight() + core.columnBarHeight()) { for (core.col_x[0..core.ncol], core.col_w[0..core.ncol]) |origin, width| { if (width == 0) continue; const left = @as(f32, @floatFromInt(origin)) * body; @@ -3403,6 +3410,12 @@ pub const Cell = struct { len: u8 = 1, style: CellStyle = .{}, default: bool = true, + /// The look-hover affordance is on this cell: the word under the pointer + /// is a real Look or Exec operand, and hosts that draw the quiet tint as + /// more than a flat fill — macOS renders it as a glass overlay — read + /// this instead of guessing from the background colour, which a real + /// selection shares. + hover: bool = false, pub fn grapheme(c: *const Cell) []const u8 { return c.text[0..c.len]; @@ -3678,8 +3691,12 @@ test "pixel attachment fit and pan do not invalidate cached pixels" { /// The canonical interface: what a frame of pardes IS. The tty shell writes /// these cells to vaxis one-to-one; the SDL shells rasterize them. pub const Surface = struct { - pub const PointerShape = enum(u8) { arrow, link }; + pub const PointerShape = enum(u8) { arrow, link, target }; pointer_shape: PointerShape = .arrow, + /// Set only for the duration of the look-hover paint, so the cells that + /// paint the quiet affordance can carry the hover bit through the ABI + /// without every selection painter learning a new parameter. + mark_hover: bool = false, body_layers: [MAX_PANES]BodyLayer = @splat(.{}), tag_layers: [MAX_TAG_LAYERS]TagLayer = @splat(.{}), cols: u16 = 0, @@ -3725,7 +3742,11 @@ pub const Surface = struct { pub fn at(s: *Surface, x: u16, y: u16) *Cell { std.debug.assert(x < s.cols and y < s.rows); - return &s.cells[@as(usize, y) * s.cols + x]; + const cell = &s.cells[@as(usize, y) * s.cols + x]; + // The look-hover paint runs with mark_hover set, and its cells are + // exactly the ones this marks — see render's preview block. + if (s.mark_hover) cell.hover = true; + return cell; } pub fn set(s: *Surface, x: u16, y: u16, text: []const u8, style: CellStyle) void { @@ -6321,6 +6342,14 @@ pub const Pardes = struct { return config.topbar_str; } + /// The workspace tag row's height: `TOPBAR_H` while the setting keeps + /// it, zero once a native shell takes its commands over. Layout and hit + /// testing read this rather than the constant, the way they read + /// `columnBarHeight`. + pub fn topBarHeight(p: *const Pardes) u16 { + return if (p.settings.workspace_tag) TOPBAR_H else 0; + } + pub fn columnBarHeight(p: *const Pardes) u16 { return if (p.settings.column_tags and p.screen_h >= 3) COLBAR_H else 0; } @@ -7834,7 +7863,7 @@ pub const Pardes = struct { } var dest: ?usize = null; for (p.panes, 0..) |slot, i| { - if (slot == null or p.rects[i].y != TOPBAR_H + p.columnBarHeight()) continue; + if (slot == null or p.rects[i].y != p.topBarHeight() + p.columnBarHeight()) continue; if (p.header_column) |c| if (p.rects[i].x != p.col_x[c]) continue; if (i == p.active) { dest = i; @@ -11099,7 +11128,7 @@ pub const Pardes = struct { pub const ChromeTarget = struct { col: u16, row: u16 }; pub fn chromeTarget(p: *const Pardes, col: u16, row: u16) ?ChromeTarget { - if (row < TOPBAR_H + p.columnBarHeight()) return .{ .col = col, .row = row }; + if (row < p.topBarHeight() + p.columnBarHeight()) return .{ .col = col, .row = row }; for (p.panes, 0..) |slot, id| { if (slot == null) continue; const rect = p.rects[id]; @@ -11414,7 +11443,7 @@ pub const Pardes = struct { p.chord_arg = null; } } - if (mrow >= TOPBAR_H and mrow < TOPBAR_H + p.columnBarHeight() and m.kind == .press) { + if (mrow >= p.topBarHeight() and mrow < p.topBarHeight() + p.columnBarHeight() and m.kind == .press) { for (0..p.ncol) |column| { if (mcol < p.col_x[column] or mcol >= p.col_x[column] + @min(config.GUTTER, p.col_w[column])) continue; if (m.button == config.select_button) { @@ -11431,7 +11460,7 @@ pub const Pardes = struct { if (m.kind == .release) p.header_drag = false; return; } - if (mrow < TOPBAR_H + p.columnBarHeight() and m.kind == .press and + if (mrow < p.topBarHeight() + p.columnBarHeight() and m.kind == .press and (m.button == config.select_button or m.button == config.exec_button or m.button == config.look_button)) { if (m.button == config.select_button) if (p.panes[p.active]) |pane| { @@ -11439,7 +11468,7 @@ pub const Pardes = struct { }; var column: ?usize = null; var x: u16 = 0; - if (mrow >= TOPBAR_H) { + if (mrow >= p.topBarHeight()) { for (0..p.ncol) |c| if (mcol >= p.col_x[c] and mcol < p.col_x[c] + p.col_w[c]) { column = c; x = p.col_x[c] + TAG_TEXT_INSET; @@ -13302,6 +13331,15 @@ pub const Pardes = struct { } } }; + // A look-hover preview means the pointed word is a real Look or Exec + // operand: the same plumbing a click would act on. Hosts that model + // the pointer per-text (the macOS I-beam) read this as "target" and + // afford it the arrow instead. + if (p.look_hover_preview != null) s.pointer_shape = .target; + // Stale hover bits must not survive into a frame whose pointer has + // moved on: the ordinary content paint does not clear fields it + // never set. + for (s.cells) |*c| c.hover = false; for (&s.body_layers) |*layer| layer.rows = 0; for (&s.tag_layers) |*layer| layer.cols = 0; s.nimages = 0; @@ -13368,21 +13406,24 @@ pub const Pardes = struct { } } - // global tagbar: full width, top row + // global tagbar: full width, top row — unless a native shell has + // taken its commands over, in which case the row is not ours to draw p.revealHeader(); - s.fill(0, 0, s.cols, TOPBAR_H, .{ - .bg = .{ .rgb = chrome.tag_bg }, - .font_role = .tagline, - }); var tb_buf: [1200]u8 = undefined; const global_bar = p.topbar(&tb_buf); const global_scroll = if (p.topbar_col != null and p.header_column == null) p.header_scroll else 0; - _ = s.print(0, 0, s.cols, global_bar[panes.File.rawAtDisplay(global_bar, global_scroll)..], .{ - .fg = .{ .rgb = chrome.tag_fg }, - .bg = .{ .rgb = chrome.tag_bg }, - .font_role = .tagline, - }); - if (p.pointer_inside and p.hover_row < TOPBAR_H) { + if (p.settings.workspace_tag) { + s.fill(0, 0, s.cols, TOPBAR_H, .{ + .bg = .{ .rgb = chrome.tag_bg }, + .font_role = .tagline, + }); + _ = s.print(0, 0, s.cols, global_bar[panes.File.rawAtDisplay(global_bar, global_scroll)..], .{ + .fg = .{ .rgb = chrome.tag_fg }, + .bg = .{ .rgb = chrome.tag_bg }, + .font_role = .tagline, + }); + } + if (p.pointer_inside and p.hover_row < p.topBarHeight()) { const bar = p.topbar(&tb_buf); if (wordBoundsAtCol(bar, panes.File.rawAtDisplay(bar, p.hover_col + global_scroll))) |bounds| { var col = panes.File.rawDisplayCol(bar, bounds.lo) -| global_scroll; @@ -13412,24 +13453,24 @@ pub const Pardes = struct { const lum = (@as(u16, grip_bg[0]) * 3 + @as(u16, grip_bg[1]) * 6 + @as(u16, grip_bg[2])) / 10; const ink: [3]u8 = if (lum > 140) .{ 0, 0, 0 } else .{ 255, 255, 255 }; const grip_style: CellStyle = .{ .bg = .{ .rgb = grip_bg }, .fg = .{ .rgb = ink }, .font_role = .tagline }; - s.fill(p.col_x[c], TOPBAR_H, grip, 1, grip_style); - s.fill(p.col_x[c] + grip, TOPBAR_H, inset - grip, 1, .{ .bg = .{ .rgb = bg }, .font_role = .tagline }); + s.fill(p.col_x[c], p.topBarHeight(), grip, 1, grip_style); + s.fill(p.col_x[c] + grip, p.topBarHeight(), inset - grip, 1, .{ .bg = .{ .rgb = bg }, .font_role = .tagline }); - s.fill(x, TOPBAR_H, w, 1, .{ .bg = .{ .rgb = bg }, .font_role = .tagline }); + s.fill(x, p.topBarHeight(), w, 1, .{ .bg = .{ .rgb = bg }, .font_role = .tagline }); const bar_scroll = if (p.topbar_col != null and p.header_column == c) p.header_scroll else 0; const tag = p.columnTag(c); - _ = s.print(x, TOPBAR_H, w, tag[panes.File.rawAtDisplay(tag, bar_scroll)..], .{ + _ = s.print(x, p.topBarHeight(), w, tag[panes.File.rawAtDisplay(tag, bar_scroll)..], .{ .fg = .{ .rgb = fg }, .bg = .{ .rgb = bg }, .font_role = .tagline, }); - if (p.pointer_inside and p.hover_row == TOPBAR_H and p.hover_col >= x and p.hover_col < x + w) { + if (p.pointer_inside and p.hover_row == p.topBarHeight() and p.hover_col >= x and p.hover_col < x + w) { const bar = p.columnTag(c); if (wordBoundsAtCol(bar, panes.File.rawAtDisplay(bar, p.hover_col - x + bar_scroll))) |bounds| { var col = panes.File.rawDisplayCol(bar, bounds.lo) -| bar_scroll; const hi = panes.File.rawDisplayCol(bar, bounds.hi) -| bar_scroll; while (col < hi and col < w) : (col += 1) { - const cell = s.at(x + @as(u16, @intCast(col)), TOPBAR_H); + const cell = s.at(x + @as(u16, @intCast(col)), p.topBarHeight()); cell.style.bg = .{ .rgb = th.sel_bg }; cell.style.fg = .{ .rgb = th.sel_fg }; } @@ -13440,7 +13481,7 @@ pub const Pardes = struct { const bar = p.headerText(); const x = if (p.header_column) |c| p.col_x[c] + TAG_TEXT_INSET else 0; const w = if (p.header_column) |c| p.col_w[c] -| TAG_TEXT_INSET else s.cols; - const y: u16 = if (p.header_column != null) TOPBAR_H else 0; + const y: u16 = if (p.header_column != null) p.topBarHeight() else 0; const anchor = p.header_anchor orelse cur; var col = panes.File.rawDisplayCol(bar, @min(cur, anchor)) -| p.header_scroll; const hi = panes.File.rawDisplayCol(bar, @max(cur, anchor)) -| p.header_scroll; @@ -13458,7 +13499,7 @@ pub const Pardes = struct { switch (p.drag) { .border_v => |d| { if (d.cur_x < s.cols) { - var row: u16 = TOPBAR_H; + var row: u16 = p.topBarHeight(); while (row < s.rows) : (row += 1) s.overlayDash(d.cur_x, row, "╎"); } if (d.corner) |k| if (d.cur_y < s.rows) { @@ -13483,11 +13524,11 @@ pub const Pardes = struct { for (&s.body_layers) |*layer| if (layer.rows > 0 and layer.viewport.x + layer.viewport.w == s.cols) { layer.viewport.w -|= 1; }; - for (&s.tag_layers) |*layer| if (layer.cols > 0 and layer.viewport.x + layer.viewport.w == s.cols and layer.viewport.y >= TOPBAR_H) { + for (&s.tag_layers) |*layer| if (layer.cols > 0 and layer.viewport.x + layer.viewport.w == s.cols and layer.viewport.y >= p.topBarHeight()) { layer.viewport.w -|= 1; }; } - var row: u16 = TOPBAR_H; + var row: u16 = p.topBarHeight(); while (row < s.rows) : (row += 1) s.overlayDash(x, row, "╎"); } }, @@ -13505,7 +13546,7 @@ pub const Pardes = struct { var c: usize = 0; while (c + 1 < p.ncol) : (c += 1) { if (p.hover_col == p.col_x[c] + p.col_w[c] -| 1) { - var row: u16 = TOPBAR_H; + var row: u16 = p.topBarHeight(); while (row < s.rows) : (row += 1) s.overlayDash(p.hover_col, row, "╎"); } } @@ -13559,27 +13600,27 @@ pub const Pardes = struct { const ow: u16 = 38; const oh: u16 = 10; const ox: u16 = if (s.cols > ow) s.cols - ow else 0; - if (ox + ow <= s.cols and TOPBAR_H + oh <= s.rows) { + if (ox + ow <= s.cols and p.topBarHeight() + oh <= s.rows) { // vaxis single_rounded border ring, default style - s.set(ox, TOPBAR_H, "╭", .{}); - s.set(ox + ow - 1, TOPBAR_H, "╮", .{}); - s.set(ox, TOPBAR_H + oh - 1, "╰", .{}); - s.set(ox + ow - 1, TOPBAR_H + oh - 1, "╯", .{}); + s.set(ox, p.topBarHeight(), "╭", .{}); + s.set(ox + ow - 1, p.topBarHeight(), "╮", .{}); + s.set(ox, p.topBarHeight() + oh - 1, "╰", .{}); + s.set(ox + ow - 1, p.topBarHeight() + oh - 1, "╯", .{}); var bx = ox + 1; while (bx < ox + ow - 1) : (bx += 1) { - s.set(bx, TOPBAR_H, "─", .{}); - s.set(bx, TOPBAR_H + oh - 1, "─", .{}); + s.set(bx, p.topBarHeight(), "─", .{}); + s.set(bx, p.topBarHeight() + oh - 1, "─", .{}); } - var by = TOPBAR_H + 1; - while (by < TOPBAR_H + oh - 1) : (by += 1) { + var by = p.topBarHeight() + 1; + while (by < p.topBarHeight() + oh - 1) : (by += 1) { s.set(ox, by, "│", .{}); s.set(ox + ow - 1, by, "│", .{}); } - s.fill(ox + 1, TOPBAR_H + 1, ow - 2, oh - 2, .{ .bg = .{ .rgb = chrome.scroll_track } }); + s.fill(ox + 1, p.topBarHeight() + 1, ow - 2, oh - 2, .{ .bg = .{ .rgb = chrome.scroll_track } }); var lines = std.mem.splitScalar(u8, text, '\n'); - var ly: u16 = TOPBAR_H + 1; + var ly: u16 = p.topBarHeight() + 1; while (lines.next()) |line| : (ly += 1) { - if (ly >= TOPBAR_H + oh - 1) break; + if (ly >= p.topBarHeight() + oh - 1) break; _ = s.print(ox + 1, ly, ow - 2, line, .{ .fg = .{ .rgb = chrome.tag_fg } }); } } @@ -13818,11 +13859,14 @@ pub const Pardes = struct { layer.cursor = if (p.topbar_col == null and pane.promptAt() == null and temporary.cursor != null) .{ .col = temporary.cursor.?.x - TAG_TEXT_INSET, .bar = temporary.cursor.?.bar } else null; } var text_buf: [1200]u8 = undefined; - try p.renderHeaderLayer(arena, MAX_PANES + MAX_COLS, .workspace, 0, .{ .x = 0, .y = 0, .w = p.screen_w, .h = TOPBAR_H }, p.topbar(&text_buf), chrome.tag_bg, chrome.tag_fg); + // With the workspace tag row handed to native chrome there is no + // workspace layer to render; the menu bar carries its commands. + if (p.settings.workspace_tag) + try p.renderHeaderLayer(arena, MAX_PANES + MAX_COLS, .workspace, 0, .{ .x = 0, .y = 0, .w = p.screen_w, .h = TOPBAR_H }, p.topbar(&text_buf), chrome.tag_bg, chrome.tag_fg); if (p.columnBarHeight() > 0) for (0..p.ncol) |column| { if (p.col_w[column] == 0) continue; const active = if (layout.findPane(p, p.active)) |found| found.col == column and p.settings.focus_tint else false; - try p.renderHeaderLayer(arena, MAX_PANES + column, .column, @intCast(column), .{ .x = p.col_x[column] + @min(TAG_TEXT_INSET, p.col_w[column]), .y = TOPBAR_H, .w = p.col_w[column] -| TAG_TEXT_INSET, .h = COLBAR_H }, p.columnTag(column), if (active) chrome.tag_active_bg else chrome.tag_bg, if (active) chrome.tag_active_fg else chrome.tag_fg); + try p.renderHeaderLayer(arena, MAX_PANES + column, .column, @intCast(column), .{ .x = p.col_x[column] + @min(TAG_TEXT_INSET, p.col_w[column]), .y = p.topBarHeight(), .w = p.col_w[column] -| TAG_TEXT_INSET, .h = COLBAR_H }, p.columnTag(column), if (active) chrome.tag_active_bg else chrome.tag_bg, if (active) chrome.tag_active_fg else chrome.tag_fg); }; } @@ -14126,13 +14170,14 @@ pub const Pardes = struct { if (p.look_hover_preview) |preview| { if (preview.pane == id and preview.serial == pane.serial) { // Preserve body ink, then let every real user selection below - // win over this quiet affordance. + // win over this quiet affordance. mark_hover carries the + // cells this paints out to the hosts as the affordance rect — + // macOS composes its glass overlay over exactly these. const preview_bg = mix(page_rgb, mix(page_rgb, th.sel_bg)); if (preview.file_word) |word| panes.File.paintWordSelection(p, pane, r, word.row, word.lo, word.hi, preview_bg) else if (preview.sel) |sel| - if (@max(sel.r0, sel.r1) >= BOX_H) - p.paintPointerSelection(pane, r, tx, tw, tag_y, body_y, sel, preview_bg, null); + p.paintPointerSelection(pane, r, tx, tw, tag_y, body_y, sel, preview_bg, null); } } const modal_hover = if (p.look_hover_preview) |preview| diff --git a/src/themes/acme.zig b/src/themes/acme.zig index 87183ebc..d91e626b 100644 --- a/src/themes/acme.zig +++ b/src/themes/acme.zig @@ -14,13 +14,13 @@ pub const theme = .{ .name = "acme", .bg = .{ 0xff, 0xff, 0xea }, .fg = .{ 0x00, 0x00, 0x00 }, - .sel_bg = .{ 0xee, 0xee, 0x9e }, + .sel_bg = .{ 0xfa, 0xfa, 0x92 }, .sel_fg = .{ 0x00, 0x00, 0x00 }, .tag_bg = .{ 0xea, 0xff, 0xff }, .tag_fg = .{ 0x00, 0x00, 0x00 }, - .box = .{ 0x6e, 0x4f, 0xd0 }, - .box_dim = .{ 0x38, 0x3c, 0x54 }, - .kw = .{ 0x88, 0x22, 0x99 }, + .box = .{ 0x66, 0x3f, 0xe0 }, + .box_dim = .{ 0x35, 0x3a, 0x58 }, + .kw = .{ 0x97, 0x0d, 0xae }, .str = .{ 0x00, 0x77, 0x33 }, .num = .{ 0xa0, 0x52, 0x00 }, .comment = .{ 0x55, 0x55, 0x55 }, diff --git a/src/themes/atelier.zig b/src/themes/atelier.zig index 0193e2e9..f367c341 100644 --- a/src/themes/atelier.zig +++ b/src/themes/atelier.zig @@ -11,27 +11,27 @@ pub const theme = .{ .tag_active_name_fg = .{ 0x65, 0x3f, 0x6b }, .tag_active_bg = .{ 0xd3, 0xe7, 0xdf }, .tag_active_fg = .{ 0x35, 0x58, 0x4d }, - .box = .{ 0x3e, 0x7a, 0x68 }, - .box_dim = .{ 0x99, 0xb6, 0xa3 }, - .column_box = .{ 0x88, 0x57, 0x22 }, - .column_box_dim = .{ 0xb6, 0xa3, 0x87 }, - .kw = .{ 0x76, 0x4d, 0x80 }, - .str = .{ 0x3c, 0x6c, 0x43 }, - .num = .{ 0x88, 0x57, 0x22 }, - .comment = .{ 0x35, 0x73, 0x3a }, + .box = .{ 0x36, 0x82, 0x6b }, + .box_dim = .{ 0x95, 0xba, 0xa2 }, + .column_box = .{ 0x95, 0x58, 0x15 }, + .column_box_dim = .{ 0xbc, 0xa4, 0x81 }, + .kw = .{ 0x7b, 0x44, 0x89 }, + .str = .{ 0x34, 0x74, 0x3d }, + .num = .{ 0x9a, 0x58, 0x10 }, + .comment = .{ 0x29, 0x7c, 0x30 }, .lineno = .{ 0x90, 0x98, 0x80 }, .lineno_active = .{ 0x88, 0x90, 0x78 }, .scroll_track = .{ 0xee, 0xec, 0xcf }, - .scroll_thumb = .{ 0x9d, 0xa9, 0x7b }, - .border = .{ 0xb8, 0xc8, 0xb4 }, - .sel_bg = .{ 0xe5, 0xe9, 0xae }, + .scroll_thumb = .{ 0x9f, 0xae, 0x76 }, + .border = .{ 0xff, 0xff, 0xff }, + .sel_bg = .{ 0xed, 0xf2, 0xa5 }, .sel_fg = .{ 0x35, 0x49, 0x2d }, - .search_bg = .{ 0xf0, 0xd7, 0x94 }, + .search_bg = .{ 0xfe, 0xdd, 0x86 }, .search_fg = .{ 0x61, 0x47, 0x15 }, - .diagnostic_error = .{ 0xa3, 0x3d, 0x37 }, - .diagnostic_warning = .{ 0x81, 0x59, 0x08 }, - .diagnostic_info = .{ 0x33, 0x65, 0x78 }, - .diagnostic_hint = .{ 0x52, 0x6e, 0x3d }, + .diagnostic_error = .{ 0xb3, 0x2f, 0x27 }, + .diagnostic_warning = .{ 0x89, 0x5c, 0x00 }, + .diagnostic_info = .{ 0x29, 0x6a, 0x82 }, + .diagnostic_hint = .{ 0x51, 0x75, 0x36 }, .palette = .{ .{ 0x29, 0x3f, 0x35 }, .{ 0xa3, 0x3d, 0x37 }, diff --git a/src/themes/clay.zig b/src/themes/clay.zig index af824764..85487d48 100644 --- a/src/themes/clay.zig +++ b/src/themes/clay.zig @@ -12,27 +12,27 @@ pub const theme = .{ .tag_active_name_fg = .{ 0xb5, 0xec, 0xec }, .tag_active_bg = .{ 0x42, 0x4f, 0x40 }, .tag_active_fg = .{ 0xe3, 0xe2, 0xc2 }, - .box = .{ 0x8e, 0xc0, 0x7c }, - .box_dim = .{ 0x67, 0x77, 0x52 }, - .column_box = .{ 0xd3, 0x86, 0x9b }, - .column_box_dim = .{ 0x83, 0x62, 0x68 }, - .kw = .{ 0xfb, 0x8a, 0x69 }, - .str = .{ 0xb8, 0xbb, 0x26 }, - .num = .{ 0xd3, 0x86, 0x9b }, - .comment = .{ 0xe2, 0xb5, 0x6f }, + .box = .{ 0x8a, 0xc8, 0x74 }, + .box_dim = .{ 0x68, 0x7c, 0x4d }, + .column_box = .{ 0xdd, 0x7c, 0x97 }, + .column_box_dim = .{ 0x87, 0x5e, 0x65 }, + .kw = .{ 0xff, 0x88, 0x65 }, + .str = .{ 0xd1, 0xd5, 0x0c }, + .num = .{ 0xe0, 0x79, 0x95 }, + .comment = .{ 0xf6, 0xb9, 0x5b }, .lineno = .{ 0x69, 0x64, 0x55 }, .lineno_active = .{ 0x71, 0x6c, 0x5d }, .scroll_track = .{ 0x22, 0x21, 0x19 }, - .scroll_thumb = .{ 0x8a, 0x90, 0x6b }, - .border = .{ 0x56, 0x64, 0x48 }, - .sel_bg = .{ 0x4f, 0x58, 0x3c }, + .scroll_thumb = .{ 0x8c, 0x94, 0x67 }, + .border = .{ 0x74, 0x87, 0x61 }, + .sel_bg = .{ 0x50, 0x5c, 0x38 }, .sel_fg = .{ 0xfb, 0xf1, 0xc7 }, - .search_bg = .{ 0x66, 0x51, 0x28 }, + .search_bg = .{ 0x6f, 0x54, 0x1f }, .search_fg = .{ 0xff, 0xe2, 0xa0 }, - .diagnostic_error = .{ 0xfb, 0x8a, 0x69 }, - .diagnostic_warning = .{ 0xfa, 0xbd, 0x2f }, - .diagnostic_info = .{ 0x83, 0xb5, 0xa8 }, - .diagnostic_hint = .{ 0x8e, 0xc0, 0x7c }, + .diagnostic_error = .{ 0xff, 0x88, 0x65 }, + .diagnostic_warning = .{ 0xff, 0xbf, 0x2a }, + .diagnostic_info = .{ 0x7b, 0xbd, 0xac }, + .diagnostic_hint = .{ 0x89, 0xca, 0x72 }, .palette = .{ .{ 0x10, 0x10, 0x0e }, .{ 0xfb, 0x8a, 0x69 }, diff --git a/src/themes/dark.zig b/src/themes/dark.zig index 5b39b1c7..82396397 100644 --- a/src/themes/dark.zig +++ b/src/themes/dark.zig @@ -13,15 +13,15 @@ pub const theme = .{ .name = "dark", .bg = null, .fg = null, - .sel_bg = .{ 0x4a, 0x3a, 0x1e }, + .sel_bg = .{ 0x51, 0x3c, 0x17 }, .sel_fg = .{ 0xd8, 0xc8, 0xa8 }, .tag_bg = .{ 0x2c, 0x2a, 0x3e }, .tag_fg = .{ 0x96, 0x96, 0x96 }, - .box = .{ 0x6e, 0x4f, 0xd0 }, - .box_dim = .{ 0x38, 0x3c, 0x54 }, - .kw = .{ 0xcc, 0x70, 0xd8 }, - .str = .{ 0x8a, 0xb8, 0x7a }, - .num = .{ 0xd0, 0xc0, 0x70 }, + .box = .{ 0x66, 0x3f, 0xe0 }, + .box_dim = .{ 0x35, 0x3a, 0x58 }, + .kw = .{ 0xda, 0x5e, 0xea }, + .str = .{ 0x85, 0xc3, 0x6f }, + .num = .{ 0xe1, 0xcb, 0x5f }, .comment = .{ 0xf0, 0xf0, 0xf0 }, .lineno = .{ 0x5a, 0x5a, 0x5a }, .scroll_track = .{ 0x2a, 0x2a, 0x2a }, diff --git a/src/themes/daybreak.zig b/src/themes/daybreak.zig index c55069a9..80f4883c 100644 --- a/src/themes/daybreak.zig +++ b/src/themes/daybreak.zig @@ -11,27 +11,27 @@ pub const theme = .{ .tag_active_name_fg = .{ 0x52, 0x25, 0x5f }, .tag_active_bg = .{ 0xdf, 0xea, 0xf3 }, .tag_active_fg = .{ 0x21, 0x3e, 0x56 }, - .box = .{ 0x07, 0x5f, 0x91 }, - .box_dim = .{ 0x81, 0x9d, 0xb1 }, - .column_box = .{ 0x76, 0x42, 0x09 }, - .column_box_dim = .{ 0xb1, 0x9a, 0x80 }, - .kw = .{ 0x62, 0x2a, 0x88 }, - .str = .{ 0x24, 0x5b, 0x30 }, - .num = .{ 0x76, 0x42, 0x09 }, - .comment = .{ 0x14, 0x56, 0x38 }, + .box = .{ 0x00, 0x61, 0x98 }, + .box_dim = .{ 0x7b, 0x9e, 0xb7 }, + .column_box = .{ 0x7f, 0x42, 0x00 }, + .column_box_dim = .{ 0xb7, 0x9a, 0x7a }, + .kw = .{ 0x65, 0x1a, 0x98 }, + .str = .{ 0x1a, 0x65, 0x2b }, + .num = .{ 0x7f, 0x42, 0x00 }, + .comment = .{ 0x08, 0x62, 0x39 }, .lineno = .{ 0x88, 0x93, 0x9b }, .lineno_active = .{ 0x80, 0x8b, 0x93 }, .scroll_track = .{ 0xe6, 0xed, 0xf3 }, - .scroll_thumb = .{ 0x6b, 0x85, 0x9b }, - .border = .{ 0x85, 0x9b, 0xac }, - .sel_bg = .{ 0xc1, 0xde, 0xf3 }, + .scroll_thumb = .{ 0x66, 0x85, 0xa0 }, + .border = .{ 0xc5, 0xcf, 0xd7 }, + .sel_bg = .{ 0xb9, 0xdf, 0xfb }, .sel_fg = .{ 0x10, 0x36, 0x4f }, .search_bg = .{ 0xff, 0xe3, 0x96 }, .search_fg = .{ 0x4b, 0x35, 0x00 }, - .diagnostic_error = .{ 0xa1, 0x1c, 0x30 }, + .diagnostic_error = .{ 0xb3, 0x08, 0x22 }, .diagnostic_warning = .{ 0x70, 0x49, 0x00 }, - .diagnostic_info = .{ 0x07, 0x5a, 0x83 }, - .diagnostic_hint = .{ 0x38, 0x55, 0x2b }, + .diagnostic_info = .{ 0x00, 0x5c, 0x8a }, + .diagnostic_hint = .{ 0x36, 0x5b, 0x25 }, .palette = .{ .{ 0x14, 0x2d, 0x40 }, .{ 0xa1, 0x1c, 0x30 }, diff --git a/src/themes/dusk.zig b/src/themes/dusk.zig index 32948d02..b9377992 100644 --- a/src/themes/dusk.zig +++ b/src/themes/dusk.zig @@ -11,27 +11,27 @@ pub const theme = .{ .tag_active_name_fg = .{ 0xa1, 0xd5, 0xc4 }, .tag_active_bg = .{ 0x3b, 0x2f, 0x3b }, .tag_active_fg = .{ 0xd2, 0xc2, 0xd1 }, - .box = .{ 0xe4, 0xb3, 0x9b }, - .box_dim = .{ 0x71, 0x57, 0x68 }, - .column_box = .{ 0xba, 0xc8, 0x94 }, - .column_box_dim = .{ 0x73, 0x77, 0x62 }, - .kw = .{ 0xcf, 0xb1, 0xe8 }, - .str = .{ 0xba, 0xc8, 0x94 }, - .num = .{ 0xe8, 0xb5, 0x92 }, - .comment = .{ 0xf1, 0xb9, 0x89 }, + .box = .{ 0xed, 0xb0, 0x92 }, + .box_dim = .{ 0x74, 0x54, 0x69 }, + .column_box = .{ 0xbd, 0xce, 0x8e }, + .column_box_dim = .{ 0x75, 0x7a, 0x5f }, + .kw = .{ 0xd0, 0xa7, 0xf2 }, + .str = .{ 0xbe, 0xd1, 0x8b }, + .num = .{ 0xf7, 0xb2, 0x83 }, + .comment = .{ 0xff, 0xb8, 0x7b }, .lineno = .{ 0x82, 0x71, 0x7e }, .lineno_active = .{ 0x8a, 0x79, 0x86 }, .scroll_track = .{ 0x3d, 0x35, 0x40 }, - .scroll_thumb = .{ 0x8b, 0x69, 0x7c }, - .border = .{ 0x51, 0x41, 0x4f }, - .sel_bg = .{ 0x61, 0x44, 0x53 }, + .scroll_thumb = .{ 0x8e, 0x66, 0x7c }, + .border = .{ 0x6d, 0x58, 0x6b }, + .sel_bg = .{ 0x65, 0x40, 0x53 }, .sel_fg = .{ 0xff, 0xf0, 0xeb }, - .search_bg = .{ 0x5e, 0x49, 0x2e }, + .search_bg = .{ 0x65, 0x4a, 0x27 }, .search_fg = .{ 0xff, 0xe3, 0xa2 }, - .diagnostic_error = .{ 0xf6, 0xaa, 0xa4 }, - .diagnostic_warning = .{ 0xed, 0xca, 0x8a }, - .diagnostic_info = .{ 0xaa, 0xcb, 0xdc }, - .diagnostic_hint = .{ 0xc2, 0xcd, 0xa1 }, + .diagnostic_error = .{ 0xff, 0xa2, 0x9b }, + .diagnostic_warning = .{ 0xfc, 0xce, 0x7b }, + .diagnostic_info = .{ 0xa2, 0xcd, 0xe4 }, + .diagnostic_hint = .{ 0xc5, 0xd4, 0x9a }, .palette = .{ .{ 0x39, 0x32, 0x3b }, .{ 0xf6, 0xaa, 0xa4 }, diff --git a/src/themes/forge.zig b/src/themes/forge.zig index 7f78f2ca..a27f66fb 100644 --- a/src/themes/forge.zig +++ b/src/themes/forge.zig @@ -12,27 +12,27 @@ pub const theme = .{ .tag_active_name_fg = .{ 0xf8, 0xcb, 0xa7 }, .tag_active_bg = .{ 0x2d, 0x34, 0x40 }, .tag_active_fg = .{ 0xd1, 0xd5, 0xde }, - .box = .{ 0xa5, 0xc4, 0xee }, - .box_dim = .{ 0x59, 0x65, 0x77 }, - .column_box = .{ 0xb5, 0xce, 0xa8 }, - .column_box_dim = .{ 0x6b, 0x79, 0x69 }, - .kw = .{ 0x75, 0xb5, 0xe8 }, - .str = .{ 0xce, 0x91, 0x78 }, - .num = .{ 0xb5, 0xce, 0xa8 }, + .box = .{ 0x9c, 0xc3, 0xf7 }, + .box_dim = .{ 0x55, 0x64, 0x7b }, + .column_box = .{ 0xb4, 0xd3, 0xa3 }, + .column_box_dim = .{ 0x6a, 0x7b, 0x67 }, + .kw = .{ 0x61, 0xb7, 0xfc }, + .str = .{ 0xdd, 0x8b, 0x69 }, + .num = .{ 0xb3, 0xd5, 0xa1 }, .comment = .{ 0xb3, 0xca, 0xff }, .lineno = .{ 0x62, 0x65, 0x6c }, .lineno_active = .{ 0x6a, 0x6d, 0x74 }, .scroll_track = .{ 0x1a, 0x1c, 0x21 }, - .scroll_thumb = .{ 0x73, 0x80, 0x92 }, - .border = .{ 0x47, 0x52, 0x63 }, - .sel_bg = .{ 0x26, 0x4f, 0x68 }, + .scroll_thumb = .{ 0x70, 0x80, 0x95 }, + .border = .{ 0x60, 0x6f, 0x86 }, + .sel_bg = .{ 0x1c, 0x51, 0x72 }, .sel_fg = .{ 0xe2, 0xee, 0xf4 }, - .search_bg = .{ 0x59, 0x4a, 0x25 }, + .search_bg = .{ 0x61, 0x4d, 0x1d }, .search_fg = .{ 0xf5, 0xdf, 0xa1 }, - .diagnostic_error = .{ 0xf2, 0x8a, 0x81 }, - .diagnostic_warning = .{ 0xd7, 0xba, 0x7d }, + .diagnostic_error = .{ 0xff, 0x7f, 0x74 }, + .diagnostic_warning = .{ 0xe4, 0xbf, 0x70 }, .diagnostic_info = .{ 0x75, 0xbe, 0xff }, - .diagnostic_hint = .{ 0x8b, 0xce, 0xba }, + .diagnostic_hint = .{ 0x81, 0xd8, 0xbe }, // ANSI roles are semantic colors, independent of peach string syntax. .palette = .{ .{ 0x0c, 0x0c, 0x0e }, diff --git a/src/themes/forge_black.zig b/src/themes/forge_black.zig index d9835110..85f6132e 100644 --- a/src/themes/forge_black.zig +++ b/src/themes/forge_black.zig @@ -12,27 +12,27 @@ pub const theme = .{ .tag_active_name_fg = .{ 0xf8, 0xcb, 0xa7 }, .tag_active_bg = .{ 0x2d, 0x34, 0x40 }, .tag_active_fg = .{ 0xd1, 0xd5, 0xde }, - .box = .{ 0xa5, 0xc4, 0xee }, - .box_dim = .{ 0x59, 0x65, 0x77 }, - .column_box = .{ 0xb5, 0xce, 0xa8 }, - .column_box_dim = .{ 0x6b, 0x79, 0x69 }, - .kw = .{ 0x75, 0xb5, 0xe8 }, - .str = .{ 0xce, 0x91, 0x78 }, - .num = .{ 0xb5, 0xce, 0xa8 }, + .box = .{ 0x9c, 0xc3, 0xf7 }, + .box_dim = .{ 0x55, 0x64, 0x7b }, + .column_box = .{ 0xb4, 0xd3, 0xa3 }, + .column_box_dim = .{ 0x6a, 0x7b, 0x67 }, + .kw = .{ 0x61, 0xb7, 0xfc }, + .str = .{ 0xdd, 0x8b, 0x69 }, + .num = .{ 0xb3, 0xd5, 0xa1 }, .comment = .{ 0xb3, 0xca, 0xff }, .lineno = .{ 0x62, 0x65, 0x6c }, .lineno_active = .{ 0x6a, 0x6d, 0x74 }, .scroll_track = .{ 0x14, 0x16, 0x1a }, - .scroll_thumb = .{ 0x73, 0x80, 0x92 }, - .border = .{ 0x47, 0x52, 0x63 }, - .sel_bg = .{ 0x26, 0x4f, 0x68 }, + .scroll_thumb = .{ 0x70, 0x80, 0x95 }, + .border = .{ 0x60, 0x6f, 0x86 }, + .sel_bg = .{ 0x1c, 0x51, 0x72 }, .sel_fg = .{ 0xe2, 0xee, 0xf4 }, - .search_bg = .{ 0x59, 0x4a, 0x25 }, + .search_bg = .{ 0x61, 0x4d, 0x1d }, .search_fg = .{ 0xf5, 0xdf, 0xa1 }, - .diagnostic_error = .{ 0xf2, 0x8a, 0x81 }, - .diagnostic_warning = .{ 0xd7, 0xba, 0x7d }, + .diagnostic_error = .{ 0xff, 0x7f, 0x74 }, + .diagnostic_warning = .{ 0xe4, 0xbf, 0x70 }, .diagnostic_info = .{ 0x75, 0xbe, 0xff }, - .diagnostic_hint = .{ 0x8b, 0xce, 0xba }, + .diagnostic_hint = .{ 0x81, 0xd8, 0xbe }, // ANSI roles are semantic colors, independent of peach string syntax. .palette = .{ .{ 0x00, 0x00, 0x00 }, diff --git a/src/themes/forge_soft.zig b/src/themes/forge_soft.zig index 896bd9e8..0ca062a4 100644 --- a/src/themes/forge_soft.zig +++ b/src/themes/forge_soft.zig @@ -12,27 +12,27 @@ pub const theme = .{ .tag_active_name_fg = .{ 0xf8, 0xcb, 0xa7 }, .tag_active_bg = .{ 0x37, 0x3e, 0x49 }, .tag_active_fg = .{ 0xd1, 0xd5, 0xde }, - .box = .{ 0xa5, 0xc4, 0xee }, - .box_dim = .{ 0x59, 0x65, 0x77 }, - .column_box = .{ 0xb5, 0xce, 0xa8 }, - .column_box_dim = .{ 0x6f, 0x7d, 0x6e }, - .kw = .{ 0x75, 0xb5, 0xe8 }, - .str = .{ 0xda, 0xa5, 0x8e }, - .num = .{ 0xb5, 0xce, 0xa8 }, + .box = .{ 0x9c, 0xc3, 0xf7 }, + .box_dim = .{ 0x55, 0x64, 0x7b }, + .column_box = .{ 0xb4, 0xd3, 0xa3 }, + .column_box_dim = .{ 0x6d, 0x7f, 0x6c }, + .kw = .{ 0x61, 0xb7, 0xfc }, + .str = .{ 0xe7, 0xa0, 0x81 }, + .num = .{ 0xb3, 0xd5, 0xa1 }, .comment = .{ 0xb3, 0xca, 0xff }, .lineno = .{ 0x86, 0x89, 0x91 }, .lineno_active = .{ 0x8e, 0x91, 0x99 }, .scroll_track = .{ 0x3d, 0x40, 0x47 }, - .scroll_thumb = .{ 0x73, 0x80, 0x92 }, - .border = .{ 0x47, 0x52, 0x63 }, - .sel_bg = .{ 0x26, 0x4f, 0x68 }, + .scroll_thumb = .{ 0x70, 0x80, 0x95 }, + .border = .{ 0x60, 0x6f, 0x86 }, + .sel_bg = .{ 0x1c, 0x51, 0x72 }, .sel_fg = .{ 0xe2, 0xee, 0xf4 }, - .search_bg = .{ 0x59, 0x4a, 0x25 }, + .search_bg = .{ 0x61, 0x4d, 0x1d }, .search_fg = .{ 0xf5, 0xdf, 0xa1 }, - .diagnostic_error = .{ 0xf2, 0x8a, 0x81 }, - .diagnostic_warning = .{ 0xd7, 0xba, 0x7d }, + .diagnostic_error = .{ 0xff, 0x7f, 0x74 }, + .diagnostic_warning = .{ 0xe4, 0xbf, 0x70 }, .diagnostic_info = .{ 0x75, 0xbe, 0xff }, - .diagnostic_hint = .{ 0x8b, 0xce, 0xba }, + .diagnostic_hint = .{ 0x81, 0xd8, 0xbe }, // ANSI roles are semantic colors, independent of peach string syntax. .palette = .{ .{ 0x35, 0x38, 0x3e }, diff --git a/src/themes/harvest.zig b/src/themes/harvest.zig index bf367a24..cf10a779 100644 --- a/src/themes/harvest.zig +++ b/src/themes/harvest.zig @@ -12,27 +12,27 @@ pub const theme = .{ .tag_active_name_fg = .{ 0xc5, 0xe7, 0xf7 }, .tag_active_bg = .{ 0x42, 0x4d, 0x38 }, .tag_active_fg = .{ 0xe1, 0xe4, 0xc4 }, - .box = .{ 0xcf, 0xba, 0x8b }, - .box_dim = .{ 0x77, 0x79, 0x53 }, - .column_box = .{ 0xf0, 0x7d, 0x63 }, - .column_box_dim = .{ 0x92, 0x5c, 0x48 }, - .kw = .{ 0xf0, 0x7d, 0x63 }, - .str = .{ 0x99, 0xbe, 0x70 }, - .num = .{ 0xfa, 0xd5, 0x66 }, - .comment = .{ 0xef, 0xc7, 0x8c }, + .box = .{ 0xd8, 0xbd, 0x83 }, + .box_dim = .{ 0x7b, 0x7e, 0x4e }, + .column_box = .{ 0xff, 0x74, 0x54 }, + .column_box_dim = .{ 0x9b, 0x58, 0x3f }, + .kw = .{ 0xff, 0x74, 0x54 }, + .str = .{ 0x9a, 0xcc, 0x62 }, + .num = .{ 0xff, 0xd8, 0x61 }, + .comment = .{ 0xff, 0xca, 0x7c }, .lineno = .{ 0x6a, 0x64, 0x56 }, .lineno_active = .{ 0x72, 0x6c, 0x5e }, .scroll_track = .{ 0x20, 0x1d, 0x17 }, - .scroll_thumb = .{ 0x85, 0x8a, 0x62 }, - .border = .{ 0x59, 0x65, 0x47 }, - .sel_bg = .{ 0x4d, 0x54, 0x38 }, + .scroll_thumb = .{ 0x88, 0x8e, 0x5e }, + .border = .{ 0x78, 0x88, 0x60 }, + .sel_bg = .{ 0x4f, 0x58, 0x34 }, .sel_fg = .{ 0xf3, 0xf2, 0xcc }, - .search_bg = .{ 0x66, 0x45, 0x2d }, + .search_bg = .{ 0x6f, 0x44, 0x24 }, .search_fg = .{ 0xff, 0xe2, 0xac }, - .diagnostic_error = .{ 0xf5, 0x8a, 0x73 }, - .diagnostic_warning = .{ 0xfa, 0xd5, 0x66 }, - .diagnostic_info = .{ 0x86, 0xc1, 0xb9 }, - .diagnostic_hint = .{ 0xb5, 0xc6, 0x8c }, + .diagnostic_error = .{ 0xff, 0x84, 0x69 }, + .diagnostic_warning = .{ 0xff, 0xd8, 0x61 }, + .diagnostic_info = .{ 0x7d, 0xca, 0xbf }, + .diagnostic_hint = .{ 0xb9, 0xcf, 0x83 }, .palette = .{ .{ 0x10, 0x0e, 0x0b }, .{ 0xf5, 0x8a, 0x73 }, diff --git a/src/themes/helix.zig b/src/themes/helix.zig index 3c2934f7..76eaeab2 100644 --- a/src/themes/helix.zig +++ b/src/themes/helix.zig @@ -45,9 +45,9 @@ pub const theme = .{ .tag_fg = .{ 0x94, 0x94, 0x94 }, .box = .{ 0x62, 0x62, 0x62 }, .box_dim = .{ 0x26, 0x26, 0x26 }, - .kw = .{ 0xcf, 0x87, 0xe8 }, - .str = .{ 0xe3, 0xc7, 0x8a }, - .num = .{ 0xe3, 0xc7, 0x8a }, + .kw = .{ 0xd7, 0x76, 0xf9 }, + .str = .{ 0xf3, 0xcd, 0x7a }, + .num = .{ 0xf3, 0xcd, 0x7a }, .comment = .{ 0xff, 0xff, 0xff }, .lineno = .{ 0x62, 0x62, 0x62 }, .scroll_track = .{ 0x12, 0x12, 0x12 }, diff --git a/src/themes/ink.zig b/src/themes/ink.zig index 3b92a635..1e254a3c 100644 --- a/src/themes/ink.zig +++ b/src/themes/ink.zig @@ -12,26 +12,26 @@ pub const theme = .{ .tag_active_bg = .{ 0x22, 0x33, 0x42 }, .tag_active_fg = .{ 0xe1, 0xee, 0xf9 }, .box = .{ 0x86, 0xd8, 0xff }, - .box_dim = .{ 0x64, 0x79, 0x8b }, + .box_dim = .{ 0x5f, 0x79, 0x90 }, .column_box = .{ 0xff, 0xd6, 0x94 }, - .column_box_dim = .{ 0x8a, 0x7b, 0x60 }, + .column_box_dim = .{ 0x8f, 0x7d, 0x5b }, .kw = .{ 0xd5, 0xb8, 0xff }, - .str = .{ 0xb5, 0xee, 0xae }, + .str = .{ 0xac, 0xf9, 0xa3 }, .num = .{ 0xff, 0xd6, 0x94 }, - .comment = .{ 0xa8, 0xe7, 0xf4 }, + .comment = .{ 0x9d, 0xee, 0xff }, .lineno = .{ 0x62, 0x6b, 0x75 }, .lineno_active = .{ 0x6a, 0x73, 0x7d }, .scroll_track = .{ 0x15, 0x21, 0x2c }, - .scroll_thumb = .{ 0x8b, 0xa7, 0xbd }, - .border = .{ 0x59, 0x6f, 0x83 }, - .sel_bg = .{ 0x26, 0x52, 0x71 }, + .scroll_thumb = .{ 0x86, 0xa8, 0xc2 }, + .border = .{ 0x80, 0x95, 0xa9 }, + .sel_bg = .{ 0x1b, 0x54, 0x7c }, .sel_fg = .{ 0xff, 0xff, 0xff }, - .search_bg = .{ 0x75, 0x50, 0x1b }, + .search_bg = .{ 0x7c, 0x4e, 0x0d }, .search_fg = .{ 0xff, 0xff, 0xff }, .diagnostic_error = .{ 0xff, 0xb1, 0xb1 }, .diagnostic_warning = .{ 0xff, 0xe2, 0x9c }, .diagnostic_info = .{ 0x9b, 0xde, 0xff }, - .diagnostic_hint = .{ 0xc6, 0xe4, 0xb5 }, + .diagnostic_hint = .{ 0xc4, 0xeb, 0xae }, .palette = .{ .{ 0x00, 0x00, 0x00 }, .{ 0xff, 0xb1, 0xb1 }, diff --git a/src/themes/lagoon.zig b/src/themes/lagoon.zig index 516639b0..4d975794 100644 --- a/src/themes/lagoon.zig +++ b/src/themes/lagoon.zig @@ -12,27 +12,27 @@ pub const theme = .{ .tag_active_name_fg = .{ 0xf0, 0xce, 0xf8 }, .tag_active_bg = .{ 0x34, 0x57, 0x4d }, .tag_active_fg = .{ 0xc7, 0xe3, 0xd7 }, - .box = .{ 0x89, 0xc7, 0xb4 }, - .box_dim = .{ 0x59, 0x7e, 0x71 }, + .box = .{ 0x81, 0xcf, 0xb7 }, + .box_dim = .{ 0x54, 0x83, 0x72 }, .column_box = .{ 0xff, 0xad, 0x91 }, - .column_box_dim = .{ 0x94, 0x78, 0x68 }, - .kw = .{ 0xd5, 0xa8, 0xed }, - .str = .{ 0xc3, 0xe8, 0x8d }, + .column_box_dim = .{ 0x9a, 0x76, 0x62 }, + .kw = .{ 0xd9, 0x9c, 0xf9 }, + .str = .{ 0xc6, 0xf8, 0x7d }, .num = .{ 0xff, 0xad, 0x91 }, - .comment = .{ 0x92, 0xe1, 0xd1 }, + .comment = .{ 0x84, 0xef, 0xd9 }, .lineno = .{ 0x7b, 0x91, 0x94 }, .lineno_active = .{ 0x83, 0x99, 0x9c }, .scroll_track = .{ 0x3a, 0x4c, 0x50 }, - .scroll_thumb = .{ 0x72, 0x97, 0x8a }, - .border = .{ 0x50, 0x74, 0x68 }, - .sel_bg = .{ 0x39, 0x5b, 0x65 }, + .scroll_thumb = .{ 0x6e, 0x9b, 0x8b }, + .border = .{ 0x6e, 0x9b, 0x8c }, + .sel_bg = .{ 0x32, 0x5f, 0x6c }, .sel_fg = .{ 0xe6, 0xf2, 0xf3 }, - .search_bg = .{ 0x60, 0x53, 0x27 }, + .search_bg = .{ 0x69, 0x58, 0x1e }, .search_fg = .{ 0xff, 0xf0, 0xb9 }, .diagnostic_error = .{ 0xff, 0x9d, 0xa1 }, .diagnostic_warning = .{ 0xff, 0xcb, 0x6b }, - .diagnostic_info = .{ 0x82, 0xc9, 0xfa }, - .diagnostic_hint = .{ 0x89, 0xdd, 0xcc }, + .diagnostic_info = .{ 0x7d, 0xca, 0xff }, + .diagnostic_hint = .{ 0x7c, 0xea, 0xd4 }, .palette = .{ .{ 0x34, 0x46, 0x4c }, .{ 0xff, 0x9d, 0xa1 }, diff --git a/src/themes/orchard.zig b/src/themes/orchard.zig index 888a5b27..f927c50e 100644 --- a/src/themes/orchard.zig +++ b/src/themes/orchard.zig @@ -11,27 +11,27 @@ pub const theme = .{ .tag_active_name_fg = .{ 0xdf, 0xbc, 0xe5 }, .tag_active_bg = .{ 0x26, 0x36, 0x2d }, .tag_active_fg = .{ 0xbe, 0xcf, 0xc4 }, - .box = .{ 0xad, 0xcc, 0x91 }, - .box_dim = .{ 0x4c, 0x63, 0x56 }, - .column_box = .{ 0xe9, 0xbf, 0x91 }, - .column_box_dim = .{ 0x83, 0x73, 0x5a }, - .kw = .{ 0xc9, 0xb0, 0xe4 }, - .str = .{ 0xb6, 0xce, 0x94 }, - .num = .{ 0xe9, 0xbf, 0x91 }, - .comment = .{ 0xb9, 0xe5, 0x8f }, + .box = .{ 0xad, 0xd3, 0x8a }, + .box_dim = .{ 0x49, 0x66, 0x56 }, + .column_box = .{ 0xf4, 0xc0, 0x86 }, + .column_box_dim = .{ 0x88, 0x74, 0x55 }, + .kw = .{ 0xc9, 0xa7, 0xed }, + .str = .{ 0xb8, 0xd8, 0x8a }, + .num = .{ 0xf8, 0xc0, 0x82 }, + .comment = .{ 0xb9, 0xf4, 0x80 }, .lineno = .{ 0x59, 0x67, 0x5c }, .lineno_active = .{ 0x61, 0x6f, 0x64 }, .scroll_track = .{ 0x15, 0x20, 0x19 }, - .scroll_thumb = .{ 0x63, 0x7f, 0x6a }, - .border = .{ 0x35, 0x48, 0x3d }, - .sel_bg = .{ 0x40, 0x5d, 0x4b }, + .scroll_thumb = .{ 0x60, 0x82, 0x69 }, + .border = .{ 0x48, 0x61, 0x52 }, + .sel_bg = .{ 0x3c, 0x61, 0x4a }, .sel_fg = .{ 0xf5, 0xf7, 0xee }, - .search_bg = .{ 0x57, 0x47, 0x2a }, + .search_bg = .{ 0x5e, 0x49, 0x23 }, .search_fg = .{ 0xf7, 0xe4, 0xa5 }, - .diagnostic_error = .{ 0xf1, 0xa3, 0x9b }, - .diagnostic_warning = .{ 0xe9, 0xc6, 0x80 }, - .diagnostic_info = .{ 0x9b, 0xcb, 0xd5 }, - .diagnostic_hint = .{ 0xb6, 0xc5, 0x9e }, + .diagnostic_error = .{ 0xfe, 0x98, 0x8e }, + .diagnostic_warning = .{ 0xf9, 0xcb, 0x70 }, + .diagnostic_info = .{ 0x92, 0xd1, 0xde }, + .diagnostic_hint = .{ 0xb7, 0xcb, 0x98 }, .palette = .{ .{ 0x0d, 0x14, 0x10 }, .{ 0xf1, 0xa3, 0x9b }, diff --git a/src/themes/orchard_black.zig b/src/themes/orchard_black.zig index be960b93..c700947f 100644 --- a/src/themes/orchard_black.zig +++ b/src/themes/orchard_black.zig @@ -11,27 +11,27 @@ pub const theme = .{ .tag_active_name_fg = .{ 0xdf, 0xbc, 0xe5 }, .tag_active_bg = .{ 0x26, 0x36, 0x2d }, .tag_active_fg = .{ 0xbe, 0xcf, 0xc4 }, - .box = .{ 0xad, 0xcc, 0x91 }, - .box_dim = .{ 0x4c, 0x63, 0x56 }, - .column_box = .{ 0xe9, 0xbf, 0x91 }, - .column_box_dim = .{ 0x83, 0x73, 0x5a }, - .kw = .{ 0xc9, 0xb0, 0xe4 }, - .str = .{ 0xb6, 0xce, 0x94 }, - .num = .{ 0xe9, 0xbf, 0x91 }, - .comment = .{ 0xb9, 0xe5, 0x8f }, + .box = .{ 0xad, 0xd3, 0x8a }, + .box_dim = .{ 0x49, 0x66, 0x56 }, + .column_box = .{ 0xf4, 0xc0, 0x86 }, + .column_box_dim = .{ 0x88, 0x74, 0x55 }, + .kw = .{ 0xc9, 0xa7, 0xed }, + .str = .{ 0xb8, 0xd8, 0x8a }, + .num = .{ 0xf8, 0xc0, 0x82 }, + .comment = .{ 0xb9, 0xf4, 0x80 }, .lineno = .{ 0x59, 0x67, 0x5c }, .lineno_active = .{ 0x61, 0x6f, 0x64 }, .scroll_track = .{ 0x11, 0x18, 0x13 }, - .scroll_thumb = .{ 0x63, 0x7f, 0x6a }, - .border = .{ 0x35, 0x48, 0x3d }, - .sel_bg = .{ 0x40, 0x5d, 0x4b }, + .scroll_thumb = .{ 0x60, 0x82, 0x69 }, + .border = .{ 0x48, 0x61, 0x52 }, + .sel_bg = .{ 0x3c, 0x61, 0x4a }, .sel_fg = .{ 0xf5, 0xf7, 0xee }, - .search_bg = .{ 0x57, 0x47, 0x2a }, + .search_bg = .{ 0x5e, 0x49, 0x23 }, .search_fg = .{ 0xf7, 0xe4, 0xa5 }, - .diagnostic_error = .{ 0xf1, 0xa3, 0x9b }, - .diagnostic_warning = .{ 0xe9, 0xc6, 0x80 }, - .diagnostic_info = .{ 0x9b, 0xcb, 0xd5 }, - .diagnostic_hint = .{ 0xb6, 0xc5, 0x9e }, + .diagnostic_error = .{ 0xfe, 0x98, 0x8e }, + .diagnostic_warning = .{ 0xf9, 0xcb, 0x70 }, + .diagnostic_info = .{ 0x92, 0xd1, 0xde }, + .diagnostic_hint = .{ 0xb7, 0xcb, 0x98 }, .palette = .{ .{ 0x00, 0x00, 0x00 }, .{ 0xf1, 0xa3, 0x9b }, diff --git a/src/themes/paper.zig b/src/themes/paper.zig index b5d24835..f1d6980b 100644 --- a/src/themes/paper.zig +++ b/src/themes/paper.zig @@ -11,27 +11,27 @@ pub const theme = .{ .tag_active_name_fg = .{ 0x69, 0x44, 0x7a }, .tag_active_bg = .{ 0xdc, 0xe5, 0xdc }, .tag_active_fg = .{ 0x4a, 0x5a, 0x52 }, - .box = .{ 0x3c, 0x6b, 0x56 }, - .box_dim = .{ 0x91, 0xa3, 0x95 }, - .column_box = .{ 0x87, 0x58, 0x26 }, - .column_box_dim = .{ 0xb7, 0xa1, 0x84 }, - .kw = .{ 0x73, 0x51, 0x90 }, - .str = .{ 0x41, 0x68, 0x48 }, - .num = .{ 0x87, 0x58, 0x26 }, - .comment = .{ 0x2e, 0x71, 0x59 }, + .box = .{ 0x36, 0x71, 0x57 }, + .box_dim = .{ 0x8f, 0xa5, 0x94 }, + .column_box = .{ 0x93, 0x58, 0x1a }, + .column_box_dim = .{ 0xbd, 0xa2, 0x7e }, + .kw = .{ 0x74, 0x46, 0x9b }, + .str = .{ 0x3a, 0x6f, 0x44 }, + .num = .{ 0x98, 0x59, 0x15 }, + .comment = .{ 0x20, 0x75, 0x56 }, .lineno = .{ 0x8b, 0x91, 0x85 }, .lineno_active = .{ 0x83, 0x89, 0x7d }, .scroll_track = .{ 0xe5, 0xe8, 0xdf }, - .scroll_thumb = .{ 0x88, 0x9b, 0x8b }, - .border = .{ 0xb5, 0xc1, 0xb3 }, - .sel_bg = .{ 0xc8, 0xdf, 0xd1 }, + .scroll_thumb = .{ 0x86, 0x9d, 0x8a }, + .border = .{ 0xfb, 0xfb, 0xfb }, + .sel_bg = .{ 0xc5, 0xe2, 0xd0 }, .sel_fg = .{ 0x20, 0x3e, 0x31 }, - .search_bg = .{ 0xf0, 0xdd, 0xa8 }, + .search_bg = .{ 0xfb, 0xe2, 0x9d }, .search_fg = .{ 0x61, 0x46, 0x1c }, - .diagnostic_error = .{ 0xa3, 0x39, 0x38 }, - .diagnostic_warning = .{ 0x80, 0x57, 0x0c }, - .diagnostic_info = .{ 0x30, 0x65, 0x75 }, - .diagnostic_hint = .{ 0x52, 0x6d, 0x3e }, + .diagnostic_error = .{ 0xb3, 0x29, 0x28 }, + .diagnostic_warning = .{ 0x8c, 0x5b, 0x00 }, + .diagnostic_info = .{ 0x26, 0x6b, 0x7f }, + .diagnostic_hint = .{ 0x51, 0x74, 0x37 }, .palette = .{ .{ 0x30, 0x3b, 0x3b }, .{ 0xa3, 0x39, 0x38 }, diff --git a/src/themes/solarium.zig b/src/themes/solarium.zig index fe2525a6..21b4c451 100644 --- a/src/themes/solarium.zig +++ b/src/themes/solarium.zig @@ -12,27 +12,27 @@ pub const theme = .{ .tag_active_name_fg = .{ 0xf5, 0xcd, 0xec }, .tag_active_bg = .{ 0x21, 0x4e, 0x50 }, .tag_active_fg = .{ 0xd3, 0xe0, 0xc9 }, - .box = .{ 0xd5, 0xbc, 0x72 }, - .box_dim = .{ 0x4c, 0x79, 0x72 }, - .column_box = .{ 0x6a, 0xae, 0xdd }, - .column_box_dim = .{ 0x3e, 0x76, 0x90 }, - .kw = .{ 0x6a, 0xae, 0xdd }, - .str = .{ 0xaf, 0xbd, 0x52 }, - .num = .{ 0xd9, 0x9a, 0x5e }, - .comment = .{ 0xe2, 0xc4, 0x6e }, + .box = .{ 0xe1, 0xc2, 0x66 }, + .box_dim = .{ 0x46, 0x7f, 0x76 }, + .column_box = .{ 0x5c, 0xb1, 0xeb }, + .column_box_dim = .{ 0x34, 0x7a, 0x9a }, + .kw = .{ 0x56, 0xb2, 0xf1 }, + .str = .{ 0xbd, 0xd0, 0x3f }, + .num = .{ 0xef, 0x99, 0x48 }, + .comment = .{ 0xf6, 0xce, 0x5a }, .lineno = .{ 0x64, 0x7e, 0x7c }, .lineno_active = .{ 0x6c, 0x86, 0x84 }, .scroll_track = .{ 0x1c, 0x45, 0x4c }, - .scroll_thumb = .{ 0x69, 0x93, 0x8a }, - .border = .{ 0x38, 0x65, 0x5f }, - .sel_bg = .{ 0x27, 0x56, 0x5c }, + .scroll_thumb = .{ 0x65, 0x97, 0x8c }, + .border = .{ 0x4c, 0x88, 0x80 }, + .sel_bg = .{ 0x1f, 0x5c, 0x64 }, .sel_fg = .{ 0xee, 0xe8, 0xd5 }, - .search_bg = .{ 0x64, 0x53, 0x26 }, + .search_bg = .{ 0x6d, 0x57, 0x1d }, .search_fg = .{ 0xff, 0xf0, 0xb9 }, - .diagnostic_error = .{ 0xed, 0x84, 0x77 }, - .diagnostic_warning = .{ 0xdf, 0xb6, 0x5b }, - .diagnostic_info = .{ 0x6a, 0xae, 0xdd }, - .diagnostic_hint = .{ 0x69, 0xbd, 0xb0 }, + .diagnostic_error = .{ 0xff, 0x7a, 0x6a }, + .diagnostic_warning = .{ 0xf3, 0xbe, 0x47 }, + .diagnostic_info = .{ 0x59, 0xb1, 0xee }, + .diagnostic_hint = .{ 0x5c, 0xca, 0xb9 }, .palette = .{ .{ 0x17, 0x3e, 0x45 }, .{ 0xed, 0x84, 0x77 }, diff --git a/src/themes/spectrum.zig b/src/themes/spectrum.zig index 5a1cecc6..a0faf897 100644 --- a/src/themes/spectrum.zig +++ b/src/themes/spectrum.zig @@ -13,26 +13,26 @@ pub const theme = .{ .tag_active_bg = .{ 0x4d, 0x47, 0x36 }, .tag_active_fg = .{ 0xe9, 0xe4, 0xc7 }, .box = .{ 0xff, 0xd8, 0x66 }, - .box_dim = .{ 0x8b, 0x80, 0x56 }, - .column_box = .{ 0xab, 0x9d, 0xf2 }, - .column_box_dim = .{ 0x73, 0x6a, 0x91 }, + .box_dim = .{ 0x92, 0x84, 0x4f }, + .column_box = .{ 0xa4, 0x92, 0xfd }, + .column_box_dim = .{ 0x70, 0x65, 0x96 }, .kw = .{ 0xff, 0x61, 0x88 }, - .str = .{ 0xa9, 0xdc, 0x76 }, - .num = .{ 0xab, 0x9d, 0xf2 }, + .str = .{ 0xa9, 0xee, 0x64 }, + .num = .{ 0xa2, 0x90, 0xff }, .comment = .{ 0xff, 0xe0, 0x9b }, .lineno = .{ 0x68, 0x61, 0x6b }, .lineno_active = .{ 0x70, 0x69, 0x73 }, .scroll_track = .{ 0x21, 0x1d, 0x23 }, - .scroll_thumb = .{ 0x91, 0x8b, 0x72 }, - .border = .{ 0x6a, 0x64, 0x50 }, - .sel_bg = .{ 0x51, 0x43, 0x5c }, + .scroll_thumb = .{ 0x94, 0x8d, 0x6f }, + .border = .{ 0x8f, 0x87, 0x6c }, + .sel_bg = .{ 0x51, 0x3f, 0x60 }, .sel_fg = .{ 0xfc, 0xfc, 0xfa }, - .search_bg = .{ 0x66, 0x56, 0x27 }, + .search_bg = .{ 0x6f, 0x5b, 0x1e }, .search_fg = .{ 0xff, 0xf0, 0xb0 }, .diagnostic_error = .{ 0xff, 0x79, 0x9b }, .diagnostic_warning = .{ 0xff, 0xd8, 0x66 }, - .diagnostic_info = .{ 0x78, 0xdc, 0xe8 }, - .diagnostic_hint = .{ 0xa9, 0xdc, 0x76 }, + .diagnostic_info = .{ 0x67, 0xe9, 0xf9 }, + .diagnostic_hint = .{ 0xa9, 0xeb, 0x67 }, .palette = .{ .{ 0x10, 0x0e, 0x11 }, .{ 0xff, 0x79, 0x9b }, diff --git a/src/web.zig b/src/web.zig index b5a992ae..7325af54 100644 --- a/src/web.zig +++ b/src/web.zig @@ -376,7 +376,7 @@ export fn pardes_tag_layer_cells(index: u32) ?[*]const WebCell { } export fn pardes_tagline_band_offset(row: u16, canvas_h: f32, cell_h: u32, tagline_h: u32) u32 { - return pardes.taglineBandOffset(row, canvas_h, cell_h, tagline_h); + return pardes.taglineBandOffset(row, canvas_h, cell_h, tagline_h, true); } export fn pardes_body_layer_value(index: u32, field: u32) u32 { diff --git a/test/macos_e2e.swift b/test/macos_e2e.swift index 5efc366c..5f172f9d 100644 --- a/test/macos_e2e.swift +++ b/test/macos_e2e.swift @@ -628,8 +628,8 @@ private final class Driver: PardesViewDelegate { let bottom = kind == 2 && pardes_tag_layer_value(i, 11) != 0 for pixel in 0..<height { var rgb = pardes_tag_layer_value(i, 10) - if kind != 0 && pixel == (bottom ? 0 : height - 1) { rgb = 0x475263 } - if pardes_tag_layer_value(i, 5) == UInt32(PARDES_TOPBAR_H) && pixel == 0 { rgb = 0x475263 } + if kind != 0 && (bottom ? pixel <= 1 : pixel >= height - 2) { rgb = 0x606f86 } + if pardes_tag_layer_value(i, 5) == UInt32(PARDES_TOPBAR_H) && pixel == 0 { rgb = 0x606f86 } try check(x: x, y: top + (CGFloat(pixel) + 0.5) / scale, rgb: rgb, name: "tag \(i), pixel \(pixel)") } @@ -656,7 +656,7 @@ private final class Driver: PardesViewDelegate { guard let c = sample(rep, x: Int(x * scale), y: Int(y * scale)) else { throw ScriptError(message: "context: no separator pixel") } - let rgb: UInt32 = 0x475263 // Forge border, also used by the Linux chrome overlay. + let rgb: UInt32 = 0x606f86 // Forge border, also used by the Linux chrome overlay. guard abs(c.redComponent * 255 - CGFloat((rgb >> 16) & 255)) < 4, abs(c.greenComponent * 255 - CGFloat((rgb >> 8) & 255)) < 4, abs(c.blueComponent * 255 - CGFloat(rgb & 255)) < 4, @@ -1270,6 +1270,10 @@ private func buildWorld(stem: String) throws -> String { // A wall clock cannot live in a golden. Everything that would print one // prints fixed characters instead when this is set. _ = setenv("PARDES_NOTIME", "1", 1) + // The scripts speak to a grid that HAS the workspace tag row: the app + // hands that row to its native menu bar, but this harness is the + // topbar's test bench, so opt back in before pardes_init reads it. + _ = setenv("PARDES_WORKSPACE_TAG", "1", 1) guard fm.changeCurrentDirectoryPath(work) else { throw ScriptError(message: "could not chdir into \(work)") } |
