aboutsummaryrefslogtreecommitdiffstats
path: root/tests/test_clipboard.py (follow)
Commit message (Collapse)AuthorAge
* selection: drag past an edge scrolls the viewport under ituser2026-08-27
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Text taller than the viewport was not selectable in one gesture: the drag stopped dead at the pane's border. Two faults, one visible and one waiting. The visible one: DRAG_SELECT motion subtracted the content origin in uint16_t, so a pointer above or left of the pane underflowed, the grid lookup failed, and the selection froze. The waiting one: the anchor was a pair of *viewport* coordinates re-resolved on every extend, so the moment anything scrolled mid-drag the anchor would have drifted with the viewport, re-anchoring to whatever slid under the press. The anchor is now a tracked grid ref -- the thing lib-vt documents as being for 'long-lived anchors such as selections' -- so it follows its cell across scrolling, pruning and reflow, and only the near end of the selection moves. If a program flooding output prunes the anchored row out of scrollback entirely, the selection keeps what it had; there is no cell left to anchor a new answer to. Motion clamps into the content rect (a pointer past an edge is pointing at the nearest cell), and holding past the top or bottom edge scrolls on a clock rather than on motion, because a resting pointer sends no events and 'keep going' is exactly what resting it there means. app_tick() does the step, app_next_deadline_ms() wakes the poll loop for it -- the same mechanism toasts and hover guides already use -- and both front ends call the tick, so the behaviour exists under the server and the headless harness alike. Rows per step is how far past the edge the pointer sits: one row past creeps, further hurries, so the hand already dragging is also the throttle. The metronome is select_scroll_ms, a knob so the test can configure the clock down instead of sleeping. The test drags to one row above the pane and simply holds: the viewport walks to the top of scrollback (clamping makes the endpoint exact), the copy starts at a line that was never on screen with the press, and still ends at the pressed row -- that last check is the tracked anchor's.
* sl0ppty: ruff format the Pythonuser2026-08-17
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | The bumpy one, again. Every `.py` outside `vendor/` plus the three `contrib/` scripts that are Python with no extension: 60 files, 6,714 lines rewritten, no behaviour changed. Nothing here is worth reading -- the next commit adds it to `.git-blame-ignore-revs` so `git blame` walks past it. It is a bigger reformat than the C was, 60% of the lines against 17%, and for a reason worth writing down rather than tuning away: ruff and black break a call that does not fit onto one argument per line, where this code aligned the continuation under the opening paren. The test suite is thousands of `check("some prose", cond, detail)` calls, so nearly every one of them changed shape and the suite grew 3,308 lines. Line length was the one lever that moved it -- 120 columns would have been a 40% reformat and no growth at all -- and it was left at 88, because picking an unusual width to dodge a one-time diff is paying forever to avoid paying once. Import sorting came with it. Two files had `import os, pty, uuid, select, struct, sys, termios, time, fcntl, signal` on one line and now have ten. Verified after, not before, because a formatter that alters behaviour would be worse than none: - `make retest`, 1801 checks green -- and this reformats the suite that does the checking, including `harness.py`, which every one of those files imports. - `make test-live`, the tty tests the fast suite skips. They matter here more than usual: `live_input.py` and `live_m0.py` are two of the files whose comma-imports were split, and nothing in the fast suite would have noticed if that broke them. - All 60 files still compile, `make fmt` twice in a row changes nothing the second time, and `ruff check --select I` has nothing left to say.
* sl0ppty: a wrapped line is one line when you copy ituser2026-08-17
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Select a line too long for the pane and the clipboard came back with a `\n` in the middle of it, at whatever column the pane happened to end. Paste that into a shell and it runs half a path. `pane_selection_text` set `trim` and left `unwrap` at whatever a zeroed struct gives, which is false. lib-vt's header names the exact trio the clipboard wants -- plain output with `unwrap` and `trim` both true, matching Ghostty's own `Screen.selectionString()` -- and we had two of the three. A row boundary in the grid is not a newline in the text: a long line occupies two rows and is still one line, and where it breaks is a fact about how wide the pane is rather than about what was copied. Hard newlines are untouched, which is the half a fix for this could easily have broken: lib-vt joins only the rows that are continuations, so three separate lines still come back as three lines. Both directions are now asserted, because "copy joins everything" would be a worse bug than the one being fixed and would look identical from the inside. `emit` is now set explicitly as well, to `GHOSTTY_FORMATTER_FORMAT_PLAIN`. It is already zero, so this changes nothing today -- but this is the API where a zeroed field means "whatever the enum happens to start with" (DESIGN.md's note on `ghostty_terminal_set`), and a silently defaulted field is how the wrap flag came to be missing. There is one call site, checked. Verified: three checks in `tests/test_clipboard.py` -- a 50-character line printed into a 40-column pane, dragged across the row boundary, arrives as the string that was printed with no newline in it; three short lines keep their two newlines; and a double-click on a word in a wrapped line brings back no newline either. 46 checks in that file, 1795 overall, all green.
* sl0ppty: double-click a word to select ituser2026-08-17
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | `pane_select_word()` was written and never wired to anything: no declaration, no caller, no way to reach it. This is the rest of it -- the gesture, the setting, and the tests. **The word is defined by what it stops at, not by what it is made of.** Anything that is not blank and not a listed separator belongs to a word, so CJK, accented letters and emoji need no table and come out whole; whitespace always separates, listed or not, because a double-click that selected the line would be a different gesture arrived at by surprise. Wide glyphs continue the run they are in rather than each being their own word -- otherwise every CJK character is a word. `word_separators` defaults to `"'`()[]{}<>;,|`, and what it leaves *out* is the decision: `/ . - _ : = @ # % ? & + ~ $` stay inside words, so a path, a dotted name, a snake_case identifier, a key=value pair, a URL and `src/app.c:1234` are each one thing you double-clicked to copy whole. `:` is the difference between copying an error location and copying half of one, which is why kitty keeps it too. The gesture reuses what selecting already has: the press starts a selection at the cell -- which anchors a drag out of the double-click -- then widens it to the word, and the release path that already copies a drag copies this. So the word *is* the copy, in the one place that rule lives, and the highlight is the copied span rather than an approximation of it. Tracked per cell as well as per pane, so clicking two different words quickly selects the second rather than everything between them. A program that asked for the mouse still keeps its clicks, with shift as the escape hatch, exactly as the drag has it. **One thing this needed on the way: quoted values were dumped unescaped.** The default separator set opens with a quote, and `cb_add(&b, "key \"%s\"\n", ...)` wrote it raw -- so `--dump-config` produced a file that no longer parsed, breaking the property the dump exists for. The escaping loop already existed inside `cb_chord`, for the same reason and with the reasoning written down; it is now `cb_qval`/`cb_qstr` and every string setting goes through it: the marks, `shell`, `editor`, `shader_dir`, `project_roots`, `project_layout`. Latent for all of those, load-bearing for this one. **And `--dump-config` does not do what four comments and the docs said it did.** It calls `config_dump_defaults()`: every setting with its *default*, never the value your config gives it. The function name was the honest one. Corrected in `docs/config.md`, `config/config.kdl`, `main.c`, `config.c`, `app.c` and the test's own docstring, rather than changing what the flag does -- starting a config from it is the documented use, and `--check` is what reads yours. There is no way to ask for the effective config; that is a gap, not a lie, and it can be a flag when somebody wants it. Verified: 22 checks in `tests/test_clipboard.py`. An error location, an identifier stopping at a bracket, a URL, a word inside quotes and a run of CJK; the highlight being exactly as wide as the copy; a separator, a blank cell and a single click each leaving the clipboard alone; two clicks on different cells not being a double; two slow clicks not being one; the separator set honoured from a config, including a quote and a backslash carried through KDL escaping; a word in a scrolled pane coming from the row on screen; and a mouse-tracking program keeping its double-clicks until shift takes them back. Three of those failed first because the test asked for the answer twice -- a second `worded()` call is a second pair of clicks -- and one because it mapped string indices to columns without counting wide glyphs as two.
* sl0ppty: the test cycle was 80s, it is now 5.5suser2026-08-14
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Measured before changing anything, because the obvious suspect was wrong: the build is not slow. zig caches objects, so even a clean rebuild is ~0.15s and an incremental one is 0.08s. The C unit tests are 0.00s. All eighty seconds were in the python suite, and almost all of that was waiting on durations that were guesses. Four things, in order of what they bought. Run the python tests in parallel. They are separate processes that share nothing — script mode opens no socket — so each is now its own make target with a stamp, and `make -j` runs them at once. 78s -> 21s for free. The stamps are worth as much as the parallelism day to day: an untouched test whose binary has not changed does not run again, so `make test` after editing one test file is 1.1s and after editing nothing is 0.02s. The binary is a prerequisite of every stamp, so changing the code still re-runs everything. Then stop guessing at durations. `settle N` is the only thing that reads a pane's pty, so it is how you wait for a program's output — but `send` and `snapshot` are already synchronous with the app, which means a settle after a mouse event, waiting to assert on our own chrome, is waiting for nothing at all. Those are removed. Where a program's output really is the thing being waited for, harness.until()/until_text() polls in 10ms steps and returns the instant it lands: measured, that is one poll, against the 200-300ms those calls used to donate. The default settle drops 120ms -> 20ms on the same reasoning. Three effects are gated on a real clock rather than on anything observable — the hover dwell, and a toast's lifetime. Those genuinely have to wait, so the tests that are not *about* the timing now configure the clock down instead (hover_delay_ms 20, toast_ms 150) and the two tests that are about it keep the real defaults, because a dwell test that cannot observe not-yet-elapsed proves nothing. Two tests turned out to be passing by accident, which is the useful kind of thing a speedup shakes loose: both asserted the resize hint was gone after a drop while the pointer was still sitting on the handle. It was only gone because the dwell had not re-elapsed — resting on a handle *should* re-arm the hint. They now move off the handle first and assert the thing they meant. Unrelated but found on the way: the Makefile tracked no header dependencies, so editing a .h rebuilt nothing and the only safe move was `make clean`. -MMD -MP fixes it; touching shader.h now rebuilds exactly the six objects that include it. That one was a correctness bug wearing a performance costume. .fastfeedback/SPEED.md records the measurements and how to write a fast test here, so the next person does not rediscover that settle-after-a-click is free to delete.
* sl0ptty: select-to-copy, middle-click paste, and toastssl0p2026-08-14
Drag over text, release, and it is on your clipboard — no chord, no menu, the selection is the copy. The clipboard lives on the client's machine, so the copy travels as OSC 52 and therefore works over ssh; the session keeps its own copy as the primary selection, which is what middle click pastes back. Selected cells are inverted rather than recoloured, so a selection reads the same over whatever theme the program inside is using. A program that is tracking the mouse keeps its own clicks (shift is the escape hatch), which is tested rather than assumed. Toasts: transient announcements in the bottom-right that expire on their own, stacked up to three. 'copied 13 chars', 'config reloaded', a pane's OSC 9 notification, or {"cmd":"notify","text":"..."} from an orchestrator — an agent in a pane can now say 'build finished' and have it appear. OSC 52 from a pane reaches the same clipboard. Two real bugs found on the way: - mouse events are hit-tested against what was painted, and nothing had been painted before the first frame, so a click that arrived first was dropped. Both front ends now compose once before accepting input, which is what really happens in a session anyway. - the plain leaf_new never installed the clipboard and notification handlers; only panes built from a layout file had them. Every ordinary pane was silently missing both. The test escaping traps continue to earn their comments: a real newline inside a driver argument terminates the *command* rather than reaching the shell, \a is not a driver escape (\x07 is), and an interactive shell's line editor eats a typed ESC — so a pane driven with escape sequences must not be one. 356 checks green (329 headless + 27 live).