diff options
27 files changed, 339 insertions, 212 deletions
@@ -21,6 +21,19 @@ pub const macos_min_version: std.SemanticVersion = .{ .major = 13, .minor = 0, . const zls_version = "0.16.1-dev+3e0d0820"; pub const TreeSitterGrammars = enum { disabled, zig, minimal, full }; +/// The GUI shell's shaders, spelled ONCE. Three things read this list: the +/// eight SPIR-V imports the gui module embeds, the glslc runs that produce +/// them, and `zig build shaders`, which writes the same set into the tracked +/// shaders/prebuilt/. Each name is `shaders/<name>.glsl`, and the `.vert`/ +/// `.frag` in it is also the glslc shader stage — adding a shader is adding a +/// name here plus the @embedFile in src/gui/gui.zig. +const gui_shaders = [_][]const u8{ + "ui.vert", "ui.frag", + "overlay.vert", "overlay.frag", + "image.vert", "image.frag", + "crt.vert", "crt.frag", +}; + pub fn build(b: *std.Build) void { const platform = b.option(Platform, "platform", "which shell to build (tty, gui, web, macos)") orelse .tty; // Default target is the Steam Deck (deckcap's trick): x86_64 linux-gnu @@ -88,6 +101,20 @@ pub fn build(b: *std.Build) void { // the machine that built it and needs no keychain; a Developer ID here is // what makes one launchable on someone else's. See the macos branch below. const macos_identity = b.option([]const u8, "macos-identity", "codesigning identity for pardes.app (default: ad-hoc)") orelse "-"; + // Shader compilation is the one build input that needs a tool nothing else + // here needs: glslc, which ships with the Vulkan SDK / shaderc and is not + // on a stock machine. It is also the input that changes least often, so + // -Dprebuilt-shaders decouples the two: the SPIR-V compiled from + // shaders/*.glsl is committed under shaders/prebuilt/, and this flag + // embeds that copy instead of shelling out. `-Dplatform=gui` then builds + // with nothing but a C toolchain. + // + // Off by default because it trades a dependency for a freshness problem: + // with the flag on, the .glsl sources are NOT build inputs, so editing one + // changes nothing until someone runs `zig build shaders` (see below). The + // default therefore stays the honest one — compile the shaders that are + // actually in the tree. + const prebuilt_shaders = b.option(bool, "prebuilt-shaders", "embed the committed shaders/prebuilt/*.spv instead of running glslc (-Dplatform=gui without a Vulkan SDK)") orelse false; // The browser shell is a freestanding wasm core plus ordinary web files. // JavaScript owns the loop and IO; HTML/CSS own rendering. @@ -102,6 +129,19 @@ pub fn build(b: *std.Build) void { if (!enable_mupdf) pdf_sections_bench_step.dependOn(&b.addFail("pdf-sections-bench is unavailable with -Dmupdf=false").step); + // The other half of -Dprebuilt-shaders: recompile every shader and write + // the SPIR-V back into shaders/prebuilt/, which is TRACKED rather than + // generated-and-gitignored, because a machine without glslc cannot + // regenerate it. Deliberately independent of -Dplatform: whoever changes a + // shader runs `zig build shaders` on a machine that has the compiler and + // commits the diff (`jj diff shaders/prebuilt` says whether the cache had + // drifted), and no gui build is involved either way. + const shaders_step = b.step("shaders", "recompile shaders/*.glsl into the tracked shaders/prebuilt/*.spv (glslc; refreshes what -Dprebuilt-shaders embeds)"); + const update_shaders = b.addUpdateSourceFiles(); + for (gui_shaders) |name| + update_shaders.addCopyFileToSource(compileGlsl(b, name), b.fmt("shaders/prebuilt/{s}.spv", .{name})); + shaders_step.dependOn(&update_shaders.step); + // Web LOOK has a tiny read-only source filesystem. Generate one Zig module // on the host from every tracked .zig path + its current working-tree // contents. The generator prefers Git as the tracked-file filter and has a @@ -610,29 +650,11 @@ pub fn build(b: *std.Build) void { root_mod.addAnonymousImport("AdwaitaMono-Regular.ttf", .{ .root_source_file = b.path("assets/AdwaitaMono-Regular.ttf"), }); - root_mod.addAnonymousImport("ui.vert.spv", .{ - .root_source_file = compileGlsl(b, "shaders/ui.vert.glsl", "vertex", "ui.vert.spv"), - }); - root_mod.addAnonymousImport("ui.frag.spv", .{ - .root_source_file = compileGlsl(b, "shaders/ui.frag.glsl", "fragment", "ui.frag.spv"), - }); - root_mod.addAnonymousImport("overlay.vert.spv", .{ - .root_source_file = compileGlsl(b, "shaders/overlay.vert.glsl", "vertex", "overlay.vert.spv"), - }); - root_mod.addAnonymousImport("overlay.frag.spv", .{ - .root_source_file = compileGlsl(b, "shaders/overlay.frag.glsl", "fragment", "overlay.frag.spv"), - }); - root_mod.addAnonymousImport("image.vert.spv", .{ - .root_source_file = compileGlsl(b, "shaders/image.vert.glsl", "vertex", "image.vert.spv"), - }); - root_mod.addAnonymousImport("image.frag.spv", .{ - .root_source_file = compileGlsl(b, "shaders/image.frag.glsl", "fragment", "image.frag.spv"), - }); - root_mod.addAnonymousImport("crt.vert.spv", .{ - .root_source_file = compileGlsl(b, "shaders/crt.vert.glsl", "vertex", "crt.vert.spv"), - }); - root_mod.addAnonymousImport("crt.frag.spv", .{ - .root_source_file = compileGlsl(b, "shaders/crt.frag.glsl", "fragment", "crt.frag.spv"), + for (gui_shaders) |name| root_mod.addAnonymousImport(b.fmt("{s}.spv", .{name}), .{ + .root_source_file = if (prebuilt_shaders) + b.path(b.fmt("shaders/prebuilt/{s}.spv", .{name})) + else + compileGlsl(b, name), }); } @@ -1180,13 +1202,14 @@ fn reindexed(b: *std.Build, archive: std.Build.LazyPath, index: usize) std.Build return run.addOutputFileArg(b.fmt("{d}.a", .{index})); } -// glslc <src> -fshader-stage=<stage> -o <out> -> .spv, returned as a LazyPath -// for @embedFile. Only used by the SDL3 GPU shell (-Dplatform=gui). -fn compileGlsl(b: *std.Build, src: []const u8, stage: []const u8, out: []const u8) std.Build.LazyPath { +// glslc shaders/<name>.glsl -fshader-stage=<vertex|fragment> -o <name>.spv, +// returned as a LazyPath for @embedFile. The stage is the name's own suffix. +fn compileGlsl(b: *std.Build, name: []const u8) std.Build.LazyPath { + const stage = if (std.mem.endsWith(u8, name, ".vert")) "vertex" else "fragment"; const cmd = b.addSystemCommand(&.{ "glslc", b.fmt("-fshader-stage={s}", .{stage}) }); - cmd.addFileArg(b.path(src)); + cmd.addFileArg(b.path(b.fmt("shaders/{s}.glsl", .{name}))); cmd.addArg("-o"); - return cmd.addOutputFileArg(out); + return cmd.addOutputFileArg(b.fmt("{s}.spv", .{name})); } /// The theme sources, sorted. SORTED because the run step is cached by its diff --git a/docs/design.typ b/docs/design.typ index ff013f49..bd8b767c 100644 --- a/docs/design.typ +++ b/docs/design.typ @@ -253,8 +253,12 @@ zig-tree-sitter + 26 grammars — all pinned through `zig fetch` and wired in `build.zig`. Generated during the build: `highlights.scm` → an options module; the vendored helix/zed theme sources → the generated half of the theme ring; the tracked `.zig` sources → -the web shell's read-only archive; eight GLSL shaders → SPIR-V. The tutor and -the embedded font are plain `@embedFile`s, not codegen. Debug builds are +the web shell's read-only archive; eight GLSL shaders → SPIR-V. That last one is +the only build input wanting a tool a stock machine lacks (`glslc`), so it is +also the only one whose output is committed: `zig build shaders` writes +`shaders/prebuilt/*.spv` and `-Dprebuilt-shaders` embeds that copy rather than +shelling out, which is what lets a gui build need nothing but a C toolchain. The +tutor and the embedded font are plain `@embedFile`s, not codegen. Debug builds are incremental for the seconds-loop; release builds are the product. = What is deliberately absent diff --git a/docs/helix-keys.md b/docs/helix-keys.md index eafe9a9c..611e7761 100644 --- a/docs/helix-keys.md +++ b/docs/helix-keys.md @@ -98,7 +98,7 @@ language-backend queries, and the shell pipe. | `Space` (normal, body) | the pardes LEADER: a key path from here runs a BUILTIN with no arguments — the same builtins the topbar and the tags hold. `SPC k` Kill, `SPC d` Del, `SPC f s` Save, `SPC f n` New (an empty temporary file focused in the calling pane's column), `SPC f f` Find (fd over the pane's directory, results into `+Search`), `SPC f g` Grep (grep -R over the CONTENTS of every pane's directory, the ones another pane already covers dropped, rows relative to the asking pane's own directory, results into `+Search`; like Find it takes an ARGUMENT — `Grep foo` executed, or a selection chorded onto the word, searches that and skips the input), `SPC h t` Tutor, `SPC c n`/`SPC c d` Newcol/Delcol, `SPC y/Y/p/P/R` the five clipboard words (next row), `SPC t d/c/n/r` Debug/Colors/NextColor/Crt, `SPC t t` ThemeSel (the whole theme ring as `Theme <name>` rows in an output buffer — rows that are COMMANDS rather than places, so a theme is previewed by EXECUTING one, Tab or a middle click; `n`/`N` do not run them, they step past to the next look-able text on the ring), `SPC t p/l/a` Petscii/Palette/Ascii (an image pane's renderer toggles — the words its tag used to spell out, so the bar stays a bar), `SPC s d`/`SPC s r` Dump/Restore, `SPC w h/j/k/l` Left/Down/Up/Right (directional pane focus — the `Ctrl-w` prefix's four moves as builtins), `SPC j o`/`SPC j i`/`SPC j j`/`SPC j l` Back/Forward/Last/Jumplist (the jump stack: walk it back, walk it forward, hop to the pane before this one — what body-normal Esc runs — and read the stack as a clickable buffer). `?` at ANY depth opens the Help builtin listing what the prefix can still reach (`SPC ?` = all, `SPC h ?` = the docs group), into a `+Help` output buffer. The pending path shows at the right edge of the active pane's tag; Esc — or any unmapped key — abandons it | helix spends Space on pickers/LSP (section C); pardes has neither, and acme's builtins are what a leader is for. Enum-derived: the paths, the Help lines and the execute dispatch all fold out of one `Builtin` enum at comptime. Normal mode on a BODY only — tags are always insert, tty keys belong to the program | pardes-specific | | `SPC y` `SPC Y` `SPC p` `SPC P` `SPC R` | helix's clipboard menu on helix's own letters: yank the selection to the system clipboard (`ClipYank`) or the PRIMARY selection alone (`ClipYankMain`), paste the system clipboard after (`ClipPaste`) / before (`ClipPasteBefore`) the selection, replace the selection with it (`ClipReplace`) | the ONLY five words in pardes that touch the desktop's clipboard — `y`/`d`/`c`/`p`/`P`/`R` and the acme cut/paste chords are the internal register alone. Builtins rather than bare chords because a leader path names a builtin: they land in Help's index and are executable words like every other verb. One divergence: pardes keeps a single register VALUE where helix keeps one per range, so `SPC y` at N cursors joins them with newlines (`Pardes.setYank`, the divergence `msel-yank-paste` already waives). On a tty the write is OSC 52 out and the READ is OSC 52 back, which many terminals refuse or gate — so `SPC y` works there and `SPC p` can be a no-op | out of corpus | | a paste from the OUTER terminal | one `Event.paste`, spliced in at the cursor | the tty shell enables bracketed paste and coalesces `paste_start`..`paste_end` into a single event; before that the bytes arrived as individual key presses and normal mode RAN them, which is how a pasted `d` deleted a line. The bytes deliberately never enter the yank register — clipboard and default register are separate stores in both directions | pardes-specific | -| `/` (any pane) | pardes' own plain-substring search into a `+Search` output buffer: the tag takes the pattern, Enter fills the buffer, and its rows are ordinary look targets | KEEP, do not touch; not in the corpus (helix `/` is regex search). The stepping half is the next row | pardes-specific | +| `/` (any pane) | pardes' own plain-substring search into a `+Search` output buffer: the tag takes the pattern, Enter fills the buffer, and its rows are ordinary look targets. Enter also GOES to the first row — the buffer is focused and then the step `n` is and the look Enter is run in it (`Pardes.lookFirstHit`), so `/foo` lands on the first hit with the matched span selected. A pattern that matched nothing opens its empty buffer and moves nothing | KEEP, do not touch; not in the corpus (helix `/` is regex search). Find and Grep answer with OTHER files and deliberately do NOT jump. The stepping half is the next row | pardes-specific | | `n` / `N` (any pane) | MOTION, not a jump: move the SELECTION to the next / previous look-able text and open NOTHING. Enter — the look chord — on what it leaves selected is what opens it | KEEP, do not touch; not in the corpus (helix's `n`/`N` walk regex search hits). The text stepped is the largest whitespace-delimited run `look.resolve` can act on (`look.lookableSpan`, wrapper punctuation peeled off both ends), and the walk is a RING across PANES: every pane that has performed a Look, most recent first (`Pardes.look_src`), then the output buffers that have not, newest first, and only when both are empty the active pane. Exhausting a pane enters the next at its first (forward) / last (backward) span and the end wraps to the start, so `N` is the exact inverse of `n`. What it lands on becomes an EXPLICIT `vsel` with the cursor on its FIRST column, in the pane the walk focuses. ONE motion in every pane kind and every buffer kind — a PDF steps the `+Search` buffer its own search filled, `n` to select the row and Enter to jump. The single thing a buffer may change is the GRAIN of what a step selects, and it changes it with one flag rather than a branch: `output_pane.Traits.commands` makes a step take the WHOLE LINE, because a command list (`ThemeSel`/`FontSel`) holds words to run and there is no path inside `Theme gruvbox` to pick out. Tab on what `n` selected wears the theme, which is the same middle click on the row is. `]d`/`[d` are helix's diagnostic motions, a different binding, and they do still jump to each diagnostic (`docs/lsp.md`) | pardes-specific | | insert: printable text | file: real edit; terminal: typed run splice | | helix-verified (file) | | insert: `Enter` | newline; keeps the current full indent levels and adds one 4-space logical tab when the text before the cursor ends in `(`, `[`, `{`, or `)` (including `})`) | plain lines match `insert_newline`; delimiter heuristic is pardes-specific | helix-verified (plain) / pardes-specific (delimiter) | diff --git a/shaders/prebuilt/crt.frag.spv b/shaders/prebuilt/crt.frag.spv Binary files differnew file mode 100644 index 00000000..326aa534 --- /dev/null +++ b/shaders/prebuilt/crt.frag.spv diff --git a/shaders/prebuilt/crt.vert.spv b/shaders/prebuilt/crt.vert.spv Binary files differnew file mode 100644 index 00000000..8a75ebb3 --- /dev/null +++ b/shaders/prebuilt/crt.vert.spv diff --git a/shaders/prebuilt/image.frag.spv b/shaders/prebuilt/image.frag.spv Binary files differnew file mode 100644 index 00000000..84a174ad --- /dev/null +++ b/shaders/prebuilt/image.frag.spv diff --git a/shaders/prebuilt/image.vert.spv b/shaders/prebuilt/image.vert.spv Binary files differnew file mode 100644 index 00000000..1105bae2 --- /dev/null +++ b/shaders/prebuilt/image.vert.spv diff --git a/shaders/prebuilt/overlay.frag.spv b/shaders/prebuilt/overlay.frag.spv Binary files differnew file mode 100644 index 00000000..59bbb9d4 --- /dev/null +++ b/shaders/prebuilt/overlay.frag.spv diff --git a/shaders/prebuilt/overlay.vert.spv b/shaders/prebuilt/overlay.vert.spv Binary files differnew file mode 100644 index 00000000..a518c576 --- /dev/null +++ b/shaders/prebuilt/overlay.vert.spv diff --git a/shaders/prebuilt/ui.frag.spv b/shaders/prebuilt/ui.frag.spv Binary files differnew file mode 100644 index 00000000..8d57242a --- /dev/null +++ b/shaders/prebuilt/ui.frag.spv diff --git a/shaders/prebuilt/ui.vert.spv b/shaders/prebuilt/ui.vert.spv Binary files differnew file mode 100644 index 00000000..186a91fc --- /dev/null +++ b/shaders/prebuilt/ui.vert.spv diff --git a/src/builtins.zig b/src/builtins.zig index 8b3d44d0..bb1a1273 100644 --- a/src/builtins.zig +++ b/src/builtins.zig @@ -202,6 +202,36 @@ pub const Restore = struct { } }; +// ---- the message row ---- + +/// TEXT onto this pane's transient message row — the row a failed save, a +/// refused Look and a language server that would not start all report through +/// (Pardes.setMessage, and Pardes.reportError one line above it). Every writer +/// of that row is something going wrong, so until this word there was no way +/// to look at it without breaking something on purpose: no wording could be +/// checked against a narrow pane, and no test could pin the row without +/// arranging a real failure first. +/// +/// Bare, it reports ITSELF through the error path, because that is the other +/// half of the same machinery — `reportError` is `setMessage` plus an +/// `<operation>: <Error>` — and because a word that needs no argument to +/// demonstrate one is a word you can also just click. +/// +/// Whether the row is FREE is not asked here and is not this word's business: +/// an armed prompt outranks a message at render time, so posting under one is +/// stored and invisible, exactly as a save finishing under one is. Nor does +/// anything here decide when it goes away — your next key or click does, on +/// every pane at once, because a message is exactly as old as your last input. +pub const Msg = struct { + pub const takes_arg = true; + pub fn run(c: Ctx) void { + if (c.arg) |text| + c.p.setMessage(c.id, text) + else + c.p.reportError(c.id, comptime word(@This()), error.NoMessage); + } +}; + // ---- display toggles ---- pub const Debug = struct { @@ -357,6 +387,7 @@ pub const Font = struct { if (path.len > fonts.want_buf.len) return; @memcpy(fonts.want_buf[0..path.len], path); fonts.want = fonts.want_buf[0..path.len]; + fonts.chosen = std.fs.path.stem(fonts.want.?); } }; diff --git a/src/config.zig b/src/config.zig index 107eb1a7..de971db9 100644 --- a/src/config.zig +++ b/src/config.zig @@ -136,6 +136,11 @@ pub const leader_path = paths: { .Newcol = "cn", .Delcol = "cd", .Debug = "td", + // `Msg` takes the text to post, so it has no path, for the reason + // `Theme` and the two acme verbs below have none: a key path names a + // builtin and can never carry an argument. Bare `Msg` still runs — it + // reports itself through the error path. + .Msg = null, .Colors = "tc", .Wrap = "tw", .Tagbottom = "tb", diff --git a/src/fonts.zig b/src/fonts.zig index e4744daf..1e82956e 100644 --- a/src/fonts.zig +++ b/src/fonts.zig @@ -70,6 +70,17 @@ pub const Font = struct { name: []const u8, path: []const u8 }; pub var want_buf: [4096]u8 = undefined; pub var want: ?[]const u8 = null; +/// ...and the same request as a NAME, for the Debug overlay to show. A slice +/// of `want_buf` rather than a second buffer: the stem of the path IS the name +/// the picker listed, and the only writer of one is the writer of the other. +/// +/// What was CHOSEN, not what is worn: the shell may turn a file down (gui.zig +/// keeps the face that works when FreeType refuses one) and it alone knows +/// what it booted in — embedded Adwaita Mono under SDL, the system monospace +/// face under AppKit. Empty means nothing has been chosen and the shell is +/// still wearing whichever of those it is. +pub var chosen: []const u8 = ""; + /// Optional fallback faces, in preference order after the always-available /// embedded Adwaita Mono face. Keep text faces before symbol faces so ordinary /// letters retain terminal-like metrics; DejaVu Sans is the final broad, diff --git a/src/pardes.zig b/src/pardes.zig index da6e540c..3db27b86 100644 --- a/src/pardes.zig +++ b/src/pardes.zig @@ -51,6 +51,14 @@ pub const platform: Platform = @field(Platform, @tagName(@import("pardes_config" /// disabled so much as meaningless — see builtins.zig. pub const font_picker = platform == .gui or platform == .macos; +/// ...and the one fact about that face the core keeps: the name `Font` last +/// resolved, which the Debug overlay prints. Behind the same comptime shim +/// builtins.zig and macos.zig import this file with, so a tty or web binary +/// never analyses a font-directory walk it cannot use. +const fonts = if (font_picker) @import("fonts.zig") else struct { + pub const chosen: []const u8 = ""; +}; + /// Native PDF quality is a shell property, but the core owns MuPDF and the /// RGBA cache. Kitty favors wire bandwidth; SDL favors physical-pixel text /// quality and asks the renderer to cover either fit axis without upscaling. @@ -2220,6 +2228,71 @@ test "New requests repeat, stay anchored, and reject stale or full completions" try std.testing.expect(std.mem.indexOf(u8, p.panes[source].?.msg[0..p.panes[source].?.msg_len], "help: NoPaneSlots") != null); } +test "Msg writes the transient row by hand, bare or with text, and input ends it" { + const p = try Pardes.init(std.testing.allocator, .{ .shells = 3, .cols = 100, .rows = 30 }); + defer p.deinit(); + while (p.nextEffect()) |_| {} + const row = struct { + fn of(pp: *Pardes, i: usize) []const u8 { + const pane = pp.panes[i].?; + return pane.msg[0..pane.msg_len]; + } + }.of; + + // the whole tail, spaces and all: the row is a sentence, not a word + try std.testing.expect(p.executeBuiltinLine(2, "Msg saved /etc/hosts")); + try std.testing.expectEqualStrings("saved /etc/hosts", row(p, 2)); + // ...on the pane that ran it, and nowhere else + try std.testing.expectEqualStrings("", row(p, 0)); + + // Bare, it reports ITSELF through reportError — the `<operation>: <Error>` + // every failed save and refused Look arrives in. + try std.testing.expect(p.executeBuiltinLine(2, "Msg")); + try std.testing.expectEqualStrings("Msg: NoMessage", row(p, 2)); + + // A message is exactly as old as your last input. + p.update(.{ .key = .{ .cp = Key.escape } }); + try std.testing.expectEqualStrings("", row(p, 2)); + + // Longer than the row's buffer is CUT, never refused and never overrun: + // this word is the only way to reach that boundary without a 300-byte path. + var long: [600]u8 = @splat('x'); + var line: [608]u8 = undefined; + const cmd = try std.fmt.bufPrint(&line, "Msg {s}", .{&long}); + try std.testing.expect(p.executeBuiltinLine(2, cmd)); + try std.testing.expectEqual(p.panes[2].?.msg.len, row(p, 2).len); +} + +test "Font records the face it chose, which is the name the Debug overlay prints" { + if (comptime !font_picker) return; // there is no Font builtin on tty or web + const p = try Pardes.init(std.testing.allocator, .{ .cols = 80, .rows = 24 }); + defer p.deinit(); + var arena: std.heap.ArenaAllocator = .init(std.testing.allocator); + defer arena.deinit(); + const installed = fonts.list(arena.allocator(), null); + if (installed.len == 0) return; // a machine with no monospace face at all + + // module state, and the shell takes it on its next pass: leave neither the + // request nor the name behind for whatever test runs after this one + defer { + fonts.want = null; + fonts.chosen = ""; + } + var line: [512]u8 = undefined; + const cmd = try std.fmt.bufPrint(&line, "Font {s}", .{installed[0].name}); + try std.testing.expect(p.executeBuiltinLine(p.active, cmd)); + // the PATH is what the shell needs and the NAME is what a person reads; + // one is the other's stem, so the two can never name different files + try std.testing.expect(fonts.want != null); + try std.testing.expectEqualStrings(installed[0].name, fonts.chosen); + try std.testing.expectEqualStrings(fonts.chosen, std.fs.path.stem(fonts.want.?)); + + // ...and a name nothing answers to changes neither, so the overlay goes on + // naming the face that is actually on + try std.testing.expect(p.executeBuiltinLine(p.active, "Font zzz-no-such-face")); + try std.testing.expectEqualStrings(installed[0].name, fonts.chosen); +} + test "startup config overrides restored styling before initFromDump returns" { const source = try Pardes.init(std.testing.allocator, .{ .startup_config = "Theme dark\n" }); defer source.deinit(); @@ -7824,7 +7897,44 @@ pub const Pardes = struct { .grep else .text; - p.runSearch(id, armed[slash + 1 ..], kind, .top) catch |err| p.reportError(id, "search", err); + p.runSearch(id, armed[slash + 1 ..], kind, .top) catch |err| return p.reportError(id, "search", err); + // ...and the bare `/` GOES there. Find and Grep answer with OTHER + // files, and opening the first of them on submit would rearrange the + // screen before you have read what was found; `/` searched the text + // already in front of you, so its first hit is a scroll, not a jump. + if (kind == .text) p.lookFirstHit(id); + } + + /// Land ON the first hit of a `/`, instead of beside a list of them. + /// + /// No new motion: the results buffer is FOCUSED and then the two ordinary + /// verbs run in the order a hand would run them — the step `n` is, and the + /// look Enter is. Focusing is the part that cannot be skipped and the + /// reason this is not just a call to the walk: lookWalkPanes deliberately + /// leaves the ACTIVE pane out of the ring, so `n` pressed straight after a + /// search steps whichever list was looked most recently, which in a + /// session with any history at all is not the one that just answered. + /// + /// Everything the walk needs to be reversible from here is left by the + /// step itself (landLookSpot's `look_at`), so `N` afterwards goes back to + /// the row above exactly as it would have if you had pressed `n` yourself. + fn lookFirstHit(p: *Pardes, id: usize) void { + const pane = p.panes[id] orelse return; + const rid = pane.search_pane orelse return; + const rp = p.panes[rid] orelse return; + // Nothing matched: the empty buffer is the answer, and aiming the walk + // at a list with no rows would send it round the ring into a NEIGHBOUR + // and open a row from some other search. + if ((rp.file orelse return).content.len == 0) return; + p.active = rid; + p.lookWalk(1); + if (p.active != rid) return; // walked out of the list; leave it there + const txt = p.currentSelText(rp) orelse return; + // spent, exactly as the look chord spends it (handleKey) + rp.vsel.active = false; + rp.msel.active = false; + rp.select = false; + p.runBuiltin(config.look_cmd, rid, "", txt); } /// Fill this pane's results buffer with everything matching `pat_raw` @@ -13373,14 +13483,21 @@ pub const Pardes = struct { sb_off = sb.offset; sb_total = sb.total; } - var ov_buf: [192]u8 = undefined; + // The face belongs to the SHELL, so the core can only name the one + // it was asked for; nothing has asked when this is empty and the + // shell is still in whatever it booted in. Clamped because a name + // is a file stem and a path component can be as long as a path. + const font_name = if (fonts.chosen.len == 0) "default" else fonts.chosen; + var ov_buf: [256]u8 = undefined; const text = std.fmt.bufPrint( &ov_buf, - "pardes resizes={d}\nterms {d} cols {d}\nactive #{d} {d}x{d}\nscroll {d}/{d}\npinch {d:6.4}\ntscroll {d:7.4}", - .{ p.resize_count, nlive, p.ncol, p.active, at.cols, at.rows, sb_off, sb_total, p.ov_pinch_scale, p.ov_touch_scroll_delta }, + "pardes resizes={d}\ntheme {s}\nfont {s}\nterms {d} cols {d}\nactive #{d} {d}x{d}\nscroll {d}/{d}\npinch {d:6.4}\ntscroll {d:7.4}", + .{ p.resize_count, themes[p.theme_idx].name, font_name[0..@min(font_name.len, 64)], nlive, p.ncol, p.active, at.cols, at.rows, sb_off, sb_total, p.ov_pinch_scale, p.ov_touch_scroll_delta }, ) catch ""; - const ow: u16 = 30; - const oh: u16 = 8; + // wide enough for the longest theme name compiled in, which is the + // only row here whose width is not ours to choose + 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) { // vaxis single_rounded border ring, default style diff --git a/src/tutor.txt b/src/tutor.txt index 9c73e5c9..c2db239d 100644 --- a/src/tutor.txt +++ b/src/tutor.txt @@ -195,14 +195,17 @@ — a file's text, a shell's scrollback, anything — for the pattern, plain and case-insensitive, and writes one row per matching LINE into a "+Search" pane — the FIRST hit on that line, named as a location, with - the line's own text after it. + the line's own text after it — and then GOES to the first of those rows. + That last part is nothing new to learn: it is the step `n` is and the + look Enter is, run for you in the list that has just answered. A pattern + that matched nothing opens its empty buffer and goes nowhere. That is an OUTPUT BUFFER: a file pane with no file behind it, so it has no Save, but everything else about it is an ordinary buffer you can read, edit, select and look in. It is not a document, though, and never takes a column of its own: it opens BELOW the pane that asked for it, in that pane's column, be that a file or a shell — and a file you then open from - its rows goes where files go, not under the list. `n`/`N` walk those - rows, and walking is ALL they do: a press moves the SELECTION to the + its rows goes where files go, not under the list. `n`/`N` carry on down + those rows, and walking is ALL they do: a press moves the SELECTION to the next look-able thing and opens nothing, so you can step past nine hits to reach the tenth without opening the nine. Enter — the look chord — opens the one you stopped on. The walk is a RING over PANES and not diff --git a/test/snapshots/builtins.golden b/test/snapshots/builtins.golden index 61d7bec1..113141d8 100644 --- a/test/snapshots/builtins.golden +++ b/test/snapshots/builtins.golden @@ -153,7 +153,6 @@ | | | /tmp/pardes-snap/builtins/cwd/+Help New Del -| 31 SPC l s Symbols | 32 SPC l w Lspwhy | 33 SPC p ClipPaste | 34 SPC s d Dump topbar @@ -178,9 +177,10 @@ | 53 SPC y ClipYank | 54 Look enter, right-click | 55 Exec tab, middle-click -| 56 Theme -| 57 Shell -| 58 +| 56 Msg +| 57 Theme +| 58 Shell +| 59 == snap not-a-picker grid=120x60 cursor=7,58 |New Newcol Find Grep Help Tutor Dump NextColor Debug Kill | /tmp/pardes-snap/builtins/cwd/notes.txt Save New Del @@ -214,7 +214,6 @@ | | | /tmp/pardes-snap/builtins/cwd/+Help New Del -| 31 SPC l s Symbols | 32 SPC l w Lspwhy | 33 SPC p ClipPaste | 34 SPC s d Dump topbar @@ -239,9 +238,10 @@ | 53 SPC y ClipYank | 54 Look enter, right-click | 55 Exec tab, middle-click -| 56 Theme -| 57 Shell -| 58 +| 56 Msg +| 57 Theme +| 58 Shell +| 59 == snap window-group grid=120x60 cursor=7,32 |New Newcol Find Grep Help Tutor Dump NextColor Debug Kill | /tmp/pardes-snap/builtins/cwd/notes.txt Save New Del diff --git a/test/snapshots/fsearch.golden b/test/snapshots/fsearch.golden index bbe7df83..317e7b5e 100644 --- a/test/snapshots/fsearch.golden +++ b/test/snapshots/fsearch.golden @@ -60,7 +60,7 @@ | | | /MARK -== snap results grid=180x30 cursor=7,2 +== snap results grid=180x30 cursor=10,3 |New Newcol Find Grep Help Tutor Dump NextColor Debug Kill | /tmp/pardes-snap/fsearch/cwd/g.txt Save New Del /tmp/pardes-snap/fsearch/cwd New Del /tmp/pardes-snap/fsearch/cwd New Del | 1 l1 ls ls @@ -91,38 +91,7 @@ | 1 g.txt:2:1-4 MARK a | 2 g.txt:6:1-4 MARK b | 3 g.txt:10:1-4 MARK c -== snap first grid=180x30 cursor=10,3 -|New Newcol Find Grep Help Tutor Dump NextColor Debug Kill -| /tmp/pardes-snap/fsearch/cwd/g.txt Save New Del /tmp/pardes-snap/fsearch/cwd New Del /tmp/pardes-snap/fsearch/cwd New Del -| 1 l1 ls ls -| 2 MARK a g.txt g.txt -| 3 l3 -| 4 l4 -| 5 l5 -| 6 MARK b g.txt -| 7 l7 -| 8 l8 -| 9 l9 -| 10 MARK c -| 11 l11 -| 12 l12 -| -| -| /tmp/pardes-snap/fsearch/cwd New Del -| ls -| g.txt -| -| -| -| -| -| -| -| /tmp/pardes-snap/fsearch/cwd/+Search New Del -| 1 g.txt:2:1-4 MARK a -| 2 g.txt:6:1-4 MARK b -| 3 g.txt:10:1-4 MARK c -== style first +== style results |0: 0-56 #949494,#121212, 57-179 d,#121212, |1: 0-1 #ffffff,#626262, 2-48 #949494,#121212, 49-59 d,#121212, 60-61 #ffffff,#262626, 62-99 #949494,#121212, 100-119 d,#121212, 120-121 #ffffff,#262626, 122-159 #949494,#121212, 160-179 d,#121212, |2: 0-0 d,#444444, 1-1 d,#080808, 2-6 #626262,#080808, 7-8 #bdbdbd,#080808, 9-59 d,#080808, 60-60 d,#444444, 61-61 d,#080808, 62-119 #bdbdbd,#080808, 120-120 d,#444444, 121-121 d,#080808, 122-179 #bdbdbd,#080808, diff --git a/test/snapshots/fsearch.snap b/test/snapshots/fsearch.snap index b9b7ddf4..3752909b 100644 --- a/test/snapshots/fsearch.snap +++ b/test/snapshots/fsearch.snap @@ -1,11 +1,13 @@ # `/` file search: the pattern types into the tag (persistent, no popup); # Enter searches the buffer in-core and writes `path:LINE:COL-ENDCOL text` into # a +Search OUTPUT BUFFER (a file pane with no file behind it — no Save in its -# tag); n/N SELECT the next/prev row there and open nothing, and Enter — the -# look chord — is what scrolls the file pane to the hit AND SELECTS IT: the row -# names the match's whole span, not just where it starts. Esc abandons the -# input; a no-match search opens an EMPTY buffer (every search gets its own, -# stacked under the pane) with nothing in it for n to stop on. +# tag) — AND GOES TO THE FIRST HIT, which is the step `n` is and the look Enter +# is, run for you in that buffer. From there n/N SELECT the next/prev row and +# open nothing, and Enter — the look chord — is what scrolls the file pane to +# the hit AND SELECTS IT: the row names the match's whole span, not just where +# it starts. Esc abandons the input; a no-match search opens an EMPTY buffer +# (every search gets its own, stacked under the pane) with nothing in it for n +# to stop on, and goes nowhere. file g.txt l1\nMARK a\nl3\nl4\nl5\nMARK b\nl7\nl8\nl9\nMARK c\nl11\nl12 start 30 180 -n 3 wait 8000 g.txt @@ -33,15 +35,13 @@ snap typing key enter wait 10000 g.txt:2:1-4 MARK stable 700 10000 +# ...and the submit already looked result 1: the file scrolled to line 2 and +# the four cells the pattern matched there came up selected — the styles below +# are what prove the second half of that. No `n` was pressed to get here. snap results -# n selects result 1 and Enter looks it: the file scrolls to line 2 and the -# four cells the pattern matched there come up selected — the styles below are -# what prove the second half of that -key n -key enter -stable 700 15000 -snap first -snapstyle first +snapstyle results +# n/N walk on from there, one row of the list per press, Enter looking what +# the step selected key n key enter stable 700 15000 diff --git a/test/snapshots/lookrange.golden b/test/snapshots/lookrange.golden index d1fc0bea..8ed06206 100644 --- a/test/snapshots/lookrange.golden +++ b/test/snapshots/lookrange.golden @@ -277,38 +277,7 @@ |27: 0-0 d,#444444, 1-1 d,#080808, 2-6 #626262,#080808, 7-149 d,#080808, |28: 0-0 d,#444444, 1-1 d,#080808, 2-6 #626262,#080808, 7-149 d,#080808, |29: 0-0 d,#444444, 1-1 d,#080808, 2-6 #626262,#080808, 7-149 d,#080808, -== snap results grid=150x30 cursor=10,24 -|New Newcol Find Grep Help Tutor Dump NextColor Debug Kill -| /tmp/pardes-snap/lookrange/cwd/look.txt Save New Del -| 1 r.txt:3-5 -| 2 r.txt:7:4-9 -| 3 r.txt:3:2-5:3 -| 4 r.txt:8:6-99:9 -| -| -| -| -| -| -| -| -| -| -| /tmp/pardes-snap/lookrange/cwd/r.txt Save New Del -| 2 bbbb -| 3 cccc dccc -| 4 dddd -| 5 eeee -| 6 ffff -| 7 gg NEEDLE gg -| 8 hhhh NEEDLE -| 9 iiii -| -| -| /tmp/pardes-snap/lookrange/cwd/+Search New Del -| 1 r.txt:7:4-9 gg NEEDLE gg -| 2 r.txt:8:6-11 hhhh NEEDLE -== snap hit1 grid=150x30 cursor=15,22 +== snap hit1 grid=150x30 cursor=15,23 |New Newcol Find Grep Help Tutor Dump NextColor Debug Kill | /tmp/pardes-snap/lookrange/cwd/look.txt Save New Del | 1 r.txt:3-5 @@ -326,6 +295,7 @@ | | | /tmp/pardes-snap/lookrange/cwd/r.txt Save New Del +| 1 aaaa | 2 bbbb | 3 cccc dccc | 4 dddd @@ -335,7 +305,6 @@ | 8 hhhh NEEDLE | 9 iiii | -| | /tmp/pardes-snap/lookrange/cwd/+Search New Del | 1 r.txt:7:4-9 gg NEEDLE gg | 2 r.txt:8:6-11 hhhh NEEDLE @@ -357,15 +326,15 @@ |14: 0-0 d,#444444, 1-1 d,#080808, 2-6 #626262,#080808, 7-149 d,#080808, |15: 0-0 d,#444444, 1-1 d,#080808, 2-6 #626262,#080808, 7-149 d,#080808, |16: 0-1 #ffffff,#626262, 2-129 #949494,#121212, 130-149 d,#121212, -|17: 0-0 d,#121212, 1-1 d,#080808, 2-6 #626262,#080808, 7-10 #bdbdbd,#080808, 11-149 d,#080808, -|18: 0-0 d,#444444, 1-1 d,#080808, 2-6 #626262,#080808, 7-15 #bdbdbd,#080808, 16-149 d,#080808, -|19: 0-0 d,#444444, 1-1 d,#080808, 2-6 #626262,#080808, 7-10 #bdbdbd,#080808, 11-149 d,#080808, +|17: 0-0 d,#444444, 1-1 d,#080808, 2-6 #626262,#080808, 7-10 #bdbdbd,#080808, 11-149 d,#080808, +|18: 0-0 d,#444444, 1-1 d,#080808, 2-6 #626262,#080808, 7-10 #bdbdbd,#080808, 11-149 d,#080808, +|19: 0-0 d,#444444, 1-1 d,#080808, 2-6 #626262,#080808, 7-15 #bdbdbd,#080808, 16-149 d,#080808, |20: 0-0 d,#444444, 1-1 d,#080808, 2-6 #626262,#080808, 7-10 #bdbdbd,#080808, 11-149 d,#080808, |21: 0-0 d,#444444, 1-1 d,#080808, 2-6 #626262,#080808, 7-10 #bdbdbd,#080808, 11-149 d,#080808, -|22: 0-0 d,#444444, 1-1 d,#080808, 2-6 #949494,#121212, 7-9 #bdbdbd,#080808, 10-15 #080808,#80a0ff, 16-18 #bdbdbd,#080808, 19-149 d,#080808, -|23: 0-0 d,#444444, 1-1 d,#080808, 2-6 #626262,#080808, 7-17 #bdbdbd,#080808, 18-149 d,#080808, -|24: 0-0 d,#444444, 1-1 d,#080808, 2-6 #626262,#080808, 7-10 #bdbdbd,#080808, 11-149 d,#080808, -|25: 0-0 d,#444444, 1-1 d,#080808, 2-6 #626262,#080808, 7-149 d,#080808, +|22: 0-0 d,#444444, 1-1 d,#080808, 2-6 #626262,#080808, 7-10 #bdbdbd,#080808, 11-149 d,#080808, +|23: 0-0 d,#444444, 1-1 d,#080808, 2-6 #949494,#121212, 7-9 #bdbdbd,#080808, 10-15 #080808,#80a0ff, 16-18 #bdbdbd,#080808, 19-149 d,#080808, +|24: 0-0 d,#444444, 1-1 d,#080808, 2-6 #626262,#080808, 7-17 #bdbdbd,#080808, 18-149 d,#080808, +|25: 0-0 d,#444444, 1-1 d,#080808, 2-6 #626262,#080808, 7-10 #bdbdbd,#080808, 11-149 d,#080808, |26: 0-0 d,#444444, 1-1 d,#080808, 2-6 #626262,#080808, 7-149 d,#080808, |27: 0-1 #ffffff,#262626, 2-129 #949494,#121212, 130-149 d,#121212, |28: 0-0 d,#444444, 1-1 d,#080808, 2-6 #626262,#080808, 7-30 #bdbdbd,#080808, 31-149 d,#080808, diff --git a/test/snapshots/lookrange.snap b/test/snapshots/lookrange.snap index 29f88d7f..b64607a1 100644 --- a/test/snapshots/lookrange.snap +++ b/test/snapshots/lookrange.snap @@ -48,19 +48,17 @@ stable 700 15000 snap stale snapstyle stale # --- and now the first consumer. The look above left r.txt focused, so `/` -# arms on it: the rows it writes carry the match's span, so n selects a row -# and the Enter after it goes to that hit with the word itself selected. +# arms on it: the rows it writes carry the match's span, and the submit goes +# straight to the first of them — with the word itself selected, which is the +# whole reason the row spells a range and not a position. key / text NEEDLE key enter wait 10000 r.txt:7:4-9 gg NEEDLE stable 700 15000 -snap results -key n -key enter -stable 700 15000 snap hit1 snapstyle hit1 +# n selects the next row, and the Enter after it goes to that hit key n key enter stable 700 15000 diff --git a/test/snapshots/msg.golden b/test/snapshots/msg.golden index 77c2678b..ea8a8ee9 100644 --- a/test/snapshots/msg.golden +++ b/test/snapshots/msg.golden @@ -164,3 +164,20 @@ | | | +== snap msgbuiltin grid=70x16 cursor=29,4 +|New Newcol Find Grep Help Tutor Dump NextColor Debug Kill +| /tmp/pardes-snap/msg/cwd/a.txt Save New Del +| 1 AGAIN +| 2 +| 3 Msg hand-written report +| +| +| +| +| +| +| +| +| +| +| hand-written report diff --git a/test/snapshots/msg.snap b/test/snapshots/msg.snap index 640e9a0c..d1371dab 100644 --- a/test/snapshots/msg.snap +++ b/test/snapshots/msg.snap @@ -75,3 +75,17 @@ key esc settle 100 stable 700 15000 snap esc +# Every other writer of this row is something going WRONG, which is a poor way +# to look at a row: `Msg <text>` is the word that writes it on purpose. An +# ordinary builtin, so it runs the ordinary way — write it in the buffer, +# select the line, execute it — and what lands is the text, verbatim, with the +# same stamped-looking left edge as the reports above. (The clock belongs to +# the SHELL, which is why this one has none: the core has no clock.) +key o +text Msg hand-written report +key esc +settle 100 +key x +key tab +stable 700 15000 +snap msgbuiltin diff --git a/test/snapshots/psearch.golden b/test/snapshots/psearch.golden index a3305345..ad8f134b 100644 --- a/test/snapshots/psearch.golden +++ b/test/snapshots/psearch.golden @@ -60,7 +60,7 @@ | | | /MARK -== snap results grid=140x30 cursor=4,8 +== snap results grid=140x30 cursor=7,3 |New Newcol Find Grep Help Tutor Dump NextColor Debug Kill | /tmp/pardes-snap/psearch/cwd New Del | printf 'x MA''RK a\nzz\nx MA''RK b\nzz\nx MA''RK c\n' @@ -91,38 +91,7 @@ | 1 @p0:2:3-6 x MARK a | 2 @p0:4:3-6 x MARK b | 3 @p0:6:3-6 x MARK c -== snap first grid=140x30 cursor=7,3 -|New Newcol Find Grep Help Tutor Dump NextColor Debug Kill -| /tmp/pardes-snap/psearch/cwd New Del -| printf 'x MA''RK a\nzz\nx MA''RK b\nzz\nx MA''RK c\n' -| x MARK a -| zz -| x MARK b -| zz -| x MARK c -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| -| /tmp/pardes-snap/psearch/cwd/+Search New Del -| 1 @p0:2:3-6 x MARK a -| 2 @p0:4:3-6 x MARK b -| 3 @p0:6:3-6 x MARK c -== style first +== style results |0: 0-56 #949494,#121212, 57-139 d,#121212, |1: 0-1 #ffffff,#626262, 2-119 #949494,#121212, 120-139 d,#121212, |2: 0-0 d,#444444, 1-1 d,#080808, 2-139 #bdbdbd,#080808, diff --git a/test/snapshots/psearch.snap b/test/snapshots/psearch.snap index 1b9eac61..beceb806 100644 --- a/test/snapshots/psearch.snap +++ b/test/snapshots/psearch.snap @@ -31,19 +31,16 @@ stable 400 5000 text MARK stable 400 5000 snap typing -# Enter opens the results buffer: three @p0:LINE:COL rows, focus stays here +# Enter opens the results buffer — three @p0:LINE:COL rows — and goes to the +# first of them: the terminal's cursor lands on that scrollback line, on the +# matched SPAN (cols 3..6, past the leading "x ") — not the start of the line, +# and selected rather than merely parked on. No `n` was pressed to get here. key enter wait 10000 @p0:2:3 stable 700 15000 snap results -# n selects result 1 and Enter looks it: the terminal's cursor lands on that -# scrollback line, on the matched SPAN (cols 3..6, past the leading "x ") — -# not the start of the line, and selected rather than merely parked on -key n -key enter -stable 700 15000 -snap first -snapstyle first +snapstyle results +# n walks on to the next row, Enter looks it key n key enter stable 700 15000 diff --git a/test/snapshots/topbarexec.golden b/test/snapshots/topbarexec.golden index bed96fbd..8d15d6c4 100644 --- a/test/snapshots/topbarexec.golden +++ b/test/snapshots/topbarexec.golden @@ -21,16 +21,16 @@ | == snap noarg-runs grid=90x20 cursor=14,3 |New Newcol Find Grep Help Tutor Dump NextColor Debug Kill -| /tmp/pardes-snap/topbarexec/cwd New Del /tmp/pardes-s╭────────────────────────────╮ -| ls ls │pardes resizes=1 │ -| zmarkfile.txt zmarkfile.txt│terms 3 cols 2 │ -| │active #0 43x9 │ -| │scroll 0/9 │ -| │pinch 1.0000 │ -| │tscroll 0.0000 │ -| ╰────────────────────────────╯ -| -| +| /tmp/pardes-snap/topbarexec/cwd New Del /tmp/╭────────────────────────────────────╮ +| ls ls │pardes resizes=1 │ +| zmarkfile.txt zmark│theme helix │ +| │font default │ +| │terms 3 cols 2 │ +| │active #0 43x9 │ +| │scroll 0/9 │ +| │pinch 1.0000 │ +| │tscroll 0.0000 │ +| ╰────────────────────────────────────╯ | /tmp/pardes-snap/topbarexec/cwd New Del | ls | zmarkfile.txt diff --git a/test/snapshots/windowops.golden b/test/snapshots/windowops.golden index 4101fa86..2ec3e46f 100644 --- a/test/snapshots/windowops.golden +++ b/test/snapshots/windowops.golden @@ -279,16 +279,16 @@ | == snap debug-on grid=100x30 cursor=44,4 |New Newcol Find Grep Help Tutor Dump NextColor Debug Kill -| /tmp/pardes-snap/windowops/cwd New Del /tmp/pardes-snap/windowops/c╭────────────────────────────╮ -| ls ls │pardes resizes=1 │ -| w.txt w.txt │terms 4 cols 2 │ -| │active #2 58x28 │ -| │scroll 0/28 │ -| │pinch 1.0000 │ -| │tscroll 0.0000 │ -| ╰────────────────────────────╯ -| -| +| /tmp/pardes-snap/windowops/cwd New Del /tmp/pardes-snap/win╭────────────────────────────────────╮ +| ls ls │pardes resizes=1 │ +| w.txt w.txt │theme helix │ +| │font default │ +| │terms 4 cols 2 │ +| │active #2 58x28 │ +| │scroll 0/28 │ +| │pinch 1.0000 │ +| │tscroll 0.0000 │ +| ╰────────────────────────────────────╯ | | /tmp/pardes-snap/windowops/cwd New Del | ls @@ -310,16 +310,16 @@ | == style debug-on |0: 0-56 #949494,#121212, 57-99 d,#121212, -|1: 0-1 #ffffff,#262626, 2-39 #949494,#121212, 40-41 #ffffff,#626262, 42-69 #949494,#121212, 70-99 d,d, -|2: 0-0 d,#444444, 1-1 d,#080808, 2-39 #bdbdbd,#080808, 40-40 d,#444444, 41-41 d,#080808, 42-69 #bdbdbd,#080808, 70-70 d,d, 71-87 #949494,d, 88-98 d,#121212, 99-99 d,d, -|3: 0-0 d,#444444, 1-1 d,#080808, 2-39 #bdbdbd,#080808, 40-40 d,#444444, 41-41 d,#080808, 42-69 #bdbdbd,#080808, 70-70 d,d, 71-85 #949494,d, 86-98 d,#121212, 99-99 d,d, -|4: 0-0 d,#444444, 1-1 d,#080808, 2-39 #bdbdbd,#080808, 40-40 d,#444444, 41-41 d,#080808, 42-69 #bdbdbd,#080808, 70-70 d,d, 71-86 #949494,d, 87-98 d,#121212, 99-99 d,d, -|5: 0-0 d,#444444, 1-1 d,#080808, 2-39 #bdbdbd,#080808, 40-40 d,#444444, 41-41 d,#080808, 42-69 #bdbdbd,#080808, 70-70 d,d, 71-81 #949494,d, 82-98 d,#121212, 99-99 d,d, -|6: 0-0 d,#444444, 1-1 d,#080808, 2-39 #bdbdbd,#080808, 40-40 d,#444444, 41-41 d,#080808, 42-69 #bdbdbd,#080808, 70-70 d,d, 71-83 #949494,d, 84-98 d,#121212, 99-99 d,d, -|7: 0-0 d,#444444, 1-1 d,#080808, 2-39 #bdbdbd,#080808, 40-40 d,#444444, 41-41 d,#080808, 42-69 #bdbdbd,#080808, 70-70 d,d, 71-85 #949494,d, 86-98 d,#121212, 99-99 d,d, -|8: 0-0 d,#444444, 1-1 d,#080808, 2-39 #bdbdbd,#080808, 40-40 d,#444444, 41-41 d,#080808, 42-69 #bdbdbd,#080808, 70-99 d,d, -|9: 0-0 d,#444444, 1-1 d,#080808, 2-39 #bdbdbd,#080808, 40-40 d,#444444, 41-41 d,#080808, 42-99 #bdbdbd,#080808, -|10: 0-0 d,#444444, 1-1 d,#080808, 2-39 #bdbdbd,#080808, 40-40 d,#444444, 41-41 d,#080808, 42-99 #bdbdbd,#080808, +|1: 0-1 #ffffff,#262626, 2-39 #949494,#121212, 40-41 #ffffff,#626262, 42-61 #949494,#121212, 62-99 d,d, +|2: 0-0 d,#444444, 1-1 d,#080808, 2-39 #bdbdbd,#080808, 40-40 d,#444444, 41-41 d,#080808, 42-61 #bdbdbd,#080808, 62-62 d,d, 63-79 #949494,d, 80-98 d,#121212, 99-99 d,d, +|3: 0-0 d,#444444, 1-1 d,#080808, 2-39 #bdbdbd,#080808, 40-40 d,#444444, 41-41 d,#080808, 42-61 #bdbdbd,#080808, 62-62 d,d, 63-74 #949494,d, 75-98 d,#121212, 99-99 d,d, +|4: 0-0 d,#444444, 1-1 d,#080808, 2-39 #bdbdbd,#080808, 40-40 d,#444444, 41-41 d,#080808, 42-61 #bdbdbd,#080808, 62-62 d,d, 63-76 #949494,d, 77-98 d,#121212, 99-99 d,d, +|5: 0-0 d,#444444, 1-1 d,#080808, 2-39 #bdbdbd,#080808, 40-40 d,#444444, 41-41 d,#080808, 42-61 #bdbdbd,#080808, 62-62 d,d, 63-77 #949494,d, 78-98 d,#121212, 99-99 d,d, +|6: 0-0 d,#444444, 1-1 d,#080808, 2-39 #bdbdbd,#080808, 40-40 d,#444444, 41-41 d,#080808, 42-61 #bdbdbd,#080808, 62-62 d,d, 63-78 #949494,d, 79-98 d,#121212, 99-99 d,d, +|7: 0-0 d,#444444, 1-1 d,#080808, 2-39 #bdbdbd,#080808, 40-40 d,#444444, 41-41 d,#080808, 42-61 #bdbdbd,#080808, 62-62 d,d, 63-73 #949494,d, 74-98 d,#121212, 99-99 d,d, +|8: 0-0 d,#444444, 1-1 d,#080808, 2-39 #bdbdbd,#080808, 40-40 d,#444444, 41-41 d,#080808, 42-61 #bdbdbd,#080808, 62-62 d,d, 63-75 #949494,d, 76-98 d,#121212, 99-99 d,d, +|9: 0-0 d,#444444, 1-1 d,#080808, 2-39 #bdbdbd,#080808, 40-40 d,#444444, 41-41 d,#080808, 42-61 #bdbdbd,#080808, 62-62 d,d, 63-77 #949494,d, 78-98 d,#121212, 99-99 d,d, +|10: 0-0 d,#444444, 1-1 d,#080808, 2-39 #bdbdbd,#080808, 40-40 d,#444444, 41-41 d,#080808, 42-61 #bdbdbd,#080808, 62-99 d,d, |11: 0-0 d,#444444, 1-1 d,#080808, 2-39 #bdbdbd,#080808, 40-40 d,#444444, 41-41 d,#080808, 42-99 #bdbdbd,#080808, |12: 0-1 #ffffff,#262626, 2-39 #949494,#121212, 40-40 d,#444444, 41-41 d,#080808, 42-99 #bdbdbd,#080808, |13: 0-0 d,#444444, 1-1 d,#080808, 2-39 #bdbdbd,#080808, 40-40 d,#444444, 41-41 d,#080808, 42-99 #bdbdbd,#080808, |
