Mouse and selection - #7
Merged
Merged
Conversation
Mouse dispatch is a hit test, so a drag only ever reached the node the pointer happened to be over - which is not the node the drag is about the moment the pointer leaves it. An `onMouse` returning true on a `down` now claims the drags and the up that follow, wherever they land. Hover was worse: `onHover` was declared on every node and called from nowhere, and the `hover` style overlay compared a focus id against `props.id`, so only a focusable node could ever be hovered. It is a hit test over laid-out boxes now, inheriting down the chain the way it does in a browser - a row is hovered while the pointer is over the label inside it, because the label is what the hit test finds. `MouseEvent.at` is a timestamp, stamped by whatever produced the event. The wire reports presses and releases and has no notion of a double click, so telling one gesture from two is arithmetic on when they arrived and where. Also here, all of it about the terminal rather than the tree: - OSC 52 was switched off in the one place it exists for. The clipboard capability required a recognised terminal, and none of the variables that name one survive an ssh hop - TERM_PROGRAM, KITTY_WINDOW_ID and WT_SESSION are set by the terminal you are sitting at, not the machine the program runs on. A remote session saw a bare xterm-256color and dropped every copy. On now for everything but screen, which shows the payload as text. - A theme can say what shape the caret is: `cursor` is block, underline or bar, applied through `TerminalAdapter.setCursorShape` (DECSCUSR) and reset at teardown only if the session set it. - A divider is not a border. `divider` and `dividerChars` are their own theme setting with their own six sets, resolved down the extends chain, so a borderless theme can still separate with a line. The harness gains `drag(from, ...to)` and `clickRepeat(x, y, times)`; `click` steps its clock past the repeat window, so two clicks in a test are two clicks.
…mselves A click puts the caret where it landed, a drag selects, and the release puts the selection on the system clipboard over OSC 52 and into the store. Dragging past the edge scrolls the field. Double click takes the run under it - letters with letters, spaces with spaces - and a third takes the logical line rather than the row it was drawn on. shift with the movement keys extends and copies; ctrl+left and ctrl+right move a word at a time. That is a debt being paid rather than a feature: reporting mouse events takes the terminal's own select-and-copy away, so an application that reads the mouse has to hand one back. The palette and the menu: - The category names the group, once, above it. In every row's right-hand column four screens read "Screens, Screens, Screens, Screens" - in the width the rows needed for saying what they do, and still without marking where a group started. - `descriptions: 'below'` gives a row's description a line of its own. Four approval modes named in two words each are told apart entirely by the sentence under them, and inline that sentence shows the same truncated half of every answer. - The panel fits what it holds, up to `maxWidth`. A constant is too wide for one-word answers and too narrow for sentences, and it is the same constant either way. - It opens on the answer in force, which `ArgSpec.default` is where a command says. Opening at the top says the first option is the current one, which is wrong on every list where it is not. And two things that were wrong about width and space: - A narrow `List` row gives up its description, not its status. All three columns shrank together, so a catalogue at 58 columns cut the label *and* the status to keep a workspace path nobody was scanning for. - The workbench shell had one pad on the left of the main area and none on the right, in every theme.
…t was done
The slash menu listed the client's own commands and then sent whatever was
typed down the session channel, so `/go.sessions` went to the agent as a
message - the one place it could not possibly mean anything. It also could
not be navigated: the list was `focusable={false}` with no highlight and no
click, so it was decoration over a field.
Arrows walk it, a click chooses, and enter runs the command. A slash the
menu does not match is left alone and sent, because that is how a command
the *agent* offers reaches it. One that still has a question to ask opens
its picker rather than `execute` refusing a missing argument.
A tool call is something the agent did, not something it said. It was drawn
inside the speech gutter, one indent in, as though it were a paragraph of
the answer - and its input went straight onto the row, newlines and all, so
a three-line JSON object made the row three lines tall with the tool's name
floating beside the middle brace. It sits at the turn's own left edge now
with its status glyph where the header's bullet is, a chevron trailing when
there is something under it, a one-line summary on the row, and the whole
input on its own lines once opened. Clicking it opens it, and it lights up
under the pointer.
A chip's panel is also that chip's toggle, and the settings it offers now
declare what they are currently set to.
The client subscribed to one session's channel and to nothing else, so a session appearing, finishing or starting to wait was invisible until somebody navigated away and came back - a reader doing by hand what the host had already said. `HostConnection.onSessions` is the catalogue moving as opposed to one session's channel, and the live client raises it from the root channel it was already draining for the agents it advertises. Reads are coalesced: a turn finishing is several actions and one `listSessions`. The header had the other half. `openSession` is a plain store read, so a title or a status arriving changed the store and left the row showing what it said when the session was opened - which is why it only caught up when navigating away and back remounted it. Also: - Only the title gives way in the header. Everything else on that row is fixed-width and says what the application is; truncating "Assistant" to "Assist…" to fit more of a session title gives up the one part that is the same on every screen. - A caption above the transcript, outside the scrolling: harness, model, workspace, when it started, and the chat uri that gets pasted into a shell. The things the header cannot fit and a reader scrolls up looking for. Not the title - that is on the line directly above. - `pageKeys="always"` on the transcript, so page up from the composer means the conversation rather than a key nothing wanted. The snapshot test asserted the tool call from the top of the transcript, which made a test about *parsing a snapshot* a hostage to the row budget: anything the screen grew pushed it out of view. It moves the cursor onto the call now, which is where the call is rather than where it fitted.
`{caption}{items.map(...)}` reaches a component as `[caption, [a, b, c]]` -
two children, one of which is an array. A feed of twenty entries with
anything written beside the map therefore counted two: the cursor clamped
to the second, every key after the first did nothing, and the scroll never
moved.
It reads as a dead list and is an arity bug. Flattened at the door, where
JSX's grouping stops being the caller's problem.
…ing it The test hardcoded `'▎'`, so changing `activeRule` in the icons file broke a test that was not about which glyph it is - which is exactly the drift the icons file exists to stop, reappearing one layer up. It now imports `Icon.activeRule` and asserts the pane is marked, leaving the choice of mark where it belongs.
The built-in row - icon, title, description, meta, on one line - is what most catalogues are, and it stays exactly what you get by passing nothing. What was missing was the way out. Every caller that wanted a different shape was going to ask for another field on `ListItem` and another flag saying where to put it, and that road ends with a component whose props are a small layout language which still cannot draw the row after next. So `renderItem` draws the contents and the list keeps everything a row cannot do for itself: the selection, the keys that move it, the window that scrolls, the highlight, the marker column and the click. `state` tells a row what it cannot know - whether it is the selected one, and whether that selection is live. `itemHeight` comes with it because the window is arithmetic, not measurement: the list decides how many rows fit before anything is drawn, which is the only way a thousand rows cost what ten do. A row taller than a line has to say so rather than be discovered. There is no `keyExtractor`. `ListItem.id` is already both the reconciliation key and the selection's name, deliberately the same string: a row that reconciled under one identity while the selection pointed at another is a highlight on the wrong row, with nothing in either place to show why. The prop extractor needed the same change made to it. A generic component cannot state its props through `defineComponent`, so it is written as a plain function and cast on the way out - and the extractor only read bare calls, so `List` lost its entire table to a "_No props of its own._". A table that disagrees with the source is the one thing that script exists to prevent.
A new row kind, because the invariant everything downstream depends on is that every row is exactly one row tall. A cell wider than its column is cut, never wrapped: a viewer windows these by index, so a table that grew a line when it was measured would put every row after it in the wrong place. The edges are rows for the same reason a fence's rules are - they take a line on the screen, so anything counting rows has to count them. Columns are measured once for the whole table and repeated on every row of it, because a column measured per row is not a column. When they will not fit they give the width back in proportion, the widest losing most, with three cells as the floor. `:---:` and `---:` are honoured, `\|` stays inside its cell, and the outer pipes are optional because that is what people write. A line of pipes with no divider under it is left alone. `cat f | grep x` is a pipeline far more often than it is a one-row table, and treating every bar as a column is how prose ends up in a grid. The box is drawn even under a theme with `border: 'none'`. That is a theme saying "do not box things" - panels, fences, dialogs - not "do not tell these columns apart": a table's rules are its structure, and without them it is text that happens to line up with nothing to say where it ended. The glyphs are asked for by name, which routes them through the terminal's own limits, so a console that cannot draw box characters gets `+`, `-` and `|` rather than a row of tofu. `tableRules: 'all'` adds a rule between every pair of rows. It is the theme's call and it opts in, because those lines are right for a table of few long rows and noise on a table of twenty short ones. It is answered in the layout rather than by the painter for the same reason as the edges: a rule between two rows is a row.
`arrowUp` and `arrowDown` were there and the other two were not, so a hint row naming a left or right key had nothing to name it with. Both sets get them, including the ASCII one - which is not a fallback nobody sees but what a Windows console actually prints, and a hint that named a key with a glyph the console draws as a box is a hint that has told you nothing.
…rkdown has an off switch The catalogue first. A title, a harness, a workspace and a status sharing a pane that is also sharing the terminal with the detail panel leaves all four truncated - a row reading `Draft replies for desk-produ…` beside `1b444e78-d050-4fb5-a5…` has answered neither of the two questions it was asked. So the title gets the width and what qualifies it goes underneath, through the list's own `renderItem` rather than through another field on `ListItem`. The harness stands where a model would. `listSessions` answers with a `SessionSummary`, and the model AHP knows is the one on the last message, which lives on the session channel: a model per row would be a subscription per row. Below `splitAt` the detail pane is a drawer. Forty cells of it take the list down to a column that cuts every title, and the pane they were taken for is still too narrow to hold the URIs it exists to show - two truncated halves rather than one whole one. Right opens it and left puts it away, the key pointing at the pane. Opening it is going to it, because the width follows the focus and a wide pane nobody asked for is the list being narrow for nothing. The width is settable at boot. The pane has to claim the keyboard itself: a focusable registers in its own effect, so on the render that opens the drawer the id does not exist yet and the screen's `focus()` quietly returns false. Then the conversation. The session header lives inside the scroll rather than above it, which is what was asked for and where it has room to say everything instead of what fits in a title bar. Page keys reach the transcript from the composer, a tool call sits a level out under a dot with its arguments on their own lines, and rows light up under the mouse. And markdown gets a switch. Drawing it is the default - reading `**this**` is reading the punctuation instead of the sentence - but off is for when the punctuation is the point: copying a fence out with its fence, or reading a link's target rather than its label. Raw is a plain `text`, not a `MarkdownView` told not to parse, because anything that lays the document out has already decided some of those characters were structure. Bound to `m` in the chat scope, and to `ctrl+m` for the terminals that can send it. Ctrl+M and Return are the same byte: in raw mode Return sends CR, `0x0d`, and this stack names that `enter` - only the kitty protocol or xterm's `modifyOtherKeys` can tell the two apart, and no binding changes that. A refresh no longer forgets what the host refused. `refresh()` clears the error on success and the new per-event refresh runs constantly, so the reason a request was turned down was being wiped a frame after it arrived.
…ttice A repeated tile reads as a lattice. The eye finds the rhythm in about a second, and a sky of evenly spaced marks is graph paper - which is what it was, drawn on a screenshot with a ruler. `spacing` is the air between copies, always. `jitter` is how much more of it is left to chance: a limit rather than a factor, so `jitter.x = 6` is "somewhere between flush and six cells further on", dealt again for every step. The two stack. Neither set is the grid it always drew, exactly, and that is what the untouched tests are for. Breaking the lattice took three things, and only the first was obvious. Jittering the step alone leaves the copies in columns, because every row of them shares one set of positions - so the walk across is re-dealt per row. That breaks the columns and not the rows, because every copy in a row still shares a `top` - so a copy also wobbles vertically. And a tile as wide as its box is drawn once at zero with no second copy for a step to differ from, so every seed dealt the same picture - so the walk also starts up to a step earlier, backwards, which shows as a different slice rather than as a blank margin. The drawing had to be rebuilt for it. Rows used to be composed by repeating the tile's own string, which only works while every copy is exactly a tile from the last; the grid is now stamped and read off afterwards, through the same `runs` split that keeps a transparent cell transparent. `seed` because a pattern re-renders whenever its box changes, and one that reached for `Math.random()` while painting is a texture that crawls.
The chat example's `bood` does the opposite on purpose - one creature per file, drawn by hand at each size, with a comment saying that "a creature that fits a template is a template wearing a hat". That is right for what it is: a still figure whose whole job is to look drawn. This is the other trade. A persona is a puppet, and the point is that its parts recombine, so hand-drawing six parts times three facings times four moods is not art - it is arithmetic done by a person. Five rows of seven. Two rows of hat, a head, a middle, a pair of feet. The head is written with its eye socket empty and the eyes are stamped into it, which is what makes a mood four short strings rather than four whole heads. Eyes carry both axes: a mood chooses which eyes, and a facing chooses what they do when the figure turns - sometimes only moving within the socket, which `eyeAt` does for nothing, and sometimes a different drawing, which is the only option left to a bug whose eyes fill its whole head. A hat belongs to nobody. It is its own list and `drawPersona` takes one, because a hat that were a field on a figure could only ever be worn by that figure - and the question was how to put this one on somebody else. Its second row exists even when a hat only uses the first: one that borrowed a row from the head would move the face down for anyone wearing a tall one. Every part is a path. Frame zero is the still - what shows with animation off, in a snapshot and in a test - and the rest is the walk. Indexed per part and wrapped, so an owl blinks at its own rate without its feet being told about it. Every glyph is CP437, the set a console font is guaranteed to have. The `⅃` and `´` of the sketch are not in it and a Windows console draws them as empty boxes; `J` and `'` are the same shapes and survive.
Sky over earth at sixty-forty, both of them patterns, with clouds and flowers nobody placed by hand. An even split reads as a diagram; a low horizon reads as a landscape and leaves the clouds the room they need. Everything random is seeded, and the seed is rolled once at mount: a new scene per run and the same scene within one. A component re-renders whenever its box changes, so a scatter rolled while painting deals a new sky on every keystroke and differs in a test from on the screen. The clouds are built rather than chosen. Three sprites in a list is three clouds in the sky and you can see it - so a cloud is a stack of runs, widest at the bottom, each starting and ending a cell or two off its neighbour, with a lighter glyph at each end so the rim fades into the sky instead of stopping at an edge. A persona walks about in it, and goes where it is pointed. The walk is a clock rather than a transition: one cell per tick towards the target, and the same tick advances the stride, so the feet move at the speed the figure travels. When it arrives the ticker stops, so a scene with a persona in it costs what one without it costs. The figure is drawn by the *bands*, not in a layer over them. A cell's background belongs to the box that painted it - `Buffer.put` writes one unconditionally, and the only transparency this library has is declining to emit a cell at all. Drawn in a layer of its own, the figure arrives as a seven-by-five rectangle of the application's canvas cut out of the sky. So its position is published in screen cells and whichever band covers a row paints it, clipped to itself; one straddling the horizon is painted by both. Only the marks are drawn, so the gap between its feet shows whatever is behind it. The sky tile, the sun and the bot's turning head are Softov's.
…that works Two bugs in one row, and the second was worse than it looked. `width` is a starting size, not a floor - shrink is 1 unless it is said - so a value with nowhere to break was taking cells off the column beside it, and `Chat` started its value four columns left of `Harness`. A column that moves per row is not a column. And a wrapped value makes the row two lines tall while its label is one. A row centres its children unasked, so the label drifted down to sit beside the *second* line of a URI with nothing at all beside the first. `values` is the option for the rest of it: `selected` keeps the pane a list and wraps only the row under the cursor, `all` wraps every row. The catalogue's pane uses `all`, because the values there are the answer and showing the first half of the one you are looking for makes somebody walk to it to find out it was not the one. `alt+m` for the markdown switch, because `ctrl+m` cannot work and no binding changes that: in raw mode the Return key sends CR, `0x0d`, and this stack names that `enter`. They are one byte. Only the kitty protocol or xterm's `modifyOtherKeys` sends them apart, and over ssh there is neither. `alt+m` arrives as ESC then the letter, which survives ssh, tmux and a console that has heard of neither.
…lace Two pages, because they are two things. The clipboard is the terminal's. There is no key an application can bind to mean "copy" - the emulator takes its own shortcut before any byte reaches the program, and `Ctrl+C` and `Ctrl+V` were never available to be that shortcut, being SIGINT and readline's literal-next. So the table of what each emulator actually uses is written down, along with tmux and screen, and the two things that matter while writing an application: reporting the mouse takes the terminal's own select-and-copy away, and an application can write the clipboard but not read it. The keys are ours. Moving, selecting, the mouse gestures, enter, and the collision family that keeps catching people out - `ctrl+m`, `ctrl+i` and `ctrl+j` are the bytes for Return, Tab and ctrl+Return, so binding one is binding that key, and `alt+<letter>` is the modifier to reach for. That last one cost a round trip today, so it is written down rather than learnt again. Every control in `components/input/` now leaves to it, and `TextArea` cites the clipboard table where it explains what it owes a reader whose first drag selected nothing.
A `text` takes one colour for the whole run, which is right for nearly
everything and no answer at all where the colour *is* the content - a
banner, a ramp across a title, a palette walked down a block of ascii
art. `ColorText` is that case: it paints on a canvas and asks an `ink`
for a colour per cell.
An ink is spelled three ways. A ramp between stops, across the columns,
down the lines or corner to corner. A palette walked in runs, so
`every: [4, 3]` is four cells of one colour and three of the next. Or a
function handed the cell, which is the escape hatch - and the one that
cannot be written in a JSON screen, the same trade canvas makes with
draw.
Two things the building of it decided:
* the cell carries a `col` and an `index`, because they stop being the
same number the moment the text is not plain ascii, and the walk
advances by grapheme width - colour by index and paint at col, and a
gradient shears through the first wide character it meets;
* `alignBlock`, because textAlign centres every line over its own
middle. That is right for prose and it shears a picture: five rows
of block letters do not have equal widths once the trailing spaces
are gone, so each row lands somewhere slightly different and the
letters lean.
A block that wraps asks for no width and takes what it is given; one
that does not is as wide as its widest line and says so. Reporting the
unwrapped width in both cases pushed every sibling off the row.
The examples/ink example is the demonstration and the test. A field, a
list of inks, a font, and a panel that scrolls - and a switch that
swaps the block letters for three paragraphs of prose, because the
component has no idea which it is looking at. Its fonts are one
hand-drawn table with both cases on one baseline, plus three transforms
of it: wide, slant and shadow. A bitmap font is a grid of characters,
and a grid of characters can be sheared, doubled or duplicated by ten
lines of code.
ScrollView takes a `focusId`. It was the only focusable control without
one, so the example had no way to name the panel it scrolls - and the
test that scrolls it was passing for the wrong reason until it did.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.