Repository navigation
The bar becomes a console: five looks, one chosen, and the defects the comparison found - #4
Merged
Merged
Conversation
A shell prompt and a running program need different buttons, and the bar was telling them apart only for programs it recognised. `classify` returns an empty kind for anything outside its table — make, pytest, docker compose up, journalctl -f — and `pushContext` left `tool` nil for those, which is the same value it uses for "we are at a prompt". So the bar drew the directory browser and the projects list while a build was running, and pressing a directory typed cd 'server' and a newline into that program's stdin. The intent was already recorded: the comment above `fallbackTool` says the same holds for anything unrecognised. The branch implementing it was never written. Nothing new is guessed here. The foreground process group is read from the pty, as it always was; if it is not the shell's, the shell is not reading input, and that alone rules out every action that assumes a prompt. What is offered instead is the fallback table, and the header keeps the binary's own name so the person in the headset still sees what is running. Found by reading the code during a design review of the panel, then confirmed against context.go before the fix. Server-side only: no APK, and every headset already paired gets it as soon as the server restarts.
The bar had one look, arrived at by accumulation rather than by decision, and no way to compare it against anything. This adds the machinery to hold several and four alternatives to hold: capped is what the bar has always been, outline draws every key as a lit edge, machined gives the plate a material and the keys a moulded cap, tactile is the same idea at instrument-panel tension, and console rearranges the whole surface into three layers — what is read, what is pressed, and the app's own controls on a rail. Skin is where every number now lives: colour per role, corner, depth, travel, whether a key is a cap or an outline, whether a screen is inset or recessed. Buttons reads it while building a view rather than capturing it, so swapping the skin and rebuilding is enough to see the whole app in the other treatment. The drawing itself is split into small drawables that each describe one object under one lamp — PhysicalKey and KeyFace for a cap, Display for a screen, Section for a milled region, Deck for the plate, RockerHalf for a pitched slope, Wear for grain and scratches, Led for a lamp. A cap catches light on its top edge and a recess is shaded by its own upper wall; that inversion, not the darkness of the fill, is what makes one read as standing proud and the other as sunk in. The font was the accident worth fixing. Typeface.MONOSPACE sounds neutral and is not: on Horizon OS it resolves to DroidSansMono, a face drawn for Android 2, which is neither what a terminal looks like nor what docs/readability.md measured with. Every threshold in that file was measured with DejaVu Sans Mono, so the client was being judged against numbers for a face it was not drawing. DejaVu is now bundled and used by the bar and by the terminal alike; JetBrains Mono rides along until the headset says which it prefers, and the loser should be deleted rather than kept. Licences are in NOTICE.md — both permit redistribution, and a font file is an asset, not a dependency. Two traps are recorded in the code where they will be read. A drawn glyph lives in the key's background, so anything replacing that background empties the key — the rocker halves do exactly that, and Buttons keeps the glyph in a WeakHashMap so it can be put back. The obvious fix, a foreground, is not drawn at all on these views; trying it emptied every arrow and the backspace key while looking correct. Verified on the emulator across all five skins: the app starts, every icon is present, and the keypad, the rockers and the listings behave. What the emulator cannot answer is how any of it reads through the lenses, and that pass belongs to the person wearing it.
Looking at the bar meant pairing a headset, starting a server, opening a shell and putting the headset on — minutes per iteration, and the wrong loop entirely for a question about colour. The concept screen draws the real ContextBar from a recorded context, so what is judged is what would ship rather than a mock that agrees with itself. The contexts are verbatim --dump-context output, trimmed. It lives in src/debug, and so does the manifest entry that launches it, so a release build contains neither by construction rather than by remembering to take it out. There is also a concept variant: the debug contents signed with the release key. Android refuses an update signed by a different key, the headset already carries a properly signed install, and the alternative was uninstalling it — which throws away the paired servers and their tokens, to look at a colour. docs/panel-review.md is what came back when four reviewers were asked to take the console layout apart: a control-surface designer on grouping, a designer of dense professional interfaces on hierarchy, a spatial-interaction designer on ray ergonomics, and an interaction designer on information architecture. Every claim recorded as settled was checked against the code before it was written down, and two of them were fixed on the spot — one is the previous commit. The third finding is in this one: the keypad was inside a scroller with the shortcuts. The rule everyone checks was being honoured — KeyPad is built once and context never touches it — and the keys still moved, because with enough shortcuts to overflow the column, scrolling to reach a chord carried Enter and ^C off the panel. The guarantee is about where the keys are, not how often they are constructed. The keypad is now outside the scroller. The lever and the rotary encoder are deleted. Both were drawn, neither was ever reachable, and the reviews agreed on why they should not come back: a lever's whole visual argument is "throw me" and it is operated by pointing and pulling a trigger, and a knob's real feedback is detents felt in the hand, which a ray cannot feel. The rocker survives on the same test — it is pressed, and pressing is the verb this input has. The sizing half of the review is recorded and then set aside, with the reason: the panel is worn close, much nearer than the 1.5 m the readability figures were measured at, and angular size is distance divided into height. The analysis is kept for the day a layout is too wide to keep close. What survives regardless is spacing where a miss is expensive — that cost does not shrink with distance.
The first pass at the console layout put five identical housings on the panel, wrote their names in the smallest type on the surface, and let one colour carry five unrelated meanings. This is the review's visual half, applied. **Green.** It was confirm, data, voice, connection and cursor at once, and the job with the most area wins — a whole column of green text against one green key, so "green means go" could not survive. The fix is saturation rather than hue: the saturated green stays with the things that act, Enter and the link lamp, and the screens drop to a paler, cooler phosphor. That is closer to a real P1 tube than the previous colour was, and it keeps the reading layer's whole reference, which moving to amber would have thrown away. The microphone loses its green outright — dictation is an input, not a confirmation, and it was that colour only because the VOICE role was invented next to ENTER. **Plates.** Both readings of the problem were right about different places. Round a screen there is now no plate: the glass is already a housing, and a box around a box at nearly the same radius reads as a framing error — two walls 10dp apart with the outer one 2% brighter than the panel, which through the lenses is the same colour, so the box was paid for entirely by its outline. Where a plate remains it is visible, and it says which kind of housing it is: a region cut into the chassis catches light along the top edge, a region the host refills does not. Furniture against weather, told by how the panel was made rather than by a decoration applied to it. **Type.** Headings were 9dp with 0.18 tracking — the least readable text on a surface whose regions they exist to name, and the wide tracking made it worse by destroying word shape, which is the only cue left when a glyph is too small to read letter by letter. Now 12dp at 0.10. Chips go from 12.5dp to 14. The bar's window opens at 900dp instead of 460. The old default was right for one row of keys beside a flow of buttons; the console arrangement stacks a header, two listings, a chip column and two readouts down one column, and at 460 it opened already scrolled — the one state a control surface must not start in, because you cannot learn where a control is if you have to go looking for it. The minimum is unchanged: the window is freely resizable by hand, and the floor is there to stop an accident rather than to enforce a taste. Checked on the emulator across the skins and worn on the headset.
Grouping was doing no work on this panel. Every control was a cap with a gap around it, so a pair reached for together looked exactly like two unrelated neighbours, and the only way to say "these belong to each other" was a rocker — which says something stronger and usually wrong: that one is a direction of the other. Dictation is not a direction of the keyboard. So there is a third arrangement now, and it is the one a keyboard has always used. A cluster sets several keys into a single cut-out: each cap rounded only where it faces the outside world, square where it meets a neighbour, two device pixels of housing showing through as the seam. The outer silhouette is one rounded rectangle and every internal edge is a straight line, which is what a moulded key block looks like from above. The corner rule is the whole of it — a corner rounds when both of its sides are external — and it produces the right answer without being told: under the arm of the arrow tee the left key has nothing above it, so its top-left rounds along with its bottom-left, and only the centre key is square all round. The arrows get that tee. The first attempt squared the bottom of the up key and closed the gap beneath it, which is most of the geometry and none of the conviction: three keys inside a housing and a fourth outside it, touching. A key block is a block because the panel was cut that shape, so TeeHousing draws the cut — one path, two rounded rectangles unioned, no interior edge. Nothing moved to get it: ^W and ^U keep their columns and the up key keeps its cell. Clusters are drawn on every skin, not only the ones with a moulded cap. Grouping is a claim about what the controls are, and that claim is true whether the panel is machined metal or flat plates; the silhouette is shared, the material is not. Enter is no longer a member of the grid. It is the key that commits, the only irreversible thing on the panel, and on a real console such a key is mounted in its own bezel with a groove around its face so the finger finds it without the eye. LaunchKey draws that: a capsule whose radius is half its height, an inlaid groove lit on its lower edge like every other recess here, and the return mark drawn by Glyphs rather than typed, so it matches the arrows instead of arriving from whatever font the system picks for the code point. Two smaller corrections in the reading layer. Hovering a row no longer paints a rectangle behind it — a row is text on glass, and a filled background turns it into the button this layer exists not to be; the phosphor brightens instead, which is what a cursor does on a character display. Pressing draws a thin outline, which is how a terminal has always marked a selection. And the console header now writes in the phosphor rather than in the panel's neutral text: it is a screen, so it is written by the same beam as the screens beneath it. LAST COMMAND and SELECTED are deleted. They existed on one skin and nowhere else, and a concept whose skins differ in what they contain compares two things at once and settles neither. They were also the weakest readouts on the panel — both echoed the press you had just made rather than reporting anything.
…ly lands Three corrections in the reading layer, and the first one is a bug with a lesson in it. **The glow never appeared.** The listener was on the TextView holding the name, and that view is neither clickable nor focusable — the row is what is pressable, so the row is what the ray hovers, and Android does not ask an inert child about hover at all. The code was correct and was attached to something that is never told. It now sits on the row and lights the label inside it: same effect, one level up. **The two listings get their corners back.** They were squared when the plates around them were removed, on the argument that a rounded screen and a scrollbar disagree — true, and it left the reading column as two square wells beneath a rounded header, which is a worse disagreement. The header is the shape the panel already teaches, so the listings take it too. **The header writes with the beam rather than with ink.** Its colour had been moved to the phosphor and its bloom had not, so it was the one lit surface on the panel drawing flat text. The spread is now a named thing — Buttons.bloom — used by the header and by every row, so the two cannot drift apart again. Wide and weak on purpose: a narrow strong bloom lands on the stroke and thickens the letter, a wide weak one lands around it and does not. Verified on the emulator for colour and shape. The hover state itself cannot be reproduced there — a tap is not a hover, and only a ray produces one — so that half is checked in the headset.
**The key.** It was a capsule spanning the row, and a capsule reads as a bar you slap — the opposite of what this control is. Enter is the one deliberate, irreversible thing on the panel, so it is now mounted the way such a control is mounted on a machine that has one: a hole cut through the plate with a deep collar, a square-shouldered cap seated inside it, an inlaid groove around the face, and knurling down both shoulders. The collar is what reads as thickness of plate, so it is seven pixels rather than the hairline the first version used; the cap grows to match, so the target does not shrink for the sake of the frame. **The mark.** The return glyph was unreadable and the reason generalises: its arrowhead was two arms of different lengths meeting at a shallow angle, and at a stroke width of an eighth of the box the arms merged and the notch filled in — a blob with a tail. The arms are now equal and meet at a right angle, symmetric about the bar, which is the one arrangement whose notch cannot close as the ink spreads. The bar sits an arm's length above the bottom so the lower arm has somewhere to go. **The glow was being cropped.** A bloom is painted outside the letter, so every container between the text and the screen has to stop clipping — the row, the flow inside the screen, and the scroller. Without that the lit row ended in a hard vertical line at the cell's edge, which is why the same effect looked right on the skins whose rows span the whole screen: there the clip falls outside the glow rather than through it. The text also carries padding now, because a TextView paints its shadow inside its own bounds however permissive its parents are. The two listings get their corners back at the same time. They were squared when the plates around them were removed, and that left the reading column as two square wells under a rounded header — a worse disagreement than the one with the scrollbar it was meant to avoid.
Picked from the twenty-variant sheet: hazard striping, full width, the legend on its own dark patch. The reasoning it wins on is not that it looks striking. Everything else on this panel is graphite with a coloured edge, so a louder graphite key would still be the same category of object and would still have to be read to be identified. Banding is a different category — and it is the only marking here that works in peripheral vision, which no legend can. Enter is the one irreversible control on the surface, so being findable without looking at it is the whole requirement. Details taken from the sheet rather than approximated: bands of twelve pixels at -45°, #F2B613 against #1A1508, a #8A6A12 edge, a 5px solid foot in #4A3A08 under an ambient blur, and a hard white line along the top where a moulded bar parts from its die. The legend sits on a patch at 82% black so the banding does not run through the letters — which is why this control is a container rather than a single view, since one TextView cannot have two backgrounds. The stripes are generated into a tile and repeated by a shader. Rotated rectangles were the obvious alternative and are worse: they have to be drawn past the corners and clipped, and the clip is exactly where a diagonal picks up a stair-stepped edge. It still behaves like every other key — travels into the plate on press, the foot collapsing under it. A control that announces itself differently should not also move differently.
The striped bar went in and came straight back out: it was findable without looking, which was the argument for it, and it was also the loudest object on a panel whose whole language is quiet graphite. Loud is not the same as legible, and a control that shouts on a surface where nothing else does reads as borrowed from another machine. The slot says the same thing by the opposite means. Every other control here stands proud of the plate; this one is sunk into a pocket, and the inversion is what marks it — a recessed key on real equipment means "not by accident". It costs no target area, because the pocket is only a few pixels wider than the cap lying in it, and it needs no colour at all. Sized to itself rather than to the row. A bar spanning the full width says "sweep at me"; this is the one deliberate press on the panel, so it is a key with edges you can see, centred in the space the row leaves. The return mark now follows the word instead of leading it — you read what the key does, then the sign that it fires — and the pocket is drawn from the same description as every other recess on the panel: shadow along the top wall where the plate overhangs, a hairline of light along the bottom. The cap keeps two pixels of foot showing beneath it so it reads as a separate part lying in the pocket rather than as paint on the bottom of it.
Third shape and the last one. The striped bar was findable without looking and was the loudest object on a quiet panel; the flat slot was quiet and read as a region rather than a part. A keycap in a retainer is both: unmistakably one mounted component, in the panel's own material. Three pieces, each doing what the others cannot. The retainer is a pocket milled in the plate, so the key is seated rather than painted on. The cap carries a three-stop gradient — bright across the top third, mid through the middle, dark at the foot — which is what a dished moulding does to light from above and in front, the same lamp the rest of the panel is lit by. The foot beneath it is solid and unblurred: an unblurred copy of a shape, offset down, is the only thing that reads as the side of a solid rather than as a shadow cast by one. Six pixels of throw, and on press the cap travels the whole of it while the foot collapses to nothing, ending flush with its pocket. That is longer than any other control here, deliberately — this is the key you commit with and it should feel like it went somewhere. **Its colours come from the skin, not from this file.** The cap is the ENTER fill, the rim is its edge, the retainer is the plate. So the same shape arrives in graphite on the flat skins and in warm alloy on the machined one, and Enter stops being the one control that ignores which machine it is part of. That was the real fault in both earlier attempts: each hardcoded a look, and a key that brings its own palette to every panel is a sticker, not a part.
… again Everything before this was a concept looked at through a debug screen. This makes it the app: the default skin is CONSOLE, so the real bar — the one a paired headset opens over a real shell — is the arrangement the concept work arrived at. Nothing about it is mocked any more; what it draws is what the server sent. The other four skins stay in Skin.java. They are not dead weight: they are how this one is argued with, and a look that cannot be compared against anything stops being a decision and becomes a habit. The debug gallery stays in src/debug for the same reason, and a release build still contains neither it nor its manifest entry. The window opens at 460dp again. It went to 900 on the argument that the console layout needs the height and should not open already scrolled, and worn, that was wrong twice: the panel is pulled in close, where 900dp of it fills the view and crowds out the terminal it exists to drive, and the listings are elastic — a short window shows fewer rows rather than a broken layout. Opening small is the right use of the one window property Horizon OS gives a 2D app: growing a window by hand takes a second, and a panel that arrives too big has already taken the space it was going to be judged on. Also fixed: the launch key's legend stayed put while its cap went down. The travel was drawn inside the background, and the text is drawn by the view — so the cap slid out from under its own label. Retainer and cap are now separate views: the pocket holds still, the cap carries the legend and travels with it. Verified against a live server from the emulator: paired, connected, and the bar drew the real cwd, the real directories, the real projects and the real command table with no recorded context anywhere in the path.
It did its job. The bar could not be looked at without a paired server, a running shell and a headset on a head, and a screen fed a recorded context turned a minutes-long loop into seconds. Five looks were built and compared through it, and the one that won is now the default. Keeping it past that point costs more than it gives. Its own switcher — SKIN, CONTEXT — sits at the top of what otherwise looks exactly like the real bar, and that is how it was opened by mistake and read as the shipped panel with a stray row of buttons in the corner. A concept screen that is indistinguishable from the product at a glance is a trap once the concept is decided. So src/debug goes, and with it the launcher entry that made it reachable. The headset now has one icon, and it opens the real thing. The concept build type stays and is now simply the debug variant signed with the release key. That is still needed for every iteration: Android refuses an update signed by a different key, the headset carries a properly signed install, and the alternative is uninstalling it and pairing every machine again. Checked after removing it: release and concept APKs contain exactly three activities — ServersActivity, TermActivity, BarActivity — and no GalleryActivity; the headset's launcher lists one entry; the installed APK matches the built one by hash; the app starts and stays alive with nothing in the crash buffer.
Both of these worked from a terminal and stopped working once the server was installed as a user service, which is why they survived so long: the way the server is developed is not the way it is run. The bar was offering two commands instead of six. runActions only offers a button for a command that exists, since a button answering with "command not found" is worse than no button — and it asked exec.LookPath, which searches THIS process's PATH. A service started by systemd --user has the bare system PATH; claude, codex, cch and cgit all live in ~/.local/bin, which .bashrc puts there. So the shell the button types into could run all four and the process deciding whether to offer them could not see any of them. It now asks the shell instead, in the mode a session is actually opened in: `$SHELL -ic` reads .bashrc, which is what pty.Start gets, while `-lc` reads .profile and answers with a shorter PATH than any session here has. Read once and cached, because it forks a shell — a tool installed while the server runs still needs a restart to appear, which is the trade the cache already made. The tray icon had never been attempted at all. trayAvailable required DISPLAY or WAYLAND_DISPLAY, and a user service starts at login, BEFORE the graphical session imports those into systemd --user — so the server's environment has neither, permanently. Measured on this machine: `systemctl --user show-environment` lists WAYLAND_DISPLAY while /proc/<pid>/environ of a server that had been up for three days does not. The question was wrong, not the answer. What a tray needs is somebody willing to hold an icon, and that is a name on the session bus, so the server now waits for org.kde.StatusNotifierWatcher and hands systray the goroutine once it is owned. Waiting costs nothing — that goroutine would be blocked inside the library anyway — and it makes the icon appear at login rather than never. fyne.io/systray already re-registers if the desktop restarts later; what it cannot do is start on a desktop we refused to look for. godbus moves from an indirect dependency to a direct one. It was already in the tree under systray, and asking the bus a question is not worth a second library. Verified: --dump-context under a stripped, service-like PATH now lists claude, codex, cch and cgit where it previously listed only `ls -la` and ^R; the installed service, restarted, registers org.kde.StatusNotifierItem-1110263-1 and the icon is in the tray. docs/service.md records both, with the commands that tell a service's environment from a terminal's.
Two things about the console layout, both of them about what the column does when there is not enough of it. The commands were being pushed off the bottom. The section had the weight and the two listings above it did not, so the listings took whatever their contents asked for — up to 230dp each — and the commands were handed what was left. In a column 460dp tall that is nothing: `claude`, `git status` and the rest measured to zero height and simply were not on the panel. They had not been removed by anything, they had been squeezed out by two directories. The listings now carry the weight and keep WRAP_CONTENT, which is not the contradiction it looks like: a weight applies to what is left over once every child is measured, and when that leftover is negative it is taken back from the children carrying weight. So the listings stay content-driven while there is room and are the first to give it up when there is not, and the commands keep their height either way. Their own scroller caps at 200dp rather than 230 — a chip is shorter than a listing row, so that is already five or six rows of them. An empty commands section hides like the two above it. And the skins are reachable again. They were built to be argued with — five of them, because a decision that has been compared against nothing is a habit — and the comparison lived in a debug gallery that shipped in no release. Removing the gallery removed the only way to see them at all, so the choice is offered where the person wearing the headset is: a key on the strip of things that are the client's own and are never sent to the shell. It is on that strip in BOTH arrangements, because a way out of a skin has to exist inside every skin. Stored, not held. The bar is recreated whenever the shell restarts it — resized, reopened, after the headset sleeps — and a look that lasted until then would read as the app losing its settings. CONSOLE remains the default and the shipped arrangement. The menu is drawn from the panel's own parts rather than as a PopupMenu: the system menu is a light rectangle in a system typeface, and at half a metre it arrives as another application landing on top of this one. Verified in the emulator, on the real server: the commands section is on the panel with the listings full, the menu opens over the bar, choosing Tactile rebuilds the window, force-stopping the app and reopening it comes back Tactile with `<string name="skin">tactile</string>` in shared_prefs, and the key to get back is on the Tactile strip. The headset carries the same build; the panel there is for the person wearing it to judge.
… panel Two lines of work that barely touch: main moved the per-tool key sets out of Go and into server/tools/*.yaml, and this branch rebuilt what the client draws and fixed what a service's environment costs the server. The only conflict was go.mod, and it was bookkeeping: godbus is a direct dependency here — the tray asks the session bus whether anything is willing to hold an icon — while main had it indirect under systray, and yaml.v3 arrives with the tool files. Both are direct now, and go mod tidy agrees. What merged cleanly and was checked rather than assumed: runActions still asks `installed`, which now searches the shell's PATH rather than this process's, so `--dump-context` under a stripped service-like PATH lists claude, codex, cch and cgit; `--tools` loads all seven files; the client builds and lints clean with the merged ContextBar, which took ICON_KEYBOARD from main's Buttons and the appearance menu from here.
…hing glyph Both cost a working session apiece and neither is guessable from the code. The first is the more expensive: a server installed as a user service has a different PATH and no display variables, and both of those quietly disable a feature rather than breaking it loudly. What makes it a trap rather than a bug is that developing the server hides it — from a terminal, everything works. The second was found twice, because the obvious fix for it is wrong in a way that looks right until the keypad is on screen.
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.
What
The bar had one look, arrived at by accumulation, and no way to hold it against
an alternative. This builds five, picks one, and ships it as the app's own.
consoleis now the default skin — the arrangement the concept work and thereviews arrived at: what is read on the left (a directory browser with a head,
a projects screen, command chips), what is pressed in the middle (the real
KeyPad, the host's shortcuts), and the app's own controls on a rail at theedge. The other four (
capped,outline,machined,tactile) stay inSkin.java, because a look that cannot be compared against anything stops beinga decision and becomes a habit.
Why it is not a repaint
Two of the five ended up nearly identical, which was worth learning: they
differed in material while sharing one arrangement, and no amount of colour could
have separated them.
consolechanges where a thing lives, and the rule is whatkind of thing it is.
The grouping primitives come from the same question. A cluster sets several keys
into one cut-out — rounded only where a cap faces the outside world, square where
it meets a neighbour — which is the third arrangement that was missing: loose
keys say "unrelated", a rocker says "one decision, two directions", and neither
fits
←↓→or a microphone beside a keyboard.Defects found and fixed on the way
cdto a program that owned the terminal.classifyreturns an empty kind for anything outside its table —
make,pytest,docker compose up— and that is the same value used for "at a prompt", sopressing a directory typed
cd 'server'into that program's stdin. Server-side:no APK, live on every paired headset as soon as the server restarts.
rebuilt — and still moved, because scrolling to a chord carried
Enterand^Coff the panel. The guarantee is about where the keys are.Typeface.MONOSPACEresolves on Horizon OS toDroidSansMono(checked on the headset's ownfonts.xml), while everythreshold in
docs/readability.mdwas measured with DejaVu Sans Mono — so theclient was judged against numbers for a face it was not drawing. DejaVu is now
bundled and used by the bar and the terminal alike.
empties the key. The rocker halves do exactly that.
Buttonskeeps the glyph ina
WeakHashMap; the obvious fix — a foreground — is simply not drawn on theseviews, and trying it emptied every arrow.
TextViewthat isneither clickable nor focusable. The row is what the ray hovers.
Reviewed
docs/panel-review.mdrecords four reviews of the layout — grouping, visualhierarchy, ray ergonomics, information architecture — with every "settled" claim
checked against the code before it was written down. It also records what was
deliberately not done: the sizing recommendations, because the panel is worn
close and angular size is distance divided into height. That analysis is kept for
the day a layout is too wide to keep near.
Testing
linuxterm, Android 34), all five skins, both recorded contexts:starts, stays alive by
pidof, every icon present, keypad and rockers behave.the real cwd, directories, projects and command table — no recorded context
anywhere in the path.
conceptbuild type that signs the debugcontents with the release key (Android refuses an update signed by a different
key, and uninstalling would throw away the paired servers and their tokens).
make fmt,make test, server builds,--dump-contextunchanged in shape.Not in this PR
Listed in
docs/panel-review.md: lamps that never change state, a debouncedefeated by the rebuild it causes, chips that appear because a repository went
dirty, and skills that arrive marked
skillvsskill-globaland are drawnalike. Also separate and pre-existing:
TermActivity.onDestroycloses a TLSsocket on the main thread, which throws
NetworkOnMainThreadExceptionwhen aterminal tab is closed.