| Commit message (Collapse) | Author | Age |
| | |
|
| |
|
|
|
|
|
|
| |
Measured in a headless browser: the band spent most of its sweep off
screen and its eased segment rushed the middle, so the pane lit up for
half a second in eight at ten percent white. Now the keyframes cover
exactly the stretch where the band crosses the pane, the sweep is
linear and 1.6s long, and the sheen is half again as bright.
|
| |
|
|
|
|
|
|
|
|
|
|
| |
The copy no longer reads as eye candy: shaders are how a pane says
what is true of it. An unfocused pane dims, a dead shell greys out,
scrollback tints, a bell shimmers until it is seen. Scanlines and CRT
phosphor stay, admitted as such.
And every eight seconds the pane demonstrates the bell: a soft sheen
crosses the body, gone within a second, painted as a clipped
background sweep so the frame title stays put. Off under
prefers-reduced-motion, like everything else here.
|
| |
|
|
|
| |
Full stops and a colon where the dashes were. The title is
slosh: coming soon.
|
| |
|
|
|
|
|
|
|
|
| |
Shaders are the thing no other multiplexer has, so the coming-soon
page should not keep quiet about them: a line names them, and the pane
itself wears two -- zebra band=1 and vignette, redrawn in CSS -- so
the claim is demonstrated on the spot.
Below the prompt: slosh is a sl0p.foo product, with the Discord
invite.
|
| |
|
|
|
|
|
|
|
|
|
|
| |
The site is not finished -- the demo video is still being cut and the
public repo URL does not exist yet -- so what slosh.foo serves is one
self-contained page in the session-drawn look: tab strip, a focused
pane that runs $ slosh and prints the announcement as its output,
status line, the sl0p palette. No scripts, nothing fetched.
web/build-site assembles just that page by default now; FULL=1 builds
the real site (front page + docs + demo) while it is being finished.
Drop the gate when the real thing ships.
|
| |
|
|
|
|
|
|
|
|
|
|
| |
There is no inotify to port to, so the watcher grows a second native
backend: kqueue's EVFILT_VNODE, feeding the same pollable fd that
server_run() already watches. Vnode events are per-inode, so each
config file is watched twice -- via its directory (catching the
temp-file-then-rename most editors do, and files appearing for the
first time) and via the file itself (catching in-place rewrites).
Either event triggers the same debounced re-read as on Linux, which
re-registers the watches and so survives the rename swapping the
inode out from under us.
|
| |
|
|
|
|
|
|
| |
In lib-vt mode upstream's build.zig auto-enables an xcframework
whenever xcodebuild is on PATH. On a Mac with only the Command Line
Tools that xcodebuild fails, turning a working build into a broken
one -- and we only ever consume libghostty-vt.a anyway. Pass
-Demit-xcframework=false.
|
| |
|
|
|
|
|
|
| |
The build/ pattern matched every directory of that name, not just our
build output at the root -- so vendor/libghostty-vt/src/build/ (the
library's own build.zig helpers) never made it into git, and make
vendor broke on fresh clones. Anchor the ignore to /build/ and restore
the 284 files it had silently dropped.
|
| |
|
|
|
|
|
|
|
|
| |
A split, the config editor or a new float while zoomed used to land the
new pane behind the pane filling the tab -- which looks exactly like the
verb having failed. Whoever asked for a new pane asked to see it, so the
zoom yields, in place_beside where every such arrival funnels through;
the move-pane unzoom was this rule as a special case and folds in.
Minimising the zoomed pane ends the zoom too, or the layout would solo a
pane the strip now holds.
|
| | |
|
| |
|
|
|
|
|
|
| |
splash_ms and the project settings join the knobs table, the pane-state
ranking gains the floating it was missing, contrib/themes' seventh file is
named for what it is, the shader table's numbers are called examples with
the real omitted-value defaults beside them, the index links workspaces,
and the verb table learns splash.
|
| |
|
|
|
|
|
|
|
| |
A new client displaces the current display, so `slosh` typed into a pane
of "main" displaced the very client showing that pane -- you watched your
own session detach, from inside it. SLOSH_SESSION names the pane's own
session; that one name is refused with the way out spelled in the message.
Any other name is deliberate nesting, and cmd/ls are untouched: they are
how a program in a pane is supposed to reach its own session.
|
| |
|
|
|
|
|
|
| |
A tiled pane sits on a cleared screen, so unpainted padding looked fine by
accident; a float sits on whatever was composited under it, and every
unpainted cell let that show through. draw_frame now fills the ring between
border and contents with exactly what screen_clear leaves, and a suspended
pane's never-composed content rect gets the same treatment.
|
| |
|
|
|
|
|
|
|
|
|
|
| |
Brackets were tried and still read as text. Each min-bar entry is now a
three-row chip wearing a real pane frame -- the same corners, because a
frame is what a pane *looks like* in this program, so a shrunken one reads
as exactly what it is: a pane, clickable. The put-away mark rides inside
each chip and the lone legend is gone; a rung pane's bell mark keeps its
own colour inside the frame; hover lights the whole chip and the whole
chip is the hit. The bar costs three rows instead of one while anything
is minimised (and nothing when nothing is), which is what an affordance
that works is worth.
|
| |
|
|
|
|
|
|
|
|
|
| |
A row of dim bare names read as a caption, and nobody clicks a caption.
Each entry now wears square brackets -- [▬ name] -- because that is what
clickable already looks like in this program: [re-run], [close], the
buttons a pane's own row offers. The put-away mark rides inside each entry
instead of standing at the head of the row as a legend: a legend explains
the row once, a mark per entry says what each button is. A rung pane's
bell mark keeps its own colour inside the brackets, and the whole bracket
is the hit, as one geometry demands.
|
| |
|
|
|
|
| |
Plain ASCII that says what it means, over a dot that had to be noticed.
The mark was always a short string rather than a glyph (the emoji-width
argument in config.kdl stands); the default just stops being shy.
|
| |
|
|
|
|
|
|
| |
Four rows deep at 60 a row instead of three at 45: the edge row is nearly
gone (#0e0e0e from white) and the melt reads as "there is more this way"
rather than as a rendering artefact. The quarter-viewport cap, the
(above > 0) gate and everything else about the derivation stand; a config
that writes its own scrolled chain is untouched, as ever.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
A dumped session quietly re-tiled its floats, which was the one data-loss
gap left in D22. dump-layout now records `floating=true x= y= w= h=` -- the
*wanted* rect, because intent is what survives a session and the next
screen's layout clamps it like any frame -- layout files declare the same
(linted: floating is a bool, x/y/w/h mean nothing without it), and a file
whose every pane floats self-heals through ensure_tiled: the top float
lands, the same rule closing the last tiled pane follows, no lint required.
save-workspace inherits all of it from the shared dumper.
`float` over the API takes x/y/w/h and then *places* rather than toggles --
a placement is a statement about where, not about whether -- floating the
pane first when it is tiled, re-placing when it already floats, with
omitted fields meaning "keep". The intent is stored exactly as asked: the
write-back rule is for hands, whose drags need the clamp to agree with the
eye; a script means what it typed.
And kitty graphics respect the stack. The cell compositor gets occlusion
free from paint order, but placements are sent after the diff and would
paint an image straight over a float covering it. Each pane's placements
are now clipped by the float rects above it -- every float for a tiled
pane, the higher-raised for a float -- cropping what one clean edge can
express (the pane-edge source-rectangle arithmetic aimed at another
clipper, including from the top and left, where the source origin moves and
the sub-cell offset is consumed) and suppressing what it cannot: a strip
across the middle or a corner overlap leaves a shape one placement cannot
say, and the image returns whole when the float moves. No z= needed --
clipping is the z-order, since nothing is ever allowed to overlap.
D22's "still owed" paragraph is paid and rewritten; M10 is fully checked.
|
| |
|
|
|
|
|
|
|
|
|
|
| |
D22 records the float contract: the minimized pattern (a flag and a wanted
rect on a leaf that keeps its seat), derived-vs-remembered rects, the hit
list as the z-order, the pop to the centre, the shadow-not-a-state, the
backdrop invariant, edge-pinned resizes, and the key decisions -- with M10c
(layout files, dump-layout, placement over the API, graphics occlusion)
recorded as still owed. M11 extends the drag machine section with the drop
grammar: swap centre plus third-width insert bands, the fill as the
promise, release derived from the painted hits, and the sibling-reorder
rule. The working plan file served its purpose and goes; git remembers it.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
lib-vt tracks a hyperlink per cell and slosh dropped every one of them: the
client's terminal can only offer a link it was sent, so `ls --hyperlink`,
gcc diagnostics and friends silently lost theirs inside the session.
The link rides beside the cell -- cell_t is the shader ABI and a link is
not a colour -- as a plane of ids into an interned URI table, append-only
so an id in prev_link still means the same URI next frame (which is what
lets the diff compare ids instead of strings), and capped so a hostile pane
cannot grow it forever. URIs with control bytes are refused outright: they
are emitted verbatim inside an OSC, and a control byte in one is a way to
write escape sequences into the client's terminal. The diff wraps emitted
runs in OSC 8, closes what it opens before the frame ends, and treats a
link-only change as a change. Chrome painted over a link takes the link
with it, so a frame, modal or float across a URL is never clickable with
the pane's link; links survive scrollback with the text that carries them.
Plain-text URLs stay the outer terminal's own matcher, with the catch
documented in panes.md: under mouse capture Ghostty only offers links with
shift added to the gesture (shift+cmd hover/click), shift being its
capture escape. Wrapped URLs can never be regex-matched through a
multiplexer -- rows repaint as hard lines -- which is exactly what OSC 8
links, passing through whole, are for.
`snapshot format:"bytes"` returns the emitter's own output for a frame (a
second call is the delta): the model and the bytes are two different
things, and this codebase already paid once for asserting on one while the
other was wrong.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
A pane can float: lifted out of the layout, drawn over the tiled panes and
under the modals, freely moved and shaped. The model is a flag and a rect on
the leaf, exactly like `minimized` -- the seat in the tree is kept, layout
skips it, the siblings absorb its share, and un-floating returns it home.
The drawn rect is the wanted one clamped to the tab every frame, never
written back, so a terminal squeeze round-trips exactly; an explicit move or
resize *does* write back, because the hand is the author of the intent.
Floating pops the pane to the centre: the jump is what says it happened.
Painted after everything tiled, so the hit list -- searched backwards --
resolves an overlap to the float you can see: the z-order and the click
order are the same list. It casts a shadow (float_shadow, one number), and
`floating` joins the states table above `unfocused`, so a float is never
dimmed as merely-unfocused.
The hands: the title moves it, any other border resizes it (a bottom corner
on both axes; the grabbed edge pins at walls and floors, never sliding the
window), H J K L move a focused float, the =/+ key grows it about its
centre, `-` shrinks it (its split was a refusal anyway), and hovering shows
which edges a grab would take, from the same derivation the press uses.
Equalize moved to `0` to free the key; `C-a f` floats (finder went to `s`,
where it reads as search); `C-a F` opens a new floating shell -- the
throwaway terminal; the function row f1..f12 became nameable in configs
along the way. Guards throughout: the last tiled pane cannot float or
minimize away, closing it lands the top float, splits are refused with
their own words, floats are not swap targets.
Dropping got a grammar: while a pane is carried, the pane under the pointer
subdivides into a swap centre and four edge bands meaning "insert me beside
this pane, on this side" -- detach_leaf + place_beside, the surgery
cross-tab moves already had. Bands are thirds (the first map of quarters
showed the swap owning half of each axis), the hovered band fills the half
the drop would hand over, the release resolves against the painted frame's
hits so it cannot disagree with the fill, and a drop between siblings along
the axis is a reorder that always fits -- refusing it was refusing the most
ordinary drag there is.
Also: rename-pane/rename-tab as palette actions (renaming was a
double-click and nothing else, the one verb the palette could not reach),
and C-u clears the rename editor like it clears the picker's query.
Design log in PLAN-FLOATING-PANES.md; to fold into DESIGN.md as D22/M10-11.
|
| |
|
|
|
|
|
|
|
|
|
|
|
| |
Targets: kdl, jsonval, expr, proto, osc5577, input — each compiled
standalone against its own .c with ASan + UBSan, no vendor deps.
Includes seed corpora from shipped configs/themes/chrome, real control
API lines, terminal escape sequences, and regression inputs from the
bugs already fixed. standalone_main.c replays files without the fuzzer
runtime for debugging and CI.
Top-level: make fuzz (run all, TIME=15 default), make fuzz-corpus
(replay seeds only, fast CI gate).
|
| |
|
|
|
|
|
|
|
|
|
|
| |
kdl: heap-buffer-overflow on trailing backslash in a string (a"\)
jsonval: heap-buffer-overflow on truncated \uXXXX escape ("\u)
jsonval: UB float-to-long cast on out-of-range numbers (2e22)
expr: UB signed overflow in arithmetic ops — wrap as uint32
expr: INT32_MIN / -1 SIGFPE guard in OP_DIV/OP_MOD
expr: stack overflow on deep nesting — add MAX_DEPTH 256
expr: 64-bit + saturation in OP_DIST to prevent squared overflow
input: NULL memcpy in input_timeout's re-parse path
input: signed overflow in CSI and SGR mouse parameter accumulation
|
| |
|
|
|
|
|
|
|
|
|
| |
$(shell find overlay -type f) skips symlinks, and the one symlink in
overlay/ is the sl0p theme pointing into contrib/themes -- the file the
README promises cannot drift was the one file whose edit did not
rebuild the rootfs. find -L includes it, and make stats prereqs through
symlinks, so the target's mtime is the one compared.
Found while tracing what a src/app.c edit does under make www: that
path was already correct (depfile -> object -> guest -> image -> site).
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Consolidate everything the website is made of:
- web_new/ -> web/: the front page, install.sh, and build-site
- contrib/webdemo/ -> web/demo/: the in-browser machine, committed for
the first time (it had never been checked in)
- make www at the top level assembles the whole site into build/www
(NO_DEMO=1 skips the guest cross-compile)
- web_plan.md deleted: the plan is fulfilled and web/README.md is now
the record
Path fallout: the overlay theme symlink gains one ../ (web/demo is a
level shallower relative to contrib/themes), PYSRC in the Makefile
follows the demo scripts, contrib/README.md drops its webdemo section.
ROOT := ../.. in the demo Makefile survives unchanged -- web/demo sits
at the same depth contrib/webdemo did.
Verified: make www builds front page + docs + demo end to end,
web/demo make check boots the image to slosh, tests/test_docs.py and
make fmt-check green.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The old name argued that the extension should say how to read the
file. The argument lost to a collision: zellij speaks the same KDL
syntax under that same extension with a different schema, and to
anyone arriving from there .kdl said *whose* file it is -- naming the
wrong owner is worse than not naming the parser. So a layout is
x.layout, a project's is slosh.layout, the name says what the file is
and --check says how it was read. (It is still KDL underneath; point
your editor's highlighting at *.layout.)
Nobody's checked-in workspace vanishes on upgrade: the project scan
still finds slosh.layout.kdl, never writes it, and where both
spellings exist the new one wins, because two files disagreeing needs
one answer. The unparseable-file heuristic in --check learned the new
suffix -- it matched '.layout.' with a trailing dot, which the new
name no longer has.
The webdemo overlay moves with it (demo.layout), rebuilt and
boot-checked.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Every non-space glyph of the logo is a particle that flies to its cell
over the first two fifths of splash_ms, then *is* the logo for the
rest. There is deliberately no particle state anywhere: a position is
a pure function of (target, elapsed, seed), recomputed every frame
like the layout and the pane states and for the same reason -- physics
that is stored can drift, physics that is derived cannot. The seed is
the splash timestamp, so every attach flies differently and one splash
is coherent frame to frame; a per-particle hash staggers departures,
and the delay rescaling lands every straggler exactly at the end,
because an almost-assembled logo reads as a bug.
Five motions, picked by the clock and pinned by {"cmd":"splash",
"motion":N}: rain (fall in, column by column), scatter (converge from
everywhere), slide (rows enter from alternating sides), vortex (spiral
in around the centre, orbit shrinking and unwinding), shuffle (the
glyphs trade places, then sort themselves). The colour chains keep
playing over the box throughout, so particles catch their hue as they
cross into it.
expr_sin() is the expression language's own sine exported for C-side
animation: a motion sketched in the repl and one compiled into the
binary cannot disagree about what sin(90) is.
The tests learned two things the flights taught: the logo's opening
run appears three times per line and assembly is staggered, so a
middle occurrence can land first -- "assembled" is the row whose first
block glyph is where the match begins -- and windows need margins that
survive a parallel suite on a busy box.
|
| |
|
|
|
|
|
|
|
| |
The logo is half-block art: its visible pixels *are* the foreground
and the notches are the terminal's own background. A pass that painted
backgrounds therefore turned the art into a slab with a logo-shaped
hole -- so every splash pass is channel fg now, the effect lands on
exactly the art's pixels, and the backdrop stays dark for free (a
blank cell has no glyph for a colour to land on).
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
One tint is one colour, and every effect wore the theme accent -- six
effects that were all the same dull blue. An effect is a *chain* now:
phase-shifted tints in loud literal colours, mixing per cell, which is
how a fixed-colour pass machinery does rainbows. Passes may light the
backdrop's background too, so the box is a scene rather than tinted
text, and the logo is drawn white -- the canvas that takes every hue at
full saturation, where tinting blue towards pink lands on mud.
The six: spectrum (rainbow bands), plasma (two interference patterns in
clashing hues), fire (heat up the glyphs, embers in the box), rings
(rainbow ripples), glitch (a reveal under two colours of static),
aurora (green and violet curtains over cyan).
The splash is deliberately the one place that does not defer to the
theme: it is branding wearing a demo, not chrome.
{"cmd":"splash","fx":N} picks an effect by index -- a test needs a
deterministic one, and "show me the aurora again" deserved an answer.
The new test counts dominant-channel hue families on the logo rows,
because two hundred shades of the same blue is exactly the failure it
guards against.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
logo.txt is the source of truth; the Makefile bakes it into
build/logo.h the way version.h is baked. When a client attaches
(MSG_HELLO turning a connection into the display), the logo appears
centered over everything for splash_ms (900, 0 for never), on a
cleared backdrop, in the accent -- and one of six effects runs over
its rect through the ordinary shader pass: a diagonal sheen, plasma,
rings, sparkle, a left-to-right reveal, a breathe. Which one is picked
by the splash's own timestamp, so every attach looks a little
different without anybody keeping a counter.
Every effect reads the clock, so the frame clock runs only while the
splash is up (note_animation, same as any animated chain), and `since`
in a splash context is milliseconds since it appeared, so one-shot
effects sweep exactly once. Any key or click ends it and still lands
where it was aimed: a greeting is not a modal. A screen the box does
not fit on gets no greeting rather than a cropped one.
Headless and scripted sessions never attach, so the whole fast suite
never sees it; {"cmd":"splash"} triggers it on demand, which is what
makes it testable from a scripted session -- and lets you replay the
greeting whenever you like.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
app.c was 7k lines wearing section markers; now the markers are files,
cut exactly along them, no behaviour change:
app.c the session core: config in force, pane and tab
lifecycle, walking, input routing, introspection
app_layout.c layout, tree edits, resizing, equalising, rotating,
focus
app_session.c layout files, checking, workspaces, dumping a session
app_draw.c drawing, the shader passes, kitty graphics
app_ui.c the finder, modals, the palette, the project picker,
renaming
include/app_internal.h carries what the five share: the tree/tab/app
structs, CFG and the palette macros, and the 50-odd formerly static
functions the cut made cross-unit, declared where they are defined.
They keep their names; only the five units see the header.
One test moved with the code: test_portable_layouts' staleness check
greps the source for the layouts section, which now lives in
app_session.c.
Mechanical, so blame for anything in the four new files continues in
app.c before this commit.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The out-of-the-box look, rethought as one piece:
- the compiled-in palette calms down: muted blue #7aa2f7 on the same
neutral darks, with the colours that mean something kept semantic --
amber bell, red-orange dead. sl0p stays one include away as the house
style, and contrib/themes/default.kdl is the starting point written
down (theme-tour shows it).
- derived state defaults now actually follow the theme: the scrolled
wash is rebuilt from scroll_bg after the theme block (it used to bake
the compiled accent), and declaring a state -- in any file of an
include chain -- still wins. The dump writes derived states as
comments so C-a e cannot pin what is meant to keep following.
- a bell you can see: the frame blinks three times in theme's bell
colour then breathes until answered, and a soft diagonal shimmer --
the bell colour halved towards default_fg, width breathing on a sine
-- crosses the body for the same 900ms. bell_indicator false silences
all of it; states { bell { } } replaces it like any state. since
carries the clock dependency, so an idle session pays nothing.
- scrollback says where it continues: while scrolled, the viewport's
top and bottom rows melt towards an edge with content past it and
turn solid at the buffer's end. New expression variables above/below
(lines hidden past each edge, own dependency bit, cached per scroll
position), carried in shade_ctx_t -- SLOSH_SHADER_ABI 3 -> 4.
- expressions survive the dump: a program keeps its source and cb_chain
writes it back, so amount="(y % 2) * 40" no longer dumps as a number.
- headers move to include/, -Iinclude everywhere; the plugin Makefile's
SLOSH var becomes SLOSH_DIR because slosh exports SLOSH=1 into every
pane it runs, and building from inside a session read that as a path.
- comment drift culled where features outran the prose, and the
derivation boilerplate folded into derived_pass()/ctx_scroll().
|
| |
|
|
|
|
|
|
|
| |
web_new/ is the front page slosh.foo serves: nav, install, what is
different, docs list. The first draft drew itself as a fake slosh
session -- tab-strip nav, box-art panes, a status-line footer -- and
that chrome is gone: the page describes the thing instead of
impersonating it. web_plan.md is the working notes.
build-site assembles it with the rendered docs and the webdemo.
|
| | |
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
sl0p + pty was error-prone to type and the zero made it worse; slosh
(slop + sh) is one syllable, impossible to typo, and slosh.foo is ours.
Mechanical rename everywhere it appears: binary, env vars (SLOSH,
SLOSH_SESSION, SLOSH_BIN, SLOSH_CONFIG), socket dir (/tmp/slosh-<uid>,
$XDG_RUNTIME_DIR/slosh), config path (~/.config/slosh), header guards,
src/sl0ppty.h -> src/slosh.h, contrib/sl0ppty-dev -> contrib/slosh-dev,
the driving-slosh skill, docs and tests. The sl0ppi fork references and
the sl0p theme stay: one is history, the other is the brand.
The rename surfaced a latent bug in test_osc5577: the hello-reply
answer always appeared on screen twice (ECHOCTL echo plus cat -v), but
a seven-character implementation name happened to wrap the second copy
across the 60-column boundary, so counting substrings in wrapped text
found one and passed for the wrong reason. A five-character name moved
the wrap and exposed it. The loop guard itself is correct -- with echo
off before the request there is exactly one reply -- so the test now
counts on de-wrapped text and tolerates the pane's own echo.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Two faults that fed each other, and only the second one made the first visible.
The handle went to the **widest** free run of the top row. A centred title leaves
two runs of nearly the same width, so a one-cell difference nobody can see threw
the handle out toward a corner: on a 116-column pane whose row middle is 60, it
sat at 85. Placement should be stated in terms of where the thing wants to be,
not of the leftovers it is allowed to occupy -- so it now aims at the middle of
the row and slides into the nearest run that is free. Nothing in the way: centred,
like the other three sides. A title holding the middle: hard against the title,
where the eye already is. That same pane now puts it at 64.
The armed edge also ruled the heavy line straight across the whole border, over
the pane's name, its buttons, its scroll indicator and a dead pane's epitaph. The
row went unreadable exactly while the pointer was on it -- and the title the
handle had just been placed beside was wiped, which is what turned a merely-off
position into an apparently random one. Each cell of the edge is now painted only
if the hit list says the edge still owns it: the same question the handle asks,
and the one the click will ask.
The epitaph had to start claiming its cells to take part. It answers
`panestatus:` and does nothing when clicked, because an epitaph is not a button;
what it buys is that ` exited: status 3 ` survives a hover over the border it
lives on, next to `[re-run]` and `[close]`, which already survived because they
were buttons.
Verified: `make retest`, **1877 checks measured**, all green. Four new: the handle
is centred on the row when the row is empty, sits against the title when the
title holds the middle, and the armed top edge leaves both the name and the three
buttons legible.
Live on a 116x36 pane, hovering the far left of the top row -- deliberately the
rim, so this is the first stage doing all of it:
╭━━━━━━━━━━━━━━━━━━━ sl0ppty ███▲███━━━━━━━━━━━━━━━━━▬ □ ✕ ╮
and on a dead pane, hovering the bottom handle:
╰ exited: status 3 ━━━━━━━━━━███▼███━━━━━━━━[re-run]━[close]━╯
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The arrow rode the middle of the dashed boundary line, which put the two halves
of one message in two places: the handle said *press here* and a glyph out in the
middle of the pane said *this is what happens*. It also meant the direction was
unreadable until you were already on the target, since the dashed line is stage
two of the hover.
So the arrow moves into the handle and the boundary line goes back to being a
plain boundary -- which is the honest shape for it anyway: it is the *edge* of the
pane you are about to make, and an edge has no middle worth marking. The handle
is now one object that explains itself, and because the handle is drawn from the
first stage, brushing any part of an edge tells you where its button is and which
way that button splits, without previewing a boundary nobody asked about.
Knocked out with ATTR_INVERSE rather than drawn in a second colour: the bar is
solid `guide`, so a `guide` glyph on top of it would have been invisible, and
inverting gets contrast that follows whatever colour the theme sets for no new
knob. Along the handle the arrow sits in the middle, where the pointer is aiming.
Across it -- a distinction only the upright pair has, being the two that grow a
second cell -- it sits on the edge's own cell rather than the one over the
content, because it marks a boundary and the boundary lands on the frame.
Verified: `make retest`, **1873 checks measured** (`test_split_ux.py` 100 of
them), all green. The test that pinned the arrow to the dashed line is now the
test that pins it to the handle, on all four sides, plus one that it is legible
from the first stage with no boundary drawn.
Checked live on a 116x36 pane: `◄█` at column 2 and `█►` at column 117 on the
middle row of their bars, `━━━███▼███━━━` on the bottom border's single row, and
-- the case that shows why this is worth it -- hovering the *far left* of the top
row draws `███▲███` at column 85, where the handle actually is, having dodged a
centred title.
A note on the two commits before this one: their messages quote 1801 and 1807
checks, which were carried over from earlier in the session rather than measured.
The figure was 1865 at that point. The code and the tests were right; the numbers
in those two messages were not.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The armed handle grew by one cell into the pane on all four sides, which is one
change on two axes and therefore two different results. A cell is about twice as
tall as it is wide -- the fact `gap_aspect` exists for, and the one the split
picker measures its longer side by -- so the extra column on a left or right
edge read as a thicker line, and the extra row on a top or bottom edge read as a
wall. It also spent a row of somebody's output to say something the glyph was
already saying: `█` is the heaviest thing one cell can draw, and against `━` it
needs no help.
So only the upright sides widen now. A horizontal handle stays on its own row.
That also removes its claim on a content cell, which is why the inner-strip hit
is registered on those two sides only -- the paint and the hit still describe the
same rect, which is the whole reason the guide registers what it thickens.
Verified: `make retest`, 1807 checks, all green -- six new, pinning the asymmetry
in both directions: the top and bottom handles occupy exactly the row their
border is on, the left and right ones exactly two columns, and the inner column
of each upright handle hit-tests as that border rather than as the pane.
Measured on a live session rather than argued: hovering the bottom border of a
116x36 pane puts seven block cells on row 37 and no others, inside the `━` rule;
hovering the left border puts them on rows 16-22 across columns 2 and 3.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
Clicking a pane's border to split toward it is the right gesture and was the
wrong target. A border is the longest thing on the screen and it sits between
two things you did mean to click -- a title, a gap, the pane beyond it -- so
reaching a cell too far changed the layout, and the top row was worse than the
sides: clicking a pane's header to focus it split it upward.
So the target shrank. Each side carries a *handle* of three to seven cells in the
middle, and only the handle splits; the rest of the edge is the *rim*, which
arms the guide and does nothing when pressed. Three is the floor the pane
buttons already settled on for the reason written there -- a one-cell target is
a thing you miss with a mouse -- and seven is the ceiling, because a third of a
tall pane's border is most of that border again and brings the accidents back
with it.
Hover now answers two questions in two stages, because they are two questions.
Anywhere on an edge, the edge goes heavy and the handle thickens by a cell into
the pane: that says where the button is, which is the thing you can no longer
infer. On the handle, the dashed boundary and the arrow as well: that says what
the click will do. The hint follows the same split, so sweeping an edge neither
draws a boundary nor offers a split you cannot get from where the pointer is.
Three things this had to get right.
**The thickened handle is registered, not just painted.** The guide adds the
hit for the inner strip in the frame that draws it, and reads the outer rect
back from the hit list rather than recomputing it, so there is one opinion about
where the handle is (D6). A handle you can see and cannot click would be worse
than either mistake alone.
**The top row cannot put its handle in the middle.** The buttons own its right
and a centred title -- the default -- owns exactly the middle. The title winning
is the existing rule, and it is what keeps a double-click rename from splitting
twice on its way, so the handle is placed last, in the widest run of cells the
row has left and nearest the middle among equals. A row with nothing left offers
no upward split; its other three edges are unaffected. Each handle cell is then
painted only if the hit list still agrees it belongs to the handle, so a block
never lands on a pane's name.
**The row keeps the drag and loses the split.** `title:<id>` takes no side now
and `title:<id>:t` -- the handle -- carries one, which is why the press path
reads the side out of the action instead of assuming it. Press-and-hold skips
the dwell on a handle, where a press means something, rather than anywhere on an
edge.
Verified: `make retest`, 1801 checks, all green. Nine of them are new or
rewritten: the two-stage guide, the caption belonging to the handle, the rim
splitting nothing on any of the four sides, the top row's handle still splitting
upward on a titled pane, and the rim of the top row not. Tests read the handle's
cell out of the hit list (`Snapshot.handle`/`.rim`) rather than deriving the
geometry, so a test cannot agree with a broken formula.
Also checked by hand on a live session, which is where the placement rule earned
itself: hovering the rim draws the heavy edge and the block and no dashed line,
hovering the handle adds the line and the arrow, three clicks on pane headers
split nothing, and on a pane titled `sl0ppty` the top handle lands to the right
of the title and left of the buttons -- `drag to move · click to split up` at
x=42, `drag to move` either side of it.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The previous commit shipped the documentation for `make tools` and the `.gitignore`
line for the venv it creates, and not the target itself. A `git reset --hard`,
undoing a throwaway probe commit, took the working tree with it -- which is what
`--hard` does. The `.gitignore` line was noticed missing and rewritten; the three
files it was standing next to were not, so `docs/design.md` has been describing a
target that did not exist. This is that target.
`ruff` is not a system package on most machines, and `pip install --user ruff` is
refused outright on a distro python (PEP 668):
$ make tools
ruff 0.16.3 in .venv
survives make clean; rm -rf .venv to start over
clang-format stays a system package, because everywhere it is one; `make tools`
says so when it is missing rather than pretending it handled it.
**The venv is looked in before `$PATH`, and that order is the point.** `ruff.toml`
now pins the version with ruff's own `required-version`, so a ruff of the wrong one
refuses to run and names both versions. Preferring `$PATH` would then strand
somebody with 0.14 from their package manager and 0.16.3 sitting in the `.venv` the
repo just built them -- refused by the tool, with the fix on disk and nothing
looking at it. Tested with exactly that, for `make fmt-check` and for the hook.
The pin exists because a formatter whose output moves between releases turns
"formatted" into "formatted by whoever committed last", and the diff lands on the
next person to touch the file. Bumping it is one line in `ruff.toml`, one in the
Makefile, and a reformat commit -- the honest cost of a new version.
The hook resolves ruff the same way, off `git rev-parse --show-toplevel` so it works
from a subdirectory, and its message when ruff is missing now says `make tools`.
Verified from a deleted `.venv`: `make fmt` stops with `ruff not found. run: make
tools`; after `make tools` both `fmt` and `fmt-check` work with nothing on `$PATH`;
`make fmt` over the whole tree changes nothing, so the pinned version agrees with
what is committed; the hook formats a staged Python file through the venv, and still
does with 0.14.0 ahead of it on `$PATH`.
The hook was tested by running `.githooks/pre-commit` directly against a staged
file. The obvious way -- a real probe commit -- needs the working tree clean to be
meaningful, and getting there is what caused the mess this commit is repairing.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
`ruff` is not a system package on most machines, and `pip install --user ruff` is
refused outright on a distro python (PEP 668) -- so the last two commits shipped a
formatter nobody could run without knowing to build a venv by hand. `make tools`
builds it:
$ make tools
ruff 0.16.3 in .venv
survives make clean; rm -rf .venv to start over
clang-format stays a system package, because everywhere it is one. `make tools`
says so if it is missing rather than pretending it handled it.
**The venv is looked in before `$PATH`, and the order is the point.** `ruff.toml`
now pins the version with ruff's own `required-version`, so a ruff of the wrong one
refuses to run and names both versions. Preferring `$PATH` would then strand
somebody who has 0.14 from their package manager and 0.16.3 sitting in the `.venv`
the repo just built for them -- refused by the tool, with the fix already on disk
and not being looked at. Tested with exactly that: 0.14.0 on `$PATH`, `make
fmt-check` still clean.
The pin exists because a formatter whose output moves between releases turns
"formatted" into "formatted by whoever committed last", and the diff lands on the
next person to touch the file. Refusing loudly is better than reformatting quietly.
Bumping it is one line in `ruff.toml`, one in the Makefile, and a reformat commit,
which is the honest cost of a new version.
The hook resolves ruff the same way, off `git rev-parse --show-toplevel` so it works
from a subdirectory, and its "not installed" message now says `make tools` instead
of naming three package managers and hoping.
Verified from the state a fresh clone is in: with no ruff anywhere, `make fmt` says
`ruff not found. run: make tools` and stops; after `make tools` (2.6s) both `fmt`
and `fmt-check` work with nothing on `$PATH`; the hook formats a staged Python file
using the venv; the venv survives `make clean`; and `.venv/` is ignored.
`make fmt` over the whole tree after all this changed nothing, which is the useful
part: the pinned version agrees with what is already committed.
One near-miss worth recording: the `.gitignore` line for `.venv/` had to be written
twice. A `git reset --hard` undoing a throwaway probe commit took the working tree
with it, including that edit -- which is what `--hard` does, and why the state was
checked afterwards rather than assumed.
|
| |
|
|
|
| |
The second mechanical commit, named beside the first so `git blame` attributes
those 6,714 lines to whoever last had something to say.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The other half of the pre-commit hook. C goes through clang-format, Python through
ruff: `ruff check --select I --fix` to sort the imports, then `ruff format` to lay
them out. `make fmt` does both, `make fmt-check` reports, `make hooks` installs it.
ruff rather than black because it is one static binary instead of a Python
dependency chain, and fast enough that a hook is free. Its formatter is
black-compatible, so this is the ecosystem's formatting rather than a dialect --
measured at 9,935 changed lines against black's 9,953, which is the same answer.
**Left entirely at the defaults**, which is the opposite of what `.clang-format`
does. That file argues with LLVM in five places because the C here was
hand-formatted into a house style worth keeping. The Python was not, and a
formatter of this kind is only worth having when there is nothing left to argue
about: every knob turned is an argument somebody has to have again later. 88
columns, double quotes, ruff's own line breaking.
**Import sorting is on, and C include sorting stays off.** Not an inconsistency: a
C include order can be load-bearing -- `_GNU_SOURCE` ahead of the system headers,
and a grouping somebody wrote by hand -- while Python import order is not. Where it
genuinely is, a statement between imports ends the block ruff will sort, and the
test files lean on exactly that:
import sys
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from harness import Session, check, report
`from harness import` cannot be hoisted above the line that makes it importable,
because the `sys.path.insert` between them is a barrier. Verified before touching
anything, since getting it wrong would have broken every test in the suite at once.
Two details in the hook worth naming. `--select I` is passed on the command line as
well as set in `ruff.toml`, so a stray `select` in somebody's own config cannot
turn a formatting hook into a linter that refuses a commit over an unused variable.
And the two tools are named together when either is missing, so setting up a
machine installs both at once instead of finding the second one on the next commit.
`set -e` and `[ -n "$c_files" ] && clang-format ...` do not mix: when the list is
empty the AND-list returns non-zero and the shell exits, so a Python-only commit
would have aborted with no message at all. They are `if` blocks. Found by testing
the case rather than by reading it.
Verified in a scratch clone, seven paths: Python alone, C alone, both in one
commit, imports sorted with the `sys.path` barrier intact, a `contrib/` script that
is Python with no extension on it, a partly staged Python file refused with `HEAD`
unchanged, and a commit with no code in it drawing no comment.
|
| |
|
|
|
|
|
|
|
|
| |
One line, naming the commit that formatted the tree and nothing else, so
`git blame` attributes those 1,843 lines to whoever last had something to say
rather than to the formatter.
Not wired up by `make hooks`: `blame.ignoreRevsFile` changes what a person
reads, and setting somebody's git config for them is a different kind of thing
from installing a hook they asked for. The file says the one command.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
The bumpy one. `make fmt` over every `.c` and `.h` outside `vendor/`: 32 files,
1,843 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.
Two thirds of it is alignment the formatter now owns: trailing comments in a
struct, continuation lines under a signature. The rest is the hand-packed tables
opening up to one entry per line -- `ACTIONS`, `NAMED_KEYS`, `ACTION_HELP`. That
is a real loss of density and a real gain in diffs: adding an action was a reflow
of the block and is now one line. Anything worth keeping packed can be fenced with
`// clang-format off`; nothing was, on the grounds that a table is the thing a
formatter is for.
**One change is not formatting.** `src/config.h` had three trailing comments glued
onto one field:
uint16_t hover_delay_ms; /* how long the pointer must rest to arm a guide */
/* how long an announcement stays up */ /* rows per wheel notch */
-- `toast_ms`'s and `scroll_lines`' comments, landed on their neighbour by an
earlier edit, documenting the wrong things ever since. It is in this commit
because it is what caught it: clang-format could not settle on that line, wrapping
it one way and then re-indenting the wrap on a second pass, so `make fmt` was not
idempotent. The comments are back where they belong and it is.
Verified after the reformat, not before: `make clean && make all` is
warning-free, `make retest` is 1801 checks green, and `make fmt` twice in a row
changes nothing the second time. A formatter that alters behaviour would be worse
than none, so the gate was the whole suite rather than a reading of the diff.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
`make hooks` points `core.hooksPath` at `.githooks`, and from then on committing C
formats it first, so what lands in git is already formatted. `make fmt` does the
whole tree, `make fmt-check` only reports. The reformat itself is the next commit.
**The config was chosen by measuring, not by taste.** LLVM is the base because it
is what this code was already written in by hand -- two spaces, 80 columns,
`char *p`, braces attached -- but LLVM alone rewrote 38% of the lines. Three
disagreements account for most of that, and each is a real house idiom rather than
an accident:
- `if (!p) return false;` on one line, which is the guard idiom throughout. LLVM
splits every one of them in two, two thirds of the churn on its own.
`WithoutElse`, so an `if` that has an `else` still opens up: a one-line `if`
hiding an `else` is the arrangement that hides a bug.
- Comments are not reflowed. They are prose, wrapped by hand, and the breaks
sometimes carry meaning -- a paragraph, a list, a diagram of a pane. A comment
that runs long is a thing for a person to fix, not for a formatter to even out.
- Include order is left alone. It is grouped by hand here and is not alphabetical
within a group; sorting it would be churn at best and, at worst, a build that
depends on an order nobody wrote down.
Those three, plus one-line `case` labels and loops, bring the reformat from 38% to
17% -- and what is left is alignment and the hand-packed tables, which is the part
a formatter is *for*.
The vendored terminal core is excluded everywhere: `.githooks/pre-commit`, `make
fmt` and `make fmt-check` all filter `^vendor/`. It is pinned by commit (D12) and
reformatting it would turn every future re-vendor into a merge.
**The hook refuses a partly staged file rather than widening the commit.**
`clang-format -i` rewrites the working tree, so re-staging afterwards would commit
work somebody deliberately left out with `git add -p`. It says which files and what
to do instead. It also says when it formatted something, because a hook that edits
your commit silently is a hook people learn to distrust, and it fails with the
package name when `clang-format` is missing rather than letting unformatted C
through the one check meant to stop it. `--no-verify` still escapes.
Verified in a scratch clone, all four paths: a badly formatted file is committed
formatted; a partly staged one is refused with `HEAD` unchanged and the index
untouched; a commit with no C in it draws no comment; and `--no-verify` puts
unformatted code in, which is what asking for it means.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
```
│help: find a package to install `watch` with `cargo search cargo-watch` │
│ │
│[ran: cargo watch -x test] │
│[process exited: status 101] │
╰ exited: status 101 ─────────────────────────────────[re-run]─[close]─╯
```
`[re-run]` is one button on a pane whose program is gone, and nothing on the pane
said what it would run. The command was never anywhere: not in the frame, not in
the backlog, not in the exit note. Worst in a tab a project's layout built, where
the command was never typed into that pane and so cannot be scrolled back to
either -- which is now the common case, since that is what `open-workspace` does.
So the death note gains a line above it. A pane running the session's shell has no
command to name and gets the exit line alone, as before: `[ran: /bin/sh]` would be
noise dressed as information.
One note rather than two calls. `pane_note` puts a blank line either side of what
it writes, so two of them would space the two halves apart as if they were about
different things, when they are two facts about one run.
Re-running keeps the previous run above it (D14), so the pair turns a pane you
re-run into a log of what ran rather than a pile of identical epitaphs.
Not put in the frame footer, which is the other place the question gets asked. It
already carries the exit words and competes with the buttons for width, so a long
command would arrive truncated in the one place that is always visible -- and the
note is on screen anyway in the case that matters, because a pane whose program
just exited is scrolled to the bottom.
**Three existing tests were counting a string that now appears twice.** `DIES_CMD`
is `echo mark-one; exit 3`, and they waited for `screen().count("mark-one") == 2`
to mean "it ran twice" -- but the note contains the command, so every run now
matches twice and the wait never finished. They ask `output_runs()` instead, which
counts lines holding the marker that are not the `[ran:` line. Per line rather
than by trimming the text, because every row still has the pane's frame border on
the end of it. That is a fair correction and not a weakening: the assertion is
still "the output appeared twice", which is what they meant.
Verified: 56 checks in `tests/test_dead.py`, 1801 overall, all green. Three new:
the command is named and sits *above* the exit line (asserted by row order, not by
reading the two strings out of one screen); a pane with no command names none; and
two runs produce two of each note. Then a real session, `command="cargo watch -x
test"` with cargo-watch not installed, which is how this was noticed.
|
| |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| |
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.
|