From 7877b48ccd603d670aec673f8d7058675a17ab4d Mon Sep 17 00:00:00 2001 From: butschster Date: Sat, 8 Aug 2026 12:50:13 +0400 Subject: [PATCH 01/15] The bar stops offering cd to a program that owns the terminal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- server/context.go | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/server/context.go b/server/context.go index 8ff2619..2dc57fd 100644 --- a/server/context.go +++ b/server/context.go @@ -77,6 +77,20 @@ func (s *Session) pushContext(force bool) { isShell = true } else if kind != "" { tool = &kind + } else { + // Unrecognised, but still not the shell: make, pytest, docker compose, + // journalctl -f. The bar used to leave tool nil here, which sent it + // down the "at a prompt" branch and offered directories to cd into — + // while a program owned the terminal. Pressing one typed cd 'server' + // and a newline into that program's stdin. + // + // What is known is read rather than guessed: the foreground process + // group is not the shell's, so the shell is not reading. That alone + // rules out every action that assumes a prompt. The fallback table is + // what is offered instead, and the name stays the binary's own so the + // header still says what is running. + fallback := fallbackTool + tool = &fallback } } From 41f67ff9437550f3201e57d8b2f2430b0fff24c8 Mon Sep 17 00:00:00 2001 From: butschster Date: Sat, 8 Aug 2026 12:50:44 +0400 Subject: [PATCH 02/15] Five panels to choose between, and a font that was never chosen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- client/app/NOTICE.md | 22 +- client/app/src/main/assets/DejaVuSansMono.ttf | Bin 0 -> 343140 bytes .../src/main/assets/JetBrainsMono-Regular.ttf | Bin 0 -> 273900 bytes .../dev/butschster/linuxterminal/Buttons.java | 569 ++++++++++++++- .../butschster/linuxterminal/ContextBar.java | 672 +++++++++++++++++- .../dev/butschster/linuxterminal/Deck.java | 91 +++ .../dev/butschster/linuxterminal/Display.java | 196 +++++ .../dev/butschster/linuxterminal/Fonts.java | 83 +++ .../dev/butschster/linuxterminal/KeyFace.java | 146 ++++ .../dev/butschster/linuxterminal/Led.java | 64 ++ .../butschster/linuxterminal/PhysicalKey.java | 210 ++++++ .../butschster/linuxterminal/RockerHalf.java | 131 ++++ .../dev/butschster/linuxterminal/Section.java | 81 +++ .../dev/butschster/linuxterminal/Skin.java | 561 +++++++++++++++ .../butschster/linuxterminal/TermView.java | 2 +- .../dev/butschster/linuxterminal/Wear.java | 112 +++ 16 files changed, 2889 insertions(+), 51 deletions(-) create mode 100644 client/app/src/main/assets/DejaVuSansMono.ttf create mode 100644 client/app/src/main/assets/JetBrainsMono-Regular.ttf create mode 100644 client/app/src/main/java/dev/butschster/linuxterminal/Deck.java create mode 100644 client/app/src/main/java/dev/butschster/linuxterminal/Display.java create mode 100644 client/app/src/main/java/dev/butschster/linuxterminal/Fonts.java create mode 100644 client/app/src/main/java/dev/butschster/linuxterminal/KeyFace.java create mode 100644 client/app/src/main/java/dev/butschster/linuxterminal/Led.java create mode 100644 client/app/src/main/java/dev/butschster/linuxterminal/PhysicalKey.java create mode 100644 client/app/src/main/java/dev/butschster/linuxterminal/RockerHalf.java create mode 100644 client/app/src/main/java/dev/butschster/linuxterminal/Section.java create mode 100644 client/app/src/main/java/dev/butschster/linuxterminal/Skin.java create mode 100644 client/app/src/main/java/dev/butschster/linuxterminal/Wear.java diff --git a/client/app/NOTICE.md b/client/app/NOTICE.md index 29de6e7..bca680b 100644 --- a/client/app/NOTICE.md +++ b/client/app/NOTICE.md @@ -1,4 +1,24 @@ -# Third-party code in this module +# Third-party code and assets in this module + +## Fonts + +`src/main/assets/DejaVuSansMono.ttf` — the [DejaVu fonts](https://dejavu-fonts.github.io/), +under the Bitstream Vera licence with the DejaVu changes under the same terms. +Both permit redistribution, including in a bundled application. + +It is here for a specific reason: the thresholds in `docs/readability.md` were +measured with this face, so a client drawing with anything else cannot be judged +against them. The system alias `Typeface.MONOSPACE` resolves on Horizon OS to +`DroidSansMono.ttf`, a face drawn for Android 2 that is neither what was measured +nor what a terminal looks like. + +`src/main/assets/JetBrainsMono-Regular.ttf` — [JetBrains Mono](https://www.jetbrains.com/lp/mono/), +SIL Open Font Licence 1.1. Carried alongside DejaVu while the two are being +compared in the headset; whichever loses should be deleted rather than kept. + +`src/main/assets/MaterialIcons-Regular.ttf` — Material Icons, Apache 2.0. + +## Emulator `src/main/java/com/termux/terminal/` and `src/main/java/com/termux/view/TerminalRenderer.java` come from [termux/termux-app](https://github.com/termux/termux-app), whose diff --git a/client/app/src/main/assets/DejaVuSansMono.ttf b/client/app/src/main/assets/DejaVuSansMono.ttf new file mode 100644 index 0000000000000000000000000000000000000000..538ee272366e7b7c7c00e1151f6fa61671ba601b GIT binary patch literal 343140 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%0DBl$}y?@Gs3DIlgd9MO!}06MwskU{uyCa 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 W(EJNtGW3N1KYv^PkDtmTxBL%2+~ooQ literal 0 HcmV?d00001 diff --git a/client/app/src/main/assets/JetBrainsMono-Regular.ttf b/client/app/src/main/assets/JetBrainsMono-Regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..dff66cc50702c75abd025dcf49f62a4dcc2d72de GIT binary patch literal 273900 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 z

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, and read at the moment a view is built rather than captured once: + * every screen in the app builds its keys through this class, so swapping the + * skin and rebuilding is enough to see the whole app in the other treatment. + * That is what makes the two comparable at all — a redesign of the bar alone, + * with the connection manager still in the old palette, compares nothing. + */ + private static Skin skin = Skin.capped(); + + public static void applySkin(Skin chosen) { + skin = chosen; + FILL = chosen.fill; + TEXT = chosen.text; + MUTED = chosen.muted; + RULE = chosen.rule; + } + + public static Skin skin() { + return skin; + } + + /** + * Whether this skin's controls are drawn as physical objects — a moulded cap + * with an extrusion under it, and rockers pitched like a roof. + * + *

Two skins share the treatment and differ in their numbers, which is the + * arrangement worth having: the recipe lives in one place and a skin says how + * tall, how round and how far it travels. Asking for the kind by name at each + * call site is how the two would drift apart. + */ + private static boolean physicalSkin() { + return skin.kind == Skin.Kind.TACTILE || skin.kind == Skin.Kind.CONSOLE; + } + + /** + * The surface a group of keys is mounted on: the dynamic half's plate, or the + * slightly different one under the fixed half. + * + *

A drawable rather than a colour because a lit skin needs the plate lit + * too — see {@link Deck}. On the skins that have no material it is exactly the + * flat fill it always was. + */ + public static Drawable deck(boolean fixedHalf) { + int colour = fixedHalf ? skin.bgPanel : skin.bg; + return new Deck(colour, skin.textured); + } + // Four roles, not eight decorative shades. The previous palette had two greys // sixteen units apart, which through pancake lenses is one grey. - private static final int[] FILL = { - Color.rgb(58, 60, 70), // KEY — a literal keystroke - Color.rgb(52, 58, 74), // DESTINATION — somewhere to go - Color.rgb(40, 82, 128), // COMMAND — a line that runs - Color.rgb(122, 58, 30), // WARN — destructive - Color.rgb(38, 86, 62), // ENTER - Color.rgb(52, 118, 84), // VOICE - Color.rgb(74, 80, 104), // ARROW — the cluster the hand finds without looking - }; + private static int[] FILL = skin.fill; - public static final int TEXT = Color.rgb(224, 226, 232); - public static final int MUTED = Color.rgb(150, 155, 168); + // Not final, because the skin replaces them. Anything reading these must read + // them while building a view, never copy them into a constant of its own — a + // static final int initialised from one of these is inlined by the compiler + // and would keep the colour it was compiled with. + public static int TEXT = skin.text; + public static int MUTED = skin.muted; /** - * Every line in the bar, horizontal or vertical. One constant because they are + * Every line in the bar, horizontal or vertical. One value because they are * one idea: the rules between groups and the divider between the panels have to * be the same weight and the same colour, or the bar looks like two designs. */ - public static final int RULE = Color.rgb(52, 54, 64); + public static int RULE = skin.rule; /** Auto-repeat, for arrows and paging. */ private static final long REPEAT_DELAY_MS = 400; @@ -72,6 +117,13 @@ public class Buttons { private final Handler handler = new Handler(Looper.getMainLooper()); private final Typeface icons; + /** + * The drawn glyph on each key that has one, kept so a background can be + * replaced without losing it. Weak, because the bar rebuilds its left half + * whenever the foreground process changes and nothing should outlive that. + */ + private final java.util.Map glyphs = new java.util.WeakHashMap<>(); + private int textDp = 15; private int padH = 11; private int padV = 8; @@ -127,23 +179,248 @@ public int iconSizeDp() { public TextView key(String label, int style, String hint, View.OnClickListener onClick) { TextView view = new TextView(context); view.setText(label); - view.setTextColor(TEXT); + view.setTextColor(labelColour(style)); view.setTextSize(TypedValue.COMPLEX_UNIT_DIP, textDp); - view.setTypeface(Typeface.MONOSPACE); + view.setTypeface(Fonts.mono(context)); + view.setSingleLine(true); + view.setGravity(Gravity.CENTER); + // The legend is centred in the cap, not in the cell. + // + // A physical key hands the bottom of its cell to the extrusion under it, so + // the cap occupies the upper part and text centred on the whole cell sits + // visibly low — worst on the short chips, where the extrusion is a fifth of + // the height. Padding the bottom by the extrusion's depth moves the text + // back to the middle of the thing it is written on. + int sink = physicalSkin() ? dp(skin.depthDp) : 0; + view.setPadding(dp(padH), dp(padV), dp(padH), dp(padV) + sink); + view.setMinWidth(dp(minWidth)); + view.setMinHeight(dp(minHeight) + sink); + view.setBackground(background(style)); + if (skin.engrave) engrave(view); + travel(view); + view.setClickable(true); + view.setFocusable(false); + debounced(view, onClick); + if (hint != null && !hint.isEmpty()) view.setContentDescription(hint); + return view; + } + + /** + * The glass of a display, sunk into the plate. + * + *

Deeper than a key stands proud — 4dp against 3 — because a recess is read + * from its shaded wall and a wall thinner than the corner radius disappears + * into the rounding. The radius itself is the key's, so the two shapes belong + * to one panel. + */ + /** + * How thick the wall of a screen is. Three device-independent pixels, always, + * and deliberately not derived from how far the keys stand proud. + * + *

It used to be {@code depthDp + 1}, which is defensible on a skin whose + * keys lift 3dp and absurd on one whose keys lift 7: the console skin drew an + * 8dp wall around every screen, and what should have read as a pane of glass + * read as a picture frame. A recess only needs enough wall to shade. + */ + private static final int SCREEN_WALL_DP = 3; + + public Drawable display() { + return screen(dp(screenCornerDp())); + } + + /** + * A screen with square corners, for one that scrolls. + * + *

A rounded screen and a scrollbar do not get along: the bar runs down the + * straight part of the edge and then the corner curves away from underneath + * it, which reads as the bar hanging off the glass. A listing is the one place + * on this panel where the content is longer than the window, so it is the one + * place the corner has to go. + */ + public Drawable squareDisplay() { + return screen(0f); + } + + private Drawable screen(float radiusPx) { + Display display = new Display(skin.glass, dp(SCREEN_WALL_DP), radiusPx, + skin.screen, skin.accent, skin.crt); + // A frame in the phosphor's own family, not in the panel's. The glass + // belongs to the display, and a grey frame would attach it to the plate. + return skin.crt ? display.withBezel(0xFF173425) : display; + } + + /** + * A screen's corner. Smaller than the section holding it, because glass is cut + * and a moulded housing is not: 10dp against the section's 12 is the same + * relationship a real panel has between a bezel and the plate around it. + */ + public int screenCornerDp() { + return Math.min(10, containerCornerDp()); + } + + /** + * A command chip: shorter and tighter than a key, because it is a line to run + * rather than a key to hold down. + * + *

The distinction is worth drawing. A keypad key is pressed by feel and + * repeatedly; a chip is read first and pressed once. Making them the same size + * would spend the panel's most valuable space — the reachable middle — on + * things that are chosen with the eyes anyway. + */ + public TextView chip(String label, boolean danger, View.OnClickListener onClick) { + TextView view = key(label, danger ? WARN : KEY, null, onClick); + view.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 12.5f); + view.setPadding(dp(12), dp(4), dp(12), dp(4)); + view.setMinHeight(dp(34)); + view.setMinWidth(0); + return view; + } + + /** The selection bar behind a row of a listing. */ + public Drawable rowSelection() { + return selection(); + } + + /** The recessed region a group of controls is mounted in. */ + /** + * The radius a container takes so that what sits inside it looks concentric. + * + *

A box and the thing mounted in it cannot share a radius: with equal + * corners the gap between them is widest exactly at the corner, and the inner + * part looks pushed into a corner it does not fit. The container's radius is + * the inner one plus the space between them, which keeps the gap even the + * whole way round. One rule, so every housing on the panel obeys it. + */ + public int containerCornerDp() { + return skin.cornerDp + 3; + } + + public Drawable section() { + return new Section(0xFF0D141E, dp(containerCornerDp())); + } + + /** + * A line inside a display: lit text on the glass, with no cap of its own. + * + *

Selection is an inversion — the phosphor fills the row and the text goes + * dark — rather than a highlight laid over it. That is what a character cell + * display does when it selects, it is what the terminal underneath this bar + * does, and it is the only "pressed" that does not imply the row moved. + */ + public TextView readout(String label, String hint, View.OnClickListener onClick) { + TextView view = new TextView(context); + view.setText(label); + view.setTextColor(skin.phosphor); + view.setTextSize(TypedValue.COMPLEX_UNIT_DIP, textDp); + view.setTypeface(Fonts.mono(context)); view.setSingleLine(true); view.setGravity(Gravity.CENTER); view.setPadding(dp(padH), dp(padV), dp(padH), dp(padV)); view.setMinWidth(dp(minWidth)); view.setMinHeight(dp(minHeight)); - view.setBackground(background(FILL[style])); - engrave(view); + view.setBackground(selection()); + if (skin.crt) { + // A little bloom, the way a phosphor spreads past the beam. Small + // enough to soften the edge of a stroke and not enough to close the + // counters — that is the line between "lit" and "out of focus". + // Halved. A phosphor does spread, but the spread was landing on the + // stroke rather than around it, and every letter came out a weight + // heavier than the face actually is — which reads as a fat font rather + // than as a lit one. + view.setShadowLayer(dp(2), 0f, 0f, Color.argb(70, Color.red(skin.phosphor), + Color.green(skin.phosphor), Color.blue(skin.phosphor))); + } view.setClickable(true); view.setFocusable(false); debounced(view, onClick); if (hint != null && !hint.isEmpty()) view.setContentDescription(hint); + + // The lit row inverts, so its text has to invert with it. A ColorStateList + // is the only way that stays in step — setting the colour on press would + // leave it dark on any state the listener did not fire for. + view.setTextColor(new android.content.res.ColorStateList( + new int[][]{{android.R.attr.state_pressed}, {}}, + new int[]{skin.glass, skin.phosphor})); return view; } + /** + * A row in a listing: the full width of the screen, a cursor mark, and the + * name after it. + * + *

This is the difference between an embedded display and a dark toolbar. A + * listing is one control with rows — the eye tracks down a single column, and + * the highlight is a bar across the row, which is what every terminal file + * browser has done since before any of this was on a headset. Separate cells + * would be N controls that happen to be inside a rectangle. + */ + public TextView row(String label, String hint, View.OnClickListener onClick) { + TextView view = readout(" " + label, hint, onClick); + view.setGravity(Gravity.CENTER_VERTICAL | Gravity.START); + view.setPadding(dp(10), dp(6), dp(10), dp(6)); + view.setMinHeight(dp(minHeight - 6)); + // A column wide enough for most names, so the rows below line up into + // columns rather than into a ragged left edge with holes in it. A name + // longer than this takes the space it needs and the column after it + // starts later, which is exactly what `ls` does. + view.setMinWidth(dp(150)); + return view; + } + + /** Transparent, then a wash of phosphor, then full inversion. */ + private StateListDrawable selection() { + StateListDrawable states = new StateListDrawable(); + GradientDrawable pressed = new GradientDrawable(); + pressed.setColor(skin.phosphor); + pressed.setCornerRadius(dp(skin.cornerDp)); + + GradientDrawable hovered = new GradientDrawable(); + hovered.setColor(Color.argb(46, Color.red(skin.phosphor), Color.green(skin.phosphor), + Color.blue(skin.phosphor))); + hovered.setCornerRadius(dp(skin.cornerDp)); + + GradientDrawable idle = new GradientDrawable(); + idle.setColor(Color.TRANSPARENT); + + states.addState(new int[]{android.R.attr.state_pressed}, pressed); + states.addState(new int[]{android.R.attr.state_hovered}, hovered); + states.addState(new int[]{}, idle); + return states; + } + + /** + * The key physically enters its housing when pressed. + * + *

A {@link android.animation.StateListAnimator} rather than a second + * drawable: the drawable can only redraw the face, and what is wanted is the + * whole cap — legend included — moving down. 90ms is inside the 80–120ms a + * real key takes to bottom out, and the release is faster than the press + * because a spring returns quicker than a finger pushes. + */ + private void travel(View view) { + if (skin.travelDp <= 0) return; + android.animation.StateListAnimator states = new android.animation.StateListAnimator(); + states.addState(new int[]{android.R.attr.state_pressed}, + android.animation.ObjectAnimator.ofFloat(view, "translationY", dp(skin.travelDp)) + .setDuration(90)); + states.addState(new int[]{}, + android.animation.ObjectAnimator.ofFloat(view, "translationY", 0f) + .setDuration(70)); + view.setStateListAnimator(states); + } + + /** + * A lit key writes its legend in its own edge colour. + * + *

On a moulded cap the fill already carries the role and the legend stays + * white; on an outlined one the fill is nearly the deck, so the colour has + * nowhere else to live. It is the same information either way. + */ + private static int labelColour(int style) { + if (physicalSkin()) return skin.legend[style]; + return skin.border > 0 && lit(style) ? skin.edge[style] : TEXT; + } + /** * The shallow shadow that makes a legend look cut into the cap rather than * printed on it. @@ -198,10 +475,17 @@ public TextView glyphKey(Glyphs.Kind kind, int style, String hint, View.OnClickL private void centre(TextView view, Glyphs.Kind kind) { view.setCompoundDrawables(null, null, null, null); Drawable glyph = Glyphs.drawable(context, kind, TEXT, iconSizeDp()); - LayerDrawable stack = new LayerDrawable(new Drawable[]{view.getBackground(), glyph}); - stack.setLayerGravity(1, Gravity.CENTER); - stack.setLayerSize(1, glyph.getIntrinsicWidth(), glyph.getIntrinsicHeight()); - view.setBackground(stack); + // Layered into the background, and also remembered. + // + // Both halves of that matter and both were learned the hard way. A + // compound drawable is laid out beside the text and centred with it, so on + // a key with no text it sits visibly left of centre; a foreground would be + // ideal and is simply not drawn on these views. That leaves a layer of the + // background — which works until something replaces that background, as + // the rocker halves do, and the glyph goes with it. So the glyph is kept + // here as well, and whoever swaps a background can put it back. + glyphs.put(view, glyph); + view.setBackground(stack(view.getBackground(), glyph)); } /** @@ -220,6 +504,28 @@ public LinearLayout rocker(View top, View bottom) { cap.setBackground(rocked()); cap.setClipToOutline(true); + if (physicalSkin()) { + // The pair is one pitched cap sitting in a recessed housing, so the + // halves are shaded as slopes and the housing is what they sit in. + // Nothing else here changes: the two views, the groove and the order + // are the same on every skin. + // The same housing colour and the same concentric radius as every + // other recessed region. It used to be near-black with a corner of its + // own, which read as a part borrowed from a different panel — the only + // hard black outline in a bar that has none anywhere else. + cap.setBackground(new Section(0xFF0D141E, dp(containerCornerDp()))); + cap.setPadding(dp(3), dp(3), dp(3), dp(3)); + mountSlope(top, true); + mountSlope(bottom, false); + cap.addView(top, new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, + LinearLayout.LayoutParams.WRAP_CONTENT)); + cap.addView(bottom, new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, + LinearLayout.LayoutParams.WRAP_CONTENT)); + return cap; + } + cap.addView(top, new LinearLayout.LayoutParams( LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT)); @@ -248,11 +554,26 @@ public LinearLayout rocker(View top, View bottom) { * gradient is a description of the object rather than decoration on it, and it * says which way each half will go before it is pressed. */ - private GradientDrawable rocked() { + private Drawable rocked() { + if (skin.kind == Skin.Kind.MACHINED) { + // The cap of a real rocker is one moulding, so it is one KeyFace; the + // groove drawn across it by rocker() is what makes it two controls. + return new KeyFace(FILL[KEY], dp(skin.cornerDp), dp(skin.depthDp), false, + skin.textured); + } GradientDrawable shape = new GradientDrawable(); + shape.setCornerRadius(dp(skin.cornerDp)); + if (skin.border > 0) { + // No lighting to describe: this cap is a drawn region like every other + // key in this skin, so it takes the same flat face and the same edge. + // What still says "rocker" is the groove, which is a fact about the + // control rather than about how light falls on it. + shape.setColor(FILL[KEY]); + shape.setStroke(dp(skin.border), skin.edge[KEY]); + return shape; + } shape.setOrientation(GradientDrawable.Orientation.TOP_BOTTOM); shape.setColors(new int[]{shade(FILL[KEY], 16), shade(FILL[KEY], -10), shade(FILL[KEY], 16)}); - shape.setCornerRadius(dp(8)); return shape; } @@ -264,6 +585,56 @@ private static int shade(int colour, int step) { Math.max(0, Math.min(255, Color.blue(colour) + step))); } + /** + * Puts a slope under a half without losing the glyph drawn on it. + * + *

The glyph comes from {@link #glyphs} rather than off the view, because + * there is nowhere on the view it survives: a foreground is not drawn on these + * views at all — trying that emptied every arrow and the backspace key on the + * keypad — and the background is the thing being replaced here. + */ + private void mountSlope(View half, boolean upper) { + Drawable glyph = glyphs.get(half); + if (glyph == null) { + half.setBackground(slope(upper)); + return; + } + half.setBackground(slopeWithGlyph(upper, glyph)); + } + + private StateListDrawable slopeWithGlyph(boolean upper, Drawable glyph) { + StateListDrawable states = new StateListDrawable(); + int fill = FILL[KEY]; + float r = dp(skin.cornerDp); + states.addState(new int[]{android.R.attr.state_pressed}, + stack(new RockerHalf(upper, fill, r, true), glyph)); + states.addState(new int[]{android.R.attr.state_hovered}, + stack(new RockerHalf(upper, lighten(fill, 0.08f), r, false), glyph)); + states.addState(new int[]{}, stack(new RockerHalf(upper, fill, r, false), glyph)); + return states; + } + + /** A face with a glyph centred over it, taking no part in layout. */ + private LayerDrawable stack(Drawable face, Drawable glyph) { + LayerDrawable layers = new LayerDrawable(new Drawable[]{face, glyph}); + layers.setLayerGravity(1, Gravity.CENTER); + layers.setLayerSize(1, glyph.getIntrinsicWidth(), glyph.getIntrinsicHeight()); + return layers; + } + + /** One slope of a pitched rocker, in its three states. */ + private StateListDrawable slope(boolean upper) { + StateListDrawable states = new StateListDrawable(); + int fill = FILL[KEY]; + float r = dp(skin.cornerDp); + states.addState(new int[]{android.R.attr.state_pressed}, + new RockerHalf(upper, fill, r, true)); + states.addState(new int[]{android.R.attr.state_hovered}, + new RockerHalf(upper, lighten(fill, 0.08f), r, false)); + states.addState(new int[]{}, new RockerHalf(upper, fill, r, false)); + return states; + } + /** * One half of a {@link #rocker}: a key with no cap of its own, so the cap's * corners are the only ones in play. @@ -284,7 +655,13 @@ public TextView glyphHalf(Glyphs.Kind kind, String hint, View.OnClickListener on private StateListDrawable pressOnly() { StateListDrawable states = new StateListDrawable(); GradientDrawable pressed = new GradientDrawable(); - pressed.setColor(Color.argb(70, 255, 255, 255)); + // The accent rather than white where there is an accent: a white wash on a + // near-black deck reads as a different material lighting up, which is the + // one thing a half of a rocker is not. + pressed.setColor(skin.border > 0 + ? Color.argb(64, Color.red(skin.accent), Color.green(skin.accent), + Color.blue(skin.accent)) + : Color.argb(70, 255, 255, 255)); GradientDrawable idle = new GradientDrawable(); idle.setColor(Color.TRANSPARENT); states.addState(new int[]{android.R.attr.state_pressed}, pressed); @@ -322,7 +699,7 @@ public LinearLayout tab(String title, boolean active, int contentColour, int str label.setText(title); label.setTextColor(active ? TEXT : MUTED); label.setTextSize(TypedValue.COMPLEX_UNIT_DIP, textDp); - label.setTypeface(Typeface.MONOSPACE); + label.setTypeface(Fonts.mono(context)); label.setSingleLine(true); label.setEllipsize(android.text.TextUtils.TruncateAt.MIDDLE); label.setMaxWidth(dp(220)); @@ -359,7 +736,7 @@ private StateListDrawable tabBackground(int fill, boolean active) { private GradientDrawable tabShape(int colour) { GradientDrawable shape = new GradientDrawable(); shape.setColor(colour); - float r = dp(10); + float r = dp(5); shape.setCornerRadii(new float[]{r, r, r, r, 0, 0, 0, 0}); return shape; } @@ -368,7 +745,12 @@ private GradientDrawable tabShape(int colour) { * Pressed and hovered states. With no touch and no mouse, the highlight under the ray * is the only thing that says where a press would land before it lands there. */ - private StateListDrawable background(int fill) { + private StateListDrawable background(int style) { + if (physicalSkin()) return physical(style); + if (skin.kind == Skin.Kind.MACHINED) return moulded(style); + if (skin.border > 0) return outlined(style); + + int fill = FILL[style]; StateListDrawable states = new StateListDrawable(); // Pressed is the one state with no lip: the cap sits down flush with the // deck, which is what pressing a key does and costs nothing to draw. @@ -378,6 +760,133 @@ private StateListDrawable background(int fill) { return states; } + /** + * The full physical recipe, per state. {@link PhysicalKey} holds the layers; + * this decides what each state changes. + * + *

Hover brightens the border and lights a halo, and does not move the cap. + * Press moves it, and that movement is the whole feedback — a hover that also + * moved would leave the press with nothing left to say. + */ + private StateListDrawable physical(int style) { + StateListDrawable states = new StateListDrawable(); + float radius = dp(skin.cornerDp); + float depth = dp(skin.depthDp); + int face = FILL[style]; + int border = skin.edge[style]; + int legend = skin.legend[style]; + + // Primary actions stay illuminated at rest. Everything else lights only + // under the ray, so a bar sitting untouched has one lit control, not forty. + int resting = style == ENTER ? skin.accentGo : 0; + + states.addState(new int[]{android.R.attr.state_pressed}, + new PhysicalKey(face, border, legend, radius, depth, true, resting)); + states.addState(new int[]{android.R.attr.state_hovered}, + new PhysicalKey(lighten(face, 0.06f), blend(border, skin.accentNav, 0.6f), + legend, radius, depth, false, skin.accentNav)); + states.addState(new int[]{}, + new PhysicalKey(face, border, legend, radius, depth, false, resting)); + return states; + } + + /** Part-way between two colours, for a border that warms up under the ray. */ + private static int blend(int from, int to, float amount) { + return Color.argb( + (int) (Color.alpha(from) + (Color.alpha(to) - Color.alpha(from)) * amount), + (int) (Color.red(from) + (Color.red(to) - Color.red(from)) * amount), + (int) (Color.green(from) + (Color.green(to) - Color.green(from)) * amount), + (int) (Color.blue(from) + (Color.blue(to) - Color.blue(from)) * amount)); + } + + /** + * A moulded cap: {@link KeyFace} does the drawing, this only says which of + * its three states is which. + * + *

Hover lightens the face and leaves the cap standing. Press puts it down — + * a different drawable rather than a lighter copy of the same one, because + * what changes is where the light lands, not how much of it there is. + */ + private StateListDrawable moulded(int style) { + StateListDrawable states = new StateListDrawable(); + float radius = dp(skin.cornerDp); + float depth = dp(skin.depthDp); + states.addState(new int[]{android.R.attr.state_pressed}, + new KeyFace(FILL[style], radius, depth, true, skin.textured)); + states.addState(new int[]{android.R.attr.state_hovered}, + new KeyFace(lighten(FILL[style], 0.14f), radius, depth, false, skin.textured)); + states.addState(new int[]{}, + new KeyFace(FILL[style], radius, depth, false, skin.textured)); + return states; + } + + /** + * A key with no volume at all: a face barely above the deck and a lit edge. + * + *

Press and hover are shown by the edge and the face brightening together + * rather than by the key moving. There is nowhere for it to move to — the + * whole point of this treatment is that the key is a drawn region, not an + * object, so a press that mimicked depth would be describing something that + * is not there. + */ + private StateListDrawable outlined(int style) { + StateListDrawable states = new StateListDrawable(); + states.addState(new int[]{android.R.attr.state_pressed}, + face(lighten(FILL[style], 0.22f), skin.edge[style], 1.0f)); + states.addState(new int[]{android.R.attr.state_hovered}, + face(lighten(FILL[style], 0.10f), skin.edge[style], 0.7f)); + states.addState(new int[]{}, face(FILL[style], skin.edge[style], lit(style) ? 0.42f : 0f)); + return states; + } + + /** + * Which roles are drawn as lit rather than merely outlined. + * + *

Everything lit either runs on its own or is destructive. A bar where + * every key glows is a bar where the glow means nothing, and the keys that + * most need to be told apart from their neighbours are exactly these. + */ + private static boolean lit(int style) { + return style == COMMAND || style == ENTER || style == WARN || style == VOICE; + } + + /** + * One face of an outlined key: fill, then rings of falling alpha just inside + * the edge, then the edge itself. + * + *

The rings are three concentric strokes rather than a blur. A real blur + * means {@link android.graphics.BlurMaskFilter}, which only works on a + * software layer, and the left half of this bar is rebuilt whenever the + * foreground process changes; at this size three strokes read the same and + * cost nothing. + */ + private LayerDrawable face(int fill, int edgeColour, float glowStrength) { + GradientDrawable body = new GradientDrawable(); + body.setColor(fill); + body.setCornerRadius(dp(skin.cornerDp)); + + if (!skin.glow || glowStrength <= 0f) { + body.setStroke(dp(skin.border), edgeColour); + return new LayerDrawable(new Drawable[]{body}); + } + + // Widest and faintest first: each later ring is drawn over the previous + // one and reaches less far in, so the alpha climbs towards the edge. + GradientDrawable wide = ring(edgeColour, skin.border + 3, (int) (34 * glowStrength)); + GradientDrawable mid = ring(edgeColour, skin.border + 1, (int) (72 * glowStrength)); + GradientDrawable line = ring(edgeColour, skin.border, 255); + return new LayerDrawable(new Drawable[]{body, wide, mid, line}); + } + + private GradientDrawable ring(int colour, int widthDp, int alpha) { + GradientDrawable shape = new GradientDrawable(); + shape.setColor(Color.TRANSPARENT); + shape.setCornerRadius(dp(skin.cornerDp)); + shape.setStroke(dp(widthDp), Color.argb(Math.min(255, alpha), + Color.red(colour), Color.green(colour), Color.blue(colour))); + return shape; + } + /** * A key with a lip: the cap, and under it a darker copy showing along the * bottom edge. @@ -391,14 +900,14 @@ private StateListDrawable background(int fill) { private LayerDrawable capped(int fill) { GradientDrawable lip = new GradientDrawable(); lip.setColor(shade(fill, -26)); - lip.setCornerRadius(dp(8)); + lip.setCornerRadius(dp(skin.cornerDp)); // The face is flat. The volume comes from the lip below it and nothing // else: a gradient across the cap was doing a second job the lip already // does, and two cues for one fact read as a style rather than as a shape. GradientDrawable cap = new GradientDrawable(); cap.setColor(fill); - cap.setCornerRadius(dp(8)); + cap.setCornerRadius(dp(skin.cornerDp)); LayerDrawable stack = new LayerDrawable(new Drawable[]{lip, cap}); // One device pixel, written as 1 rather than dp(1): on a dense panel a dp @@ -411,7 +920,7 @@ private LayerDrawable capped(int fill) { private GradientDrawable solid(int colour) { GradientDrawable shape = new GradientDrawable(); shape.setColor(colour); - shape.setCornerRadius(dp(8)); + shape.setCornerRadius(dp(skin.cornerDp)); return shape; } diff --git a/client/app/src/main/java/dev/butschster/linuxterminal/ContextBar.java b/client/app/src/main/java/dev/butschster/linuxterminal/ContextBar.java index fce9e75..e3dad25 100644 --- a/client/app/src/main/java/dev/butschster/linuxterminal/ContextBar.java +++ b/client/app/src/main/java/dev/butschster/linuxterminal/ContextBar.java @@ -1,7 +1,6 @@ package dev.butschster.linuxterminal; import android.content.Context; -import android.graphics.Color; import android.graphics.Typeface; import android.text.TextUtils; import android.util.TypedValue; @@ -54,8 +53,10 @@ public interface Host { /** The app's own strip. One button wide: it is a margin, not a third of the bar. */ private static final int STRIP_WIDTH_DP = 64; - private static final int BG = Color.rgb(24, 25, 31); - private static final int BG_FIXED = Color.rgb(31, 33, 40); + // The two plates come from the skin at build time rather than from constants + // here: the whole point of a skin is that a plate changes together with the + // keys standing on it, and a deck that stayed grey under lit keys would + // compare nothing. private static final String ICON_MIC = "\ue029"; private static final String ICON_KEYBOARD = "\ue312"; @@ -78,6 +79,19 @@ public interface Host { private final FlowLayout toolKeys; private View divider; + /** Non-null only on a skin with displays: the column the screens are stacked in. */ + private LinearLayout stack; + + /** Console layout only: the listing, the chip column and the two mini screens. */ + private FlowLayout rows; + private FlowLayout projects; + private View browserBox; + private View projectBox; + private LinearLayout chipColumn; + private LinearLayout shortcuts; + private TextView lastCommand; + private TextView selectedName; + private LinearLayout left; private LinearLayout right; private LinearLayout recordingRow; @@ -96,7 +110,28 @@ public ContextBar(Context context, Host host) { this.buttons = new Buttons(context, icons); setOrientation(HORIZONTAL); - setBackgroundColor(BG); + setBackground(Buttons.deck(false)); + + // The four views every layout needs, built before either branch claims + // them: the two lines of the status, the flow of tool keys, and the flow + // the older skins put their groups in. Which container they end up inside + // is what the layouts disagree about; that they exist is not. + where = label(path, Buttons.TEXT, 16); + where.setTypeface(Fonts.mono(context)); + where.setEllipsize(TextUtils.TruncateAt.MIDDLE); + where.setGravity(Gravity.CENTER_VERTICAL | Gravity.START); + + what = label("", Buttons.MUTED, 15); + what.setTypeface(Fonts.mono(context)); + what.setPadding(buttons.dp(10), 0, buttons.dp(8), 0); + + dynamic = new FlowLayout(context, buttons.gap()); + toolKeys = new FlowLayout(context, buttons.gap()); + + if (Buttons.skin().kind == Skin.Kind.CONSOLE) { + buildConsole(context); + return; + } // ---------------------------------------------------------- left: dynamic @@ -108,25 +143,38 @@ public ContextBar(Context context, Host host) { left.setPadding(0, buttons.dp(8), 0, buttons.dp(8)); addView(left, new LayoutParams(0, LayoutParams.MATCH_PARENT, 1f)); + boolean screens = Buttons.skin().displays; + LinearLayout status = new LinearLayout(context); status.setOrientation(HORIZONTAL); status.setGravity(Gravity.CENTER_VERTICAL); status.setPadding(buttons.dp(10), 0, buttons.dp(6), 0); - left.addView(status, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); + if (screens) { + // The first display, and the one that is only ever read: where you are. + // It was already text-only — nothing on this line has ever been + // pressable — so putting it behind glass says out loud what the layout + // was already doing quietly. + status.setBackground(buttons.display()); + status.setPadding(buttons.dp(14), buttons.dp(9), buttons.dp(14), buttons.dp(9)); + LayoutParams inset = new LayoutParams( + LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT); + inset.setMargins(buttons.inset(), 0, buttons.inset(), buttons.dp(8)); + left.addView(status, inset); + } else { + left.addView(status, + new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); + } // Text only — nothing here is pressable. A status line that can be clicked // is a row of accidental targets above the row you were aiming at. - where = label(path, Buttons.TEXT, 16); - where.setTypeface(Typeface.MONOSPACE); - where.setEllipsize(TextUtils.TruncateAt.MIDDLE); - where.setGravity(Gravity.CENTER_VERTICAL | Gravity.START); + where.setTextColor(screens ? Buttons.skin().phosphor : Buttons.TEXT); status.addView(where, new LayoutParams(0, LayoutParams.WRAP_CONTENT, 1f)); - what = label("", Buttons.MUTED, 15); - what.setPadding(buttons.dp(10), 0, 0, 0); + // Dimmer phosphor rather than grey: a second colour inside one display + // would read as a second display. Same lamp, less of it. + what.setTextColor(screens ? dim(Buttons.skin().phosphor, 0.62f) : Buttons.MUTED); status.addView(what); - dynamic = new FlowLayout(context, buttons.gap()); dynamic.setPadding(buttons.inset(), buttons.dp(2), buttons.inset(), 0); // The rules are drawn past this padding, out to the panel's own edges. // Both flags are needed and they are not the same thing: clipToPadding @@ -136,8 +184,19 @@ public ContextBar(Context context, Host host) { dynamic.setClipToPadding(false); dynamic.setClipChildren(false); ScrollView dynamicScroll = new ScrollView(context); - dynamicScroll.addView(dynamic, new ScrollView.LayoutParams( - LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); + if (screens) { + // One display per group, stacked. The flow layout is still built and + // still holds the buttons — it just moves inside a screen instead of + // sitting on the plate, so setContext keeps one way of filling it. + stack = new LinearLayout(context); + stack.setOrientation(VERTICAL); + stack.setPadding(buttons.inset(), 0, buttons.inset(), 0); + dynamicScroll.addView(stack, new ScrollView.LayoutParams( + LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); + } else { + dynamicScroll.addView(dynamic, new ScrollView.LayoutParams( + LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); + } left.addView(dynamicScroll, new LayoutParams(LayoutParams.MATCH_PARENT, 0, 1f)); // ---------------------------------------------------------- right: fixed @@ -148,7 +207,7 @@ public ContextBar(Context context, Host host) { right = new LinearLayout(context); right.setOrientation(VERTICAL); - right.setBackgroundColor(BG_FIXED); + right.setBackground(Buttons.deck(true)); // No horizontal padding on the panel, for the same reason the left one has // none: a rule that stops short of the edge looks like a mistake. What // needs the inset takes it individually, and then everything below a rule @@ -178,7 +237,6 @@ public ContextBar(Context context, Host host) { new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); // The same inset the keys above it have, so the two blocks share an edge. - toolKeys = new FlowLayout(context, buttons.gap()); // The same gap below the rule as above it. Two above and eight below is // not a rhythm, it is a rule that has slid into the row underneath. // Its children carry no margins of their own, so it takes the inset whole. @@ -207,7 +265,7 @@ public ContextBar(Context context, Host host) { LinearLayout controls = new LinearLayout(context); controls.setOrientation(VERTICAL); - controls.setBackgroundColor(BG_FIXED); + controls.setBackground(Buttons.deck(true)); controls.setPadding(buttons.dp(6), buttons.dp(8), buttons.dp(6), buttons.dp(8)); addView(controls, new LayoutParams(buttons.dp(STRIP_WIDTH_DP), LayoutParams.MATCH_PARENT)); @@ -271,7 +329,7 @@ public ContextBar(Context context, Host host) { new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); timer = label("0:00", Buttons.TEXT, 22); - timer.setTypeface(Typeface.MONOSPACE); + timer.setTypeface(Fonts.mono(context)); timer.setPadding(0, 0, buttons.dp(20), 0); under.addView(timer); @@ -360,6 +418,17 @@ public void setContext(JSONObject ctx) { } } hint(toolKeys, null); + + if (rows != null) { + fillConsole(groups); + return; + } + + if (stack != null) { + buildScreens(groups); + return; + } + for (int i = 0; i < groups.length(); i++) { JSONObject group = groups.optJSONObject(i); if (group == null) continue; @@ -379,7 +448,7 @@ public void setContext(JSONObject ctx) { // for what follows rather than a line competing with the buttons. The // rule carries the separation, so the heading does not have to. TextView heading = label(group.optString("name").toUpperCase(Locale.ROOT), - Buttons.MUTED, 9); + Buttons.skin().heading, 9); heading.setPadding(0, buttons.dp(1), buttons.dp(10), buttons.dp(1)); heading.setLetterSpacing(0.12f); heading.setTag(FlowLayout.BREAK); @@ -405,6 +474,571 @@ public void setContext(JSONObject ctx) { hint(dynamic, where); } + // --------------------------------------------------------------- console + + /** + * The console arrangement, built from this bar's own parts. + * + *

The layout is borrowed; the contents are not. The keypad here is the real + * {@link KeyPad} — the same keys, built once, that never move — and the + * shortcuts are whatever the host sent for the program in front, not a fixed + * list of Claude's chords. A concept that hardcoded either would prove nothing + * about this bar: the whole design rests on the host deciding what is offered, + * and a mock that decides for itself is a different product. + * + *

What the arrangement changes is where a thing lives, and the rule is what + * kind of thing it is. Read on the left, pressed in the middle, and the app's + * own controls on a rail at the edge. + */ + private void buildConsole(Context context) { + setPadding(buttons.dp(12), buttons.dp(12), buttons.dp(12), buttons.dp(12)); + + // ---- read + LinearLayout reading = new LinearLayout(context); + reading.setOrientation(VERTICAL); + addView(reading, new LayoutParams(0, LayoutParams.MATCH_PARENT, 1.35f)); + + // The head is its own screen, the full width of the column and with no + // section around it. It answers a different question from the listing — + // "where am I" against "where could I go" — and wrapping the two in one + // housing said they were one instrument. Nothing else on the panel is + // unwrapped, which is what makes this one read as the panel's own readout. + LinearLayout head = new LinearLayout(context); + head.setOrientation(HORIZONTAL); + head.setGravity(Gravity.CENTER_VERTICAL); + head.setBackground(buttons.display()); + head.setPadding(buttons.dp(14), buttons.dp(10), buttons.dp(14), buttons.dp(10)); + head.addView(where, new LayoutParams(0, LayoutParams.WRAP_CONTENT, 1f)); + head.addView(what); + head.addView(new Led(context, Buttons.skin().accentGo, true), + new LayoutParams(buttons.dp(9), buttons.dp(9))); + LayoutParams headParams = new LayoutParams( + LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT); + headParams.bottomMargin = buttons.dp(12); + reading.addView(head, headParams); + + LinearLayout browser = consoleSection(context, "directory browser"); + browser.addView(consoleScreen(context), new LayoutParams( + LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); + addSection(reading, browser, 0f); + browserBox = browser; + + LinearLayout projectBox = consoleSection(context, "projects"); + projects = new FlowLayout(context, 0); + projects.setPadding(buttons.dp(6), buttons.dp(6), buttons.dp(6), buttons.dp(6)); + ScrollView projectScroll = elasticScroll(context); + projectScroll.addView(projects, new ScrollView.LayoutParams( + LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); + projectBox.addView(projectScroll, new LayoutParams( + LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); + addSection(reading, projectBox, 0f); + this.projectBox = projectBox; + + LinearLayout commandBox = consoleSection(context, "commands"); + chipColumn = new LinearLayout(context); + chipColumn.setOrientation(VERTICAL); + ScrollView chipScroll = new ScrollView(context); + chipScroll.addView(chipColumn, new ScrollView.LayoutParams( + LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); + commandBox.addView(chipScroll, new LayoutParams(LayoutParams.MATCH_PARENT, 0, 1f)); + addSection(reading, commandBox, 1f); + + reading.addView(consoleMinis(context), new LayoutParams( + LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); + + addView(gap(context, 12), new LayoutParams(buttons.dp(12), LayoutParams.MATCH_PARENT)); + + // ---- press + // + // The keypad is OUTSIDE the scroller and the shortcuts are inside it, and + // that split is the whole point of this column. + // + // The first version put both in one ScrollView. The keypad is still built + // once and never rebuilt — the rule everyone checks — and it still moved: + // when the host sent enough shortcuts to overflow the column, scrolling + // down to reach a chord carried Enter and ^C up and off the panel. That is + // the Touch Bar's failure arriving through the scroll container instead of + // through the layout order, and the fixed half's guarantee is about where + // the keys ARE, not about how often they are constructed. + LinearLayout pressing = new LinearLayout(context); + pressing.setOrientation(VERTICAL); + addView(pressing, new LayoutParams(0, LayoutParams.MATCH_PARENT, 0.95f)); + + LinearLayout control = consoleSection(context, "terminal control"); + KeyPad pad = new KeyPad(context, buttons, text -> host.onAction(text, false)); + control.addView(pad, new LayoutParams( + LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); + addSection(pressing, control, 0f); + + shortcuts = consoleSection(context, "shortcuts"); + ScrollView shortcutScroll = new ScrollView(context); + shortcutScroll.addView(toolKeys, new ScrollView.LayoutParams( + LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); + shortcuts.addView(shortcutScroll, new LayoutParams( + LayoutParams.MATCH_PARENT, 0, 1f)); + LayoutParams shortcutParams = new LayoutParams(LayoutParams.MATCH_PARENT, 0, 1f); + shortcutParams.bottomMargin = buttons.dp(12); + pressing.addView(shortcuts, shortcutParams); + + addView(gap(context, 12), new LayoutParams(buttons.dp(12), LayoutParams.MATCH_PARENT)); + + // ---- the app's own + addView(consoleRail(context), new LayoutParams( + buttons.dp(86), LayoutParams.MATCH_PARENT)); + } + + /** The screen: nothing but the listing, in columns. */ + private View consoleScreen(Context context) { + rows = new FlowLayout(context, 0); + rows.setPadding(buttons.dp(6), buttons.dp(6), buttons.dp(6), buttons.dp(6)); + ScrollView scroll = elasticScroll(context); + scroll.addView(rows, new ScrollView.LayoutParams( + LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); + return scroll; + } + + /** + * A screen that is as tall as what is on it, up to a ceiling. + * + *

Two directories should not be given the same height as twenty. A weight + * would divide the column by a ratio decided when the panel was built, which is + * exactly the wrong time to decide it — how many entries a directory has is + * known only when the host says so, and it changes on every `cd`. + * + *

The ceiling matters as much as the elasticity: a home directory with sixty + * entries would otherwise push the commands and the readouts off the panel + * entirely. Past it the listing scrolls, which is the behaviour a listing + * should have had anyway. + */ + private ScrollView elasticScroll(Context context) { + ScrollView scroll = new ScrollView(context) { + @Override + protected void onMeasure(int widthSpec, int heightSpec) { + super.onMeasure(widthSpec, MeasureSpec.makeMeasureSpec( + buttons.dp(230), MeasureSpec.AT_MOST)); + } + }; + scroll.setBackground(buttons.squareDisplay()); + return scroll; + } + + private View consoleMinis(Context context) { + LinearLayout strip = new LinearLayout(context); + strip.setOrientation(HORIZONTAL); + lastCommand = miniValue(context); + selectedName = miniValue(context); + strip.addView(mini(context, "last command", lastCommand, "ready", + Buttons.skin().accentGo), miniParams()); + strip.addView(gap(context, 10)); + strip.addView(mini(context, "selected", selectedName, "—", + Buttons.skin().accentWarn), miniParams()); + return strip; + } + + private LayoutParams miniParams() { + return new LayoutParams(0, LayoutParams.WRAP_CONTENT, 1f); + } + + private TextView miniValue(Context context) { + TextView view = label("—", Buttons.skin().phosphor, 13); + view.setTypeface(Fonts.mono(context)); + view.setSingleLine(true); + view.setEllipsize(TextUtils.TruncateAt.END); + return view; + } + + private View mini(Context context, String caption, TextView value, String sub, int lamp) { + LinearLayout box = new LinearLayout(context); + box.setOrientation(VERTICAL); + box.setBackground(buttons.display()); + box.setPadding(buttons.dp(10), buttons.dp(8), buttons.dp(10), buttons.dp(8)); + + TextView label = label(caption.toUpperCase(Locale.ROOT), 0xFF1F7B41, 9); + label.setLetterSpacing(0.18f); + box.addView(label); + box.addView(value); + + LinearLayout footer = new LinearLayout(context); + footer.setOrientation(HORIZONTAL); + footer.setGravity(Gravity.CENTER_VERTICAL); + footer.addView(new Led(context, lamp, true), + new LayoutParams(buttons.dp(9), buttons.dp(9))); + TextView note = label(sub, Buttons.skin().phosphorDim, 10); + note.setTypeface(Typeface.MONOSPACE); + LayoutParams noteParams = new LayoutParams( + LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); + noteParams.setMarginStart(buttons.dp(6)); + footer.addView(note, noteParams); + box.addView(footer); + return box; + } + + /** + * The rail: everything that moves you through text, as rockers. + * + *

All three pairs live here rather than being split between an edge rail and + * a navigation block in the middle. Paging, scrolling and text size are one + * intention — move me through this, or change how much of it I see — and a + * rocker is what says two actions are the same decision taken twice. + */ + private View consoleRail(Context context) { + LinearLayout rail = new LinearLayout(context); + rail.setOrientation(VERTICAL); + rail.setBackground(buttons.section()); + rail.setPadding(buttons.dp(8), buttons.dp(10), buttons.dp(8), buttons.dp(10)); + + // Dictation and the keyboard are buttons, not levers. + // + // A lever states a position you set and leave; these two are momentary — + // you press to start talking, and you press to summon a keyboard that + // dismisses itself. Drawing them as switches promised a state they do not + // hold. The sticky-modifier lever is gone outright: nothing in the client + // implements it, and a control that latches nothing is a lie with a hinge. + TextView mic = buttons.icon(ICON_MIC, Buttons.VOICE, "dictate", v -> host.onDictate()); + mic.setTextSize(TypedValue.COMPLEX_UNIT_DIP, buttons.iconSizeDp() + 8); + rail.addView(railControl(mic, 58)); + + TextView keyboard = buttons.icon(ICON_KEYBOARD, Buttons.DESTINATION, + "on-screen keyboard", v -> host.onKeyboard()); + keyboard.setTextSize(TypedValue.COMPLEX_UNIT_DIP, buttons.iconSizeDp() + 4); + rail.addView(railControl(keyboard, 48)); + + View pageUp = pageKey(Glyphs.Kind.PAGE_UP, ESC + "[5~", "page up, inside the program"); + View pageDown = pageKey(Glyphs.Kind.PAGE_DOWN, ESC + "[6~", "page down, inside the program"); + rail.addView(railRocker(context, pageUp, pageDown)); + + View back = buttons.glyphHalf(Glyphs.Kind.UP, "scroll this window back", + v -> host.onScroll(-10)); + buttons.repeatOnHold(back, () -> host.onScroll(-10)); + View forward = buttons.glyphHalf(Glyphs.Kind.DOWN, "scroll this window forward", + v -> host.onScroll(10)); + buttons.repeatOnHold(forward, () -> host.onScroll(10)); + rail.addView(railRocker(context, back, forward)); + + rail.addView(railRocker(context, + buttons.half("A+", "larger text", v -> host.onFontStep(2)), + buttons.half("A\u2212", "smaller text", v -> host.onFontStep(-2)))); + + hint(rail, null); + return rail; + } + + /** A single control on the rail, at the height its job deserves. */ + private View railControl(View view, int heightDp) { + LayoutParams params = new LayoutParams( + LayoutParams.MATCH_PARENT, buttons.dp(heightDp)); + params.bottomMargin = buttons.dp(10); + view.setLayoutParams(params); + return view; + } + + private View railRocker(Context context, View top, View bottom) { + View rocker = buttons.rocker(top, bottom); + LayoutParams params = new LayoutParams( + LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT); + params.bottomMargin = buttons.dp(10); + rocker.setLayoutParams(params); + return rocker; + } + + /** + * Add a section to a column, with the gap between sections. + * + *

It exists because the obvious way silently does not work: a section sets + * its own {@code bottomMargin} in the constructor, and then {@code addView} + * with fresh {@code LayoutParams} throws those away — the parameters passed at + * add time replace the view's own. Every section on the panel was flush + * against the next one for exactly that reason. Routing every add through here + * means the gap is applied where it survives. + */ + private void addSection(LinearLayout column, View section, float weight) { + LayoutParams params = weight > 0 + ? new LayoutParams(LayoutParams.MATCH_PARENT, 0, weight) + : new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT); + params.bottomMargin = buttons.dp(12); + column.addView(section, params); + } + + private LinearLayout consoleSection(Context context, String caption) { + LinearLayout box = new LinearLayout(context); + box.setOrientation(VERTICAL); + box.setBackground(buttons.section()); + box.setPadding(buttons.dp(10), buttons.dp(10), buttons.dp(10), buttons.dp(10)); + + TextView label = label(caption.toUpperCase(Locale.ROOT), Buttons.skin().heading, 9); + label.setLetterSpacing(0.18f); + label.setPadding(buttons.dp(2), buttons.dp(2), 0, buttons.dp(9)); + box.addView(label); + + return box; + } + + private View gap(Context context, int dp) { + View view = new View(context); + view.setLayoutParams(new LayoutParams(buttons.dp(dp), buttons.dp(1))); + return view; + } + + /** + * Fill the console layout: two listings and a column of chips. + * + *

The split between the two listings is by what the action is, not + * by which group it arrived in. A directory is a step down from where you are; + * a favourite is a jump to somewhere else entirely — different distances, + * different risk of hitting the wrong one, and the host already marks them + * apart as {@code dir} against {@code fav}. Reading that instead of the group + * name also means a host that renames a group does not silently move its + * contents onto the wrong screen. + */ + private void fillConsole(JSONArray groups) { + rows.removeAllViews(); + projects.removeAllViews(); + chipColumn.removeAllViews(); + + for (int i = 0; i < groups.length(); i++) { + JSONObject group = groups.optJSONObject(i); + if (group == null) continue; + JSONArray actions = group.optJSONArray("actions"); + if (actions == null || actions.length() == 0) continue; + + boolean places = true; + for (int j = 0; j < actions.length(); j++) { + JSONObject action = actions.optJSONObject(j); + if (action != null && !isKeystroke(action)) places &= isPlace(action); + } + + if (places) { + for (int j = 0; j < actions.length(); j++) { + JSONObject action = actions.optJSONObject(j); + if (action == null || isKeystroke(action)) continue; + boolean bookmark = "fav".equals(action.optString("style", "key")); + (bookmark ? projects : rows).addView(consoleRow(action)); + } + continue; + } + + TextView caption = label(group.optString("name").toUpperCase(Locale.ROOT), + Buttons.skin().heading, 9); + caption.setLetterSpacing(0.18f); + caption.setPadding(buttons.dp(2), buttons.dp(6), 0, buttons.dp(6)); + chipColumn.addView(caption); + + FlowLayout flow = new FlowLayout(getContext(), buttons.gap()); + for (int j = 0; j < actions.length(); j++) { + JSONObject action = actions.optJSONObject(j); + if (action == null || isKeystroke(action)) continue; + String label = action.optString("label"); + String send = action.optString("send"); + boolean enter = action.optBoolean("enter"); + boolean danger = "warn".equals(action.optString("style", "key")); + View chip = buttons.chip(label, danger, v -> { + lastCommand.setText(label); + host.onAction(send, enter); + }); + String hint = action.isNull("hint") ? null : action.optString("hint"); + if (hint != null && !hint.isEmpty()) chip.setContentDescription(hint); + flow.addView(chip); + } + chipColumn.addView(flow, new LayoutParams( + LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); + } + + // A section with nothing in it is not a section, it is a hole. + // + // With a program in front the host sends its keys and its skills and stops + // sending directories — there is nowhere to cd to from inside Claude Code — + // so the two reading screens would sit empty, taking the height the chips + // need. Hiding them gives that height back, and it happens by itself + // because it follows from what arrived rather than from knowing the name + // of the tool. + boolean anywhereToGo = rows.getChildCount() > 0; + boolean anyProjects = projects.getChildCount() > 0; + browserBox.setVisibility(anywhereToGo ? VISIBLE : GONE); + projectBox.setVisibility(anyProjects ? VISIBLE : GONE); + + hint(chipColumn, where); + } + + /** One line of the listing: a mark, the name, and what it is. */ + private View consoleRow(JSONObject action) { + String send = action.optString("send"); + boolean enter = action.optBoolean("enter"); + String style = action.optString("style", "key"); + boolean up = "up".equals(style); + String name = up ? ".." : action.optString("label"); + + // A cell, not a full-width line. `ls` prints a short listing in columns and + // so does this: one name per line filled the screen with six directories + // and pushed everything under it off the panel. The cell has a minimum + // width so the columns line up; a longer name takes what it needs and the + // next column starts later, which is again what `ls` does. + LinearLayout row = new LinearLayout(getContext()) { + @Override + protected void onMeasure(int widthSpec, int heightSpec) { + super.onMeasure(widthSpec, heightSpec); + int min = buttons.dp(150); + if (getMeasuredWidth() < min) { + setMeasuredDimension(min, getMeasuredHeight()); + } + } + }; + row.setOrientation(HORIZONTAL); + row.setGravity(Gravity.CENTER_VERTICAL); + row.setPadding(buttons.dp(8), buttons.dp(4), buttons.dp(8), buttons.dp(4)); + row.setClickable(true); + row.setBackground(buttons.rowSelection()); + + TextView mark = label(up ? "\u2191" : "\u25b8", Buttons.skin().phosphorDim, 11); + mark.setTypeface(Fonts.mono(getContext())); + mark.setWidth(buttons.dp(16)); + row.addView(mark); + + TextView title = label(name, 0xFF39D863, 14); + title.setTypeface(Fonts.mono(getContext())); + title.setSingleLine(true); + title.setEllipsize(TextUtils.TruncateAt.MIDDLE); + LayoutParams grow = new LayoutParams(0, LayoutParams.WRAP_CONTENT, 1f); + grow.setMarginStart(buttons.dp(8)); + row.addView(title, grow); + + // No "dir" / "proj" column any more. It was there when one listing held + // both kinds; with a screen each, the label repeated what the heading above + // it had already said — and in a narrow column it ended up jammed against + // the name it was describing. + + String hint = action.isNull("hint") ? null : action.optString("hint"); + if (hint != null && !hint.isEmpty()) row.setContentDescription(hint); + + row.setOnClickListener(v -> { + selectedName.setText(name); + lastCommand.setText(send); + host.onAction(send, enter); + }); + return row; + } + + // ---------------------------------------------------------------- screens + + /** + * One display per group of places, and ordinary keys for everything else. + * + *

The split is by what the group is, not by which group it happens to be. + * A directory, a project, the parent — those are read and then chosen from, + * which is a list on a screen. `claude`, `git status`, `^R` are things that + * happen when pressed, and a thing that happens is a key. Putting the second + * kind behind glass would be the same mistake as making the first kind look + * pressable, in the other direction. + */ + private void buildScreens(JSONArray groups) { + stack.removeAllViews(); + for (int i = 0; i < groups.length(); i++) { + JSONObject group = groups.optJSONObject(i); + if (group == null) continue; + JSONArray actions = group.optJSONArray("actions"); + if (actions == null || actions.length() == 0) continue; + + java.util.List shown = new java.util.ArrayList<>(); + boolean allPlaces = true; + for (int j = 0; j < actions.length(); j++) { + JSONObject action = actions.optJSONObject(j); + if (action == null || isKeystroke(action)) continue; // drawn on the other side + shown.add(action); + allPlaces &= isPlace(action); + } + if (shown.isEmpty()) continue; + + // Engraved into the plate above the screen, not printed inside it. A + // label inside the glass would be one more lit thing to read past on + // the way to the list it names. + TextView heading = label(group.optString("name").toUpperCase(Locale.ROOT), + Buttons.MUTED, 9); + heading.setLetterSpacing(0.14f); + heading.setPadding(buttons.dp(3), buttons.dp(4), 0, buttons.dp(3)); + stack.addView(heading); + + boolean asListing = allPlaces && Buttons.skin().listing; + + android.view.ViewGroup items; + if (asListing) { + // Columns, the way `ls` prints them, not one name per line. A + // single column of six directories filled the whole panel and + // pushed the projects off the bottom — and it was not even what a + // terminal does with a short listing. + FlowLayout column = new FlowLayout(getContext(), 0); + for (JSONObject action : shown) { + column.addView(listingRow(action)); + } + items = column; + } else { + FlowLayout flow = new FlowLayout(getContext(), buttons.gap()); + for (JSONObject action : shown) { + flow.addView(allPlaces ? readout(action) : actionButton(action)); + } + items = flow; + } + + LinearLayout.LayoutParams params = new LinearLayout.LayoutParams( + LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT); + params.bottomMargin = buttons.dp(8); + if (allPlaces) { + items.setBackground(buttons.display()); + int pad = asListing ? buttons.dp(6) : buttons.dp(10); + items.setPadding(buttons.dp(8), pad, buttons.dp(8), pad); + } else if (Buttons.skin().sections) { + // Keys are mounted in a milled region rather than standing on the + // bare panel, so a group of them reads as one block of controls. + items.setBackground(buttons.section()); + items.setPadding(buttons.dp(10), buttons.dp(10), + buttons.dp(10), buttons.dp(10)); + } + stack.addView(items, params); + } + hint(stack, where); + } + + /** One line of a listing: the cursor mark, then the name. */ + private View listingRow(JSONObject action) { + String send = action.optString("send"); + boolean enter = action.optBoolean("enter"); + String hint = action.isNull("hint") ? null : action.optString("hint"); + // The parent directory keeps its own mark. Everything else is a plain name + // with room in front of it for the cursor the highlight provides. + String label = "up".equals(action.optString("style", "key")) + ? ".." : action.optString("label"); + return buttons.row(label, hint, v -> host.onAction(send, enter)); + } + + /** A line on a screen: lit text, no cap, and the selection is an inversion. */ + private View readout(JSONObject action) { + String send = action.optString("send"); + boolean enter = action.optBoolean("enter"); + String hint = action.isNull("hint") ? null : action.optString("hint"); + View view = buttons.readout(action.optString("label"), hint, + v -> host.onAction(send, enter)); + + if ("up".equals(action.optString("style", "key")) && view instanceof TextView) { + ((TextView) view).setCompoundDrawablesWithIntrinsicBounds( + Glyphs.drawable(getContext(), Glyphs.Kind.UP, Buttons.skin().phosphor, + buttons.iconSizeDp()), null, null, null); + ((TextView) view).setCompoundDrawablePadding(buttons.dp(6)); + } + return view; + } + + /** Somewhere to go, as opposed to something to do. */ + private static boolean isPlace(JSONObject action) { + String style = action.optString("style", "key"); + return "up".equals(style) || "dir".equals(style) || "git".equals(style) + || "fav".equals(style); + } + + private static int dim(int colour, float amount) { + return android.graphics.Color.rgb( + (int) (android.graphics.Color.red(colour) * amount), + (int) (android.graphics.Color.green(colour) * amount), + (int) (android.graphics.Color.blue(colour) * amount)); + } + // -------------------------------------------------------------- dictation public void showRecording() { diff --git a/client/app/src/main/java/dev/butschster/linuxterminal/Deck.java b/client/app/src/main/java/dev/butschster/linuxterminal/Deck.java new file mode 100644 index 0000000..2b7c5f8 --- /dev/null +++ b/client/app/src/main/java/dev/butschster/linuxterminal/Deck.java @@ -0,0 +1,91 @@ +package dev.butschster.linuxterminal; + +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.ColorFilter; +import android.graphics.LinearGradient; +import android.graphics.Paint; +import android.graphics.PixelFormat; +import android.graphics.Rect; +import android.graphics.Shader; +import android.graphics.drawable.Drawable; + +/** + * The plate the keys are mounted on. + * + *

Lit by the same source as {@link KeyFace} — above and in front — so the top + * of the plate is fractionally brighter than the bottom. The gradient is about + * 6%, far too little to notice as a gradient and just enough that the plate stops + * looking like a flat fill; a bar where the keys are lit and the plate is not + * reads as keys pasted onto a background. + * + *

The brushing runs horizontally, the length of the panel, which is how a + * plate this shape would actually be finished. + */ +public class Deck extends Drawable { + + private final int colour; + private final boolean textured; + + private final Paint base = new Paint(); + private final Paint texture = new Paint(); + private final Paint edge = new Paint(); + + public Deck(int colour, boolean textured) { + this.colour = colour; + this.textured = textured; + } + + @Override + protected void onBoundsChange(Rect bounds) { + base.setShader(new LinearGradient(0, bounds.top, 0, bounds.bottom, + shade(colour, 7), shade(colour, -5), Shader.TileMode.CLAMP)); + } + + @Override + public void draw(Canvas canvas) { + canvas.drawRect(getBounds(), base); + + // The housing's own edges: a hairline of light along the top of the panel + // and a darker one along the bottom. One pixel each — this is the outermost + // surface, and anything heavier here competes with the controls on it. + Rect bounds = getBounds(); + edge.setColor(0x0FFFFFFF); + canvas.drawRect(bounds.left, bounds.top, bounds.right, bounds.top + 1, edge); + edge.setColor(0xA6000000); + canvas.drawRect(bounds.left, bounds.bottom - 2, bounds.right, bounds.bottom, edge); + + if (!textured) return; + + // Fainter than on a key, deliberately. The plate is the largest surface in + // the window, so the same alpha that reads as a finish on a 46dp cap reads + // as dirt across 800dp of panel. + texture.setShader(Wear.grain()); + texture.setAlpha(13); + canvas.drawRect(getBounds(), texture); + + texture.setShader(Wear.use()); + texture.setAlpha(19); + canvas.drawRect(getBounds(), texture); + } + + private static int shade(int colour, int step) { + return Color.rgb( + Math.max(0, Math.min(255, Color.red(colour) + step)), + Math.max(0, Math.min(255, Color.green(colour) + step)), + Math.max(0, Math.min(255, Color.blue(colour) + step))); + } + + @Override + public void setAlpha(int alpha) { + } + + @Override + public void setColorFilter(ColorFilter filter) { + } + + @Override + public int getOpacity() { + return PixelFormat.OPAQUE; + } +} diff --git a/client/app/src/main/java/dev/butschster/linuxterminal/Display.java b/client/app/src/main/java/dev/butschster/linuxterminal/Display.java new file mode 100644 index 0000000..1237277 --- /dev/null +++ b/client/app/src/main/java/dev/butschster/linuxterminal/Display.java @@ -0,0 +1,196 @@ +package dev.butschster.linuxterminal; + +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.ColorFilter; +import android.graphics.LinearGradient; +import android.graphics.Paint; +import android.graphics.PixelFormat; +import android.graphics.Rect; +import android.graphics.RectF; +import android.graphics.Shader; +import android.graphics.drawable.Drawable; + +/** + * A window cut into the panel: dark glass, set below the surface, with a phosphor + * lit behind it. + * + *

The opposite object to {@link KeyFace} and lit by the same lamp, which is the + * whole reason it reads as recessed rather than merely dark. A cap standing above + * the plate catches light on its top edge and casts a shadow below it; a well sunk + * into the plate is shadowed by its own upper wall and catches light on the lower + * one. Same light, inverted geometry — and that inversion is what the eye reads, + * not the darkness of the fill. + * + *

Why a display and not simply another group of keys: a key is a thing you press + * and a display is a thing you read, and the bar does both. Where you are, and what + * you could go to, are information — putting them behind glass says so before a + * word is read, and it takes them out of the population of things that look + * pressable. What stays a moulded cap is what is genuinely a key. + * + *

The glass carries a faint diagonal sheen across the top. It is the one purely + * decorative thing in this file and it earns its place by killing the flatness that + * a plain black rectangle has: a real cover glass reflects the room, and without any + * reflection at all the well reads as a hole rather than as a window. + */ +public class Display extends Drawable { + + private final int glass; + private final int depth; + private final Skin.Screen manner; + private final int edge; + private final boolean crt; + + /** A hairline around the glass, drawn over the wall. Zero means none. */ + private int bezel; + + private final Paint body = new Paint(Paint.ANTI_ALIAS_FLAG); + private final Paint wall = new Paint(Paint.ANTI_ALIAS_FLAG); + private final Paint sheen = new Paint(Paint.ANTI_ALIAS_FLAG); + private final Paint scan = new Paint(); + private final Paint vignette = new Paint(Paint.ANTI_ALIAS_FLAG); + + private final RectF face = new RectF(); + private final float radius; + + /** + * @param glass the colour of the unlit screen — near black, never black. A + * true black cannot be shaded, so the well would have no walls. + * @param depthPx how deep the recess is; the width of the shaded wall. + * @param radiusPx corner radius, matched to the keys so the panel reads as one + * design rather than as two. + */ + public Display(int glass, float depthPx, float radiusPx, Skin.Screen manner, int edge) { + this(glass, depthPx, radiusPx, manner, edge, false); + } + + public Display withBezel(int colour) { + this.bezel = colour; + return this; + } + + public Display(int glass, float depthPx, float radiusPx, Skin.Screen manner, int edge, + boolean crt) { + this.crt = crt; + this.glass = glass; + this.manner = manner; + this.edge = edge; + // A framed screen has no wall to speak of: the line around it is a line, + // the same weight as the line around a key, not a modelled thickness. + this.depth = manner == Skin.Screen.FRAMED ? 1 : (int) Math.max(1f, depthPx); + this.radius = radiusPx; + wall.setStyle(Paint.Style.STROKE); + wall.setStrokeWidth(this.depth); + } + + @Override + protected void onBoundsChange(Rect bounds) { + face.set(bounds.left + depth * 0.5f, bounds.top + depth * 0.5f, + bounds.right - depth * 0.5f, bounds.bottom - depth * 0.5f); + + // Darker at the top, where the recess is deepest in shadow, lifting very + // slightly towards the bottom where the lower wall bounces light back in. + body.setShader(new LinearGradient(0, face.top, 0, face.bottom, + shade(glass, -3), shade(glass, 6), Shader.TileMode.CLAMP)); + + switch (manner) { + case FRAMED: + // One flat line in the accent, at the alpha the panel's own keys are + // outlined with. Nothing is being modelled here, so nothing gradates. + wall.setShader(null); + wall.setColor(Color.argb(150, Color.red(edge), Color.green(edge), + Color.blue(edge))); + break; + case INSET: + // Only the shadow along the top wall. The reflected light on the + // lower one is what sells a deep recess, and claiming a deep recess + // in a skin whose keys are 1px proud is claiming too much. + wall.setShader(new LinearGradient(0, face.top, 0, face.bottom, + new int[]{Color.argb(120, 0, 0, 0), Color.TRANSPARENT}, + new float[]{0f, 0.6f}, Shader.TileMode.CLAMP)); + break; + default: + // The wall of the well: shadow along the top, light along the bottom. + // The exact inverse of a key's bevel, deliberately — the two are + // drawn from one description of where the lamp is. + wall.setShader(new LinearGradient(0, face.top, 0, face.bottom, + new int[]{Color.argb(150, 0, 0, 0), Color.argb(20, 0, 0, 0), + Color.argb(40, 255, 255, 255)}, + new float[]{0f, 0.55f, 1f}, Shader.TileMode.CLAMP)); + break; + } + + sheen.setShader(new LinearGradient(face.left, face.top, face.right, face.bottom * 0.5f, + new int[]{Color.argb(16, 255, 255, 255), Color.TRANSPARENT}, + new float[]{0f, 1f}, Shader.TileMode.CLAMP)); + + if (crt) { + // Darkening towards the edges, the way a tube falls off away from the + // centre of its deflection. Radial rather than linear, because the + // falloff is about distance from the middle in both axes. + float cx = face.centerX(); + float cy = face.centerY(); + float r = Math.max(face.width(), face.height()) * 0.62f; + vignette.setShader(new android.graphics.RadialGradient(cx, cy, Math.max(1f, r), + new int[]{Color.TRANSPARENT, Color.argb(58, 0, 0, 0)}, + new float[]{0.55f, 1f}, Shader.TileMode.CLAMP)); + } + } + + @Override + public void draw(Canvas canvas) { + canvas.drawRoundRect(face, radius, radius, body); + // No reflection on a framed screen: there is no glass over it to reflect + // anything, only an outlined region of the deck. + if (manner != Skin.Screen.FRAMED) { + canvas.drawRoundRect(face, radius, radius, sheen); + } + if (crt) { + // Every third device pixel, at 6% black. Fine enough that at any + // sensible text size it reads as a surface the letters sit on rather + // than as stripes across them — `docs/readability.md` is the reason it + // is not the fat, obvious scanline of a filter. + canvas.save(); + canvas.clipRect(face); + scan.setColor(Color.argb(15, 0, 0, 0)); + for (float y = face.top; y < face.bottom; y += 3f) { + canvas.drawRect(face.left, y, face.right, y + 1f, scan); + } + canvas.drawRoundRect(face, radius, radius, vignette); + canvas.restore(); + } + + canvas.drawRoundRect(face, radius, radius, wall); + + if (bezel != 0) { + // The frame of the glass, one pixel, in the screen's own colour family. + // The shaded wall above does the depth; this only says where the glass + // ends — and it has to be a hairline, because a thick frame around a + // panel of text is read as a border and starts competing with it. + wall.setShader(null); + wall.setStrokeWidth(1f); + wall.setColor(bezel); + canvas.drawRoundRect(face, radius, radius, wall); + } + } + + private static int shade(int colour, int step) { + return Color.rgb( + Math.max(0, Math.min(255, Color.red(colour) + step)), + Math.max(0, Math.min(255, Color.green(colour) + step)), + Math.max(0, Math.min(255, Color.blue(colour) + step))); + } + + @Override + public void setAlpha(int alpha) { + } + + @Override + public void setColorFilter(ColorFilter filter) { + } + + @Override + public int getOpacity() { + return PixelFormat.TRANSLUCENT; + } +} diff --git a/client/app/src/main/java/dev/butschster/linuxterminal/Fonts.java b/client/app/src/main/java/dev/butschster/linuxterminal/Fonts.java new file mode 100644 index 0000000..5362e82 --- /dev/null +++ b/client/app/src/main/java/dev/butschster/linuxterminal/Fonts.java @@ -0,0 +1,83 @@ +package dev.butschster.linuxterminal; + +import android.content.Context; +import android.graphics.Typeface; + +import java.util.HashMap; +import java.util.Map; + +/** + * The one monospaced face the whole app is set in. + * + *

It used to be {@link Typeface#MONOSPACE}, which sounds neutral and is not: on + * Horizon OS that alias resolves to DroidSansMono.ttf — checked on the + * headset's own {@code /system/etc/fonts.xml}, not assumed — a face drawn for + * Android 2 and shipped since out of compatibility. It is narrow, low in + * x-height, and it does not look like a terminal because it was never meant to be + * one. + * + *

That matters more here than it would on a phone. `docs/readability.md` puts + * the comfort threshold at about 0.39° of angular size per glyph and the failure + * point near 0.31°, and those numbers were measured with DejaVu Sans Mono. + * Every conclusion in that file therefore describes this face and not the one the + * client was actually drawing with, so bundling it is not a preference — it makes + * the measurements apply to what is on screen. + * + *

Bundled rather than asked of the system: a system alias can resolve to + * anything on any device, and a bar whose columns depend on which build of Horizon + * OS the headset is running is not a bar anybody can measure. + * + *

An asset, not a dependency. The "no new dependencies" rule is about code that + * has to keep working — a font file has no version, no transitive graph and no + * behaviour. + */ +public final class Fonts { + + /** + * Measured with, and therefore the default. Bitstream Vera / DejaVu licence, + * which permits redistribution; the notice belongs in NOTICE.md. + */ + public static final String DEJAVU = "DejaVuSansMono.ttf"; + + /** + * Drawn for reading code for hours: about 10% more x-height at the same em, and + * 0/O and 1/l/I deliberately pulled apart. SIL Open Font Licence. + * + *

The higher x-height is the interesting part for this project — angular + * size is what readability follows, and x-height is what angular size is + * actually spent on in a lowercase path like {@code ~/repos/home}. + */ + public static final String JETBRAINS = "JetBrainsMono-Regular.ttf"; + + /** What the system hands back for "monospace". Kept only for comparison. */ + public static final String SYSTEM = ""; + + private static final Map loaded = new HashMap<>(); + + private static String chosen = DEJAVU; + + private Fonts() {} + + public static void choose(String asset) { + chosen = asset; + } + + public static String chosen() { + return chosen; + } + + /** + * The face to draw with. Cached: {@code createFromAsset} parses the file every + * call, and this bar rebuilds its left half whenever the foreground process + * changes — several times a second while a program is starting. + */ + public static synchronized Typeface mono(Context context) { + if (chosen == null || chosen.isEmpty()) return Typeface.MONOSPACE; + Typeface face = loaded.get(chosen); + if (face == null) { + face = Typeface.createFromAsset(context.getAssets(), chosen); + loaded.put(chosen, face); + } + return face; + } +} diff --git a/client/app/src/main/java/dev/butschster/linuxterminal/KeyFace.java b/client/app/src/main/java/dev/butschster/linuxterminal/KeyFace.java new file mode 100644 index 0000000..3f0628f --- /dev/null +++ b/client/app/src/main/java/dev/butschster/linuxterminal/KeyFace.java @@ -0,0 +1,146 @@ +package dev.butschster.linuxterminal; + +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.ColorFilter; +import android.graphics.LinearGradient; +import android.graphics.Paint; +import android.graphics.PixelFormat; +import android.graphics.RectF; +import android.graphics.Shader; +import android.graphics.drawable.Drawable; + +/** + * A key drawn as an object rather than as a rectangle: a moulded cap with a + * bevel, sitting on a shadow, with the grain and the scratches of the panel it + * belongs to. + * + *

Everything here describes one light source, above and slightly in front, + * which is what a bar mounted below eye level actually gets. That single decision + * settles every gradient in the file: the face is lighter at the top, the bevel + * catches light on its upper edge and shadow on its lower one, and the cast + * shadow falls downwards. A shape lit two ways reads as a drawing of a shape. + * + *

Pressed inverts it. Not "lighter", which is what a flat design does — the + * highlight moves to the bottom edge, the cast shadow disappears because the cap + * is now flush with the deck, and the face darkens slightly because it has moved + * away from the light. Those three together are what a pressed key looks like, + * and each of them on its own is not. + * + *

The cost of all this is contrast spent on the object instead of on the + * legend, and `docs/readability.md` is the reason to keep the amounts small: the + * bevel is a single device-independent pixel, the texture sits under 5% alpha, + * and the face gradient spans about 8%. On a monitor it reads as material; the + * question the headset has to answer is whether it survives the lenses or turns + * into mud. + */ +public class KeyFace extends Drawable { + + private final int fill; + private final float radius; + private final float shadow; + private final boolean pressed; + private final boolean textured; + + private final Paint body = new Paint(Paint.ANTI_ALIAS_FLAG); + private final Paint bevel = new Paint(Paint.ANTI_ALIAS_FLAG); + private final Paint texture = new Paint(Paint.ANTI_ALIAS_FLAG); + private final Paint cast = new Paint(Paint.ANTI_ALIAS_FLAG); + + private final RectF face = new RectF(); + private final RectF under = new RectF(); + + /** + * @param shadowPx how far the cap stands above the deck, in pixels. Drawn + * inside the view's own bounds — an elevation shadow falls + * outside the child, so every container in the bar would have + * to stop clipping and one that was missed would crop it. + */ + public KeyFace(int fill, float radiusPx, float shadowPx, boolean pressed, boolean textured) { + this.fill = fill; + this.radius = radiusPx; + this.shadow = shadowPx; + this.pressed = pressed; + this.textured = textured; + + bevel.setStyle(Paint.Style.STROKE); + cast.setColor(Color.argb(150, 0, 0, 0)); + } + + @Override + protected void onBoundsChange(android.graphics.Rect bounds) { + // Half a pixel in, so a 1px stroke lands on the pixel rather than across + // two of them: an antialiased edge straddling a boundary is drawn at half + // strength on each side, which is exactly how a crisp bevel turns into a + // grey smear. + face.set(bounds.left + 0.5f, bounds.top + 0.5f, + bounds.right - 0.5f, bounds.bottom - (pressed ? 0.5f : shadow + 0.5f)); + under.set(bounds.left + 0.5f, bounds.top + shadow, + bounds.right - 0.5f, bounds.bottom - 0.5f); + + int top = shade(fill, pressed ? -6 : 12); + int bottom = shade(fill, pressed ? -20 : -10); + body.setShader(new LinearGradient(0, face.top, 0, face.bottom, + top, bottom, Shader.TileMode.CLAMP)); + + // One shader for both edges of the bevel: white at the top fading out by + // the middle, black arriving again at the bottom. Two paints would drift + // apart the moment either was adjusted. + bevel.setShader(new LinearGradient(0, face.top, 0, face.bottom, + new int[]{ + pressed ? Color.argb(90, 0, 0, 0) : Color.argb(58, 255, 255, 255), + Color.TRANSPARENT, + pressed ? Color.argb(46, 255, 255, 255) : Color.argb(96, 0, 0, 0), + }, + new float[]{0f, 0.5f, 1f}, Shader.TileMode.CLAMP)); + } + + @Override + public void draw(Canvas canvas) { + // The deck showing under the cap. Skipped when pressed: the cap is down. + if (!pressed && shadow >= 1f) { + canvas.drawRoundRect(under, radius, radius, cast); + } + + canvas.drawRoundRect(face, radius, radius, body); + + if (textured) { + // Clipped to the cap, so the grain stops where the object does. Alpha + // rather than a colour: the same two tiles then work over every fill + // in the palette without tinting any of them. + canvas.save(); + canvas.clipRect(face); + texture.setShader(Wear.grain()); + texture.setAlpha(pressed ? 12 : 17); + canvas.drawRoundRect(face, radius, radius, texture); + texture.setShader(Wear.use()); + texture.setAlpha(pressed ? 18 : 27); + canvas.drawRoundRect(face, radius, radius, texture); + canvas.restore(); + } + + bevel.setStrokeWidth(Math.max(1f, shadow * 0.5f)); + canvas.drawRoundRect(face, radius, radius, bevel); + } + + /** Lighter or darker by an equal step on every channel, so the hue does not drift. */ + private static int shade(int colour, int step) { + return Color.rgb( + Math.max(0, Math.min(255, Color.red(colour) + step)), + Math.max(0, Math.min(255, Color.green(colour) + step)), + Math.max(0, Math.min(255, Color.blue(colour) + step))); + } + + @Override + public void setAlpha(int alpha) { + } + + @Override + public void setColorFilter(ColorFilter filter) { + } + + @Override + public int getOpacity() { + return PixelFormat.TRANSLUCENT; + } +} diff --git a/client/app/src/main/java/dev/butschster/linuxterminal/Led.java b/client/app/src/main/java/dev/butschster/linuxterminal/Led.java new file mode 100644 index 0000000..2ace088 --- /dev/null +++ b/client/app/src/main/java/dev/butschster/linuxterminal/Led.java @@ -0,0 +1,64 @@ +package dev.butschster.linuxterminal; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.view.View; + +/** + * A status lamp: a small dot that is either dark or lit with a halo around it. + * + *

It reports one bit and says so instantly, which is the whole reason a panel + * has lamps rather than another line of text. Unlit it is not black but a dark + * version of its own colour — an unlit LED still has a coloured lens, and a black + * dot reads as a hole in the panel. + */ +public class Led extends View { + + private final int colour; + private boolean on; + + private final Paint lens = new Paint(Paint.ANTI_ALIAS_FLAG); + private final Paint halo = new Paint(Paint.ANTI_ALIAS_FLAG); + + public Led(Context context, int colour, boolean on) { + super(context); + this.colour = colour; + this.on = on; + } + + public void setOn(boolean lit) { + this.on = lit; + invalidate(); + } + + @Override + protected void onDraw(Canvas canvas) { + float cx = getWidth() / 2f; + float cy = getHeight() / 2f; + float r = Math.min(cx, cy); + + if (on) { + // Two rings standing in for the bloom around a lit lamp. Drawn inside + // the view, so no parent has to stop clipping to show it. + halo.setColor(Color.argb(70, Color.red(colour), Color.green(colour), + Color.blue(colour))); + canvas.drawCircle(cx, cy, r, halo); + halo.setColor(Color.argb(120, Color.red(colour), Color.green(colour), + Color.blue(colour))); + canvas.drawCircle(cx, cy, r * 0.78f, halo); + } + + lens.setColor(on ? colour : dark(colour)); + canvas.drawCircle(cx, cy, r * 0.55f, lens); + } + + /** The lens with the lamp behind it off: the colour at a fifth, over the panel. */ + private static int dark(int colour) { + return Color.rgb( + (int) (Color.red(colour) * 0.22f) + 20, + (int) (Color.green(colour) * 0.22f) + 26, + (int) (Color.blue(colour) * 0.22f) + 22); + } +} diff --git a/client/app/src/main/java/dev/butschster/linuxterminal/PhysicalKey.java b/client/app/src/main/java/dev/butschster/linuxterminal/PhysicalKey.java new file mode 100644 index 0000000..e6687a0 --- /dev/null +++ b/client/app/src/main/java/dev/butschster/linuxterminal/PhysicalKey.java @@ -0,0 +1,210 @@ +package dev.butschster.linuxterminal; + +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.ColorFilter; +import android.graphics.LinearGradient; +import android.graphics.Paint; +import android.graphics.PixelFormat; +import android.graphics.Rect; +import android.graphics.RectF; +import android.graphics.Shader; +import android.graphics.drawable.Drawable; + +/** + * The whole recipe for a shallow physical control, in the order the layers are + * laid down: + * + *

+ *   ambient blur        soft, wide, well below the cap
+ *   solid extrusion     hard-edged, no blur — this is the cap's height
+ *   face                three-stop vertical gradient
+ *   top bevel           1px of white inside the upper edge
+ *   bottom bevel        1px of black inside the lower edge
+ *   border              1px, coloured by the control's role
+ * 
+ * + *

The solid extrusion is the layer that does the work and the one a flat design + * leaves out. A blurred shadow says "this floats"; an unblurred copy of the shape, + * offset downwards, says "this is a solid object of that height", because that is + * what the side of an extruded cap actually looks like. The blur underneath is the + * ambient light in the room, and it is the smaller effect of the two. + * + *

Pressing inverts the whole description at once rather than tinting it. The + * cap travels down (see {@code Buttons.travel}), the extrusion collapses to almost + * nothing, the face gradient flips so the light now comes from below, and a real + * inner shadow appears along the top edge — which is the shadow of the housing + * falling onto a cap that has gone into it. + * + *

Blur is faked by stacking translucent copies rather than by + * {@link android.graphics.BlurMaskFilter}: a mask filter forces the view onto a + * software layer, and the left half of this bar is rebuilt whenever the foreground + * process changes. At this radius the stack is indistinguishable and costs nothing. + */ +public class PhysicalKey extends Drawable { + + private final int face; + private final int border; + private final int legend; + private final float radius; + private final float depth; + private final boolean pressed; + + /** Extra illumination around the whole control: focus, or a primary action. */ + private final int halo; + + private final Paint body = new Paint(Paint.ANTI_ALIAS_FLAG); + private final Paint solid = new Paint(Paint.ANTI_ALIAS_FLAG); + private final Paint ambient = new Paint(Paint.ANTI_ALIAS_FLAG); + private final Paint bevel = new Paint(Paint.ANTI_ALIAS_FLAG); + private final Paint edge = new Paint(Paint.ANTI_ALIAS_FLAG); + private final Paint inner = new Paint(Paint.ANTI_ALIAS_FLAG); + private final Paint glow = new Paint(Paint.ANTI_ALIAS_FLAG); + + private final RectF cap = new RectF(); + private final RectF extrusion = new RectF(); + + public PhysicalKey(int face, int border, int legend, float radiusPx, float depthPx, + boolean pressed, int halo) { + this.face = face; + this.border = border; + this.legend = legend; + this.radius = radiusPx; + this.depth = depthPx; + this.pressed = pressed; + this.halo = halo; + + bevel.setStyle(Paint.Style.STROKE); + edge.setStyle(Paint.Style.STROKE); + inner.setStyle(Paint.Style.STROKE); + glow.setStyle(Paint.Style.STROKE); + solid.setColor(0xE0090D13); + } + + @Override + protected void onBoundsChange(Rect bounds) { + // The drawable is asked for the whole cell and gives the height back to the + // shadow. The view's own translationY does the travelling, so the cap keeps + // the same size in both states and only what is under it changes. + float stand = pressed ? Math.min(depth, 1f) : depth; + cap.set(bounds.left + 0.5f, bounds.top + 0.5f, + bounds.right - 0.5f, bounds.bottom - depth - 0.5f); + extrusion.set(cap.left, cap.top + stand, cap.right, cap.bottom + stand); + + int top, mid, low; + if (pressed) { + // 180deg #192330 → #222d3b: darker at the top now, because the cap has + // gone into the housing and the light no longer reaches its upper edge. + top = 0xFF192330; + mid = 0xFF1D2735; + low = 0xFF222D3B; + } else { + top = lighten(face, 0.14f); + mid = face; + low = shade(face, -12); + } + body.setShader(new LinearGradient(0, cap.top, 0, cap.bottom, + new int[]{top, mid, low}, new float[]{0f, 0.48f, 1f}, Shader.TileMode.CLAMP)); + + bevel.setShader(new LinearGradient(0, cap.top, 0, cap.bottom, + new int[]{ + pressed ? Color.TRANSPARENT : 0x1CFFFFFF, + Color.TRANSPARENT, + pressed ? 0x0AFFFFFF : 0x73000000, + }, + new float[]{0f, 0.5f, 1f}, Shader.TileMode.CLAMP)); + + // The housing's shadow falling onto a cap that is now inside it. Only when + // pressed: an unpressed cap stands proud of the housing and has nothing + // above it to cast one. + inner.setShader(new LinearGradient(0, cap.top, 0, cap.top + depth * 2f, + new int[]{0xA6000000, Color.TRANSPARENT}, + new float[]{0f, 1f}, Shader.TileMode.CLAMP)); + } + + @Override + public void draw(Canvas canvas) { + if (!pressed) { + // Ambient: three copies, each wider and fainter, standing in for a + // 9px blur at 35%. + for (int i = 3; i >= 1; i--) { + ambient.setColor(Color.argb(20, 0, 0, 0)); + RectF soft = new RectF(extrusion); + soft.inset(-i * 1.5f, -i * 1.5f); + soft.offset(0, i * 1.2f); + canvas.drawRoundRect(soft, radius + i, radius + i, ambient); + } + } + + // The height of the cap: hard-edged, so it reads as the side of a solid. + canvas.drawRoundRect(extrusion, radius, radius, solid); + + canvas.drawRoundRect(cap, radius, radius, body); + + if (pressed) { + canvas.save(); + canvas.clipRect(cap); + inner.setStrokeWidth(depth); + canvas.drawRoundRect(cap, radius, radius, inner); + canvas.restore(); + } + + bevel.setStrokeWidth(1f); + canvas.drawRoundRect(cap, radius, radius, bevel); + + if (halo != 0) { + // Illumination is two rings, not a blur: the outer one is what a glow + // reads as at this size, and it stays inside the cell so no container + // has to stop clipping. + // Halved from the first attempt. Illumination marks which control is + // primary; at the old strength it was brighter than the legend it was + // meant to draw attention to, and the panel read as decorated. + glow.setStrokeWidth(3f); + glow.setColor(withAlpha(halo, 16)); + canvas.drawRoundRect(cap, radius, radius, glow); + glow.setStrokeWidth(1.5f); + glow.setColor(withAlpha(halo, 38)); + canvas.drawRoundRect(cap, radius, radius, glow); + } + + edge.setStrokeWidth(1f); + edge.setColor(border); + canvas.drawRoundRect(cap, radius, radius, edge); + } + + /** The colour this control's legend is written in. */ + public int legend() { + return legend; + } + + private static int withAlpha(int colour, int alpha) { + return Color.argb(alpha, Color.red(colour), Color.green(colour), Color.blue(colour)); + } + + private static int shade(int colour, int step) { + return Color.rgb( + Math.max(0, Math.min(255, Color.red(colour) + step)), + Math.max(0, Math.min(255, Color.green(colour) + step)), + Math.max(0, Math.min(255, Color.blue(colour) + step))); + } + + private static int lighten(int colour, float amount) { + return Color.rgb( + (int) (Color.red(colour) + (255 - Color.red(colour)) * amount), + (int) (Color.green(colour) + (255 - Color.green(colour)) * amount), + (int) (Color.blue(colour) + (255 - Color.blue(colour)) * amount)); + } + + @Override + public void setAlpha(int alpha) { + } + + @Override + public void setColorFilter(ColorFilter filter) { + } + + @Override + public int getOpacity() { + return PixelFormat.TRANSLUCENT; + } +} diff --git a/client/app/src/main/java/dev/butschster/linuxterminal/RockerHalf.java b/client/app/src/main/java/dev/butschster/linuxterminal/RockerHalf.java new file mode 100644 index 0000000..1682a19 --- /dev/null +++ b/client/app/src/main/java/dev/butschster/linuxterminal/RockerHalf.java @@ -0,0 +1,131 @@ +package dev.butschster.linuxterminal; + +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.ColorFilter; +import android.graphics.LinearGradient; +import android.graphics.Paint; +import android.graphics.PixelFormat; +import android.graphics.Path; +import android.graphics.Rect; +import android.graphics.RectF; +import android.graphics.Shader; +import android.graphics.drawable.Drawable; + +/** + * One slope of a rocker: half of a cap that is pitched like a shallow roof, with + * the ridge between the two halves. + * + *

The two halves are lit as one object, not as two buttons that happen to + * touch. Light comes from above, so the upper slope faces it and is bright, the + * lower slope turns away and is dark, and the ridge between them is the brightest + * line on the control. That difference is the whole point: it says the thing + * pivots, and it says which way each end will go before it is touched. Two + * identically shaded halves are a split rectangle and read as two keys. + * + *

Only the outer corners are rounded. The pair share a straight edge at the + * ridge, because a rounded edge there would be a gap between two objects, and + * these are two faces of one. + * + *

Pressing a half swaps its gradient: the slope that went down now points away + * from the lamp, so it darkens, while the ridge slides towards it. The opposite + * half is left alone — a real rocker's other end rises, and that is drawn by its + * neighbour keeping the brightness it already had against a darkened partner. + */ +public class RockerHalf extends Drawable { + + private final boolean upper; + private final int fill; + private final float radius; + private final boolean pressed; + + private final Paint body = new Paint(Paint.ANTI_ALIAS_FLAG); + private final Paint ridge = new Paint(Paint.ANTI_ALIAS_FLAG); + + private final RectF face = new RectF(); + private final Path shape = new Path(); + + public RockerHalf(boolean upper, int fill, float radiusPx, boolean pressed) { + this.upper = upper; + this.fill = fill; + this.radius = radiusPx; + this.pressed = pressed; + ridge.setStyle(Paint.Style.STROKE); + ridge.setStrokeWidth(1f); + } + + @Override + protected void onBoundsChange(Rect bounds) { + face.set(bounds.left, bounds.top, bounds.right, bounds.bottom); + + float r = radius; + shape.reset(); + // Round away from the ridge only: the outer end of the control is a moulded + // corner, the inner end is where the two faces meet. + float[] corners = upper + ? new float[]{r, r, r, r, 0, 0, 0, 0} + : new float[]{0, 0, 0, 0, r, r, r, r}; + shape.addRoundRect(face, corners, Path.Direction.CW); + + int bright, dim; + if (upper) { + // Facing the lamp. Brightest at its outer edge, easing towards the ridge + // — a slope is lit most where it is most square to the light. + bright = lighten(fill, pressed ? 0.05f : 0.24f); + dim = lighten(fill, pressed ? 0.00f : 0.08f); + body.setShader(new LinearGradient(0, face.top, 0, face.bottom, + bright, dim, Shader.TileMode.CLAMP)); + } else { + // Turned away from it. Starts near the ridge at the material's own value + // and falls off to well under it. + bright = shade(fill, pressed ? -30 : -8); + dim = shade(fill, pressed ? -46 : -30); + body.setShader(new LinearGradient(0, face.top, 0, face.bottom, + bright, dim, Shader.TileMode.CLAMP)); + } + } + + @Override + public void draw(Canvas canvas) { + canvas.drawPath(shape, body); + + // The ridge itself. Drawn by the upper half only, along its lower edge, so + // there is exactly one line and it cannot end up doubled or a pixel apart. + if (upper) { + ridge.setColor(pressed ? 0x26FFFFFF : 0x59FFFFFF); + canvas.drawLine(face.left + radius * 0.5f, face.bottom - 0.5f, + face.right - radius * 0.5f, face.bottom - 0.5f, ridge); + } else { + ridge.setColor(0x8C000000); + canvas.drawLine(face.left + radius * 0.5f, face.top + 0.5f, + face.right - radius * 0.5f, face.top + 0.5f, ridge); + } + } + + private static int shade(int colour, int step) { + return Color.rgb( + Math.max(0, Math.min(255, Color.red(colour) + step)), + Math.max(0, Math.min(255, Color.green(colour) + step)), + Math.max(0, Math.min(255, Color.blue(colour) + step))); + } + + private static int lighten(int colour, float amount) { + return Color.rgb( + (int) (Color.red(colour) + (255 - Color.red(colour)) * amount), + (int) (Color.green(colour) + (255 - Color.green(colour)) * amount), + (int) (Color.blue(colour) + (255 - Color.blue(colour)) * amount)); + } + + @Override + public void setAlpha(int alpha) { + } + + @Override + public void setColorFilter(ColorFilter filter) { + } + + @Override + public int getOpacity() { + return PixelFormat.TRANSLUCENT; + } +} diff --git a/client/app/src/main/java/dev/butschster/linuxterminal/Section.java b/client/app/src/main/java/dev/butschster/linuxterminal/Section.java new file mode 100644 index 0000000..de17e5e --- /dev/null +++ b/client/app/src/main/java/dev/butschster/linuxterminal/Section.java @@ -0,0 +1,81 @@ +package dev.butschster.linuxterminal; + +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.ColorFilter; +import android.graphics.LinearGradient; +import android.graphics.Paint; +import android.graphics.PixelFormat; +import android.graphics.Rect; +import android.graphics.RectF; +import android.graphics.Shader; +import android.graphics.drawable.Drawable; + +/** + * A recessed region of the panel that a group of controls is mounted in. + * + *

It exists to give the panel four surfaces instead of two: panel → section → + * housing → face. With only two, everything on the plate is the same distance + * from the eye and the grouping has to be carried by whitespace and a caption, + * which is how a dashboard of cards reads rather than how a device reads. + * + *

It is not a card. A card sits on the surface and casts a shadow + * outwards; this is milled into it — the shadow is along the inside of + * its own top edge, and the only light is the thin line where the lower wall + * catches it. Cards would put every group at the same height as the keys standing + * in them, which is the arrangement this is meant to avoid. + */ +public class Section extends Drawable { + + private final int fill; + private final float radius; + + private final Paint body = new Paint(Paint.ANTI_ALIAS_FLAG); + private final Paint wall = new Paint(Paint.ANTI_ALIAS_FLAG); + private final Paint edge = new Paint(Paint.ANTI_ALIAS_FLAG); + + private final RectF face = new RectF(); + + public Section(int fill, float radiusPx) { + this.fill = fill; + this.radius = radiusPx; + body.setColor(fill); + wall.setStyle(Paint.Style.STROKE); + edge.setStyle(Paint.Style.STROKE); + edge.setStrokeWidth(1f); + edge.setColor(0x0AFFFFFF); + } + + @Override + protected void onBoundsChange(Rect bounds) { + face.set(bounds.left + 0.5f, bounds.top + 0.5f, + bounds.right - 0.5f, bounds.bottom - 0.5f); + wall.setShader(new LinearGradient(0, face.top, 0, face.bottom, + new int[]{0xBF000000, Color.TRANSPARENT, 0x08FFFFFF}, + new float[]{0f, 0.35f, 1f}, Shader.TileMode.CLAMP)); + } + + @Override + public void draw(Canvas canvas) { + canvas.drawRoundRect(face, radius, radius, body); + canvas.save(); + canvas.clipRect(face); + wall.setStrokeWidth(6f); + canvas.drawRoundRect(face, radius, radius, wall); + canvas.restore(); + canvas.drawRoundRect(face, radius, radius, edge); + } + + @Override + public void setAlpha(int alpha) { + } + + @Override + public void setColorFilter(ColorFilter filter) { + } + + @Override + public int getOpacity() { + return PixelFormat.TRANSLUCENT; + } +} diff --git a/client/app/src/main/java/dev/butschster/linuxterminal/Skin.java b/client/app/src/main/java/dev/butschster/linuxterminal/Skin.java new file mode 100644 index 0000000..5b1ebf6 --- /dev/null +++ b/client/app/src/main/java/dev/butschster/linuxterminal/Skin.java @@ -0,0 +1,561 @@ +package dev.butschster.linuxterminal; + +import android.graphics.Color; + +/** + * What the bar is made of: colour, corner, and whether a key is a moulded cap or + * a lit outline. + * + *

This exists so the two answers can be held side by side and compared while + * wearing the headset, which is the only place the question can be settled. Every + * number the bar draws with used to be a constant in {@link Buttons}; they are + * here instead, and {@link Buttons#applySkin} copies the chosen set into place. + * + *

CAPPED is what the bar has always been: a solid moulded key with a + * darker lip along its bottom edge and its legend engraved into the face. The + * volume is physical — you read which way it would move. + * + *

OUTLINE is the other tradition: a near-black deck, keys drawn as thin + * lit rectangles, and one accent colour carrying every "this one does something". + * It reads as an instrument rather than as a keyboard. + * + *

Neither is obviously right for this display. Through pancake lenses the + * contrast that survives is luminance contrast, and the two skins spend it + * differently: CAPPED puts it in the fill and keeps the edges quiet, OUTLINE puts + * nearly all of it in a 1dp edge and leaves the fill almost as dark as the deck. + * A 1dp edge at arm's length is roughly 0.02° across — well under the 0.31° where + * `docs/readability.md` measured this project's own text becoming unreadable. That + * argument says the outline should fail; it is an argument, and the person wearing + * it outranks one, so both are built and looked at. + */ +public final class Skin { + + public enum Kind { CAPPED, OUTLINE, MACHINED, TACTILE, CONSOLE } + + public final Kind kind; + + /** The deck the dynamic half sits on, and the slightly different one for the fixed half. */ + public final int bg; + public final int bgPanel; + + /** Every line in the bar: the rules between groups and the dividers between panels. */ + public final int rule; + + public final int text; + public final int muted; + + /** Group headings. On OUTLINE they carry the accent; on CAPPED they stay muted. */ + public final int heading; + + /** The one colour that says "lit" — borders, indicators, the active state. */ + public final int accent; + + /** Fill per role, indexed by the style constants in {@link Buttons}. */ + public final int[] fill; + + /** Edge per role. Ignored when {@link #border} is zero. */ + public final int[] edge; + + public final int cornerDp; + + /** Edge width in dp. Zero means the key has no drawn edge at all. */ + public final int border; + + /** A darker copy showing below the cap. The moulded look; false on OUTLINE. */ + public final boolean lip; + + /** Legend cut into the face rather than printed on it. */ + public final boolean engrave; + + /** + * Rings of falling alpha just inside the edge, so a lit key looks lit rather + * than merely outlined. + * + *

Drawn as three concentric strokes rather than a real blur: a blur needs + * {@link android.graphics.BlurMaskFilter}, which forces the view onto a + * software layer, and this bar redraws its left half whenever the foreground + * process changes. Three strokes cost nothing and, at this size, read the same. + */ + public final boolean glow; + + /** Grain and scratches from {@link Wear}, on the plate and on every cap. */ + public final boolean textured; + + /** How far a cap stands above the plate, in dp. Zero is a flat key. */ + public final int depthDp; + + /** + * Whether the reading half is drawn as recessed displays rather than as groups + * of keys on the plate. + * + *

It is a claim about what those things are. Where you are, and the places + * you could go, are read before they are pressed; behind glass they stop + * competing with the keys for the same kind of attention. Only the skins with + * a material can carry it — a display sunk into a flat colour is just a darker + * rectangle. + */ + public final boolean displays; + + /** + * How a screen is set into this particular panel. + * + *

The idea is the same in all three — a thing you read, behind glass, told + * apart from the things you press — but a panel states it in its own material. + * A deep machined well in a flat skin would be a rendering of a well sitting on + * a surface that has no depth anywhere else, which reads as a mistake rather + * than as a screen. + */ + public enum Screen { + /** A shallow inset: shadow along the top wall, nothing else. */ + INSET, + /** No depth at all — the glass is outlined in the accent, like every key. */ + FRAMED, + /** A true recess with a lit lower wall. Only honest where the plate has material. */ + RECESSED, + } + + public final Screen screen; + + /** The unlit screen, and the phosphor lit behind it. */ + public final int glass; + public final int phosphor; + + /** + * Semantic accents. Named by what they mean, never by what they look like — + * a palette whose entries are "blue" and "green" gets used decoratively within + * a week, and then nothing on the panel means anything. + */ + public final int accentNav; + public final int accentGo; + public final int accentStop; + public final int accentWarn; + + /** How far a control travels when pressed, in dp. Zero means it does not move. */ + public final int travelDp; + + /** + * Whether places are drawn as a terminal listing with a cursor row rather than + * as a row of separate targets. + * + *

A listing is one control with N rows; a row of buttons is N controls. The + * first is what a small embedded display does and it reads as one device; the + * second is a toolbar that happens to be dark. + */ + public final boolean listing; + + /** Scanlines, vignette and a little bloom on lit text. */ + public final boolean crt; + + /** Legend colour per role. The role is told by the letter as well as the edge. */ + public final int[] legend; + + /** The dimmer phosphor, for anything on a screen that is not the subject. */ + public final int phosphorDim; + + /** + * Whether groups of keys sit in recessed sections rather than directly on the + * panel. + * + *

Four surfaces, in order: panel, recessed section, control housing, + * interactive face. Without the section the panel has two, and everything on + * it reads as one plane of buttons. + */ + public final boolean sections; + + private Skin(Builder b) { + this.kind = b.kind; + this.bg = b.bg; + this.bgPanel = b.bgPanel; + this.rule = b.rule; + this.text = b.text; + this.muted = b.muted; + this.heading = b.heading; + this.accent = b.accent; + this.fill = b.fill; + this.edge = b.edge; + this.cornerDp = b.cornerDp; + this.border = b.border; + this.lip = b.lip; + this.engrave = b.engrave; + this.glow = b.glow; + this.textured = b.textured; + this.depthDp = b.depthDp; + this.displays = b.displays; + this.screen = b.screen; + this.glass = b.glass; + this.phosphor = b.phosphor; + this.accentNav = b.accentNav != 0 ? b.accentNav : b.accent; + this.accentGo = b.accentGo != 0 ? b.accentGo : b.fill[4]; + this.accentStop = b.accentStop != 0 ? b.accentStop : b.fill[3]; + this.accentWarn = b.accentWarn != 0 ? b.accentWarn : b.fill[3]; + this.travelDp = b.travelDp; + this.listing = b.listing; + this.crt = b.crt; + // Every skin that did not name its legends writes them in its ordinary + // text colour, which is what all three did before this existed. + if (b.legend != null) { + this.legend = b.legend; + } else { + int[] plain = new int[b.fill.length]; + java.util.Arrays.fill(plain, b.text); + this.legend = plain; + } + this.phosphorDim = b.phosphorDim != 0 ? b.phosphorDim : b.muted; + this.sections = b.sections; + } + + // ------------------------------------------------------------------ the two + + /** + * The bar as it has always been. + * + *

Four roles, not eight decorative shades: an earlier palette had two greys + * sixteen units apart, which through pancake lenses is one grey. + */ + public static Skin capped() { + Builder b = new Builder(Kind.CAPPED); + b.bg = Color.rgb(24, 25, 31); + b.bgPanel = Color.rgb(31, 33, 40); + b.rule = Color.rgb(52, 54, 64); + b.text = Color.rgb(224, 226, 232); + b.muted = Color.rgb(150, 155, 168); + b.heading = b.muted; + b.accent = Color.rgb(40, 82, 128); + b.fill = new int[]{ + Color.rgb(58, 60, 70), // KEY — a literal keystroke + Color.rgb(52, 58, 74), // DESTINATION — somewhere to go + Color.rgb(40, 82, 128), // COMMAND — a line that runs + Color.rgb(122, 58, 30), // WARN — destructive + Color.rgb(38, 86, 62), // ENTER + Color.rgb(52, 118, 84), // VOICE + Color.rgb(74, 80, 104), // ARROW — the cluster found without looking + }; + b.edge = b.fill; // unused: border is zero + // Halved across every skin at once. A softer corner reads as consumer + // hardware; this is a tool, and the tighter break makes the caps read as + // parts rather than as pills. One number per skin, so they stay in step. + b.cornerDp = 4; + b.border = 0; + b.lip = true; + b.engrave = true; + b.glow = false; + b.displays = true; + b.screen = Screen.INSET; + // Darker than the plate by more than a shade — a screen in a flat skin has + // no wall to be read by, so the step in luminance is doing the whole job. + b.glass = Color.rgb(14, 15, 18); + // Amber, the colour of a gas-discharge panel. Not the skin's own blue: the + // blue is what a key that runs is coloured, and a screen that shared it + // would look like an enormous button. + b.phosphor = Color.rgb(255, 178, 84); + return new Skin(b); + } + + /** + * The instrument reading: a near-black deck and keys drawn as lit outlines. + * + *

The fills are deliberately close to the deck — the edge is what is meant + * to be seen. Roles are told apart by edge hue, and only the two that act on + * their own (COMMAND, ENTER) plus the two that are dangerous (WARN, and VOICE + * while it is live) are lit; a plain keystroke stays unlit, so a bar at rest + * has a handful of lit keys rather than forty. + */ + public static Skin outline() { + Builder b = new Builder(Kind.OUTLINE); + b.bg = Color.rgb(9, 13, 20); + b.bgPanel = Color.rgb(12, 17, 25); + b.rule = Color.rgb(28, 40, 55); + b.text = Color.rgb(205, 217, 232); + b.muted = Color.rgb(120, 138, 160); + b.accent = Color.rgb(56, 200, 235); + b.heading = b.accent; + int face = Color.rgb(17, 24, 34); + b.fill = new int[]{ + face, // KEY + Color.rgb(18, 27, 40), // DESTINATION + Color.rgb(14, 34, 48), // COMMAND + Color.rgb(38, 20, 18), // WARN + Color.rgb(15, 33, 28), // ENTER + Color.rgb(15, 35, 30), // VOICE + Color.rgb(19, 29, 42), // ARROW + }; + b.edge = new int[]{ + Color.rgb(44, 58, 76), // KEY — unlit; it only types + Color.rgb(60, 96, 130), // DESTINATION + b.accent, // COMMAND — lit: it runs on press + Color.rgb(226, 106, 74), // WARN + Color.rgb(70, 200, 140), // ENTER — lit: it runs on press + Color.rgb(70, 200, 140), // VOICE + Color.rgb(70, 104, 140), // ARROW + }; + b.cornerDp = 5; + b.border = 1; + b.lip = false; + b.engrave = false; + b.glow = true; + b.displays = true; + b.screen = Screen.FRAMED; + // A shade under the deck, no more. This skin has no depth to spend, so the + // screen is told by its outline and by what is written on it. + b.glass = Color.rgb(6, 9, 14); + b.phosphor = b.accent; + return new Skin(b); + } + + /** + * The instrument panel as an object: anodised plate, moulded caps with a + * bevel, and the grain and scratches of something that has been used. + * + *

The colours are warmer and lighter than either of the others, and both + * moves are needed for the material to read. A near-black cap has nowhere to + * put a highlight — a bevel needs a face bright enough that lightening its top + * edge is visible — and a perfectly neutral grey reads as a rendering rather + * than as metal, because anodising and moulded ABS both carry a little warmth. + * + *

The roles keep their hues but arrive at them through the material: a + * destructive key is a red-brown cap of the same plastic, not a red rectangle. + */ + public static Skin machined() { + Builder b = new Builder(Kind.MACHINED); + b.bg = Color.rgb(38, 38, 42); + b.bgPanel = Color.rgb(44, 44, 49); + b.rule = Color.rgb(62, 62, 68); + b.text = Color.rgb(232, 230, 226); + b.muted = Color.rgb(158, 156, 152); + b.heading = b.muted; + b.accent = Color.rgb(96, 148, 196); + b.fill = new int[]{ + Color.rgb(74, 74, 80), // KEY + Color.rgb(70, 78, 92), // DESTINATION + Color.rgb(56, 96, 140), // COMMAND + Color.rgb(134, 68, 44), // WARN + Color.rgb(54, 100, 76), // ENTER + Color.rgb(64, 128, 96), // VOICE + Color.rgb(88, 92, 112), // ARROW + }; + b.edge = b.fill; + b.cornerDp = 3; + b.border = 0; + b.lip = false; // the depth comes from KeyFace instead + b.engrave = true; + b.glow = false; + b.textured = true; + // Three, not one. The lip in CAPPED is a single device pixel and is meant + // to be felt rather than seen; here the shadow has to be wide enough to + // hold a gradient, or the cap has no height to catch light on. + b.depthDp = 3; + b.displays = true; + b.screen = Screen.RECESSED; + // Near black with a trace of green in it, because an unlit phosphor screen + // is never neutral — the coating is visible even with nothing driving it. + b.glass = Color.rgb(9, 13, 10); + // The green of a VFD rather than of an LED: slightly yellow, which is what + // a phosphor actually emits, and it survives being thin far better than a + // pure primary green does. + b.phosphor = Color.rgb(126, 240, 138); + return new Skin(b); + } + + /** + * A spatial control surface: graphite plate, shallow mechanical keys, and the + * reading half as a real embedded terminal rather than as buttons. + * + *

Depth is deliberately small — five to fifteen millimetres of apparent + * relief, not a modelled machine. What the depth is spent on is state: + * a key at rest stands proud, a pressed one travels into its housing, a latched + * one stays down. That is the whole argument for the style — the panel explains + * its own mechanics before anything is read. + * + *

Colour is semantic and sparing. Navigation is cool blue, confirmation is + * green, interruption is red, a suspended or special action is amber, and + * everything else is graphite. Glow marks focus and state, never importance in + * general: a panel where forty keys glow has told you nothing. + */ + public static Skin tactile() { + Builder b = new Builder(Kind.TACTILE); + // Values taken literally from the panel's own token sheet rather than + // re-derived, so the two can be compared without an argument about whether + // the port was faithful. + b.bg = 0xFF0B1017; // --bg + b.bgPanel = 0xFF111824; // --panel + b.rule = 0xFF1B2432; + b.text = 0xFFE7EEF8; // --text + b.muted = 0xFF8391A5; // --text-muted + + b.accentNav = 0xFF2DA8FF; // --blue + b.accentGo = 0xFF36E37B; // --green + b.accentStop = 0xFFFF5252; // --red + b.accentWarn = 0xFFFFAD42; // --amber + b.accent = b.accentNav; + b.heading = 0xFF536072; // --text-dim + + // One material for every key. The variant changes the edge, the legend and + // a tint of the face — never the whole cap. A key repainted end to end + // stops being the same object in a different role and becomes a coloured + // rectangle, which is the flat look this style exists to avoid. + b.fill = new int[]{ + 0xFF202A38, // KEY — --surface-raised, mid stop + 0xFF202A38, // DESTINATION + 0xFF202A38, // COMMAND + 0xFF2B1D22, // WARN — danger keeps a red-brown body + 0xFF1B2C24, // ENTER — success, faintly green + 0xFF1B2C24, // VOICE + 0xFF202A38, // ARROW + }; + // One border colour for the whole panel, warmed a quarter of the way + // towards the role's accent and no further. + // + // The first version used the accent at full strength on danger and success, + // and it read as two loud keys among quiet ones — the eye went to the + // outline rather than to the legend, which is the thing that actually says + // what the key does. A cabinet of real keys does not change the colour of + // its shells per function; it changes the legend, and sometimes the tint of + // the plastic. That is what these are now. + b.edge = new int[]{ + 0xFF334052, // KEY — the neutral border + 0xFF334052, + 0xFF334052, + 0xFF4C3B44, // WARN — neutral, a quarter towards red + 0xFF35473F, // ENTER — neutral, a quarter towards green + 0xFF35473F, + 0xFF3B4A5E, + }; + // The legend carries the role too: pale red on danger, pale green on + // success. Both are lighter than the accent itself — an accent-strength + // letterform on a dark cap is a thin bright shape, and thin bright shapes + // are the first thing the lenses smear. + b.legend = new int[]{ + b.text, b.text, b.text, + 0xFFFFB0B0, // --key--danger colour + 0xFFBAFFCF, // --key--success colour + 0xFFBAFFCF, + b.text, + }; + + // Half of what the token sheet asked for. At 9dp the caps read as soft and + // consumer; at 4 they read as machined parts with a break on the edge, + // which is what the rest of the panel is claiming to be. The screens keep + // their own radius from --radius-lg, so glass and key are still told apart + // by shape as well as by depth. + b.cornerDp = 4; + b.border = 1; + b.lip = false; + b.engrave = false; + b.glow = false; + b.textured = false; + // 0 4px 0 — the solid extrusion that gives the cap its height, plus an + // ambient blur under it. + b.depthDp = 4; + b.travelDp = 3; // translateY(3px) on press + b.displays = true; + b.screen = Screen.RECESSED; + b.listing = true; + b.crt = true; + b.glass = 0xFF031009; // --crt-bg + b.phosphor = 0xFF52FF7D; // --crt-green + b.phosphorDim = 0xFF1B9E47; // --crt-green-dim + b.sections = true; + return new Skin(b); + } + + /** + * The whole device, not only its keys: three layers side by side — the screens + * that are read, the controls that are pressed, and a narrow rail of utilities + * — with the panel drawn as one housing around them. + * + *

This is the first skin whose difference is structural rather than + * material. Every earlier one changed colour, depth and texture while keeping + * the same arrangement, which is why two of them ended up looking alike. Here + * the arrangement itself is the design: a directory listing with a head, mini + * displays reporting state, chips for commands, keys grouped into labelled + * sections, and mechanical controls for the things that latch. + * + *

Its keys stand higher and are rounder than {@code tactile} — 7dp of lift + * against 4, 12dp of radius against 4. That is deliberate contrast, not drift: + * a taller, softer cap belongs to a console you rest your hands on, where a + * tight low one belongs to an instrument face. + */ + public static Skin console() { + Builder b = new Builder(Kind.CONSOLE); + b.bg = 0xFF0B1017; + b.bgPanel = 0xFF111824; + b.rule = 0xFF1B2432; + b.text = 0xFFE7EEF8; + b.muted = 0xFF8391A5; + b.heading = 0xFF536072; + + b.accentNav = 0xFF2DA8FF; + b.accentGo = 0xFF36E37B; + b.accentStop = 0xFFFF5252; + b.accentWarn = 0xFFFFAD42; + b.accent = b.accentNav; + + // --key-face for this skin: 180deg #2E3A4C → #222C3B → #151D28 + b.fill = new int[]{ + 0xFF222C3B, // KEY + 0xFF222C3B, // DESTINATION + 0xFF222C3B, // COMMAND + 0xFF2B1D22, // WARN + 0xFF15291E, // ENTER — the primary face + 0xFF16281E, // VOICE + // The arrow cluster is a shade bluer than the keys around it. It + // is the one group on the pad the hand finds without reading, and + // the eye needs to find it the same way — but it is still a key, + // so the difference is a tint of the same material rather than a + // colour of its own. + 0xFF23334A, // ARROW + }; + b.edge = new int[]{ + 0xFF2B3747, + 0x612DA8FF, // nav keys carry a blue edge at 38% + 0x612DA8FF, + 0x73FF5252, // danger at 45% + 0x9936E37B, // primary at 60% + 0x8036E37B, + 0x662DA8FF, // ARROW — blue at 40% + }; + b.legend = new int[]{ + b.text, + 0xFFCFE6FF, // --key--nav + 0xFFCFE6FF, + 0xFFFFB0B0, // --key--danger + 0xFFC9FFDC, // --key--primary + 0xFFBAFFCF, + 0xFFCFE6FF, // ARROW — the nav legend + }; + + b.cornerDp = 12; // --key-radius for this skin + b.border = 1; + b.lip = false; + b.engrave = false; + b.glow = false; + b.textured = false; + b.depthDp = 7; // --key-lift + b.travelDp = 6; // --key-travel + b.displays = true; + b.screen = Screen.RECESSED; + b.listing = true; + b.crt = true; + b.sections = true; + b.glass = 0xFF031009; + b.phosphor = 0xFF52FF7D; + b.phosphorDim = 0xFF1B9E47; + return new Skin(b); + } + + private static final class Builder { + final Kind kind; + int bg, bgPanel, rule, text, muted, heading, accent, cornerDp, border, depthDp; + int glass, phosphor; + int accentNav, accentGo, accentStop, accentWarn, travelDp, phosphorDim; + int[] legend; + boolean listing, crt, sections; + Screen screen = Screen.INSET; + int[] fill, edge; + boolean lip, engrave, glow, textured, displays; + + Builder(Kind kind) { + this.kind = kind; + } + } +} diff --git a/client/app/src/main/java/dev/butschster/linuxterminal/TermView.java b/client/app/src/main/java/dev/butschster/linuxterminal/TermView.java index cb92aab..0bc00ff 100644 --- a/client/app/src/main/java/dev/butschster/linuxterminal/TermView.java +++ b/client/app/src/main/java/dev/butschster/linuxterminal/TermView.java @@ -61,7 +61,7 @@ public void attach(HostSession session) { } private void rebuildRenderer() { - renderer = new TerminalRenderer(fontSize, Typeface.MONOSPACE); + renderer = new TerminalRenderer(fontSize, Fonts.mono(getContext())); } public int getFontSize() { diff --git a/client/app/src/main/java/dev/butschster/linuxterminal/Wear.java b/client/app/src/main/java/dev/butschster/linuxterminal/Wear.java new file mode 100644 index 0000000..2f1ba01 --- /dev/null +++ b/client/app/src/main/java/dev/butschster/linuxterminal/Wear.java @@ -0,0 +1,112 @@ +package dev.butschster.linuxterminal; + +import android.graphics.Bitmap; +import android.graphics.BitmapShader; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Shader; + +import java.util.Random; + +/** + * The texture of a real object: brushed metal, grain, and the scratches a panel + * picks up from being used. + * + *

Generated rather than shipped. A photographed texture would be an asset to + * license, several hundred kilobytes in the APK, and wrong at any density it was + * not authored for; a tile drawn here is a few kilobytes of pixels made at the + * density it will be drawn at, and it costs one allocation for the life of the + * process. + * + *

Seeded, never random at runtime. {@link Random} with a fixed seed + * gives the same panel every launch. Genuinely random wear would mean a key that + * has a scratch across its legend today and not tomorrow, which is the one way a + * texture can actually cost readability. + * + *

All of it is faint on purpose — grain at roughly 4% and scratches at 6%. + * `docs/readability.md` measured this project's own text going from readable to + * unreadable across a third of a degree of angular size, and every percent of + * contrast spent on texture is a percent not spent on the legend standing on it. + * Texture that is obvious on a monitor is noise through the lenses. + */ +public final class Wear { + + /** One tile, reused everywhere by a repeating shader. */ + private static final int TILE = 192; + + private static Bitmap grain; + private static Bitmap scratches; + + private Wear() {} + + /** + * Fine grain: the sandblasted or moulded surface itself, before any wear. + * + *

Monochrome noise rather than coloured: a coloured grain shifts the hue of + * whatever it lies on, and the palette here is deliberate. Alpha carries it, so + * the same tile works over dark metal and over a lit key alike. + */ + public static synchronized Shader grain() { + if (grain == null) { + grain = Bitmap.createBitmap(TILE, TILE, Bitmap.Config.ARGB_8888); + Random random = new Random(0x5EEDL); + int[] pixels = new int[TILE * TILE]; + for (int i = 0; i < pixels.length; i++) { + // Two draws averaged, which is cheap and lands the distribution + // nearer the middle than a single uniform one — pure uniform noise + // reads as static, this reads as a surface. + int value = (random.nextInt(255) + random.nextInt(255)) / 2; + int alpha = 26 + random.nextInt(20); + pixels[i] = Color.argb(alpha, value, value, value); + } + grain.setPixels(pixels, 0, TILE, 0, 0, TILE, TILE); + } + return new BitmapShader(grain, Shader.TileMode.REPEAT, Shader.TileMode.REPEAT); + } + + /** + * Use: brushed lines in one direction, plus the scratches a working panel has. + * + *

Horizontal, because that is what brushing a flat face leaves and because a + * vertical streak on a key would be read as a division of the key. The + * scratches are drawn over the brushing rather than under it, since a scratch + * is later than the finish underneath it. + */ + public static synchronized Shader use() { + if (scratches == null) { + scratches = Bitmap.createBitmap(TILE, TILE, Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(scratches); + canvas.drawColor(Color.TRANSPARENT); + Random random = new Random(0xB4005AL); + + // The brushed finish: many faint full-width lines, alternately lighter + // and darker, so the surface has direction without having a pattern. + Paint brush = new Paint(Paint.ANTI_ALIAS_FLAG); + brush.setStrokeWidth(1f); + for (int i = 0; i < TILE * 2; i++) { + float y = random.nextFloat() * TILE; + boolean light = random.nextBoolean(); + int alpha = 6 + random.nextInt(10); + brush.setColor(light ? Color.argb(alpha, 255, 255, 255) + : Color.argb(alpha, 0, 0, 0)); + canvas.drawLine(0, y, TILE, y, brush); + } + + // Scratches: short, near-horizontal, and light, because a scratch in a + // dark anodised surface exposes the brighter metal under it. + Paint scratch = new Paint(Paint.ANTI_ALIAS_FLAG); + scratch.setStrokeCap(Paint.Cap.ROUND); + for (int i = 0; i < 26; i++) { + float x = random.nextFloat() * TILE; + float y = random.nextFloat() * TILE; + float length = 8 + random.nextFloat() * 60; + float drop = (random.nextFloat() - 0.5f) * 7; + scratch.setStrokeWidth(0.6f + random.nextFloat() * 0.8f); + scratch.setColor(Color.argb(18 + random.nextInt(26), 255, 255, 255)); + canvas.drawLine(x, y, x + length, y + drop, scratch); + } + } + return new BitmapShader(scratches, Shader.TileMode.REPEAT, Shader.TileMode.REPEAT); + } +} From 8fe49a934edb595998941ddc2f960358579e7235 Mon Sep 17 00:00:00 2001 From: butschster Date: Sat, 8 Aug 2026 12:52:57 +0400 Subject: [PATCH 03/15] A window onto the bar that needs no server, and the review it produced MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- client/app/build.gradle.kts | 22 ++ client/app/src/debug/AndroidManifest.xml | 34 +++ .../linuxterminal/GalleryActivity.java | 247 ++++++++++++++++++ docs/panel-review.md | 187 +++++++++++++ 4 files changed, 490 insertions(+) create mode 100644 client/app/src/debug/AndroidManifest.xml create mode 100644 client/app/src/debug/java/dev/butschster/linuxterminal/GalleryActivity.java create mode 100644 docs/panel-review.md diff --git a/client/app/build.gradle.kts b/client/app/build.gradle.kts index 5619197..62fe056 100644 --- a/client/app/build.gradle.kts +++ b/client/app/build.gradle.kts @@ -51,6 +51,28 @@ android { signingConfig = signingConfigs.findByName("release") ?: signingConfigs.getByName("debug") } + + // A build for wearing, not for shipping: the debug variant's contents — + // including the concept screen in src/debug — signed with the release key. + // + // It exists because Android refuses an update signed by a different key, and + // the headset already carries a properly signed install. The alternative was + // to uninstall it, which throws away the paired servers and their tokens and + // means pairing every machine again to look at a colour. + create("concept") { + initWith(getByName("debug")) + isDebuggable = true + signingConfig = signingConfigs.findByName("release") + ?: signingConfigs.getByName("debug") + } + } + + sourceSets { + // One copy of the concept screen, built into both variants that may show it. + getByName("concept") { + java.srcDirs("src/debug/java") + manifest.srcFile("src/debug/AndroidManifest.xml") + } } compileOptions { diff --git a/client/app/src/debug/AndroidManifest.xml b/client/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..ab0e17e --- /dev/null +++ b/client/app/src/debug/AndroidManifest.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + diff --git a/client/app/src/debug/java/dev/butschster/linuxterminal/GalleryActivity.java b/client/app/src/debug/java/dev/butschster/linuxterminal/GalleryActivity.java new file mode 100644 index 0000000..18283f4 --- /dev/null +++ b/client/app/src/debug/java/dev/butschster/linuxterminal/GalleryActivity.java @@ -0,0 +1,247 @@ +package dev.butschster.linuxterminal; + +import android.app.Activity; +import android.graphics.Typeface; +import android.os.Bundle; +import android.util.TypedValue; +import android.view.Gravity; +import android.view.View; +import android.widget.LinearLayout; +import android.widget.TextView; + +import org.json.JSONException; +import org.json.JSONObject; + +/** + * The bar, off its leash: the real {@link ContextBar}, fed a recorded context, with + * the skin switchable underneath it. + * + *

Debug builds only. This class lives in {@code src/debug} and so does the + * manifest entry that launches it — a release build contains neither, which is the + * point. A concept screen that shipped would be one more thing for a store reviewer + * to find and ask about. + * + *

Why it exists: the bar cannot otherwise be looked at without a paired server, a + * running shell and a headset on a head. That loop is minutes long and it is the + * wrong loop for a question about colour. The contexts below are verbatim output of + * {@code --dump-context}, trimmed, so what is drawn here is what the server actually + * sends rather than a designer's idea of it. + * + *

What it cannot answer: how any of this reads through the lenses. + * `docs/readability.md` measured that threshold on the hardware and an emulator has + * no opinion about it. This screen settles layout, weight and palette; the headset + * settles whether a 1dp lit edge survives at all. + */ +public class GalleryActivity extends Activity implements ContextBar.Host { + + /** At a prompt: somewhere to go, projects, things to run. */ + private static final String SHELL_CONTEXT = "{" + + "\"cwd_label\":\"~/repos/home/linux-terminal\"," + + "\"tool\":null,\"tool_name\":\"shell\"," + + "\"git\":{\"branch\":\"concept/panel-look\",\"dirty\":4}," + + "\"groups\":[" + + "{\"name\":\"go to\",\"actions\":[" + + "{\"label\":\"..\",\"send\":\"cd ..\",\"style\":\"up\",\"enter\":true," + + "\"hint\":\"~/repos/home\"}," + + "{\"label\":\".claude\",\"send\":\"cd .claude\",\"style\":\"dir\",\"enter\":true}," + + "{\"label\":\"client\",\"send\":\"cd client\",\"style\":\"dir\",\"enter\":true}," + + "{\"label\":\"docs\",\"send\":\"cd docs\",\"style\":\"dir\",\"enter\":true}," + + "{\"label\":\"packaging\",\"send\":\"cd packaging\",\"style\":\"dir\",\"enter\":true}," + + "{\"label\":\"server\",\"send\":\"cd server\",\"style\":\"git\",\"enter\":true}]}," + + "{\"name\":\"projects\",\"actions\":[" + + "{\"label\":\"docker-infra\",\"send\":\"cd x\",\"style\":\"fav\",\"enter\":true," + + "\"hint\":\"~/repos/intruforce/docker-infra · 34×\"}," + + "{\"label\":\"cve-analyzer\",\"send\":\"cd x\",\"style\":\"fav\",\"enter\":true}," + + "{\"label\":\"go-services\",\"send\":\"cd x\",\"style\":\"fav\",\"enter\":true}," + + "{\"label\":\"k8s\",\"send\":\"cd x\",\"style\":\"fav\",\"enter\":true}," + + "{\"label\":\"ai-agents\",\"send\":\"cd x\",\"style\":\"fav\",\"enter\":true}," + + "{\"label\":\"pentax\",\"send\":\"cd x\",\"style\":\"fav\",\"enter\":true}," + + "{\"label\":\"home/terminal\",\"send\":\"cd x\",\"style\":\"fav\",\"enter\":true}," + + "{\"label\":\"shared\",\"send\":\"cd x\",\"style\":\"fav\",\"enter\":true}]}," + + "{\"name\":\"run\",\"actions\":[" + + "{\"label\":\"claude\",\"send\":\"claude\",\"style\":\"cmd\",\"enter\":true}," + + "{\"label\":\"codex\",\"send\":\"codex\",\"style\":\"cmd\",\"enter\":true}," + + "{\"label\":\"ls -la\",\"send\":\"ls -la\",\"style\":\"cmd\",\"enter\":true}," + + "{\"label\":\"^R\",\"send\":\"\\u0012\",\"style\":\"key\",\"enter\":false," + + "\"hint\":\"history search\"}," + + "{\"label\":\"git status\",\"send\":\"git status\",\"style\":\"cmd\",\"enter\":true}," + + "{\"label\":\"git diff\",\"send\":\"git diff\",\"style\":\"cmd\",\"enter\":true}]}]}"; + + /** With Claude Code in front: its own keys on the right, its skills on the left. */ + private static final String CLAUDE_CONTEXT = "{" + + "\"cwd_label\":\"~/repos/home/linux-terminal\"," + + "\"tool\":\"claude\",\"tool_name\":\"claude\"," + + "\"git\":{\"branch\":\"concept/panel-look\",\"dirty\":4}," + + "\"groups\":[" + + "{\"name\":\"claude\",\"actions\":[" + + "{\"label\":\"⇧Tab\",\"send\":\"\\u001b[Z\",\"style\":\"key\",\"enter\":false," + + "\"hint\":\"cycle permission mode\"}," + + "{\"label\":\"1\",\"send\":\"1\",\"style\":\"key\",\"enter\":false}," + + "{\"label\":\"2\",\"send\":\"2\",\"style\":\"key\",\"enter\":false}," + + "{\"label\":\"3\",\"send\":\"3\",\"style\":\"key\",\"enter\":false}," + + "{\"label\":\"Esc Esc\",\"send\":\"\\u001b\\u001b\",\"style\":\"warn\",\"enter\":false}," + + "{\"label\":\"^T\",\"send\":\"\\u0014\",\"style\":\"key\",\"enter\":false}," + + "{\"label\":\"^O\",\"send\":\"\\u000f\",\"style\":\"key\",\"enter\":false}," + + "{\"label\":\"^B\",\"send\":\"\\u0002\",\"style\":\"key\",\"enter\":false}," + + "{\"label\":\"^S\",\"send\":\"\\u0013\",\"style\":\"key\",\"enter\":false}," + + "{\"label\":\"/\",\"send\":\"/\",\"style\":\"key\",\"enter\":false}," + + "{\"label\":\"!\",\"send\":\"!\",\"style\":\"key\",\"enter\":false}," + + "{\"label\":\"#\",\"send\":\"#\",\"style\":\"key\",\"enter\":false}," + + "{\"label\":\"/plan\",\"send\":\"/plan\",\"style\":\"cmd\",\"enter\":true}," + + "{\"label\":\"/rewind\",\"send\":\"/rewind\",\"style\":\"cmd\",\"enter\":true}," + + "{\"label\":\"/context\",\"send\":\"/context\",\"style\":\"cmd\",\"enter\":true}," + + "{\"label\":\"/review\",\"send\":\"/review\",\"style\":\"cmd\",\"enter\":true}," + + "{\"label\":\"/compact\",\"send\":\"/compact\",\"style\":\"cmd\",\"enter\":true}," + + "{\"label\":\"/usage\",\"send\":\"/usage\",\"style\":\"cmd\",\"enter\":true}," + + "{\"label\":\"/clear\",\"send\":\"/clear\",\"style\":\"warn\",\"enter\":true," + + "\"hint\":\"wipe the conversation\"}]}," + + "{\"name\":\"skills\",\"actions\":[" + + "{\"label\":\"/diagnose\",\"send\":\"/diagnose \",\"style\":\"skill\",\"enter\":false}," + + "{\"label\":\"/headset\",\"send\":\"/headset \",\"style\":\"skill\",\"enter\":false}," + + "{\"label\":\"/install\",\"send\":\"/install \",\"style\":\"skill\",\"enter\":false}," + + "{\"label\":\"/new-feature\",\"send\":\"/new-feature \",\"style\":\"skill\",\"enter\":false}," + + "{\"label\":\"/pairing\",\"send\":\"/pairing \",\"style\":\"skill\",\"enter\":false}," + + "{\"label\":\"/release\",\"send\":\"/release \",\"style\":\"skill\",\"enter\":false}," + + "{\"label\":\"/minimalist-ui\",\"send\":\"/minimalist-ui \",\"style\":\"skill-global\"," + + "\"enter\":false}," + + "{\"label\":\"/infraq\",\"send\":\"/infraq \",\"style\":\"skill-global\",\"enter\":false}]}]}"; + + private static final Skin[] SKINS = + {Skin.capped(), Skin.outline(), Skin.machined(), Skin.tactile(), Skin.console()}; + private static final String[] SKIN_NAMES = + {"capped", "outline", "machined", "tactile", "console"}; + + // The font switcher is gone: DejaVu won, and it is what docs/readability.md + // measured with. JetBrains Mono stays in assets until the headset says which + // it prefers; if DejaVu holds, the other should be deleted rather than kept. + private LinearLayout root; + private int chosen; + private boolean claude; + + @Override + protected void onCreate(Bundle state) { + super.onCreate(state); + root = new LinearLayout(this); + root.setOrientation(LinearLayout.VERTICAL); + setContentView(root); + build(); + } + + /** + * Everything is rebuilt on every switch, deliberately. + * + *

{@link Buttons} reads the skin while it builds a view, so an existing bar + * keeps the colours it was born with. Rebuilding is also the honest test: it is + * exactly what the bar does whenever the foreground process changes. + */ + private void build() { + Buttons.applySkin(SKINS[chosen]); + Fonts.choose(Fonts.DEJAVU); + root.removeAllViews(); + root.setBackgroundColor(Buttons.skin().bg); + + root.addView(switcher(), new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT)); + + // Every skin, console included, is the real ContextBar. The console one + // rearranges the same parts rather than replacing them, so what is looked + // at here is what would ship. + ContextBar bar = new ContextBar(this, this); + try { + bar.setContext(new JSONObject(claude ? CLAUDE_CONTEXT : SHELL_CONTEXT)); + } catch (JSONException e) { + bar.setStatus("bad mock context: " + e.getMessage()); + } + View panel = bar; + root.addView(panel, new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, 0, 1f)); + } + + /** The one row that is not part of the concept: what to draw, and in which skin. */ + private LinearLayout switcher() { + Typeface icons = Typeface.createFromAsset(getAssets(), "MaterialIcons-Regular.ttf"); + Buttons buttons = new Buttons(this, icons); + + LinearLayout row = new LinearLayout(this); + row.setOrientation(LinearLayout.HORIZONTAL); + row.setGravity(Gravity.CENTER_VERTICAL); + row.setBackgroundColor(Buttons.skin().bgPanel); + row.setPadding(buttons.inset(), buttons.dp(6), buttons.inset(), buttons.dp(6)); + + row.addView(caption(buttons, "SKIN")); + for (int i = 0; i < SKINS.length; i++) { + final int which = i; + row.addView(choice(buttons, SKIN_NAMES[i], chosen == i, v -> { + chosen = which; + build(); + })); + } + + row.addView(caption(buttons, "CONTEXT")); + row.addView(choice(buttons, "shell", !claude, v -> { + claude = false; + build(); + })); + row.addView(choice(buttons, "claude", claude, v -> { + claude = true; + build(); + })); + + TextView note = new TextView(this); + note.setText("debug · mocked context · lenses not simulated"); + note.setTextColor(Buttons.MUTED); + note.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 12); + LinearLayout.LayoutParams fill = new LinearLayout.LayoutParams( + 0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f); + fill.setMarginStart(buttons.dp(16)); + note.setGravity(Gravity.END); + row.addView(note, fill); + + return row; + } + + private TextView caption(Buttons buttons, String text) { + TextView view = new TextView(this); + view.setText(text); + view.setTextColor(Buttons.MUTED); + view.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 10); + view.setLetterSpacing(0.12f); + view.setPadding(buttons.dp(14), 0, buttons.dp(8), 0); + return view; + } + + private View choice(Buttons buttons, String label, boolean active, View.OnClickListener click) { + View view = buttons.key(label, active ? Buttons.COMMAND : Buttons.KEY, null, click); + LinearLayout.LayoutParams params = new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT); + params.setMarginEnd(buttons.gap()); + view.setLayoutParams(params); + return view; + } + + // ------------------------------------------------------- nothing is connected + + @Override + public void onAction(String send, boolean enter) { + } + + @Override + public void onDictate() { + } + + @Override + public void onStopDictating() { + } + + @Override + public void onKeyboard() { + } + + @Override + public void onFontStep(int delta) { + } + + @Override + public void onScroll(int rows) { + } +} diff --git a/docs/panel-review.md b/docs/panel-review.md new file mode 100644 index 0000000..2284d7b --- /dev/null +++ b/docs/panel-review.md @@ -0,0 +1,187 @@ +# Review of the console panel + +Four reviews of the `console` concept layout, taken in November against the +running build: a control-surface designer on **grouping**, a designer of dense +professional interfaces on **visual hierarchy**, a spatial-interaction designer +on **ray ergonomics**, and an interaction designer on **information +architecture**. + +Every claim below that is marked *settled* was checked against the code or the +running panel before it was written down. Everything else is marked as a +derivation and says what would settle it. + +The screenshots the reviews were taken against are the `console` skin at a shell +prompt and with Claude Code in front. + +## Already fixed + +**The keypad was inside a scroller with the shortcuts.** The rule everyone +checks — `KeyPad` is built once and context never touches it — was being +honoured, and the keys still moved: 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; only the shortcuts scroll. + +**The bar offered `cd` while a program owned the terminal.** `classify` returns +an empty kind for anything not in its table — `make`, `pytest`, `docker compose +up`, `journalctl -f` — and `pushContext` left `tool` nil, which sent the bar +down the at-a-prompt branch. Pressing a directory typed `cd 'server'` and a +newline into that program's stdin. The intent was already written in the comment +above `fallbackTool`; the branch was not. Now: foreground process group is not +the shell's, therefore the shell is not reading, therefore no action that +assumes a prompt is offered. Server-side, no APK. + +## Defects confirmed by reading the code + +These are not opinions. Each was verified. + +| | | +|---|---| +| `Buttons.dangerGap()` has **zero call sites** | The double gap beside a destructive control is documented, implemented, and used nowhere. `^D` (closes the shell) sits 8dp from `^C` (pressed constantly); both are red, so colour discriminates nothing. | +| `Rotary` is never instantiated; `Lever` is reachable only from `leverCell()` and `railLever()`, **neither of which is called** | Both are dead code. The rail replaced them with keys and rockers. | +| Every `Led` is constructed lit and `setOn` is never called | A lamp that is always on trains the eye to stop reading it. | +| Chip text is 12.5dp | `docs/readability.md` puts the unreadable line at 16px ≈ 12.5dp. Section headings (9dp), mini notes (10dp) and row marks (11dp) are below it. | +| `BarActivity` declares 1150×460dp | The console layout needs roughly 840dp of height. Window size is the one property Horizon OS lets the app set, and it is set for the old layout. | +| `debounced()` keeps its timestamp **per view**, and `fillConsole` rebuilds every view on `cd` | A trigger bounce after a directory press lands on a freshly built view with a zero debounce, under a ray that has not moved. The guard is defeated by the rebuild it causes. | +| `runActions` adds `git status` / `git diff` only when `Git.Dirty > 0` | Editing a file in another window inserts two chips into a row the user never touched. | +| The server distinguishes `skill` from `skill-global`; the client draws them in one row | The distinction survives only as a colour, the weakest channel through these lenses. | +| `root` is computed, sent, and never drawn | The only context field the client ignores. In a deep subdirectory nothing says which project the path belongs to. | + +## Where the reviews agree + +**Spacing carries safety, not colour.** Through pancake lenses a mark beats a +tint. Everything that fires on press should carry the Enter mark, not only the +`warn` ones; everything irreversible — `/clear`, `:q!`, `^D` — needs a moat of +20dp and, on the worst of them, a hold to fire. + +**Sub-threshold text is texture, not text.** Headings at 9dp are the least +readable thing on a surface whose regions they are supposed to name. Raise +anything meant to be read to 12dp minimum; chips to 14dp. + +**The rocker earns its keep; the lever and the knob do not.** A rocker is +pressed, which is the verb ray input actually has, and its two halves cannot +drift apart. A lever's whole visual argument is "throw me" and it is operated by +pointing and pulling a trigger — it teaches a gesture that does not exist. A +knob promises a continuous range for a value that is stepped, and its real +feedback is detents felt in the hand, which a ray cannot feel. + +**Mode may move the panel; data may not.** A section can disappear because the +foreground program changed — the user caused that and the header confirms it. A +section must not appear because a repository went dirty. Mode is a fact about +your hands; data is weather. + +**State is the panel's weak half.** It says a great deal about where you are and +almost nothing about what is happening. The two readouts it does have echo the +user's own presses rather than reporting the machine. + +## Where the reviews disagree — decisions for the owner + +**1. Section plates: differentiate or delete.** +*Grouping* wants fixed clusters and dynamic clusters to carry different plate +finishes, so the eye learns which housings are furniture and which are weather. +*Hierarchy* wants the plates deleted wherever the content is already a screen — +the glass is the container, and a box around a box at nearly the same radius +reads as a framing error. Both note the plate is currently 2% brighter than the +panel, which through the lenses is the same colour, so today it is paid for +entirely by its outline. + +**2. The phosphor's colour.** +*Hierarchy* argues green now means five things at once — confirm, data, voice, +connection, cursor — so "green = go" cannot survive, and wants the screens moved +to amber. Against it: `capped` already uses amber for exactly this reason, and +moving `console` there costs the CRT reference that the whole reading layer is +built on. + +**3. Column order.** +*Ergonomics* observes that precision demand and reach are inversely matched: the +smallest targets (listing rows) sit in the left column, the longest reach for a +right hand, while the rail — high frequency, low precision — is correctly at the +cheap edge. It prefers fixing sizes over reordering. *Architecture* wants +`COMMANDS` moved to the top of the reading column, so that hiding the browser +and the projects does not lift the one section that survives both modes. + +## Sizes are settled, and not by arithmetic + +**The panel is worn close.** It is pulled in until it sits where a console sits +when you are sitting at one — much nearer than the 1.5 m the readability figures +were measured at. Read at that distance, every size on the panel is comfortable, +including the ones the reviews flagged as sub-threshold. + +That single fact retires most of the sizing section below. Angular size is what +readability follows, and angular size is distance divided into height: a panel +brought to half the distance doubles every glyph without a single number +changing in the code. The reviews were right about the arithmetic and were +reasoning from a distance nobody uses. + +So: **no size changes, and no dp-per-degree measurement.** The numbers below are +kept because they are correct *if the panel is ever pushed out* — if a future +layout is too wide to keep close, or somebody works with it across the room, +this is the analysis that applies. Until then it is not a defect list. + +What survives from it regardless of distance is everything about **spacing and +consequence** rather than size: a moat beside a control that closes the shell is +about the cost of a miss, and that cost does not shrink when the panel comes +closer. + +## What the reviews assumed about distance + +The reviews converge on one measurement that everything else depends on: +**dp per degree for this panel at its own default distance.** + +`docs/readability.md` measured 51.2 px/degree on a 1.5 m cylindrical layer, not +on a native Horizon window. Dividing by the manifest's ~1.25 px/dp gives roughly +**40dp per degree**, and from that: a 46×40dp key is 1.15°×1.00°, a listing row +pitch is 0.68°, chip text is 0.31°. + +That conversion also reconciles the old argument. If "1° of ray error" is the +full spread rather than one standard deviation, a 1.0° target is 2σ — correct +for a harmless key. **The wearer was right and the arithmetic was wrong**, and +40dp becomes a validated floor rather than a compromise. + +Until the number is measured on this window, every size recommendation here is +scaled from an assumption. Measure it first. + +Also for the headset, in order: whether the 1dp key border survives at all (it +is ~0.02°, an order of magnitude under the smear floor — if it does not survive, +the entire role-coding budget is being spent on something invisible); whether +the paging rocker and the scrolling rocker are confusable when they differ only +by one chevron against two; whether any section plate can be made perceptible +while staying dark; and whether phosphor green and Enter green actually confuse. + +## Protocol additions the grouping needs + +Keep `Group{name, items[]}` and give each item a `kind`. Fewer kinds is better: + +- **`key`** — as today. +- **`rocker`** — `{up, down, repeat, axis}`. A stepper is a rocker with a + different glyph pair, not a separate kind. +- **`segmented`** — `{label, options[]}`, momentary. **No `selected` field, by + design**: the server cannot read which answer a program is waiting for, and a + control that shows unreadable state is forbidden. +- **`cluster`** — `{name, items[]}`, a named housing inside a group. This is + what restores headings to the keystroke side, which the client currently + discards when it hoists keystrokes out of every group into one flow. +- **`place`** — `{label, send, rank}` where rank is parent / child / jump. +- **`readout`** — `{label, value, lamp}`, so the server owns what the lamps say. +- **`isolate: true`** — an item-level flag asking for `dangerGap()` around this + control. One boolean beats a spacer widget. + +`⇧Tab` must stay a plain key. It cycles a permission mode that lives inside +Claude, where nothing can read it; drawing it as a selector would promise a +state nobody can honour. This is the most tempting wrong move on the panel. + +## Order of work + +Sizes and the dp-per-degree measurement are **out of scope** — see above; the +panel is worn close and reads comfortably as it is. The visual language is what +is being worked on, so: + +1. The two decisions on plates and phosphor, and the visual hierarchy work that + follows from them. +2. Spacing where a miss is expensive — `dangerGap()` around anything that closes + a shell or wipes a conversation. Distance does not make a mis-hit cheaper. +3. Delete what is dead: `Rotary`, `Lever`, and the two methods that are the only + things reaching them. +4. The confirmed behavioural defects: lamps that never change, the debounce + defeated by the rebuild, chips that appear because a repository went dirty. +5. The protocol additions, once the grouping they serve is settled. From 3332d5bf3471646b475b3b0ac8ed8410154e632a Mon Sep 17 00:00:00 2001 From: butschster Date: Sat, 8 Aug 2026 13:06:58 +0400 Subject: [PATCH 04/15] Green goes back to meaning go, and a heading can be read again MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- client/app/src/debug/AndroidManifest.xml | 2 +- client/app/src/main/AndroidManifest.xml | 17 +++++- .../dev/butschster/linuxterminal/Buttons.java | 25 ++++++-- .../butschster/linuxterminal/ContextBar.java | 61 ++++++++++++++----- .../dev/butschster/linuxterminal/Section.java | 20 ++++++ .../dev/butschster/linuxterminal/Skin.java | 25 ++++++-- 6 files changed, 122 insertions(+), 28 deletions(-) diff --git a/client/app/src/debug/AndroidManifest.xml b/client/app/src/debug/AndroidManifest.xml index ab0e17e..6c1dec4 100644 --- a/client/app/src/debug/AndroidManifest.xml +++ b/client/app/src/debug/AndroidManifest.xml @@ -18,7 +18,7 @@ android:configChanges="density|keyboard|keyboardHidden|navigation|orientation|screenLayout|screenSize|uiMode"> diff --git a/client/app/src/main/AndroidManifest.xml b/client/app/src/main/AndroidManifest.xml index 0889751..0037698 100644 --- a/client/app/src/main/AndroidManifest.xml +++ b/client/app/src/main/AndroidManifest.xml @@ -74,9 +74,24 @@ android:label="terminal bar" android:windowSoftInputMode="adjustResize|stateHidden" android:configChanges="density|keyboard|keyboardHidden|navigation|orientation|screenLayout|screenSize|uiMode"> + diff --git a/client/app/src/main/java/dev/butschster/linuxterminal/Buttons.java b/client/app/src/main/java/dev/butschster/linuxterminal/Buttons.java index 2887b1a..d779849 100644 --- a/client/app/src/main/java/dev/butschster/linuxterminal/Buttons.java +++ b/client/app/src/main/java/dev/butschster/linuxterminal/Buttons.java @@ -269,9 +269,9 @@ public int screenCornerDp() { */ public TextView chip(String label, boolean danger, View.OnClickListener onClick) { TextView view = key(label, danger ? WARN : KEY, null, onClick); - view.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 12.5f); - view.setPadding(dp(12), dp(4), dp(12), dp(4)); - view.setMinHeight(dp(34)); + view.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 14f); + view.setPadding(dp(12), dp(5), dp(12), dp(5)); + view.setMinHeight(dp(36)); view.setMinWidth(0); return view; } @@ -296,7 +296,24 @@ public int containerCornerDp() { } public Drawable section() { - return new Section(0xFF0D141E, dp(containerCornerDp())); + return section(false); + } + + /** + * A milled region. {@code fixed} marks the housings whose contents never + * change with context — the keypad, the rail — against the ones the host + * refills as you work. + * + *

Two things are fixed here that were both wrong before. The plate was + * 0xFF0D141E against a panel of 0xFF111824: four units per channel, under 2% + * of luminance, which through pancake lenses is the same colour — so the box + * was paid for entirely by its outline. And a fixed housing looked exactly + * like a dynamic one, which gave the hand no way to learn that one of them is + * furniture and the other is weather. + */ + public Drawable section(boolean fixed) { + Section plate = new Section(fixed ? 0xFF0A1119 : 0xFF070C12, dp(containerCornerDp())); + return fixed ? plate.lit() : plate; } /** diff --git a/client/app/src/main/java/dev/butschster/linuxterminal/ContextBar.java b/client/app/src/main/java/dev/butschster/linuxterminal/ContextBar.java index e3dad25..6bf72e7 100644 --- a/client/app/src/main/java/dev/butschster/linuxterminal/ContextBar.java +++ b/client/app/src/main/java/dev/butschster/linuxterminal/ContextBar.java @@ -517,24 +517,26 @@ private void buildConsole(Context context) { headParams.bottomMargin = buttons.dp(12); reading.addView(head, headParams); - LinearLayout browser = consoleSection(context, "directory browser"); - browser.addView(consoleScreen(context), new LayoutParams( - LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); + // No plate around the listings. 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, the outer one only 2% brighter than the panel, which + // through the lenses is the same colour. The caption stays; it is engraved + // on the panel, which is where a label belongs. + LinearLayout browser = captioned(context, "directory browser", + consoleScreen(context)); addSection(reading, browser, 0f); browserBox = browser; - LinearLayout projectBox = consoleSection(context, "projects"); projects = new FlowLayout(context, 0); projects.setPadding(buttons.dp(6), buttons.dp(6), buttons.dp(6), buttons.dp(6)); ScrollView projectScroll = elasticScroll(context); projectScroll.addView(projects, new ScrollView.LayoutParams( LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); - projectBox.addView(projectScroll, new LayoutParams( - LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); + LinearLayout projectBox = captioned(context, "projects", projectScroll); addSection(reading, projectBox, 0f); this.projectBox = projectBox; - LinearLayout commandBox = consoleSection(context, "commands"); + LinearLayout commandBox = captioned(context, "commands", null); chipColumn = new LinearLayout(context); chipColumn.setOrientation(VERTICAL); ScrollView chipScroll = new ScrollView(context); @@ -564,13 +566,13 @@ private void buildConsole(Context context) { pressing.setOrientation(VERTICAL); addView(pressing, new LayoutParams(0, LayoutParams.MATCH_PARENT, 0.95f)); - LinearLayout control = consoleSection(context, "terminal control"); + LinearLayout control = consoleSection(context, "terminal control", true); KeyPad pad = new KeyPad(context, buttons, text -> host.onAction(text, false)); control.addView(pad, new LayoutParams( LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); addSection(pressing, control, 0f); - shortcuts = consoleSection(context, "shortcuts"); + shortcuts = consoleSection(context, "shortcuts", false); ScrollView shortcutScroll = new ScrollView(context); shortcutScroll.addView(toolKeys, new ScrollView.LayoutParams( LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); @@ -684,7 +686,7 @@ private View mini(Context context, String caption, TextView value, String sub, i private View consoleRail(Context context) { LinearLayout rail = new LinearLayout(context); rail.setOrientation(VERTICAL); - rail.setBackground(buttons.section()); + rail.setBackground(buttons.section(true)); rail.setPadding(buttons.dp(8), buttons.dp(10), buttons.dp(8), buttons.dp(10)); // Dictation and the keyboard are buttons, not levers. @@ -759,14 +761,41 @@ private void addSection(LinearLayout column, View section, float weight) { column.addView(section, params); } - private LinearLayout consoleSection(Context context, String caption) { + /** + * A caption engraved on the panel with its content beneath it, and no plate. + * + *

For anything that is already a housing of its own — a screen, a scroller + * full of chips. What a plate buys is the statement "these belong together", + * and a screen makes that statement by being a screen. + */ + private LinearLayout captioned(Context context, String caption, View content) { LinearLayout box = new LinearLayout(context); box.setOrientation(VERTICAL); - box.setBackground(buttons.section()); + + TextView label = label(caption.toUpperCase(Locale.ROOT), Buttons.skin().heading, 12); + label.setLetterSpacing(0.10f); + label.setPadding(buttons.dp(2), buttons.dp(2), 0, buttons.dp(7)); + box.addView(label); + + if (content != null) { + box.addView(content, new LayoutParams( + LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); + } + return box; + } + + private LinearLayout consoleSection(Context context, String caption, boolean fixed) { + LinearLayout box = new LinearLayout(context); + box.setOrientation(VERTICAL); + box.setBackground(buttons.section(fixed)); box.setPadding(buttons.dp(10), buttons.dp(10), buttons.dp(10), buttons.dp(10)); - TextView label = label(caption.toUpperCase(Locale.ROOT), Buttons.skin().heading, 9); - label.setLetterSpacing(0.18f); + // 12dp and half the tracking. At 9dp with 0.18 tracking these were the + // least readable text on a surface whose regions they exist to name — and + // wide tracking makes it worse, because it destroys word shape, which is + // the only cue left once a glyph is too small to read letter by letter. + TextView label = label(caption.toUpperCase(Locale.ROOT), Buttons.skin().heading, 12); + label.setLetterSpacing(0.10f); label.setPadding(buttons.dp(2), buttons.dp(2), 0, buttons.dp(9)); box.addView(label); @@ -818,8 +847,8 @@ private void fillConsole(JSONArray groups) { } TextView caption = label(group.optString("name").toUpperCase(Locale.ROOT), - Buttons.skin().heading, 9); - caption.setLetterSpacing(0.18f); + Buttons.skin().heading, 12); + caption.setLetterSpacing(0.10f); caption.setPadding(buttons.dp(2), buttons.dp(6), 0, buttons.dp(6)); chipColumn.addView(caption); diff --git a/client/app/src/main/java/dev/butschster/linuxterminal/Section.java b/client/app/src/main/java/dev/butschster/linuxterminal/Section.java index de17e5e..c80ff37 100644 --- a/client/app/src/main/java/dev/butschster/linuxterminal/Section.java +++ b/client/app/src/main/java/dev/butschster/linuxterminal/Section.java @@ -30,12 +30,21 @@ public class Section extends Drawable { private final int fill; private final float radius; + /** A lit rim along the top: this housing is furniture, not weather. */ + private boolean lit; + private final Paint body = new Paint(Paint.ANTI_ALIAS_FLAG); private final Paint wall = new Paint(Paint.ANTI_ALIAS_FLAG); private final Paint edge = new Paint(Paint.ANTI_ALIAS_FLAG); private final RectF face = new RectF(); + public Section lit() { + this.lit = true; + edge.setColor(0x1FFFFFFF); + return this; + } + public Section(int fill, float radiusPx) { this.fill = fill; this.radius = radiusPx; @@ -64,6 +73,17 @@ public void draw(Canvas canvas) { canvas.drawRoundRect(face, radius, radius, wall); canvas.restore(); canvas.drawRoundRect(face, radius, radius, edge); + + if (lit) { + // One hairline along the top edge only. A machined recess that is part + // of the chassis catches light where the plate was cut; a region the + // software refills does not get one, so the difference reads as how + // the panel was made rather than as a decoration applied to it. + edge.setColor(0x2EFFFFFF); + canvas.drawLine(face.left + radius, face.top + 0.5f, + face.right - radius, face.top + 0.5f, edge); + edge.setColor(lit ? 0x0AFFFFFF : 0); + } } @Override diff --git a/client/app/src/main/java/dev/butschster/linuxterminal/Skin.java b/client/app/src/main/java/dev/butschster/linuxterminal/Skin.java index 5b1ebf6..893c36c 100644 --- a/client/app/src/main/java/dev/butschster/linuxterminal/Skin.java +++ b/client/app/src/main/java/dev/butschster/linuxterminal/Skin.java @@ -482,7 +482,7 @@ public static Skin console() { b.rule = 0xFF1B2432; b.text = 0xFFE7EEF8; b.muted = 0xFF8391A5; - b.heading = 0xFF536072; + b.heading = 0xFF7A8CA4; b.accentNav = 0xFF2DA8FF; b.accentGo = 0xFF36E37B; @@ -497,7 +497,7 @@ public static Skin console() { 0xFF222C3B, // COMMAND 0xFF2B1D22, // WARN 0xFF15291E, // ENTER — the primary face - 0xFF16281E, // VOICE + 0xFF222C3B, // VOICE — graphite; see the edge below // The arrow cluster is a shade bluer than the keys around it. It // is the one group on the pad the hand finds without reading, and // the eye needs to find it the same way — but it is still a key, @@ -511,7 +511,11 @@ public static Skin console() { 0x612DA8FF, 0x73FF5252, // danger at 45% 0x9936E37B, // primary at 60% - 0x8036E37B, + // The microphone is an input, not a confirmation. It was green + // only because the VOICE role was invented next to ENTER; a + // control that starts dictation has nothing to do with running + // a command, and green now belongs to the things that act. + 0x662DA8FF, // VOICE — blue, with the keyboard 0x662DA8FF, // ARROW — blue at 40% }; b.legend = new int[]{ @@ -520,7 +524,7 @@ public static Skin console() { 0xFFCFE6FF, 0xFFFFB0B0, // --key--danger 0xFFC9FFDC, // --key--primary - 0xFFBAFFCF, + 0xFFCFE6FF, // VOICE — the nav legend 0xFFCFE6FF, // ARROW — the nav legend }; @@ -538,8 +542,17 @@ public static Skin console() { b.crt = true; b.sections = true; b.glass = 0xFF031009; - b.phosphor = 0xFF52FF7D; - b.phosphorDim = 0xFF1B9E47; + // Green was doing five jobs at once — confirm, data, voice, connection, + // cursor — and the one with the most area wins, so "green means go" could + // not survive next to a whole column of green text. + // + // The fix is saturation, not hue. A saturated green stays the property of + // things that act: Enter, and the lamp that says the link is up. The + // screens drop to a paler, cooler phosphor — which is what a real P1 tube + // looks like anyway, and keeps the reading layer's whole reference intact + // instead of throwing it away for amber. + b.phosphor = 0xFF8FDCA6; + b.phosphorDim = 0xFF4E8F68; return new Skin(b); } From 6c63d8d57bf6c27c07f6b9895bff2619a3eeb02c Mon Sep 17 00:00:00 2001 From: butschster Date: Sat, 8 Aug 2026 13:25:20 +0400 Subject: [PATCH 05/15] Keys that belong together share one cut-out, and Enter stops being a key MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../dev/butschster/linuxterminal/Buttons.java | 359 +++++++++++++++++- .../butschster/linuxterminal/ContextBar.java | 100 ++--- .../dev/butschster/linuxterminal/Display.java | 42 +- .../dev/butschster/linuxterminal/KeyPad.java | 36 +- .../butschster/linuxterminal/LaunchKey.java | 159 ++++++++ .../butschster/linuxterminal/PhysicalKey.java | 40 +- .../butschster/linuxterminal/TeeHousing.java | 111 ++++++ 7 files changed, 734 insertions(+), 113 deletions(-) create mode 100644 client/app/src/main/java/dev/butschster/linuxterminal/LaunchKey.java create mode 100644 client/app/src/main/java/dev/butschster/linuxterminal/TeeHousing.java diff --git a/client/app/src/main/java/dev/butschster/linuxterminal/Buttons.java b/client/app/src/main/java/dev/butschster/linuxterminal/Buttons.java index d779849..ece87bb 100644 --- a/client/app/src/main/java/dev/butschster/linuxterminal/Buttons.java +++ b/client/app/src/main/java/dev/butschster/linuxterminal/Buttons.java @@ -124,6 +124,9 @@ public static Drawable deck(boolean fixedHalf) { */ private final java.util.Map glyphs = new java.util.WeakHashMap<>(); + /** The role each key was built with, so a cluster can rebuild its face. */ + private final java.util.Map styles = new java.util.WeakHashMap<>(); + private int textDp = 15; private int padH = 11; private int padV = 8; @@ -196,6 +199,7 @@ public TextView key(String label, int style, String hint, View.OnClickListener o view.setMinWidth(dp(minWidth)); view.setMinHeight(dp(minHeight) + sink); view.setBackground(background(style)); + styles.put(view, style); if (skin.engrave) engrave(view); travel(view); view.setClickable(true); @@ -270,8 +274,13 @@ public int screenCornerDp() { public TextView chip(String label, boolean danger, View.OnClickListener onClick) { TextView view = key(label, danger ? WARN : KEY, null, onClick); view.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 14f); - view.setPadding(dp(12), dp(5), dp(12), dp(5)); - view.setMinHeight(dp(36)); + // The same compensation `key` applies, restated because this overrides the + // padding: a physical cap gives the bottom of its cell to the extrusion, so + // text centred on the whole cell sits low on the cap. Worst exactly here, + // where the extrusion is a fifth of the height. + int sink = physicalSkin() ? dp(skin.depthDp) : 0; + view.setPadding(dp(12), dp(5), dp(12), dp(5) + sink); + view.setMinHeight(dp(36) + sink); view.setMinWidth(0); return view; } @@ -316,6 +325,264 @@ public Drawable section(boolean fixed) { return fixed ? plate.lit() : plate; } + /** + * The launch key: Enter, drawn as the one control that is not a member of the + * grid. See {@link LaunchKey} for why it earns a shape of its own. + */ + public TextView launchKey(String label, String hint, View.OnClickListener onClick) { + TextView view = key(label, ENTER, hint, onClick); + view.setMinHeight(dp(minHeight) + dp(10)); + view.setLetterSpacing(0.08f); + + StateListDrawable states = new StateListDrawable(); + float depth = dp(Math.max(2, skin.depthDp)); + int fill = FILL[ENTER]; + states.addState(new int[]{android.R.attr.state_pressed}, + new LaunchKey(fill, skin.accentGo, depth, true)); + states.addState(new int[]{android.R.attr.state_hovered}, + new LaunchKey(lighten(fill, 0.10f), skin.accentGo, depth, false)); + states.addState(new int[]{}, new LaunchKey(fill, skin.accentGo, depth, false)); + view.setBackground(states); + + // The mark every terminal puts on this key, drawn rather than typed so it + // matches the arrows beside it instead of arriving from whatever font the + // system hands back for the code point. + view.setCompoundDrawablesWithIntrinsicBounds( + Glyphs.drawable(context, Glyphs.Kind.ENTER, skin.legend[ENTER], + iconSizeDp() + 2), null, null, null); + view.setCompoundDrawablePadding(dp(10)); + return view; + } + + /** + * Several keys in one housing, touching, divided by a seam. + * + *

This is the third way of putting controls together and it is neither of + * the others. Loose keys with a gap between them are separate objects. A + * rocker is one pitched cap where pressing an end tilts the whole thing — one + * control, two directions. A cluster is what a keyboard does: the keys are + * individually pressable and individually flat, but they are set into one + * cut-out with no space between them, so the eye takes them as a block while + * the hand still finds each cap. + * + *

Drawn by rounding each cap only where it faces the outside world and + * leaving it square where it meets its neighbour, with two device pixels of + * housing showing through as the seam. That is the whole trick: 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. + * + *

Use it where the members are the same kind of thing and are reached for + * together — the arrow keys, or dictation beside the keyboard. Controls that + * merely sit near each other keep their gap. + */ + public LinearLayout cluster(boolean vertical, View... cells) { + LinearLayout housing = new LinearLayout(context); + housing.setOrientation(vertical ? LinearLayout.VERTICAL : LinearLayout.HORIZONTAL); + housing.setBackground(section(true)); + int pad = dp(3); + housing.setPadding(pad, pad, pad, pad); + + float r = dp(skin.cornerDp); + for (int i = 0; i < cells.length; i++) { + View cell = cells[i]; + boolean first = i == 0; + boolean last = i == cells.length - 1; + + float tl = first ? r : 0; + float tr = (vertical ? first : last) ? r : 0; + float br = last ? r : 0; + float bl = (vertical ? last : first) ? r : 0; + float[] radii = {tl, tl, tr, tr, br, br, bl, bl}; + + Integer style = styles.get(cell); + if (style != null) { + cell.setBackground(clusteredFace(style, radii, glyphs.get(cell))); + } + + LinearLayout.LayoutParams params = vertical + ? new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, 0, 1f) + : new LinearLayout.LayoutParams( + 0, LinearLayout.LayoutParams.MATCH_PARENT, 1f); + // The seam: two pixels of the housing showing between neighbours. + if (!first) { + if (vertical) { + params.topMargin = dp(2); + } else { + params.setMarginStart(dp(2)); + } + } + housing.addView(cell, params); + } + return housing; + } + + /** + * The arrow block: one key above, several below, one cut-out around all of + * them. + * + *

The caller passes the row that already exists — the up key with whatever + * sits either side of it — and the keys that go beneath. Nothing moves: the + * cells keep their columns and their widths, and only the housing under them + * changes, which is the difference between grouping and rearranging. + */ + public LinearLayout tee(View[] topRow, int stemIndex, View... bottom) { + LinearLayout block = new LinearLayout(context); + block.setOrientation(LinearLayout.VERTICAL); + + int columns = topRow.length; + float left = (stemIndex + 0.02f) / columns; + float right = (stemIndex + 0.98f) / columns; + block.setBackground(new TeeHousing(0xFF0A1119, dp(skin.cornerDp) + dp(3), + left, right, 0.5f)); + + float r = dp(skin.cornerDp); + LinearLayout top = new LinearLayout(context); + top.setOrientation(LinearLayout.HORIZONTAL); + for (int i = 0; i < columns; i++) { + View cell = topRow[i]; + if (i == stemIndex) { + // Rounded above, square below: the stem runs on into the arm. + float[] radii = {r, r, r, r, 0, 0, 0, 0}; + Integer style = styles.get(cell); + if (style != null) { + cell.setBackground(clusteredFace(style, radii, glyphs.get(cell))); + } + } + LinearLayout.LayoutParams params = new LinearLayout.LayoutParams( + 0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f); + int inset = i == stemIndex ? dp(3) : gap() / 2; + params.setMargins(inset, i == stemIndex ? dp(3) : gap() / 2, inset, 0); + top.addView(cell, params); + } + block.addView(top, new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, + LinearLayout.LayoutParams.WRAP_CONTENT)); + + LinearLayout row = new LinearLayout(context); + row.setOrientation(LinearLayout.HORIZONTAL); + for (int i = 0; i < bottom.length; i++) { + View cell = bottom[i]; + boolean first = i == 0; + boolean last = i == bottom.length - 1; + + // A corner is rounded when both of its sides face the outside world, + // and square when either one meets a neighbour. + // + // Under the arm of the tee that gives the answer by itself: the left + // key has nothing above it — the stem is over the middle one — so its + // top-left is as external as its bottom-left and rounds with it. Only + // the centre key is square all round, because it has a neighbour on + // three sides. The first version rounded the bottom row along its + // bottom edge alone, which left the outer keys with a square shoulder + // above a round foot and no reason for the difference. + boolean stemAbove = i == stemIndex; + float tl = first ? r : 0; + float tr = last ? r : 0; + float br = last ? r : 0; + float bl = first ? r : 0; + if (stemAbove) { + tl = 0; + tr = 0; + } + float[] radii = {tl, tl, tr, tr, br, br, bl, bl}; + Integer style = styles.get(cell); + if (style != null) { + cell.setBackground(clusteredFace(style, radii, glyphs.get(cell))); + } + LinearLayout.LayoutParams params = new LinearLayout.LayoutParams( + 0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f); + params.setMargins(first ? dp(3) : dp(2), dp(2), last ? dp(3) : 0, dp(3)); + row.addView(cell, params); + } + block.addView(row, new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, + LinearLayout.LayoutParams.WRAP_CONTENT)); + + // Twice the ordinary gap beneath the block. What follows it is Enter, and + // a cluster that ends flush against the next control reads as five keys in + // one housing rather than four — the seam that means "same block" and the + // gap that means "different control" have to differ by more than a pixel. + LinearLayout.LayoutParams params = new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, + LinearLayout.LayoutParams.WRAP_CONTENT); + params.bottomMargin = gap(); + block.setLayoutParams(params); + return block; + } + + /** + * Square off one edge of a key so it can meet a cluster on that side. + * + *

For the arrow cluster: the up key lives in the row above, between two + * unrelated keys, and cannot join the housing without moving — which this + * keypad does not do for appearance. Squaring its lower corners and closing + * the gap beneath it gets the inverted T that every keyboard has, while every + * key stays in the cell it has always occupied. + */ + public void joinBelow(View key) { + Integer style = styles.get(key); + if (style == null) return; + float r = dp(skin.cornerDp); + float[] radii = {r, r, r, r, 0, 0, 0, 0}; + key.setBackground(clusteredFace(style, radii, glyphs.get(key))); + } + + private StateListDrawable clusteredFace(int style, float[] radii, Drawable glyph) { + if (!physicalSkin()) return flatClusteredFace(style, radii, glyph); + + StateListDrawable states = new StateListDrawable(); + float depth = dp(skin.depthDp); + int face = FILL[style]; + int border = skin.edge[style]; + int legend = skin.legend[style]; + + states.addState(new int[]{android.R.attr.state_pressed}, + withGlyph(new PhysicalKey(face, border, legend, 0, depth, true, 0) + .corners(radii), glyph)); + states.addState(new int[]{android.R.attr.state_hovered}, + withGlyph(new PhysicalKey(lighten(face, 0.06f), + blend(border, skin.accentNav, 0.6f), legend, 0, depth, false, + skin.accentNav).corners(radii), glyph)); + states.addState(new int[]{}, + withGlyph(new PhysicalKey(face, border, legend, 0, depth, false, 0) + .corners(radii), glyph)); + return states; + } + + /** + * A cluster cell on a skin that has no moulded cap. + * + *

Grouping is not a property of one skin. A block of keys in a single + * cut-out is a statement about what the controls are — reached for together, + * pressed separately — and that is true whether the panel is drawn as + * machined metal or as flat plates. So the shape is built here from the same + * per-corner radii, using whatever the skin's own fill and edge are: the + * silhouette is the same everywhere, the material is not. + */ + private StateListDrawable flatClusteredFace(int style, float[] radii, Drawable glyph) { + StateListDrawable states = new StateListDrawable(); + int face = FILL[style]; + states.addState(new int[]{android.R.attr.state_pressed}, + withGlyph(flatFace(lighten(face, 0.35f), radii, style), glyph)); + states.addState(new int[]{android.R.attr.state_hovered}, + withGlyph(flatFace(lighten(face, 0.18f), radii, style), glyph)); + states.addState(new int[]{}, withGlyph(flatFace(face, radii, style), glyph)); + return states; + } + + private GradientDrawable flatFace(int fill, float[] radii, int style) { + GradientDrawable shape = new GradientDrawable(); + shape.setColor(fill); + shape.setCornerRadii(radii); + if (skin.border > 0) shape.setStroke(dp(skin.border), skin.edge[style]); + return shape; + } + + private Drawable withGlyph(Drawable face, Drawable glyph) { + return glyph == null ? face : stack(face, glyph); + } + /** * A line inside a display: lit text on the glass, with no cap of its own. * @@ -340,11 +607,20 @@ public TextView readout(String label, String hint, View.OnClickListener onClick) // A little bloom, the way a phosphor spreads past the beam. Small // enough to soften the edge of a stroke and not enough to close the // counters — that is the line between "lit" and "out of focus". - // Halved. A phosphor does spread, but the spread was landing on the - // stroke rather than around it, and every letter came out a weight - // heavier than the face actually is — which reads as a fat font rather - // than as a lit one. - view.setShadowLayer(dp(2), 0f, 0f, Color.argb(70, Color.red(skin.phosphor), + // Back up to the reference's 4px at 35%, having been halved once and + // then found too faint to read as lit at all. + // + // The earlier complaint was real but was about the wrong number: a + // wide, strong bloom lands on the stroke and thickens the letter. A + // wider, weaker one lands around it and does not. So the radius goes + // up and the alpha stays low — spread without weight, which is what a + // phosphor actually does. + // text-shadow: 0 0 6px rgba(82,255,125,.5) — the selected row's value + // from the reference sheet, which is the brighter of the two it gives. + // .5 alpha is 128 of 255. The colour comes from this skin's own + // phosphor rather than the sheet's, so a skin that changes its tube + // changes its bloom with it. + view.setShadowLayer(dp(6), 0f, 0f, Color.argb(128, Color.red(skin.phosphor), Color.green(skin.phosphor), Color.blue(skin.phosphor))); } view.setClickable(true); @@ -352,12 +628,7 @@ public TextView readout(String label, String hint, View.OnClickListener onClick) debounced(view, onClick); if (hint != null && !hint.isEmpty()) view.setContentDescription(hint); - // The lit row inverts, so its text has to invert with it. A ColorStateList - // is the only way that stays in step — setting the colour on press would - // leave it dark on any state the listener did not fire for. - view.setTextColor(new android.content.res.ColorStateList( - new int[][]{{android.R.attr.state_pressed}, {}}, - new int[]{skin.glass, skin.phosphor})); + glowOnHover(view, skin.phosphor, brighten(skin.phosphor)); return view; } @@ -384,23 +655,29 @@ public TextView row(String label, String hint, View.OnClickListener onClick) { return view; } - /** Transparent, then a wash of phosphor, then full inversion. */ + /** + * What a row on a screen does when the ray is on it, and when it is pressed. + * + *

Nothing fills. A row is text on glass, and painting a rectangle behind it + * turns it into a button — which is exactly the thing this layer exists not to + * be. Under the ray the phosphor simply burns brighter, which is what a cursor + * does on a character display and costs no new shape. Pressed, the row takes a + * thin outline: the beam is now drawing a box, and a box around a line is how + * a terminal has always said "this one". + */ private StateListDrawable selection() { StateListDrawable states = new StateListDrawable(); + GradientDrawable pressed = new GradientDrawable(); - pressed.setColor(skin.phosphor); + pressed.setColor(Color.TRANSPARENT); pressed.setCornerRadius(dp(skin.cornerDp)); - - GradientDrawable hovered = new GradientDrawable(); - hovered.setColor(Color.argb(46, Color.red(skin.phosphor), Color.green(skin.phosphor), - Color.blue(skin.phosphor))); - hovered.setCornerRadius(dp(skin.cornerDp)); + pressed.setStroke(dp(1), Color.argb(190, Color.red(skin.phosphor), + Color.green(skin.phosphor), Color.blue(skin.phosphor))); GradientDrawable idle = new GradientDrawable(); idle.setColor(Color.TRANSPARENT); states.addState(new int[]{android.R.attr.state_pressed}, pressed); - states.addState(new int[]{android.R.attr.state_hovered}, hovered); states.addState(new int[]{}, idle); return states; } @@ -426,6 +703,40 @@ private void travel(View view) { view.setStateListAnimator(states); } + /** The same phosphor with the beam turned up: towards white, not another hue. */ + public static int brighten(int colour) { + return Color.rgb( + (int) (Color.red(colour) + (255 - Color.red(colour)) * 0.45f), + (int) (Color.green(colour) + (255 - Color.green(colour)) * 0.45f), + (int) (Color.blue(colour) + (255 - Color.blue(colour)) * 0.45f)); + } + + /** + * Make a line of phosphor brighten under the ray instead of gaining a + * background. The text keeps its place, its weight and its colour family — only + * the beam's intensity changes, which is the one thing a tube can actually do. + */ + public void glowOnHover(final TextView text, final int resting, final int lit) { + text.setOnHoverListener((v, event) -> { + switch (event.getActionMasked()) { + case MotionEvent.ACTION_HOVER_ENTER: + text.setTextColor(lit); + text.setShadowLayer(dp(10), 0f, 0f, Color.argb(170, Color.red(lit), + Color.green(lit), Color.blue(lit))); + break; + case MotionEvent.ACTION_HOVER_EXIT: + text.setTextColor(resting); + text.setShadowLayer(dp(6), 0f, 0f, Color.argb(128, + Color.red(skin.phosphor), Color.green(skin.phosphor), + Color.blue(skin.phosphor))); + break; + default: + break; + } + return false; + }); + } + /** * A lit key writes its legend in its own edge colour. * @@ -659,6 +970,12 @@ private StateListDrawable slope(boolean upper) { public TextView half(String label, String hint, View.OnClickListener onClick) { TextView view = key(label, KEY, hint, onClick); view.setBackground(pressOnly()); + // And undo the extrusion compensation `key` just applied. A half has no + // extrusion — it is a face on a cap that belongs to the pair — so the + // padding that centres a legend on a standing key pushes this one off + // centre instead. Visible on A+ / A− before it was noticed anywhere else. + view.setPadding(dp(padH), dp(padV), dp(padH), dp(padV)); + view.setMinHeight(dp(minHeight)); return view; } diff --git a/client/app/src/main/java/dev/butschster/linuxterminal/ContextBar.java b/client/app/src/main/java/dev/butschster/linuxterminal/ContextBar.java index 6bf72e7..8da84e0 100644 --- a/client/app/src/main/java/dev/butschster/linuxterminal/ContextBar.java +++ b/client/app/src/main/java/dev/butschster/linuxterminal/ContextBar.java @@ -89,8 +89,6 @@ public interface Host { private View projectBox; private LinearLayout chipColumn; private LinearLayout shortcuts; - private TextView lastCommand; - private TextView selectedName; private LinearLayout left; private LinearLayout right; @@ -269,9 +267,14 @@ public ContextBar(Context context, Host host) { controls.setPadding(buttons.dp(6), buttons.dp(8), buttons.dp(6), buttons.dp(8)); addView(controls, new LayoutParams(buttons.dp(STRIP_WIDTH_DP), LayoutParams.MATCH_PARENT)); - addControl(controls, buttons.icon(ICON_MIC, Buttons.VOICE, "dictate", v -> host.onDictate())); - addControl(controls, buttons.icon(ICON_KEYBOARD, Buttons.COMMAND, "on-screen keyboard", - v -> host.onKeyboard())); + // Dictation and the keyboard as one block on every skin, not only on the + // console one. They answer the same question — how does text get into the + // line — and a cluster says so without implying, as a rocker would, that + // one is a direction of the other. + addControl(controls, buttons.cluster(true, + buttons.icon(ICON_MIC, Buttons.VOICE, "dictate", v -> host.onDictate()), + buttons.icon(ICON_KEYBOARD, Buttons.COMMAND, "on-screen keyboard", + v -> host.onKeyboard()))); // Up over down, everywhere on this strip: in every rocker here the upper // half is the one that moves away from where you are. // @@ -508,6 +511,13 @@ private void buildConsole(Context context) { head.setGravity(Gravity.CENTER_VERTICAL); head.setBackground(buttons.display()); head.setPadding(buttons.dp(14), buttons.dp(10), buttons.dp(14), buttons.dp(10)); + // The header is a screen, so it is written by the same beam as the screens + // under it. It was inheriting the panel's neutral text and dim grey from + // the constructor, which put two colours behind one piece of glass — the + // exact fault fixed once already for the typeface, arriving again through + // the palette. + where.setTextColor(Buttons.skin().phosphor); + what.setTextColor(Buttons.skin().phosphorDim); head.addView(where, new LayoutParams(0, LayoutParams.WRAP_CONTENT, 1f)); head.addView(what); head.addView(new Led(context, Buttons.skin().accentGo, true), @@ -545,9 +555,6 @@ private void buildConsole(Context context) { commandBox.addView(chipScroll, new LayoutParams(LayoutParams.MATCH_PARENT, 0, 1f)); addSection(reading, commandBox, 1f); - reading.addView(consoleMinis(context), new LayoutParams( - LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); - addView(gap(context, 12), new LayoutParams(buttons.dp(12), LayoutParams.MATCH_PARENT)); // ---- press @@ -624,57 +631,6 @@ protected void onMeasure(int widthSpec, int heightSpec) { return scroll; } - private View consoleMinis(Context context) { - LinearLayout strip = new LinearLayout(context); - strip.setOrientation(HORIZONTAL); - lastCommand = miniValue(context); - selectedName = miniValue(context); - strip.addView(mini(context, "last command", lastCommand, "ready", - Buttons.skin().accentGo), miniParams()); - strip.addView(gap(context, 10)); - strip.addView(mini(context, "selected", selectedName, "—", - Buttons.skin().accentWarn), miniParams()); - return strip; - } - - private LayoutParams miniParams() { - return new LayoutParams(0, LayoutParams.WRAP_CONTENT, 1f); - } - - private TextView miniValue(Context context) { - TextView view = label("—", Buttons.skin().phosphor, 13); - view.setTypeface(Fonts.mono(context)); - view.setSingleLine(true); - view.setEllipsize(TextUtils.TruncateAt.END); - return view; - } - - private View mini(Context context, String caption, TextView value, String sub, int lamp) { - LinearLayout box = new LinearLayout(context); - box.setOrientation(VERTICAL); - box.setBackground(buttons.display()); - box.setPadding(buttons.dp(10), buttons.dp(8), buttons.dp(10), buttons.dp(8)); - - TextView label = label(caption.toUpperCase(Locale.ROOT), 0xFF1F7B41, 9); - label.setLetterSpacing(0.18f); - box.addView(label); - box.addView(value); - - LinearLayout footer = new LinearLayout(context); - footer.setOrientation(HORIZONTAL); - footer.setGravity(Gravity.CENTER_VERTICAL); - footer.addView(new Led(context, lamp, true), - new LayoutParams(buttons.dp(9), buttons.dp(9))); - TextView note = label(sub, Buttons.skin().phosphorDim, 10); - note.setTypeface(Typeface.MONOSPACE); - LayoutParams noteParams = new LayoutParams( - LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); - noteParams.setMarginStart(buttons.dp(6)); - footer.addView(note, noteParams); - box.addView(footer); - return box; - } - /** * The rail: everything that moves you through text, as rockers. * @@ -696,14 +652,21 @@ private View consoleRail(Context context) { // dismisses itself. Drawing them as switches promised a state they do not // hold. The sticky-modifier lever is gone outright: nothing in the client // implements it, and a control that latches nothing is a lie with a hinge. + // One block, because they are one question: how does text get into the + // line. Not a rocker — neither is a direction of the other, and pressing + // one must not suggest the pair tilts. A cluster says "these belong + // together" while leaving each cap its own face to be pressed. TextView mic = buttons.icon(ICON_MIC, Buttons.VOICE, "dictate", v -> host.onDictate()); - mic.setTextSize(TypedValue.COMPLEX_UNIT_DIP, buttons.iconSizeDp() + 8); - rail.addView(railControl(mic, 58)); - + mic.setTextSize(TypedValue.COMPLEX_UNIT_DIP, buttons.iconSizeDp() + 6); TextView keyboard = buttons.icon(ICON_KEYBOARD, Buttons.DESTINATION, "on-screen keyboard", v -> host.onKeyboard()); keyboard.setTextSize(TypedValue.COMPLEX_UNIT_DIP, buttons.iconSizeDp() + 4); - rail.addView(railControl(keyboard, 48)); + + View input = buttons.cluster(true, mic, keyboard); + LayoutParams inputParams = new LayoutParams( + LayoutParams.MATCH_PARENT, buttons.dp(104)); + inputParams.bottomMargin = buttons.dp(10); + rail.addView(input, inputParams); View pageUp = pageKey(Glyphs.Kind.PAGE_UP, ESC + "[5~", "page up, inside the program"); View pageDown = pageKey(Glyphs.Kind.PAGE_DOWN, ESC + "[6~", "page down, inside the program"); @@ -860,10 +823,7 @@ private void fillConsole(JSONArray groups) { String send = action.optString("send"); boolean enter = action.optBoolean("enter"); boolean danger = "warn".equals(action.optString("style", "key")); - View chip = buttons.chip(label, danger, v -> { - lastCommand.setText(label); - host.onAction(send, enter); - }); + View chip = buttons.chip(label, danger, v -> host.onAction(send, enter)); String hint = action.isNull("hint") ? null : action.optString("hint"); if (hint != null && !hint.isEmpty()) chip.setContentDescription(hint); flow.addView(chip); @@ -938,11 +898,7 @@ protected void onMeasure(int widthSpec, int heightSpec) { String hint = action.isNull("hint") ? null : action.optString("hint"); if (hint != null && !hint.isEmpty()) row.setContentDescription(hint); - row.setOnClickListener(v -> { - selectedName.setText(name); - lastCommand.setText(send); - host.onAction(send, enter); - }); + row.setOnClickListener(v -> host.onAction(send, enter)); return row; } diff --git a/client/app/src/main/java/dev/butschster/linuxterminal/Display.java b/client/app/src/main/java/dev/butschster/linuxterminal/Display.java index 1237277..e553274 100644 --- a/client/app/src/main/java/dev/butschster/linuxterminal/Display.java +++ b/client/app/src/main/java/dev/butschster/linuxterminal/Display.java @@ -49,6 +49,7 @@ public class Display extends Drawable { private final Paint sheen = new Paint(Paint.ANTI_ALIAS_FLAG); private final Paint scan = new Paint(); private final Paint vignette = new Paint(Paint.ANTI_ALIAS_FLAG); + private final Paint halo = new Paint(Paint.ANTI_ALIAS_FLAG); private final RectF face = new RectF(); private final float radius; @@ -134,6 +135,20 @@ protected void onBoundsChange(Rect bounds) { vignette.setShader(new android.graphics.RadialGradient(cx, cy, Math.max(1f, r), new int[]{Color.TRANSPARENT, Color.argb(58, 0, 0, 0)}, new float[]{0.55f, 1f}, Shader.TileMode.CLAMP)); + + // radial-gradient(120% 90% at 50% 45%, rgba(20,90,45,.14), transparent 70%) + // + // Taken from the reference sheet rather than approximated, including + // the offset centre: the bright spot of a tube sits a little above the + // middle, because the deflection yoke is not symmetrical about it. + // .14 alpha is 36 of 255; the stop at 70% is where it reaches nothing. + float haloX = face.left + face.width() * 0.50f; + float haloY = face.top + face.height() * 0.45f; + float haloR = Math.max(face.width() * 1.20f, face.height() * 0.90f) * 0.5f; + halo.setShader(new android.graphics.RadialGradient(haloX, haloY, + Math.max(1f, haloR), + new int[]{Color.argb(36, 20, 90, 45), Color.TRANSPARENT}, + new float[]{0f, 0.70f}, Shader.TileMode.CLAMP)); } } @@ -146,16 +161,29 @@ public void draw(Canvas canvas) { canvas.drawRoundRect(face, radius, radius, sheen); } if (crt) { - // Every third device pixel, at 6% black. Fine enough that at any - // sensible text size it reads as a surface the letters sit on rather - // than as stripes across them — `docs/readability.md` is the reason it - // is not the fat, obvious scanline of a filter. canvas.save(); canvas.clipRect(face); - scan.setColor(Color.argb(15, 0, 0, 0)); - for (float y = face.top; y < face.bottom; y += 3f) { - canvas.drawRect(face.left, y, face.right, y + 1f, scan); + + // Two lines per period, not one: a faintly green band and a faintly + // black one, two device pixels each. That is what the reference sheet + // asks for and it is also what a shadow mask actually does — the gaps + // between rows are not black, they are the phosphor not being struck, + // so they keep the tube's colour. A single dark line every third pixel + // reads as a dirty screen; this reads as a raster. + // linear-gradient(rgba(10,70,30,.045) 50%, rgba(0,0,0,.045) 50%) + // at background-size 100% 4px: two device pixels of each, and .045 + // alpha is 11 of 255. + for (float y = face.top; y < face.bottom; y += 4f) { + scan.setColor(Color.argb(11, 10, 70, 30)); + canvas.drawRect(face.left, y, face.right, y + 2f, scan); + scan.setColor(Color.argb(11, 0, 0, 0)); + canvas.drawRect(face.left, y + 2f, face.right, y + 4f, scan); } + + // The tube's own glow, brightest where the beam spends most of its + // time. Drawn before the vignette so the two meet in the middle + // distance rather than fighting at the edge. + canvas.drawRoundRect(face, radius, radius, halo); canvas.drawRoundRect(face, radius, radius, vignette); canvas.restore(); } diff --git a/client/app/src/main/java/dev/butschster/linuxterminal/KeyPad.java b/client/app/src/main/java/dev/butschster/linuxterminal/KeyPad.java index 9c2387c..510452a 100644 --- a/client/app/src/main/java/dev/butschster/linuxterminal/KeyPad.java +++ b/client/app/src/main/java/dev/butschster/linuxterminal/KeyPad.java @@ -55,12 +55,17 @@ public KeyPad(Context context, Buttons buttons, Send send) { // Nothing is left empty and the cluster still reads as the inverted T every // keyboard has, because the arrows carry a colour of their own — the shape // survives having neighbours. - addView(row( - key("^W", "\u0017", Buttons.KEY, "delete the word before the cursor"), - repeating(Glyphs.Kind.UP, ESC + "[A", Buttons.ARROW, "up — previous command"), - key("^U", "\u0015", Buttons.KEY, "delete to the start of the line"))); - - addView(row( + // The arrow block: ^W and ^U keep their columns, the up key keeps its cell, + // and one T-shaped cut-out is drawn under all four arrows. Grouping by + // changing the housing rather than by moving anything — see Buttons.tee. + View up = repeating(Glyphs.Kind.UP, ESC + "[A", Buttons.ARROW, "up — previous command"); + addView(buttons.tee( + new View[]{ + key("^W", "\u0017", Buttons.KEY, "delete the word before the cursor"), + up, + key("^U", "\u0015", Buttons.KEY, "delete to the start of the line"), + }, + 1, repeating(Glyphs.Kind.LEFT, ESC + "[D", Buttons.ARROW, "left"), repeating(Glyphs.Kind.DOWN, ESC + "[B", Buttons.ARROW, "down"), repeating(Glyphs.Kind.RIGHT, ESC + "[C", Buttons.ARROW, "right"))); @@ -72,7 +77,7 @@ public KeyPad(Context context, Buttons buttons, Send send) { // Enter takes the full width at the bottom, where two of its four error // directions are the window edge and a miss costs nothing. - addView(row(key("Enter", "\r", Buttons.ENTER, "run it"))); + addView(row(buttons.launchKey("Enter", "run it", v -> send.send("\r")))); } /** @@ -104,6 +109,23 @@ private View repeating(Glyphs.Kind kind, String bytes, int style, String hint) { return view; } + /** + * A row that touches the one above it: the arrow cluster meeting the up key. + * + *

Same cells, same widths, same positions — only the margin between the two + * rows is gone, so the block below sits against the key above and the pair + * reads as one arrow cluster in the shape a keyboard uses. + */ + private LinearLayout joined(View cell) { + LinearLayout row = new LinearLayout(getContext()); + row.setOrientation(HORIZONTAL); + LayoutParams params = new LayoutParams( + LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT, 1f); + params.setMargins(buttons.gap() / 2, 0, buttons.gap() / 2, buttons.gap() / 2); + row.addView(cell, params); + return row; + } + private LinearLayout row(View... cells) { LinearLayout row = new LinearLayout(getContext()); row.setOrientation(HORIZONTAL); diff --git a/client/app/src/main/java/dev/butschster/linuxterminal/LaunchKey.java b/client/app/src/main/java/dev/butschster/linuxterminal/LaunchKey.java new file mode 100644 index 0000000..9be4282 --- /dev/null +++ b/client/app/src/main/java/dev/butschster/linuxterminal/LaunchKey.java @@ -0,0 +1,159 @@ +package dev.butschster.linuxterminal; + +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.ColorFilter; +import android.graphics.LinearGradient; +import android.graphics.Paint; +import android.graphics.PixelFormat; +import android.graphics.Rect; +import android.graphics.RectF; +import android.graphics.Shader; +import android.graphics.drawable.Drawable; + +/** + * The one key on the panel that is a different object: the launch key. + * + *

Every other control is a cap in a family of caps, told apart by colour and + * legend. Enter is the key you press to commit — the end of every sentence typed + * here — and on a real console that key is not a member of the grid. It is longer, + * it is set in its own bezel, and it carries a groove around its face so the + * finger finds it without the eye. A machine that has one irreversible action + * gives it a shape nothing else has, so that "which one was it" is never a + * question. + * + *

Three things make it that shape, and none of them is a colour: + * + *

    + *
  • A bezel. The cap sits inside a ring of the panel's own material + * rather than flush against its neighbours — the key is mounted, not tiled. + *
  • An inlaid groove a couple of pixels inside the face, lit on its + * lower edge like every other recess on this panel. It is the detail that + * reads as a moulded part rather than a rounded rectangle. + *
  • Shoulders. The corner radius is half the cap's height, so the ends + * are round while the middle stays straight: a capsule, which is the profile + * of a bar key on every machine that has one. + *
+ */ +public class LaunchKey extends Drawable { + + private final int face; + private final int accent; + private final float depth; + private final boolean pressed; + + private final Paint bezel = new Paint(Paint.ANTI_ALIAS_FLAG); + private final Paint solid = new Paint(Paint.ANTI_ALIAS_FLAG); + private final Paint body = new Paint(Paint.ANTI_ALIAS_FLAG); + private final Paint groove = new Paint(Paint.ANTI_ALIAS_FLAG); + private final Paint rim = new Paint(Paint.ANTI_ALIAS_FLAG); + private final Paint glow = new Paint(Paint.ANTI_ALIAS_FLAG); + + private final RectF housing = new RectF(); + private final RectF cap = new RectF(); + private final RectF extrusion = new RectF(); + private final RectF inlay = new RectF(); + + private float radius; + private float inlayRadius; + + public LaunchKey(int face, int accent, float depthPx, boolean pressed) { + this.face = face; + this.accent = accent; + this.depth = depthPx; + this.pressed = pressed; + + bezel.setColor(0xFF0A1119); + solid.setColor(0xE0071009); + groove.setStyle(Paint.Style.STROKE); + rim.setStyle(Paint.Style.STROKE); + glow.setStyle(Paint.Style.STROKE); + } + + @Override + protected void onBoundsChange(Rect bounds) { + housing.set(bounds.left + 0.5f, bounds.top + 0.5f, + bounds.right - 0.5f, bounds.bottom - 0.5f); + + float inset = 3f; + float stand = pressed ? Math.min(depth, 1f) : depth; + cap.set(housing.left + inset, housing.top + inset, + housing.right - inset, housing.bottom - inset - depth); + extrusion.set(cap.left, cap.top + stand, cap.right, cap.bottom + stand); + + // Half the cap's height: round ends, straight middle. + radius = cap.height() / 2f; + + inlay.set(cap.left + 5f, cap.top + 4f, cap.right - 5f, cap.bottom - 4f); + inlayRadius = Math.max(2f, inlay.height() / 2f); + + body.setShader(new LinearGradient(0, cap.top, 0, cap.bottom, + pressed + ? new int[]{shade(face, -14), shade(face, -4), shade(face, 6)} + : new int[]{lighten(face, 0.20f), face, shade(face, -14)}, + new float[]{0f, 0.5f, 1f}, Shader.TileMode.CLAMP)); + + // The groove: shadow on its upper wall, light on its lower one — the same + // description every recess on this panel is drawn from. + groove.setShader(new LinearGradient(0, inlay.top, 0, inlay.bottom, + new int[]{0x8C000000, 0x1AFFFFFF}, + new float[]{0f, 1f}, Shader.TileMode.CLAMP)); + } + + @Override + public void draw(Canvas canvas) { + // The bezel it is mounted in. + canvas.drawRoundRect(housing, radius + 3f, radius + 3f, bezel); + + if (!pressed) { + canvas.drawRoundRect(extrusion, radius, radius, solid); + } + + canvas.drawRoundRect(cap, radius, radius, body); + + groove.setStrokeWidth(1.5f); + canvas.drawRoundRect(inlay, inlayRadius, inlayRadius, groove); + + // Lit at rest, because this is the key the panel is built around. Two + // rings rather than a blur, and quiet: it marks the primary control, it + // does not shout over the legend on it. + glow.setStrokeWidth(3f); + glow.setColor(withAlpha(accent, pressed ? 30 : 20)); + canvas.drawRoundRect(cap, radius, radius, glow); + + rim.setStrokeWidth(1f); + rim.setColor(withAlpha(accent, pressed ? 190 : 150)); + canvas.drawRoundRect(cap, radius, radius, rim); + } + + private static int withAlpha(int colour, int alpha) { + return Color.argb(alpha, Color.red(colour), Color.green(colour), Color.blue(colour)); + } + + private static int shade(int colour, int step) { + return Color.rgb( + Math.max(0, Math.min(255, Color.red(colour) + step)), + Math.max(0, Math.min(255, Color.green(colour) + step)), + Math.max(0, Math.min(255, Color.blue(colour) + step))); + } + + private static int lighten(int colour, float amount) { + return Color.rgb( + (int) (Color.red(colour) + (255 - Color.red(colour)) * amount), + (int) (Color.green(colour) + (255 - Color.green(colour)) * amount), + (int) (Color.blue(colour) + (255 - Color.blue(colour)) * amount)); + } + + @Override + public void setAlpha(int alpha) { + } + + @Override + public void setColorFilter(ColorFilter filter) { + } + + @Override + public int getOpacity() { + return PixelFormat.TRANSLUCENT; + } +} diff --git a/client/app/src/main/java/dev/butschster/linuxterminal/PhysicalKey.java b/client/app/src/main/java/dev/butschster/linuxterminal/PhysicalKey.java index e6687a0..040cb95 100644 --- a/client/app/src/main/java/dev/butschster/linuxterminal/PhysicalKey.java +++ b/client/app/src/main/java/dev/butschster/linuxterminal/PhysicalKey.java @@ -64,6 +64,25 @@ public class PhysicalKey extends Drawable { private final RectF cap = new RectF(); private final RectF extrusion = new RectF(); + /** + * Per-corner radii, when this key is one cell of a cluster. + * + *

A key sitting alone is rounded on all four corners. A key that is one of + * several inside a single housing is rounded only where it meets the outside + * world and square where it meets its neighbour — which is what makes a block + * of keys read as one part with seams rather than as separate caps that happen + * to be close together. Null means the ordinary all-round radius. + */ + private float[] radii; + + private final android.graphics.Path shape = new android.graphics.Path(); + + /** The same key, rounded only on the corners given. See {@link #radii}. */ + public PhysicalKey corners(float[] perCorner) { + this.radii = perCorner; + return this; + } + public PhysicalKey(int face, int border, int legend, float radiusPx, float depthPx, boolean pressed, int halo) { this.face = face; @@ -137,20 +156,19 @@ public void draw(Canvas canvas) { } // The height of the cap: hard-edged, so it reads as the side of a solid. - canvas.drawRoundRect(extrusion, radius, radius, solid); - - canvas.drawRoundRect(cap, radius, radius, body); + drawShape(canvas, extrusion, solid); + drawShape(canvas, cap, body); if (pressed) { canvas.save(); canvas.clipRect(cap); inner.setStrokeWidth(depth); - canvas.drawRoundRect(cap, radius, radius, inner); + drawShape(canvas, cap, inner); canvas.restore(); } bevel.setStrokeWidth(1f); - canvas.drawRoundRect(cap, radius, radius, bevel); + drawShape(canvas, cap, bevel); if (halo != 0) { // Illumination is two rings, not a blur: the outer one is what a glow @@ -169,7 +187,17 @@ public void draw(Canvas canvas) { edge.setStrokeWidth(1f); edge.setColor(border); - canvas.drawRoundRect(cap, radius, radius, edge); + drawShape(canvas, cap, edge); + } + + private void drawShape(Canvas canvas, RectF rect, Paint paint) { + if (radii == null) { + canvas.drawRoundRect(rect, radius, radius, paint); + return; + } + shape.reset(); + shape.addRoundRect(rect, radii, android.graphics.Path.Direction.CW); + canvas.drawPath(shape, paint); } /** The colour this control's legend is written in. */ diff --git a/client/app/src/main/java/dev/butschster/linuxterminal/TeeHousing.java b/client/app/src/main/java/dev/butschster/linuxterminal/TeeHousing.java new file mode 100644 index 0000000..fa9dd14 --- /dev/null +++ b/client/app/src/main/java/dev/butschster/linuxterminal/TeeHousing.java @@ -0,0 +1,111 @@ +package dev.butschster.linuxterminal; + +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.ColorFilter; +import android.graphics.LinearGradient; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.PixelFormat; +import android.graphics.Rect; +import android.graphics.RectF; +import android.graphics.Shader; +import android.graphics.drawable.Drawable; + +/** + * The cut-out an arrow cluster sits in: a wide well along the bottom with a + * narrower one rising from the middle of it. The inverted T every keyboard has. + * + *

It exists because the first attempt squared off the bottom corners of the up + * key and closed the gap under it, which is most of the geometry and none of the + * conviction: the three lower keys were inside a housing and the fourth was + * outside it, touching. A key block is a block because the panel was cut + * that shape, so the cut has to include all four. + * + *

Drawn as one path — two rounded rectangles unioned — rather than as two + * overlapping shapes. The difference shows at the join: two shapes drawn one over + * the other leave the seam of the upper one crossing the lower's field, and a + * union has no interior edge at all, which is what a milled recess looks like. + */ +public class TeeHousing extends Drawable { + + private final int fill; + private final float radius; + + /** Which slice of the width the stem occupies: left and right as fractions. */ + private final float stemLeft; + private final float stemRight; + + /** Where the arm begins, as a fraction of the height. */ + private final float armTop; + + private final Paint body = new Paint(Paint.ANTI_ALIAS_FLAG); + private final Paint wall = new Paint(Paint.ANTI_ALIAS_FLAG); + private final Paint lip = new Paint(Paint.ANTI_ALIAS_FLAG); + + private final Path shape = new Path(); + private final Path scratch = new Path(); + + public TeeHousing(int fill, float radiusPx, float stemLeft, float stemRight, float armTop) { + this.fill = fill; + this.radius = radiusPx; + this.stemLeft = stemLeft; + this.stemRight = stemRight; + this.armTop = armTop; + body.setColor(fill); + wall.setStyle(Paint.Style.STROKE); + lip.setStyle(Paint.Style.STROKE); + lip.setStrokeWidth(1f); + lip.setColor(0x2EFFFFFF); + } + + @Override + protected void onBoundsChange(Rect bounds) { + float w = bounds.width(); + float h = bounds.height(); + + RectF stem = new RectF(bounds.left + w * stemLeft, bounds.top + 0.5f, + bounds.left + w * stemRight, bounds.top + h * armTop + radius); + RectF arm = new RectF(bounds.left + 0.5f, bounds.top + h * armTop, + bounds.right - 0.5f, bounds.bottom - 0.5f); + + shape.reset(); + shape.addRoundRect(stem, radius, radius, Path.Direction.CW); + scratch.reset(); + scratch.addRoundRect(arm, radius, radius, Path.Direction.CW); + shape.op(scratch, Path.Op.UNION); + + // The wall of the recess: shaded at the top, catching light at the bottom. + // The same description Section uses, so a cut-out is a cut-out wherever it + // appears on the panel. + wall.setShader(new LinearGradient(0, bounds.top, 0, bounds.bottom, + new int[]{0xBF000000, Color.TRANSPARENT, 0x08FFFFFF}, + new float[]{0f, 0.35f, 1f}, Shader.TileMode.CLAMP)); + } + + @Override + public void draw(Canvas canvas) { + canvas.drawPath(shape, body); + + canvas.save(); + canvas.clipPath(shape); + wall.setStrokeWidth(6f); + canvas.drawPath(shape, wall); + canvas.restore(); + + canvas.drawPath(shape, lip); + } + + @Override + public void setAlpha(int alpha) { + } + + @Override + public void setColorFilter(ColorFilter filter) { + } + + @Override + public int getOpacity() { + return PixelFormat.TRANSLUCENT; + } +} From 5fa5b50a84299493b7264e61c8304bc30f1dec80 Mon Sep 17 00:00:00 2001 From: butschster Date: Sat, 8 Aug 2026 13:28:17 +0400 Subject: [PATCH 06/15] The hover glow fires, because it is listened for where the ray actually lands MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../dev/butschster/linuxterminal/Buttons.java | 24 ++++++++++++++----- .../butschster/linuxterminal/ContextBar.java | 15 +++++++++++- 2 files changed, 32 insertions(+), 7 deletions(-) diff --git a/client/app/src/main/java/dev/butschster/linuxterminal/Buttons.java b/client/app/src/main/java/dev/butschster/linuxterminal/Buttons.java index ece87bb..6dc680f 100644 --- a/client/app/src/main/java/dev/butschster/linuxterminal/Buttons.java +++ b/client/app/src/main/java/dev/butschster/linuxterminal/Buttons.java @@ -628,7 +628,7 @@ public TextView readout(String label, String hint, View.OnClickListener onClick) debounced(view, onClick); if (hint != null && !hint.isEmpty()) view.setContentDescription(hint); - glowOnHover(view, skin.phosphor, brighten(skin.phosphor)); + glowOnHover(view, view, skin.phosphor); return view; } @@ -711,13 +711,27 @@ public static int brighten(int colour) { (int) (Color.blue(colour) + (255 - Color.blue(colour)) * 0.45f)); } + /** + * The spread a lit phosphor has around its stroke. + * + *

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. 6dp at + * half alpha is the reference sheet's own text-shadow. + */ + public void bloom(TextView text, int colour) { + if (!skin.crt) return; + text.setShadowLayer(dp(6), 0f, 0f, Color.argb(128, Color.red(colour), + Color.green(colour), Color.blue(colour))); + } + /** * Make a line of phosphor brighten under the ray instead of gaining a * background. The text keeps its place, its weight and its colour family — only * the beam's intensity changes, which is the one thing a tube can actually do. */ - public void glowOnHover(final TextView text, final int resting, final int lit) { - text.setOnHoverListener((v, event) -> { + public void glowOnHover(final View target, final TextView text, final int resting) { + final int lit = brighten(resting); + target.setOnHoverListener((v, event) -> { switch (event.getActionMasked()) { case MotionEvent.ACTION_HOVER_ENTER: text.setTextColor(lit); @@ -726,9 +740,7 @@ public void glowOnHover(final TextView text, final int resting, final int lit) { break; case MotionEvent.ACTION_HOVER_EXIT: text.setTextColor(resting); - text.setShadowLayer(dp(6), 0f, 0f, Color.argb(128, - Color.red(skin.phosphor), Color.green(skin.phosphor), - Color.blue(skin.phosphor))); + bloom(text, resting); break; default: break; diff --git a/client/app/src/main/java/dev/butschster/linuxterminal/ContextBar.java b/client/app/src/main/java/dev/butschster/linuxterminal/ContextBar.java index 8da84e0..5ed12f3 100644 --- a/client/app/src/main/java/dev/butschster/linuxterminal/ContextBar.java +++ b/client/app/src/main/java/dev/butschster/linuxterminal/ContextBar.java @@ -518,6 +518,11 @@ private void buildConsole(Context context) { // the palette. where.setTextColor(Buttons.skin().phosphor); what.setTextColor(Buttons.skin().phosphorDim); + // And the same bloom. The header was the one lit surface drawing its text + // flat: the colour had been fixed, the beam had not, so it read as green + // ink rather than as a lit line. + buttons.bloom(where, Buttons.skin().phosphor); + buttons.bloom(what, Buttons.skin().phosphorDim); head.addView(where, new LayoutParams(0, LayoutParams.WRAP_CONTENT, 1f)); head.addView(what); head.addView(new Led(context, Buttons.skin().accentGo, true), @@ -540,6 +545,7 @@ private void buildConsole(Context context) { projects = new FlowLayout(context, 0); projects.setPadding(buttons.dp(6), buttons.dp(6), buttons.dp(6), buttons.dp(6)); ScrollView projectScroll = elasticScroll(context); + projectScroll.setBackground(buttons.display()); projectScroll.addView(projects, new ScrollView.LayoutParams( LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); LinearLayout projectBox = captioned(context, "projects", projectScroll); @@ -601,6 +607,7 @@ private View consoleScreen(Context context) { rows = new FlowLayout(context, 0); rows.setPadding(buttons.dp(6), buttons.dp(6), buttons.dp(6), buttons.dp(6)); ScrollView scroll = elasticScroll(context); + scroll.setBackground(buttons.display()); scroll.addView(rows, new ScrollView.LayoutParams( LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); return scroll; @@ -627,7 +634,6 @@ protected void onMeasure(int widthSpec, int heightSpec) { buttons.dp(230), MeasureSpec.AT_MOST)); } }; - scroll.setBackground(buttons.squareDisplay()); return scroll; } @@ -898,6 +904,13 @@ protected void onMeasure(int widthSpec, int heightSpec) { String hint = action.isNull("hint") ? null : action.optString("hint"); if (hint != null && !hint.isEmpty()) row.setContentDescription(hint); + // The listener goes on the row, not on the text. + // + // It was on the text and never fired: the row is what is clickable, so the + // row is what the ray hovers, and a TextView inside it that is neither + // clickable nor focusable is not asked about hover at all. Same effect, + // one level up — the row hears it and turns up the beam on its own label. + buttons.glowOnHover(row, title, 0xFF39D863); row.setOnClickListener(v -> host.onAction(send, enter)); return row; } From b478501c77e1c8c4446d4df1de55e04acdb8bd26 Mon Sep 17 00:00:00 2001 From: butschster Date: Sat, 8 Aug 2026 13:33:25 +0400 Subject: [PATCH 07/15] Enter is seated in the plate, and the return mark stops being a blob MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **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. --- .../dev/butschster/linuxterminal/Buttons.java | 4 +- .../butschster/linuxterminal/ContextBar.java | 17 +++++++ .../dev/butschster/linuxterminal/Glyphs.java | 43 +++++++++++----- .../butschster/linuxterminal/LaunchKey.java | 51 +++++++++++++++---- 4 files changed, 92 insertions(+), 23 deletions(-) diff --git a/client/app/src/main/java/dev/butschster/linuxterminal/Buttons.java b/client/app/src/main/java/dev/butschster/linuxterminal/Buttons.java index 6dc680f..d4b0412 100644 --- a/client/app/src/main/java/dev/butschster/linuxterminal/Buttons.java +++ b/client/app/src/main/java/dev/butschster/linuxterminal/Buttons.java @@ -331,7 +331,9 @@ public Drawable section(boolean fixed) { */ public TextView launchKey(String label, String hint, View.OnClickListener onClick) { TextView view = key(label, ENTER, hint, onClick); - view.setMinHeight(dp(minHeight) + dp(10)); + // Taller than a key, because the collar eats into it from both sides and + // the cap inside must still be a comfortable target. + view.setMinHeight(dp(minHeight) + dp(22)); view.setLetterSpacing(0.08f); StateListDrawable states = new StateListDrawable(); diff --git a/client/app/src/main/java/dev/butschster/linuxterminal/ContextBar.java b/client/app/src/main/java/dev/butschster/linuxterminal/ContextBar.java index 5ed12f3..b4da618 100644 --- a/client/app/src/main/java/dev/butschster/linuxterminal/ContextBar.java +++ b/client/app/src/main/java/dev/butschster/linuxterminal/ContextBar.java @@ -544,8 +544,11 @@ private void buildConsole(Context context) { projects = new FlowLayout(context, 0); projects.setPadding(buttons.dp(6), buttons.dp(6), buttons.dp(6), buttons.dp(6)); + projects.setClipChildren(false); + projects.setClipToPadding(false); ScrollView projectScroll = elasticScroll(context); projectScroll.setBackground(buttons.display()); + projectScroll.setClipChildren(false); projectScroll.addView(projects, new ScrollView.LayoutParams( LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); LinearLayout projectBox = captioned(context, "projects", projectScroll); @@ -606,8 +609,16 @@ private void buildConsole(Context context) { private View consoleScreen(Context context) { rows = new FlowLayout(context, 0); rows.setPadding(buttons.dp(6), buttons.dp(6), buttons.dp(6), buttons.dp(6)); + // A bloom is drawn outside the letter, so every container between the text + // and the screen has to stop clipping — otherwise the glow ends in a hard + // vertical line at the cell's edge, which is what a lit row was doing here + // while the same effect looked right on the other skins, where the row is + // the full width of the screen and the clip falls outside the glow. + rows.setClipChildren(false); + rows.setClipToPadding(false); ScrollView scroll = elasticScroll(context); scroll.setBackground(buttons.display()); + scroll.setClipChildren(false); scroll.addView(rows, new ScrollView.LayoutParams( LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); return scroll; @@ -881,6 +892,8 @@ protected void onMeasure(int widthSpec, int heightSpec) { row.setGravity(Gravity.CENTER_VERTICAL); row.setPadding(buttons.dp(8), buttons.dp(4), buttons.dp(8), buttons.dp(4)); row.setClickable(true); + row.setClipChildren(false); + row.setClipToPadding(false); row.setBackground(buttons.rowSelection()); TextView mark = label(up ? "\u2191" : "\u25b8", Buttons.skin().phosphorDim, 11); @@ -892,6 +905,10 @@ protected void onMeasure(int widthSpec, int heightSpec) { title.setTypeface(Fonts.mono(getContext())); title.setSingleLine(true); title.setEllipsize(TextUtils.TruncateAt.MIDDLE); + // Room inside the view for the glow to land in. Clipping is off above, but + // a TextView still paints its shadow within its own bounds, so without the + // padding the spread has nowhere to go on the side the name ends at. + title.setPadding(0, buttons.dp(3), buttons.dp(8), buttons.dp(3)); LayoutParams grow = new LayoutParams(0, LayoutParams.WRAP_CONTENT, 1f); grow.setMarginStart(buttons.dp(8)); row.addView(title, grow); diff --git a/client/app/src/main/java/dev/butschster/linuxterminal/Glyphs.java b/client/app/src/main/java/dev/butschster/linuxterminal/Glyphs.java index 751aeb8..b95929c 100644 --- a/client/app/src/main/java/dev/butschster/linuxterminal/Glyphs.java +++ b/client/app/src/main/java/dev/butschster/linuxterminal/Glyphs.java @@ -197,27 +197,44 @@ private void drawBackspace(Canvas canvas, Rect bounds) { * KeyPad (a context action marked "runs on press") that still typed a * Unicode glyph instead of drawing one. */ + /** + * The return mark: a stem down the right, a bar left along the bottom, + * and an arrowhead on the end of the bar. + * + *

Rewritten because the first version was unreadable at this size and + * the reason is worth keeping. Its arrowhead was built from two arms of + * different lengths meeting at a shallow angle, so at a stroke width of an + * eighth of the box the two nearly merged and the tip filled in — a blob + * with a tail rather than an arrow. Here the arms are the same length and + * meet at a right angle, symmetric about the bar, which is the one shape + * that stays an arrow when the ink spreads: the notch between the arms + * cannot close while they are 90° apart. + * + *

The bar sits an arm's length above the bottom rather than on it, so + * the lower arm has somewhere to go. Everything is proportional to the box, + * so the mark holds together at any size the bar asks for. + */ private void drawEnter(Canvas canvas, Rect bounds) { float half = paint.getStrokeWidth() / 2f; - float left = bounds.left + half + bounds.width() * 0.16f; - float right = bounds.right - half - bounds.width() * 0.16f; - float top = bounds.top + half + bounds.height() * 0.18f; - float bottom = bounds.bottom - half - bounds.height() * 0.18f; - float hookX = left + (right - left) * 0.32f; + float left = bounds.left + half + bounds.width() * 0.14f; + float right = bounds.right - half - bounds.width() * 0.14f; + float top = bounds.top + half + bounds.height() * 0.14f; + float bottom = bounds.bottom - half - bounds.height() * 0.14f; + + float arm = (bottom - top) * 0.28f; + float barY = bottom - arm; + float tipX = left + arm; Path stem = new Path(); stem.moveTo(right, top); - stem.lineTo(right, bottom); - stem.lineTo(hookX, bottom); + stem.lineTo(right, barY); + stem.lineTo(tipX, barY); canvas.drawPath(stem, paint); - float armX = hookX + (right - hookX) * 0.42f; - float armTopY = top + (bottom - top) * 0.30f; - float armBottomY = bottom - (bottom - top) * 0.05f; Path head = new Path(); - head.moveTo(armX, armTopY); - head.lineTo(hookX, bottom); - head.lineTo(armX, armBottomY); + head.moveTo(tipX + arm, barY - arm); + head.lineTo(tipX, barY); + head.lineTo(tipX + arm, barY + arm); canvas.drawPath(head, paint); } diff --git a/client/app/src/main/java/dev/butschster/linuxterminal/LaunchKey.java b/client/app/src/main/java/dev/butschster/linuxterminal/LaunchKey.java index 9be4282..58b6e53 100644 --- a/client/app/src/main/java/dev/butschster/linuxterminal/LaunchKey.java +++ b/client/app/src/main/java/dev/butschster/linuxterminal/LaunchKey.java @@ -48,6 +48,8 @@ public class LaunchKey extends Drawable { private final Paint groove = new Paint(Paint.ANTI_ALIAS_FLAG); private final Paint rim = new Paint(Paint.ANTI_ALIAS_FLAG); private final Paint glow = new Paint(Paint.ANTI_ALIAS_FLAG); + private final Paint collarWall = new Paint(Paint.ANTI_ALIAS_FLAG); + private final Paint knurl = new Paint(Paint.ANTI_ALIAS_FLAG); private final RectF housing = new RectF(); private final RectF cap = new RectF(); @@ -68,6 +70,8 @@ public LaunchKey(int face, int accent, float depthPx, boolean pressed) { groove.setStyle(Paint.Style.STROKE); rim.setStyle(Paint.Style.STROKE); glow.setStyle(Paint.Style.STROKE); + collarWall.setStyle(Paint.Style.STROKE); + knurl.setStyle(Paint.Style.STROKE); } @Override @@ -75,17 +79,24 @@ protected void onBoundsChange(Rect bounds) { housing.set(bounds.left + 0.5f, bounds.top + 0.5f, bounds.right - 0.5f, bounds.bottom - 0.5f); - float inset = 3f; + // A deep collar rather than a hairline bezel: this is a button seated in a + // hole cut through the panel, the way a launch or arm control is mounted. + // The collar is what your eye reads as thickness of plate, so it has to be + // several pixels, not one. + float collar = 7f; float stand = pressed ? Math.min(depth, 1f) : depth; - cap.set(housing.left + inset, housing.top + inset, - housing.right - inset, housing.bottom - inset - depth); + cap.set(housing.left + collar, housing.top + collar, + housing.right - collar, housing.bottom - collar - depth); extrusion.set(cap.left, cap.top + stand, cap.right, cap.bottom + stand); - // Half the cap's height: round ends, straight middle. - radius = cap.height() / 2f; + // Square-shouldered, not a capsule. A capsule reads as a bar key — a wide + // thing you slap — and this is the opposite claim: one deliberate control, + // seated, pressed once. Its corners are the panel's own radius so it is + // still made of the same material as everything else. + radius = 9f; - inlay.set(cap.left + 5f, cap.top + 4f, cap.right - 5f, cap.bottom - 4f); - inlayRadius = Math.max(2f, inlay.height() / 2f); + inlay.set(cap.left + 7f, cap.top + 6f, cap.right - 7f, cap.bottom - 6f); + inlayRadius = Math.max(2f, radius - 3f); body.setShader(new LinearGradient(0, cap.top, 0, cap.bottom, pressed @@ -98,12 +109,22 @@ protected void onBoundsChange(Rect bounds) { groove.setShader(new LinearGradient(0, inlay.top, 0, inlay.bottom, new int[]{0x8C000000, 0x1AFFFFFF}, new float[]{0f, 1f}, Shader.TileMode.CLAMP)); + + collarWall.setShader(new LinearGradient(0, housing.top, 0, housing.bottom, + new int[]{0xCC000000, Color.TRANSPARENT, 0x0FFFFFFF}, + new float[]{0f, 0.4f, 1f}, Shader.TileMode.CLAMP)); } @Override public void draw(Canvas canvas) { - // The bezel it is mounted in. - canvas.drawRoundRect(housing, radius + 3f, radius + 3f, bezel); + // The hole in the plate, and the shaded wall of it: the same recess this + // panel draws everywhere, at the size a mounted control needs. + canvas.drawRoundRect(housing, radius + 7f, radius + 7f, bezel); + canvas.save(); + canvas.clipRect(housing); + collarWall.setStrokeWidth(6f); + canvas.drawRoundRect(housing, radius + 7f, radius + 7f, collarWall); + canvas.restore(); if (!pressed) { canvas.drawRoundRect(extrusion, radius, radius, solid); @@ -114,6 +135,18 @@ public void draw(Canvas canvas) { groove.setStrokeWidth(1.5f); canvas.drawRoundRect(inlay, inlayRadius, inlayRadius, groove); + // Knurling down both shoulders — the grip a guarded control has so a glove + // finds it. Four short strokes a side, inside the groove, at the same + // alpha as the bevel so it reads as moulded rather than printed. + knurl.setStrokeWidth(1f); + float step = cap.height() / 5f; + for (int i = 1; i <= 4; i++) { + float y = cap.top + step * i; + knurl.setColor(0x1AFFFFFF); + canvas.drawLine(inlay.left + 3f, y, inlay.left + 9f, y, knurl); + canvas.drawLine(inlay.right - 9f, y, inlay.right - 3f, y, knurl); + } + // Lit at rest, because this is the key the panel is built around. Two // rings rather than a blur, and quiet: it marks the primary control, it // does not shout over the legend on it. From 0c5dbbf2758ba7d988760991a3f7c680211c577b Mon Sep 17 00:00:00 2001 From: butschster Date: Sat, 8 Aug 2026 13:40:45 +0400 Subject: [PATCH 08/15] Enter is marked like the control it is MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../dev/butschster/linuxterminal/Buttons.java | 68 +++++--- .../butschster/linuxterminal/HazardFace.java | 157 ++++++++++++++++++ 2 files changed, 202 insertions(+), 23 deletions(-) create mode 100644 client/app/src/main/java/dev/butschster/linuxterminal/HazardFace.java diff --git a/client/app/src/main/java/dev/butschster/linuxterminal/Buttons.java b/client/app/src/main/java/dev/butschster/linuxterminal/Buttons.java index d4b0412..f5156dc 100644 --- a/client/app/src/main/java/dev/butschster/linuxterminal/Buttons.java +++ b/client/app/src/main/java/dev/butschster/linuxterminal/Buttons.java @@ -329,31 +329,53 @@ public Drawable section(boolean fixed) { * The launch key: Enter, drawn as the one control that is not a member of the * grid. See {@link LaunchKey} for why it earns a shape of its own. */ - public TextView launchKey(String label, String hint, View.OnClickListener onClick) { - TextView view = key(label, ENTER, hint, onClick); - // Taller than a key, because the collar eats into it from both sides and - // the cap inside must still be a comfortable target. - view.setMinHeight(dp(minHeight) + dp(22)); - view.setLetterSpacing(0.08f); - + public View launchKey(String label, String hint, View.OnClickListener onClick) { + // A striped bar rather than another cap, and a container rather than a + // single view: the legend sits on its own dark plate, the way a marked + // control carries its label on a patch so the banding does not run through + // the letters. One TextView cannot have two backgrounds. + LinearLayout bar = new LinearLayout(context); + bar.setOrientation(LinearLayout.HORIZONTAL); + bar.setGravity(Gravity.CENTER); + bar.setMinimumHeight(dp(minHeight) + dp(18)); + bar.setClickable(true); + bar.setFocusable(false); + + float radius = dp(7); + float depth = dp(5); StateListDrawable states = new StateListDrawable(); - float depth = dp(Math.max(2, skin.depthDp)); - int fill = FILL[ENTER]; states.addState(new int[]{android.R.attr.state_pressed}, - new LaunchKey(fill, skin.accentGo, depth, true)); - states.addState(new int[]{android.R.attr.state_hovered}, - new LaunchKey(lighten(fill, 0.10f), skin.accentGo, depth, false)); - states.addState(new int[]{}, new LaunchKey(fill, skin.accentGo, depth, false)); - view.setBackground(states); - - // The mark every terminal puts on this key, drawn rather than typed so it - // matches the arrows beside it instead of arriving from whatever font the - // system hands back for the code point. - view.setCompoundDrawablesWithIntrinsicBounds( - Glyphs.drawable(context, Glyphs.Kind.ENTER, skin.legend[ENTER], - iconSizeDp() + 2), null, null, null); - view.setCompoundDrawablePadding(dp(10)); - return view; + new HazardFace(radius, depth, true)); + states.addState(new int[]{}, new HazardFace(radius, depth, false)); + bar.setBackground(states); + travel(bar); + debounced(bar, onClick); + if (hint != null && !hint.isEmpty()) bar.setContentDescription(hint); + + TextView plate = new TextView(context); + plate.setText(label.toUpperCase(java.util.Locale.ROOT)); + plate.setTextColor(0xFFFFDB7A); + plate.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 13f); + plate.setTypeface(Fonts.mono(context)); + plate.setLetterSpacing(0.18f); + plate.setSingleLine(true); + plate.setGravity(Gravity.CENTER); + plate.setPadding(dp(14), dp(5), dp(14), dp(5)); + + GradientDrawable patch = new GradientDrawable(); + patch.setColor(0xD1080A06); + patch.setCornerRadius(dp(4)); + plate.setBackground(patch); + + plate.setCompoundDrawablesWithIntrinsicBounds( + Glyphs.drawable(context, Glyphs.Kind.ENTER, 0xFFFFDB7A, iconSizeDp()), + null, null, null); + plate.setCompoundDrawablePadding(dp(9)); + + bar.addView(plate, new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.WRAP_CONTENT, + LinearLayout.LayoutParams.WRAP_CONTENT)); + return bar; } /** diff --git a/client/app/src/main/java/dev/butschster/linuxterminal/HazardFace.java b/client/app/src/main/java/dev/butschster/linuxterminal/HazardFace.java new file mode 100644 index 0000000..b0af585 --- /dev/null +++ b/client/app/src/main/java/dev/butschster/linuxterminal/HazardFace.java @@ -0,0 +1,157 @@ +package dev.butschster.linuxterminal; + +import android.graphics.Bitmap; +import android.graphics.BitmapShader; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.ColorFilter; +import android.graphics.LinearGradient; +import android.graphics.Paint; +import android.graphics.PixelFormat; +import android.graphics.Rect; +import android.graphics.RectF; +import android.graphics.Shader; +import android.graphics.drawable.Drawable; + +/** + * The hazard-striped face: diagonal yellow and near-black bands, the marking a + * machine puts on the control that does the irreversible thing. + * + *

It is the loudest object on the panel and that is the point. Everything else + * is graphite with a coloured edge, so a striped bar is not a louder version of a + * key — it is a different category of thing, which is exactly the claim Enter + * needs to make. The stripes are also the one pattern here that survives being + * looked at indirectly: you can find it in peripheral vision, which no legend can. + * + *

The bands are generated into a small tile and repeated by a shader rather + * than drawn as rotated rectangles. A tile costs one allocation and tiles exactly; + * rotated rectangles have to be over-drawn past the corners and clipped, and the + * clip is where the diagonal ends up with a stair-stepped edge. + * + *

Pressed, the cap travels into the plate and the extrusion under it collapses + * — the same description every other key on this panel uses. A control that + * announces itself differently should still behave like the rest. + */ +public class HazardFace extends Drawable { + + /** -45°, twelve pixels of each band: the reference sheet's own numbers. */ + private static final int BAND = 12; + private static final int TILE = BAND * 2; + + private static final int YELLOW = 0xFFF2B613; + private static final int DARK = 0xFF1A1508; + private static final int EDGE = 0xFF8A6A12; + private static final int FOOT = 0xFF4A3A08; + + private static Bitmap stripes; + + private final float radius; + private final float depth; + private final boolean pressed; + + private final Paint body = new Paint(Paint.ANTI_ALIAS_FLAG); + private final Paint solid = new Paint(Paint.ANTI_ALIAS_FLAG); + private final Paint ambient = new Paint(Paint.ANTI_ALIAS_FLAG); + private final Paint sheen = new Paint(Paint.ANTI_ALIAS_FLAG); + private final Paint edge = new Paint(Paint.ANTI_ALIAS_FLAG); + private final Paint inner = new Paint(Paint.ANTI_ALIAS_FLAG); + + private final RectF cap = new RectF(); + private final RectF extrusion = new RectF(); + + public HazardFace(float radiusPx, float depthPx, boolean pressed) { + this.radius = radiusPx; + this.depth = depthPx; + this.pressed = pressed; + + body.setShader(tile()); + solid.setColor(FOOT); + sheen.setStyle(Paint.Style.STROKE); + edge.setStyle(Paint.Style.STROKE); + edge.setColor(EDGE); + inner.setStyle(Paint.Style.STROKE); + } + + /** One tile of banding, made once for the life of the process. */ + private static synchronized Shader tile() { + if (stripes == null) { + stripes = Bitmap.createBitmap(TILE, TILE, Bitmap.Config.ARGB_8888); + int[] pixels = new int[TILE * TILE]; + for (int y = 0; y < TILE; y++) { + for (int x = 0; x < TILE; x++) { + // (x - y) mod period leans the bands to the left, which is the + // direction -45deg means; (x + y) would lean them the other way + // and read as a different marking entirely. + int band = Math.floorMod(x - y, TILE); + pixels[y * TILE + x] = band < BAND ? YELLOW : DARK; + } + } + stripes.setPixels(pixels, 0, TILE, 0, 0, TILE, TILE); + } + return new BitmapShader(stripes, Shader.TileMode.REPEAT, Shader.TileMode.REPEAT); + } + + @Override + protected void onBoundsChange(Rect bounds) { + float stand = pressed ? 1f : depth; + cap.set(bounds.left + 0.5f, bounds.top + 0.5f, + bounds.right - 0.5f, bounds.bottom - depth - 0.5f); + extrusion.set(cap.left, cap.top + stand, cap.right, cap.bottom + stand); + + // inset 0 1px 0 rgba(255,255,255,.3) — a hard lit line along the top edge, + // which is what a moulded plastic bar has where the mould parts. + sheen.setShader(new LinearGradient(0, cap.top, 0, cap.top + 2f, + new int[]{0x4DFFFFFF, Color.TRANSPARENT}, + new float[]{0f, 1f}, Shader.TileMode.CLAMP)); + + inner.setShader(new LinearGradient(0, cap.top, 0, cap.top + depth * 2f, + new int[]{0x80000000, Color.TRANSPARENT}, + new float[]{0f, 1f}, Shader.TileMode.CLAMP)); + } + + @Override + public void draw(Canvas canvas) { + if (!pressed) { + for (int i = 3; i >= 1; i--) { + ambient.setColor(Color.argb(22, 0, 0, 0)); + RectF soft = new RectF(extrusion); + soft.inset(-i * 1.5f, -i * 1.5f); + soft.offset(0, i * 1.4f); + canvas.drawRoundRect(soft, radius + i, radius + i, ambient); + } + canvas.drawRoundRect(extrusion, radius, radius, solid); + } + + canvas.drawRoundRect(cap, radius, radius, body); + + if (pressed) { + canvas.save(); + canvas.clipRect(cap); + inner.setStrokeWidth(depth); + canvas.drawRoundRect(cap, radius, radius, inner); + canvas.restore(); + } else { + canvas.save(); + canvas.clipRect(cap); + sheen.setStrokeWidth(2f); + canvas.drawRoundRect(cap, radius, radius, sheen); + canvas.restore(); + } + + edge.setStrokeWidth(1f); + canvas.drawRoundRect(cap, radius, radius, edge); + } + + @Override + public void setAlpha(int alpha) { + } + + @Override + public void setColorFilter(ColorFilter filter) { + } + + @Override + public int getOpacity() { + return PixelFormat.OPAQUE; + } +} From 598c8821be5b93255c406bde2647d7903b3deb2e Mon Sep 17 00:00:00 2001 From: butschster Date: Sat, 8 Aug 2026 13:43:47 +0400 Subject: [PATCH 09/15] Enter lies in a milled slot, sized to itself MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../dev/butschster/linuxterminal/Buttons.java | 122 +++++++++----- .../butschster/linuxterminal/HazardFace.java | 157 ------------------ .../butschster/linuxterminal/SlotWell.java | 85 ++++++++++ 3 files changed, 163 insertions(+), 201 deletions(-) delete mode 100644 client/app/src/main/java/dev/butschster/linuxterminal/HazardFace.java create mode 100644 client/app/src/main/java/dev/butschster/linuxterminal/SlotWell.java diff --git a/client/app/src/main/java/dev/butschster/linuxterminal/Buttons.java b/client/app/src/main/java/dev/butschster/linuxterminal/Buttons.java index f5156dc..bca7f69 100644 --- a/client/app/src/main/java/dev/butschster/linuxterminal/Buttons.java +++ b/client/app/src/main/java/dev/butschster/linuxterminal/Buttons.java @@ -330,52 +330,86 @@ public Drawable section(boolean fixed) { * grid. See {@link LaunchKey} for why it earns a shape of its own. */ public View launchKey(String label, String hint, View.OnClickListener onClick) { - // A striped bar rather than another cap, and a container rather than a - // single view: the legend sits on its own dark plate, the way a marked - // control carries its label on a patch so the banding does not run through - // the letters. One TextView cannot have two backgrounds. - LinearLayout bar = new LinearLayout(context); - bar.setOrientation(LinearLayout.HORIZONTAL); - bar.setGravity(Gravity.CENTER); - bar.setMinimumHeight(dp(minHeight) + dp(18)); - bar.setClickable(true); - bar.setFocusable(false); - - float radius = dp(7); - float depth = dp(5); - StateListDrawable states = new StateListDrawable(); - states.addState(new int[]{android.R.attr.state_pressed}, - new HazardFace(radius, depth, true)); - states.addState(new int[]{}, new HazardFace(radius, depth, false)); - bar.setBackground(states); - travel(bar); - debounced(bar, onClick); - if (hint != null && !hint.isEmpty()) bar.setContentDescription(hint); - - TextView plate = new TextView(context); - plate.setText(label.toUpperCase(java.util.Locale.ROOT)); - plate.setTextColor(0xFFFFDB7A); - plate.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 13f); - plate.setTypeface(Fonts.mono(context)); - plate.setLetterSpacing(0.18f); - plate.setSingleLine(true); - plate.setGravity(Gravity.CENTER); - plate.setPadding(dp(14), dp(5), dp(14), dp(5)); - - GradientDrawable patch = new GradientDrawable(); - patch.setColor(0xD1080A06); - patch.setCornerRadius(dp(4)); - plate.setBackground(patch); - - plate.setCompoundDrawablesWithIntrinsicBounds( - Glyphs.drawable(context, Glyphs.Kind.ENTER, 0xFFFFDB7A, iconSizeDp()), - null, null, null); - plate.setCompoundDrawablePadding(dp(9)); - - bar.addView(plate, new LinearLayout.LayoutParams( + // A key lying in a milled pocket rather than standing on the plate, and + // sized to itself rather than to the row: this is the one control on the + // panel you press into the surface, and a bar spanning the width would say + // the opposite — that it is there to be swept at. + LinearLayout centred = new LinearLayout(context); + centred.setOrientation(LinearLayout.HORIZONTAL); + centred.setGravity(Gravity.CENTER); + + LinearLayout well = new LinearLayout(context); + well.setOrientation(LinearLayout.HORIZONTAL); + well.setGravity(Gravity.CENTER); + well.setBackground(new SlotWell(dp(10))); + int pad = dp(8); + well.setPadding(pad, pad, pad, pad); + well.setClickable(true); + well.setFocusable(false); + debounced(well, onClick); + if (hint != null && !hint.isEmpty()) well.setContentDescription(hint); + + TextView face = new TextView(context); + face.setText(label.toUpperCase(java.util.Locale.ROOT)); + face.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 13f); + face.setTypeface(Fonts.mono(context)); + face.setLetterSpacing(0.16f); + face.setSingleLine(true); + face.setGravity(Gravity.CENTER); + face.setPadding(dp(20), dp(9), dp(18), dp(9)); + face.setMinWidth(dp(150)); + face.setDuplicateParentStateEnabled(true); + face.setBackground(slotFace()); + face.setTextColor(new android.content.res.ColorStateList( + new int[][]{{android.R.attr.state_pressed}, {android.R.attr.state_hovered}, {}}, + new int[]{0xFFBAFFCF, 0xFFD9ECFF, 0xFF9DB6CF})); + + // The mark goes after the word, where a key that commits puts it: you read + // what it does, then the sign that it fires. + face.setCompoundDrawablesWithIntrinsicBounds(null, null, + Glyphs.drawable(context, Glyphs.Kind.ENTER, 0xFF9DB6CF, iconSizeDp()), null); + face.setCompoundDrawablePadding(dp(10)); + + well.addView(face, new LinearLayout.LayoutParams( LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT)); - return bar; + centred.addView(well, new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.WRAP_CONTENT, + LinearLayout.LayoutParams.WRAP_CONTENT)); + return centred; + } + + /** + * The key lying in the slot: a flat cap with a two-pixel foot, brightening its + * border under the ray and turning green as it goes down. + */ + private StateListDrawable slotFace() { + StateListDrawable states = new StateListDrawable(); + states.addState(new int[]{android.R.attr.state_pressed}, + slotCap(0xFF161E29, 0x8036E37B)); + states.addState(new int[]{android.R.attr.state_hovered}, + slotCap(0xFF1F2937, 0x8046B4FF)); + states.addState(new int[]{}, slotCap(0xFF1D2634, 0xFF2B3644)); + return states; + } + + private LayerDrawable slotCap(int fill, int border) { + GradientDrawable foot = new GradientDrawable(); + foot.setColor(0xFF070B10); + foot.setCornerRadius(dp(6)); + + GradientDrawable cap = new GradientDrawable(); + cap.setOrientation(GradientDrawable.Orientation.TOP_BOTTOM); + cap.setColors(new int[]{fill, shade(fill, -8)}); + cap.setCornerRadius(dp(6)); + cap.setStroke(dp(1), border); + + LayerDrawable stack = new LayerDrawable(new Drawable[]{foot, cap}); + // Two pixels of foot showing beneath, as the sheet has it — enough to say + // the cap is a separate part lying in the pocket rather than painted on + // the bottom of it. + stack.setLayerInset(1, 0, 0, 0, 2); + return stack; } /** diff --git a/client/app/src/main/java/dev/butschster/linuxterminal/HazardFace.java b/client/app/src/main/java/dev/butschster/linuxterminal/HazardFace.java deleted file mode 100644 index b0af585..0000000 --- a/client/app/src/main/java/dev/butschster/linuxterminal/HazardFace.java +++ /dev/null @@ -1,157 +0,0 @@ -package dev.butschster.linuxterminal; - -import android.graphics.Bitmap; -import android.graphics.BitmapShader; -import android.graphics.Canvas; -import android.graphics.Color; -import android.graphics.ColorFilter; -import android.graphics.LinearGradient; -import android.graphics.Paint; -import android.graphics.PixelFormat; -import android.graphics.Rect; -import android.graphics.RectF; -import android.graphics.Shader; -import android.graphics.drawable.Drawable; - -/** - * The hazard-striped face: diagonal yellow and near-black bands, the marking a - * machine puts on the control that does the irreversible thing. - * - *

It is the loudest object on the panel and that is the point. Everything else - * is graphite with a coloured edge, so a striped bar is not a louder version of a - * key — it is a different category of thing, which is exactly the claim Enter - * needs to make. The stripes are also the one pattern here that survives being - * looked at indirectly: you can find it in peripheral vision, which no legend can. - * - *

The bands are generated into a small tile and repeated by a shader rather - * than drawn as rotated rectangles. A tile costs one allocation and tiles exactly; - * rotated rectangles have to be over-drawn past the corners and clipped, and the - * clip is where the diagonal ends up with a stair-stepped edge. - * - *

Pressed, the cap travels into the plate and the extrusion under it collapses - * — the same description every other key on this panel uses. A control that - * announces itself differently should still behave like the rest. - */ -public class HazardFace extends Drawable { - - /** -45°, twelve pixels of each band: the reference sheet's own numbers. */ - private static final int BAND = 12; - private static final int TILE = BAND * 2; - - private static final int YELLOW = 0xFFF2B613; - private static final int DARK = 0xFF1A1508; - private static final int EDGE = 0xFF8A6A12; - private static final int FOOT = 0xFF4A3A08; - - private static Bitmap stripes; - - private final float radius; - private final float depth; - private final boolean pressed; - - private final Paint body = new Paint(Paint.ANTI_ALIAS_FLAG); - private final Paint solid = new Paint(Paint.ANTI_ALIAS_FLAG); - private final Paint ambient = new Paint(Paint.ANTI_ALIAS_FLAG); - private final Paint sheen = new Paint(Paint.ANTI_ALIAS_FLAG); - private final Paint edge = new Paint(Paint.ANTI_ALIAS_FLAG); - private final Paint inner = new Paint(Paint.ANTI_ALIAS_FLAG); - - private final RectF cap = new RectF(); - private final RectF extrusion = new RectF(); - - public HazardFace(float radiusPx, float depthPx, boolean pressed) { - this.radius = radiusPx; - this.depth = depthPx; - this.pressed = pressed; - - body.setShader(tile()); - solid.setColor(FOOT); - sheen.setStyle(Paint.Style.STROKE); - edge.setStyle(Paint.Style.STROKE); - edge.setColor(EDGE); - inner.setStyle(Paint.Style.STROKE); - } - - /** One tile of banding, made once for the life of the process. */ - private static synchronized Shader tile() { - if (stripes == null) { - stripes = Bitmap.createBitmap(TILE, TILE, Bitmap.Config.ARGB_8888); - int[] pixels = new int[TILE * TILE]; - for (int y = 0; y < TILE; y++) { - for (int x = 0; x < TILE; x++) { - // (x - y) mod period leans the bands to the left, which is the - // direction -45deg means; (x + y) would lean them the other way - // and read as a different marking entirely. - int band = Math.floorMod(x - y, TILE); - pixels[y * TILE + x] = band < BAND ? YELLOW : DARK; - } - } - stripes.setPixels(pixels, 0, TILE, 0, 0, TILE, TILE); - } - return new BitmapShader(stripes, Shader.TileMode.REPEAT, Shader.TileMode.REPEAT); - } - - @Override - protected void onBoundsChange(Rect bounds) { - float stand = pressed ? 1f : depth; - cap.set(bounds.left + 0.5f, bounds.top + 0.5f, - bounds.right - 0.5f, bounds.bottom - depth - 0.5f); - extrusion.set(cap.left, cap.top + stand, cap.right, cap.bottom + stand); - - // inset 0 1px 0 rgba(255,255,255,.3) — a hard lit line along the top edge, - // which is what a moulded plastic bar has where the mould parts. - sheen.setShader(new LinearGradient(0, cap.top, 0, cap.top + 2f, - new int[]{0x4DFFFFFF, Color.TRANSPARENT}, - new float[]{0f, 1f}, Shader.TileMode.CLAMP)); - - inner.setShader(new LinearGradient(0, cap.top, 0, cap.top + depth * 2f, - new int[]{0x80000000, Color.TRANSPARENT}, - new float[]{0f, 1f}, Shader.TileMode.CLAMP)); - } - - @Override - public void draw(Canvas canvas) { - if (!pressed) { - for (int i = 3; i >= 1; i--) { - ambient.setColor(Color.argb(22, 0, 0, 0)); - RectF soft = new RectF(extrusion); - soft.inset(-i * 1.5f, -i * 1.5f); - soft.offset(0, i * 1.4f); - canvas.drawRoundRect(soft, radius + i, radius + i, ambient); - } - canvas.drawRoundRect(extrusion, radius, radius, solid); - } - - canvas.drawRoundRect(cap, radius, radius, body); - - if (pressed) { - canvas.save(); - canvas.clipRect(cap); - inner.setStrokeWidth(depth); - canvas.drawRoundRect(cap, radius, radius, inner); - canvas.restore(); - } else { - canvas.save(); - canvas.clipRect(cap); - sheen.setStrokeWidth(2f); - canvas.drawRoundRect(cap, radius, radius, sheen); - canvas.restore(); - } - - edge.setStrokeWidth(1f); - canvas.drawRoundRect(cap, radius, radius, edge); - } - - @Override - public void setAlpha(int alpha) { - } - - @Override - public void setColorFilter(ColorFilter filter) { - } - - @Override - public int getOpacity() { - return PixelFormat.OPAQUE; - } -} diff --git a/client/app/src/main/java/dev/butschster/linuxterminal/SlotWell.java b/client/app/src/main/java/dev/butschster/linuxterminal/SlotWell.java new file mode 100644 index 0000000..6e5fa59 --- /dev/null +++ b/client/app/src/main/java/dev/butschster/linuxterminal/SlotWell.java @@ -0,0 +1,85 @@ +package dev.butschster.linuxterminal; + +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.ColorFilter; +import android.graphics.LinearGradient; +import android.graphics.Paint; +import android.graphics.PixelFormat; +import android.graphics.Rect; +import android.graphics.RectF; +import android.graphics.Shader; +import android.graphics.drawable.Drawable; + +/** + * The slot Enter is sunk into: a shallow pocket milled in the plate, with the key + * lying inside it. + * + *

Every other control on this panel stands proud of the surface. This one is + * the opposite, and the inversion is the point — a key you press into the + * plate rather than one that stands up to meet you reads as deliberate, the way a + * recessed switch on real equipment means "not by accident". It costs nothing in + * target size, because the pocket is only a few pixels wider than what it holds. + * + *

Drawn from the same description as every other recess here: shadow along the + * top wall where the plate overhangs, a hairline of light along the bottom where + * the lower wall catches it. One lamp, above and in front, for the whole panel. + */ +public class SlotWell extends Drawable { + + private final float radius; + + private final Paint body = new Paint(Paint.ANTI_ALIAS_FLAG); + private final Paint wall = new Paint(Paint.ANTI_ALIAS_FLAG); + private final Paint edge = new Paint(Paint.ANTI_ALIAS_FLAG); + + private final RectF face = new RectF(); + + public SlotWell(float radiusPx) { + this.radius = radiusPx; + wall.setStyle(Paint.Style.STROKE); + edge.setStyle(Paint.Style.STROKE); + edge.setStrokeWidth(1f); + edge.setColor(0xFF232D3A); + } + + @Override + protected void onBoundsChange(Rect bounds) { + face.set(bounds.left + 0.5f, bounds.top + 0.5f, + bounds.right - 0.5f, bounds.bottom - 0.5f); + + // linear-gradient(180deg,#0a0f16,#060a0e) + body.setShader(new LinearGradient(0, face.top, 0, face.bottom, + 0xFF0A0F16, 0xFF060A0E, Shader.TileMode.CLAMP)); + + // inset 0 5px 12px rgba(0,0,0,.95) over the top, and the .03 white line + // along the bottom. + wall.setShader(new LinearGradient(0, face.top, 0, face.bottom, + new int[]{0xF2000000, Color.TRANSPARENT, 0x08FFFFFF}, + new float[]{0f, 0.42f, 1f}, Shader.TileMode.CLAMP)); + } + + @Override + public void draw(Canvas canvas) { + canvas.drawRoundRect(face, radius, radius, body); + canvas.save(); + canvas.clipRect(face); + wall.setStrokeWidth(10f); + canvas.drawRoundRect(face, radius, radius, wall); + canvas.restore(); + canvas.drawRoundRect(face, radius, radius, edge); + } + + @Override + public void setAlpha(int alpha) { + } + + @Override + public void setColorFilter(ColorFilter filter) { + } + + @Override + public int getOpacity() { + return PixelFormat.OPAQUE; + } +} From e2b419e8f26189b0d4ff0dba619c13ced1f01d03 Mon Sep 17 00:00:00 2001 From: butschster Date: Sat, 8 Aug 2026 13:46:37 +0400 Subject: [PATCH 10/15] Enter is a sculpted keycap, and it is made of whatever panel it sits on MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../dev/butschster/linuxterminal/Buttons.java | 110 +++++------- .../dev/butschster/linuxterminal/Keycap.java | 166 ++++++++++++++++++ .../butschster/linuxterminal/SlotWell.java | 85 --------- 3 files changed, 206 insertions(+), 155 deletions(-) create mode 100644 client/app/src/main/java/dev/butschster/linuxterminal/Keycap.java delete mode 100644 client/app/src/main/java/dev/butschster/linuxterminal/SlotWell.java diff --git a/client/app/src/main/java/dev/butschster/linuxterminal/Buttons.java b/client/app/src/main/java/dev/butschster/linuxterminal/Buttons.java index bca7f69..934e553 100644 --- a/client/app/src/main/java/dev/butschster/linuxterminal/Buttons.java +++ b/client/app/src/main/java/dev/butschster/linuxterminal/Buttons.java @@ -330,86 +330,56 @@ public Drawable section(boolean fixed) { * grid. See {@link LaunchKey} for why it earns a shape of its own. */ public View launchKey(String label, String hint, View.OnClickListener onClick) { - // A key lying in a milled pocket rather than standing on the plate, and - // sized to itself rather than to the row: this is the one control on the - // panel you press into the surface, and a bar spanning the width would say - // the opposite — that it is there to be swept at. + // Centred and sized to itself: this is one deliberate press, not a bar to + // sweep at, so it keeps edges you can see and leaves the rest of the row + // as dead space around it. LinearLayout centred = new LinearLayout(context); centred.setOrientation(LinearLayout.HORIZONTAL); centred.setGravity(Gravity.CENTER); - LinearLayout well = new LinearLayout(context); - well.setOrientation(LinearLayout.HORIZONTAL); - well.setGravity(Gravity.CENTER); - well.setBackground(new SlotWell(dp(10))); - int pad = dp(8); - well.setPadding(pad, pad, pad, pad); - well.setClickable(true); - well.setFocusable(false); - debounced(well, onClick); - if (hint != null && !hint.isEmpty()) well.setContentDescription(hint); - - TextView face = new TextView(context); - face.setText(label.toUpperCase(java.util.Locale.ROOT)); - face.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 13f); - face.setTypeface(Fonts.mono(context)); - face.setLetterSpacing(0.16f); - face.setSingleLine(true); - face.setGravity(Gravity.CENTER); - face.setPadding(dp(20), dp(9), dp(18), dp(9)); - face.setMinWidth(dp(150)); - face.setDuplicateParentStateEnabled(true); - face.setBackground(slotFace()); - face.setTextColor(new android.content.res.ColorStateList( - new int[][]{{android.R.attr.state_pressed}, {android.R.attr.state_hovered}, {}}, - new int[]{0xFFBAFFCF, 0xFFD9ECFF, 0xFF9DB6CF})); - - // The mark goes after the word, where a key that commits puts it: you read - // what it does, then the sign that it fires. - face.setCompoundDrawablesWithIntrinsicBounds(null, null, - Glyphs.drawable(context, Glyphs.Kind.ENTER, 0xFF9DB6CF, iconSizeDp()), null); - face.setCompoundDrawablePadding(dp(10)); - - well.addView(face, new LinearLayout.LayoutParams( - LinearLayout.LayoutParams.WRAP_CONTENT, - LinearLayout.LayoutParams.WRAP_CONTENT)); - centred.addView(well, new LinearLayout.LayoutParams( - LinearLayout.LayoutParams.WRAP_CONTENT, - LinearLayout.LayoutParams.WRAP_CONTENT)); - return centred; - } + TextView view = new TextView(context); + view.setText(label.toUpperCase(java.util.Locale.ROOT)); + view.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 14f); + view.setTypeface(Fonts.mono(context)); + view.setLetterSpacing(0.10f); + view.setSingleLine(true); + view.setGravity(Gravity.CENTER); + view.setTextColor(skin.legend[ENTER]); + view.setPadding(dp(22), dp(12), dp(20), dp(12) + dp(6)); + view.setMinWidth(dp(150)); + view.setClickable(true); + view.setFocusable(false); + debounced(view, onClick); + if (hint != null && !hint.isEmpty()) view.setContentDescription(hint); - /** - * The key lying in the slot: a flat cap with a two-pixel foot, brightening its - * border under the ray and turning green as it goes down. - */ - private StateListDrawable slotFace() { + // Every colour comes from the skin: the cap is the ENTER fill, the rim is + // its edge, the retainer is the plate it is mounted in. The shape is the + // same on every skin, the material is whatever that panel is made of — + // otherwise Enter becomes the one control that ignores which machine it is + // part of. + float radius = dp(Math.max(6, skin.cornerDp)); + float throwPx = dp(6); StateListDrawable states = new StateListDrawable(); states.addState(new int[]{android.R.attr.state_pressed}, - slotCap(0xFF161E29, 0x8036E37B)); + new Keycap(FILL[ENTER], skin.edge[ENTER], skin.bgPanel, radius, throwPx, true)); states.addState(new int[]{android.R.attr.state_hovered}, - slotCap(0xFF1F2937, 0x8046B4FF)); - states.addState(new int[]{}, slotCap(0xFF1D2634, 0xFF2B3644)); - return states; - } + new Keycap(lighten(FILL[ENTER], 0.10f), skin.accentGo, skin.bgPanel, + radius, throwPx, false)); + states.addState(new int[]{}, + new Keycap(FILL[ENTER], skin.edge[ENTER], skin.bgPanel, radius, throwPx, false)); + view.setBackground(states); - private LayerDrawable slotCap(int fill, int border) { - GradientDrawable foot = new GradientDrawable(); - foot.setColor(0xFF070B10); - foot.setCornerRadius(dp(6)); + // The mark follows the word: you read what the key does, then the sign + // that it fires. + view.setCompoundDrawablesWithIntrinsicBounds(null, null, + Glyphs.drawable(context, Glyphs.Kind.ENTER, skin.legend[ENTER], iconSizeDp()), + null); + view.setCompoundDrawablePadding(dp(12)); - GradientDrawable cap = new GradientDrawable(); - cap.setOrientation(GradientDrawable.Orientation.TOP_BOTTOM); - cap.setColors(new int[]{fill, shade(fill, -8)}); - cap.setCornerRadius(dp(6)); - cap.setStroke(dp(1), border); - - LayerDrawable stack = new LayerDrawable(new Drawable[]{foot, cap}); - // Two pixels of foot showing beneath, as the sheet has it — enough to say - // the cap is a separate part lying in the pocket rather than painted on - // the bottom of it. - stack.setLayerInset(1, 0, 0, 0, 2); - return stack; + centred.addView(view, new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.WRAP_CONTENT, + LinearLayout.LayoutParams.WRAP_CONTENT)); + return centred; } /** diff --git a/client/app/src/main/java/dev/butschster/linuxterminal/Keycap.java b/client/app/src/main/java/dev/butschster/linuxterminal/Keycap.java new file mode 100644 index 0000000..42ccf16 --- /dev/null +++ b/client/app/src/main/java/dev/butschster/linuxterminal/Keycap.java @@ -0,0 +1,166 @@ +package dev.butschster.linuxterminal; + +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.ColorFilter; +import android.graphics.LinearGradient; +import android.graphics.Paint; +import android.graphics.PixelFormat; +import android.graphics.Rect; +import android.graphics.RectF; +import android.graphics.Shader; +import android.graphics.drawable.Drawable; + +/** + * A sculpted keycap in a recessed retainer: the most keyboard-like of the + * shapes, and the one Enter now wears. + * + *

Three parts, and each does a job the others cannot. The retainer is a + * shallow pocket in the plate with the cap sitting inside it, so the key is a + * mounted part rather than a rectangle 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 dish-shaped moulding does to light coming from + * above. And the foot below it is solid, unblurred, six pixels tall: that + * is the height of the cap, and an unblurred copy of a shape offset downwards is + * the only thing that reads as the side of a solid rather than as a shadow it + * casts. + * + *

Pressed, the cap travels the full six pixels into the retainer and the foot + * collapses to nothing, so the key ends flush with the pocket it lives in. That + * is a longer throw than any other control on the panel and it is deliberate: + * this is the key you commit with, and it should feel like it went somewhere. + * + *

The colours come from the skin rather than from this file. A cap on the flat + * panel and a cap on the machined one are the same object under different light, + * and hardcoding the greys here would have made Enter the one control that + * ignores which panel it is mounted in. + */ +public class Keycap extends Drawable { + + private final int face; + private final int edge; + private final float radius; + private final float travel; + private final boolean pressed; + + private final Paint retainer = new Paint(Paint.ANTI_ALIAS_FLAG); + private final Paint pocket = new Paint(Paint.ANTI_ALIAS_FLAG); + private final Paint foot = new Paint(Paint.ANTI_ALIAS_FLAG); + private final Paint ambient = new Paint(Paint.ANTI_ALIAS_FLAG); + private final Paint body = new Paint(Paint.ANTI_ALIAS_FLAG); + private final Paint dish = new Paint(Paint.ANTI_ALIAS_FLAG); + private final Paint lip = new Paint(Paint.ANTI_ALIAS_FLAG); + private final Paint rim = new Paint(Paint.ANTI_ALIAS_FLAG); + + private final RectF well = new RectF(); + private final RectF cap = new RectF(); + private final RectF stand = new RectF(); + + public Keycap(int face, int edge, int plate, float radiusPx, float travelPx, + boolean pressed) { + this.face = face; + this.edge = edge; + this.radius = radiusPx; + this.travel = travelPx; + this.pressed = pressed; + + retainer.setColor(shade(plate, -10)); + foot.setColor(shade(plate, -18)); + pocket.setStyle(Paint.Style.STROKE); + lip.setStyle(Paint.Style.STROKE); + rim.setStyle(Paint.Style.STROKE); + rim.setColor(edge); + } + + @Override + protected void onBoundsChange(Rect bounds) { + well.set(bounds.left + 0.5f, bounds.top + 0.5f, + bounds.right - 0.5f, bounds.bottom - 0.5f); + + float inset = 5f; + float drop = pressed ? travel : 0f; + cap.set(well.left + inset, well.top + inset + drop, + well.right - inset, well.bottom - inset - travel + drop); + stand.set(cap.left, cap.top + travel, cap.right, cap.bottom + travel); + + // inset 0 2px 6px rgba(0,0,0,.9) — the pocket's own shadow. + pocket.setShader(new LinearGradient(0, well.top, 0, well.bottom, + new int[]{0xE6000000, Color.TRANSPARENT}, + new float[]{0f, 0.5f}, Shader.TileMode.CLAMP)); + + // 180deg: bright top third, mid, dark foot — a dished moulding under a + // light that is above and in front. + body.setShader(new LinearGradient(0, cap.top, 0, cap.bottom, + new int[]{lighten(face, 0.22f), face, shade(face, -22)}, + new float[]{0f, 0.42f, 1f}, Shader.TileMode.CLAMP)); + + // inset 0 -12px 18px rgba(0,0,0,.35): the dish, darkening towards the foot + // of the cap where the surface curves away. + dish.setShader(new LinearGradient(0, cap.bottom - travel * 2.5f, 0, cap.bottom, + new int[]{Color.TRANSPARENT, 0x59000000}, + new float[]{0f, 1f}, Shader.TileMode.CLAMP)); + + lip.setShader(new LinearGradient(0, cap.top, 0, cap.top + 3f, + new int[]{pressed ? 0x1A000000 : 0x29FFFFFF, Color.TRANSPARENT}, + new float[]{0f, 1f}, Shader.TileMode.CLAMP)); + } + + @Override + public void draw(Canvas canvas) { + canvas.drawRoundRect(well, radius, radius, retainer); + canvas.save(); + canvas.clipRect(well); + pocket.setStrokeWidth(6f); + canvas.drawRoundRect(well, radius, radius, pocket); + canvas.restore(); + + if (!pressed) { + for (int i = 3; i >= 1; i--) { + ambient.setColor(Color.argb(20, 0, 0, 0)); + RectF soft = new RectF(stand); + soft.inset(-i * 1.5f, -i * 1.5f); + soft.offset(0, i * 1.3f); + canvas.drawRoundRect(soft, radius + i, radius + i, ambient); + } + canvas.drawRoundRect(stand, radius - 2f, radius - 2f, foot); + } + + canvas.drawRoundRect(cap, radius - 2f, radius - 2f, body); + canvas.save(); + canvas.clipRect(cap); + canvas.drawRoundRect(cap, radius - 2f, radius - 2f, dish); + lip.setStrokeWidth(3f); + canvas.drawRoundRect(cap, radius - 2f, radius - 2f, lip); + canvas.restore(); + + rim.setStrokeWidth(1f); + canvas.drawRoundRect(cap, radius - 2f, radius - 2f, rim); + } + + private static int shade(int colour, int step) { + return Color.rgb( + Math.max(0, Math.min(255, Color.red(colour) + step)), + Math.max(0, Math.min(255, Color.green(colour) + step)), + Math.max(0, Math.min(255, Color.blue(colour) + step))); + } + + private static int lighten(int colour, float amount) { + return Color.rgb( + (int) (Color.red(colour) + (255 - Color.red(colour)) * amount), + (int) (Color.green(colour) + (255 - Color.green(colour)) * amount), + (int) (Color.blue(colour) + (255 - Color.blue(colour)) * amount)); + } + + @Override + public void setAlpha(int alpha) { + } + + @Override + public void setColorFilter(ColorFilter filter) { + } + + @Override + public int getOpacity() { + return PixelFormat.TRANSLUCENT; + } +} diff --git a/client/app/src/main/java/dev/butschster/linuxterminal/SlotWell.java b/client/app/src/main/java/dev/butschster/linuxterminal/SlotWell.java deleted file mode 100644 index 6e5fa59..0000000 --- a/client/app/src/main/java/dev/butschster/linuxterminal/SlotWell.java +++ /dev/null @@ -1,85 +0,0 @@ -package dev.butschster.linuxterminal; - -import android.graphics.Canvas; -import android.graphics.Color; -import android.graphics.ColorFilter; -import android.graphics.LinearGradient; -import android.graphics.Paint; -import android.graphics.PixelFormat; -import android.graphics.Rect; -import android.graphics.RectF; -import android.graphics.Shader; -import android.graphics.drawable.Drawable; - -/** - * The slot Enter is sunk into: a shallow pocket milled in the plate, with the key - * lying inside it. - * - *

Every other control on this panel stands proud of the surface. This one is - * the opposite, and the inversion is the point — a key you press into the - * plate rather than one that stands up to meet you reads as deliberate, the way a - * recessed switch on real equipment means "not by accident". It costs nothing in - * target size, because the pocket is only a few pixels wider than what it holds. - * - *

Drawn from the same description as every other recess here: shadow along the - * top wall where the plate overhangs, a hairline of light along the bottom where - * the lower wall catches it. One lamp, above and in front, for the whole panel. - */ -public class SlotWell extends Drawable { - - private final float radius; - - private final Paint body = new Paint(Paint.ANTI_ALIAS_FLAG); - private final Paint wall = new Paint(Paint.ANTI_ALIAS_FLAG); - private final Paint edge = new Paint(Paint.ANTI_ALIAS_FLAG); - - private final RectF face = new RectF(); - - public SlotWell(float radiusPx) { - this.radius = radiusPx; - wall.setStyle(Paint.Style.STROKE); - edge.setStyle(Paint.Style.STROKE); - edge.setStrokeWidth(1f); - edge.setColor(0xFF232D3A); - } - - @Override - protected void onBoundsChange(Rect bounds) { - face.set(bounds.left + 0.5f, bounds.top + 0.5f, - bounds.right - 0.5f, bounds.bottom - 0.5f); - - // linear-gradient(180deg,#0a0f16,#060a0e) - body.setShader(new LinearGradient(0, face.top, 0, face.bottom, - 0xFF0A0F16, 0xFF060A0E, Shader.TileMode.CLAMP)); - - // inset 0 5px 12px rgba(0,0,0,.95) over the top, and the .03 white line - // along the bottom. - wall.setShader(new LinearGradient(0, face.top, 0, face.bottom, - new int[]{0xF2000000, Color.TRANSPARENT, 0x08FFFFFF}, - new float[]{0f, 0.42f, 1f}, Shader.TileMode.CLAMP)); - } - - @Override - public void draw(Canvas canvas) { - canvas.drawRoundRect(face, radius, radius, body); - canvas.save(); - canvas.clipRect(face); - wall.setStrokeWidth(10f); - canvas.drawRoundRect(face, radius, radius, wall); - canvas.restore(); - canvas.drawRoundRect(face, radius, radius, edge); - } - - @Override - public void setAlpha(int alpha) { - } - - @Override - public void setColorFilter(ColorFilter filter) { - } - - @Override - public int getOpacity() { - return PixelFormat.OPAQUE; - } -} From 8810282c1402cf942ff6e8ae9e539c0912c321b5 Mon Sep 17 00:00:00 2001 From: butschster Date: Sat, 8 Aug 2026 13:52:16 +0400 Subject: [PATCH 11/15] The console arrangement becomes the bar, and the bar opens as a strip again MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- client/app/src/main/AndroidManifest.xml | 25 ++--- .../dev/butschster/linuxterminal/Buttons.java | 100 ++++++++++++------ .../dev/butschster/linuxterminal/Keycap.java | 33 ++++-- 3 files changed, 100 insertions(+), 58 deletions(-) diff --git a/client/app/src/main/AndroidManifest.xml b/client/app/src/main/AndroidManifest.xml index 0037698..acabb2c 100644 --- a/client/app/src/main/AndroidManifest.xml +++ b/client/app/src/main/AndroidManifest.xml @@ -78,20 +78,21 @@ floor low enough that the bar can still be squeezed into a strip when that is what the moment wants. - 460dp was the right default when the bar was one row of keys with - a flow of buttons beside it. The console arrangement stacks a - header, two listings, a chip column and two readouts down one - column and a keypad down another; at 460 it opened already - scrolled, which is the one state a control surface must not start - in — you cannot learn where a control is if you have to go looking - for it. Window size is the only window property Horizon OS gives a - 2D app (docs/horizon.md), and the floor stays where it was: the - window is freely resizable by hand, so the minimum is there to - stop an accident, not to enforce a taste. Only the size it opens - at changes. --> + It opens as a strip and grows if you want more of it. + + This went to 900dp for a while, on the argument that the console + arrangement needs the height and should not open already scrolled. + Worn, that was wrong twice over: a panel is pulled in close, where + 900dp of it fills the view and pushes the terminal out of the way, + and the listings are elastic anyway — a short window shows fewer + rows rather than a broken layout. Window size is the only window + property Horizon OS gives a 2D app (docs/horizon.md), and the + right use of it is to open small: 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. --> diff --git a/client/app/src/main/java/dev/butschster/linuxterminal/Buttons.java b/client/app/src/main/java/dev/butschster/linuxterminal/Buttons.java index 934e553..5b74e14 100644 --- a/client/app/src/main/java/dev/butschster/linuxterminal/Buttons.java +++ b/client/app/src/main/java/dev/butschster/linuxterminal/Buttons.java @@ -45,10 +45,18 @@ public class Buttons { *

Static, and read at the moment a view is built rather than captured once: * every screen in the app builds its keys through this class, so swapping the * skin and rebuilding is enough to see the whole app in the other treatment. - * That is what makes the two comparable at all — a redesign of the bar alone, + * That is what makes them comparable at all — a redesign of the bar alone, * with the connection manager still in the old palette, compares nothing. + * + *

The default is what the app ships as. It is CONSOLE: the arrangement that + * came out of the concept work and the reviews, chosen over the other four + * because it is the only one whose difference is structural rather than + * decorative — read on the left, press in the middle, the app's own controls + * on a rail. The rest stay in {@link Skin} because they are how this one is + * argued with, and because a skin that cannot be compared against anything + * stops being a decision and becomes a habit. */ - private static Skin skin = Skin.capped(); + private static Skin skin = Skin.console(); public static void applySkin(Skin chosen) { skin = chosen; @@ -330,53 +338,75 @@ public Drawable section(boolean fixed) { * grid. See {@link LaunchKey} for why it earns a shape of its own. */ public View launchKey(String label, String hint, View.OnClickListener onClick) { - // Centred and sized to itself: this is one deliberate press, not a bar to - // sweep at, so it keeps edges you can see and leaves the rest of the row - // as dead space around it. LinearLayout centred = new LinearLayout(context); centred.setOrientation(LinearLayout.HORIZONTAL); centred.setGravity(Gravity.CENTER); - TextView view = new TextView(context); - view.setText(label.toUpperCase(java.util.Locale.ROOT)); - view.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 14f); - view.setTypeface(Fonts.mono(context)); - view.setLetterSpacing(0.10f); - view.setSingleLine(true); - view.setGravity(Gravity.CENTER); - view.setTextColor(skin.legend[ENTER]); - view.setPadding(dp(22), dp(12), dp(20), dp(12) + dp(6)); - view.setMinWidth(dp(150)); - view.setClickable(true); - view.setFocusable(false); - debounced(view, onClick); - if (hint != null && !hint.isEmpty()) view.setContentDescription(hint); - - // Every colour comes from the skin: the cap is the ENTER fill, the rim is - // its edge, the retainer is the plate it is mounted in. The shape is the - // same on every skin, the material is whatever that panel is made of — - // otherwise Enter becomes the one control that ignores which machine it is - // part of. float radius = dp(Math.max(6, skin.cornerDp)); float throwPx = dp(6); + + // Two views, because two things happen on press and only one of them is + // the background: the cap goes down and the pocket does not. The legend is + // drawn by the view, so the view is what has to travel — a cap that moved + // inside its own background would slide out from under its own label, + // which is exactly what it did. + LinearLayout retainer = new LinearLayout(context); + retainer.setOrientation(LinearLayout.HORIZONTAL); + retainer.setGravity(Gravity.CENTER); + retainer.setBackground(new Keycap(Keycap.Part.RETAINER, FILL[ENTER], + skin.edge[ENTER], skin.bgPanel, radius, throwPx, false)); + int pad = dp(5); + retainer.setPadding(pad, pad, pad, pad); + retainer.setClickable(true); + retainer.setFocusable(false); + debounced(retainer, onClick); + if (hint != null && !hint.isEmpty()) retainer.setContentDescription(hint); + + TextView cap = new TextView(context); + cap.setText(label.toUpperCase(java.util.Locale.ROOT)); + cap.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 14f); + cap.setTypeface(Fonts.mono(context)); + cap.setLetterSpacing(0.10f); + cap.setSingleLine(true); + cap.setGravity(Gravity.CENTER); + cap.setTextColor(skin.legend[ENTER]); + cap.setPadding(dp(20), dp(11), dp(18), dp(11) + dp(6)); + cap.setMinWidth(dp(150)); + // It takes the retainer's pressed state, so the cap lights and travels + // when the thing you actually aimed at is pressed. + cap.setDuplicateParentStateEnabled(true); + StateListDrawable states = new StateListDrawable(); states.addState(new int[]{android.R.attr.state_pressed}, - new Keycap(FILL[ENTER], skin.edge[ENTER], skin.bgPanel, radius, throwPx, true)); + new Keycap(Keycap.Part.CAP, FILL[ENTER], skin.edge[ENTER], skin.bgPanel, + radius, throwPx, true)); states.addState(new int[]{android.R.attr.state_hovered}, - new Keycap(lighten(FILL[ENTER], 0.10f), skin.accentGo, skin.bgPanel, - radius, throwPx, false)); + new Keycap(Keycap.Part.CAP, lighten(FILL[ENTER], 0.10f), skin.accentGo, + skin.bgPanel, radius, throwPx, false)); states.addState(new int[]{}, - new Keycap(FILL[ENTER], skin.edge[ENTER], skin.bgPanel, radius, throwPx, false)); - view.setBackground(states); + new Keycap(Keycap.Part.CAP, FILL[ENTER], skin.edge[ENTER], skin.bgPanel, + radius, throwPx, false)); + cap.setBackground(states); - // The mark follows the word: you read what the key does, then the sign - // that it fires. - view.setCompoundDrawablesWithIntrinsicBounds(null, null, + // The travel itself: the whole cap, legend included, six pixels down. + android.animation.StateListAnimator moving = new android.animation.StateListAnimator(); + moving.addState(new int[]{android.R.attr.state_pressed}, + android.animation.ObjectAnimator.ofFloat(cap, "translationY", throwPx) + .setDuration(90)); + moving.addState(new int[]{}, + android.animation.ObjectAnimator.ofFloat(cap, "translationY", 0f) + .setDuration(70)); + cap.setStateListAnimator(moving); + + cap.setCompoundDrawablesWithIntrinsicBounds(null, null, Glyphs.drawable(context, Glyphs.Kind.ENTER, skin.legend[ENTER], iconSizeDp()), null); - view.setCompoundDrawablePadding(dp(12)); + cap.setCompoundDrawablePadding(dp(12)); - centred.addView(view, new LinearLayout.LayoutParams( + retainer.addView(cap, new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.WRAP_CONTENT, + LinearLayout.LayoutParams.WRAP_CONTENT)); + centred.addView(retainer, new LinearLayout.LayoutParams( LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT)); return centred; diff --git a/client/app/src/main/java/dev/butschster/linuxterminal/Keycap.java b/client/app/src/main/java/dev/butschster/linuxterminal/Keycap.java index 42ccf16..d8a17ca 100644 --- a/client/app/src/main/java/dev/butschster/linuxterminal/Keycap.java +++ b/client/app/src/main/java/dev/butschster/linuxterminal/Keycap.java @@ -37,6 +37,10 @@ */ public class Keycap extends Drawable { + /** Which half of the assembly this drawable is: the pocket, or the cap in it. */ + public enum Part { RETAINER, CAP } + + private final Part part; private final int face; private final int edge; private final float radius; @@ -56,8 +60,9 @@ public class Keycap extends Drawable { private final RectF cap = new RectF(); private final RectF stand = new RectF(); - public Keycap(int face, int edge, int plate, float radiusPx, float travelPx, + public Keycap(Part part, int face, int edge, int plate, float radiusPx, float travelPx, boolean pressed) { + this.part = part; this.face = face; this.edge = edge; this.radius = radiusPx; @@ -77,10 +82,13 @@ protected void onBoundsChange(Rect bounds) { well.set(bounds.left + 0.5f, bounds.top + 0.5f, bounds.right - 0.5f, bounds.bottom - 0.5f); - float inset = 5f; - float drop = pressed ? travel : 0f; - cap.set(well.left + inset, well.top + inset + drop, - well.right - inset, well.bottom - inset - travel + drop); + // The cap does not move within its own bounds. The view carries it down — + // see Buttons.travel — because the legend is drawn by the view, and a cap + // that travelled inside its background would slide out from under its own + // label. What changes on press is only what is under it: the foot goes. + float inset = part == Part.RETAINER ? 0f : 0f; + cap.set(well.left + inset, well.top + inset, + well.right - inset, well.bottom - inset - travel); stand.set(cap.left, cap.top + travel, cap.right, cap.bottom + travel); // inset 0 2px 6px rgba(0,0,0,.9) — the pocket's own shadow. @@ -107,12 +115,15 @@ protected void onBoundsChange(Rect bounds) { @Override public void draw(Canvas canvas) { - canvas.drawRoundRect(well, radius, radius, retainer); - canvas.save(); - canvas.clipRect(well); - pocket.setStrokeWidth(6f); - canvas.drawRoundRect(well, radius, radius, pocket); - canvas.restore(); + if (part == Part.RETAINER) { + canvas.drawRoundRect(well, radius, radius, retainer); + canvas.save(); + canvas.clipRect(well); + pocket.setStrokeWidth(6f); + canvas.drawRoundRect(well, radius, radius, pocket); + canvas.restore(); + return; + } if (!pressed) { for (int i = 3; i >= 1; i--) { From 0467e8c90f8e7cb9f5e1b53ec135093be2e6e65f Mon Sep 17 00:00:00 2001 From: butschster Date: Wed, 12 Aug 2026 17:23:14 +0400 Subject: [PATCH 12/15] The gallery is gone: the panel is the product now MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- client/app/build.gradle.kts | 14 +- client/app/src/debug/AndroidManifest.xml | 34 --- .../linuxterminal/GalleryActivity.java | 247 ------------------ 3 files changed, 3 insertions(+), 292 deletions(-) delete mode 100644 client/app/src/debug/AndroidManifest.xml delete mode 100644 client/app/src/debug/java/dev/butschster/linuxterminal/GalleryActivity.java diff --git a/client/app/build.gradle.kts b/client/app/build.gradle.kts index 62fe056..6220f91 100644 --- a/client/app/build.gradle.kts +++ b/client/app/build.gradle.kts @@ -52,13 +52,13 @@ android { ?: signingConfigs.getByName("debug") } - // A build for wearing, not for shipping: the debug variant's contents — - // including the concept screen in src/debug — signed with the release key. + // A build for wearing, not for shipping: the debug variant's contents, + // signed with the release key. // // It exists because Android refuses an update signed by a different key, and // the headset already carries a properly signed install. The alternative was // to uninstall it, which throws away the paired servers and their tokens and - // means pairing every machine again to look at a colour. + // means pairing every machine again to look at a change. create("concept") { initWith(getByName("debug")) isDebuggable = true @@ -67,14 +67,6 @@ android { } } - sourceSets { - // One copy of the concept screen, built into both variants that may show it. - getByName("concept") { - java.srcDirs("src/debug/java") - manifest.srcFile("src/debug/AndroidManifest.xml") - } - } - compileOptions { sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 diff --git a/client/app/src/debug/AndroidManifest.xml b/client/app/src/debug/AndroidManifest.xml deleted file mode 100644 index 6c1dec4..0000000 --- a/client/app/src/debug/AndroidManifest.xml +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - - - - - - - - - - - - - - diff --git a/client/app/src/debug/java/dev/butschster/linuxterminal/GalleryActivity.java b/client/app/src/debug/java/dev/butschster/linuxterminal/GalleryActivity.java deleted file mode 100644 index 18283f4..0000000 --- a/client/app/src/debug/java/dev/butschster/linuxterminal/GalleryActivity.java +++ /dev/null @@ -1,247 +0,0 @@ -package dev.butschster.linuxterminal; - -import android.app.Activity; -import android.graphics.Typeface; -import android.os.Bundle; -import android.util.TypedValue; -import android.view.Gravity; -import android.view.View; -import android.widget.LinearLayout; -import android.widget.TextView; - -import org.json.JSONException; -import org.json.JSONObject; - -/** - * The bar, off its leash: the real {@link ContextBar}, fed a recorded context, with - * the skin switchable underneath it. - * - *

Debug builds only. This class lives in {@code src/debug} and so does the - * manifest entry that launches it — a release build contains neither, which is the - * point. A concept screen that shipped would be one more thing for a store reviewer - * to find and ask about. - * - *

Why it exists: the bar cannot otherwise be looked at without a paired server, a - * running shell and a headset on a head. That loop is minutes long and it is the - * wrong loop for a question about colour. The contexts below are verbatim output of - * {@code --dump-context}, trimmed, so what is drawn here is what the server actually - * sends rather than a designer's idea of it. - * - *

What it cannot answer: how any of this reads through the lenses. - * `docs/readability.md` measured that threshold on the hardware and an emulator has - * no opinion about it. This screen settles layout, weight and palette; the headset - * settles whether a 1dp lit edge survives at all. - */ -public class GalleryActivity extends Activity implements ContextBar.Host { - - /** At a prompt: somewhere to go, projects, things to run. */ - private static final String SHELL_CONTEXT = "{" - + "\"cwd_label\":\"~/repos/home/linux-terminal\"," - + "\"tool\":null,\"tool_name\":\"shell\"," - + "\"git\":{\"branch\":\"concept/panel-look\",\"dirty\":4}," - + "\"groups\":[" - + "{\"name\":\"go to\",\"actions\":[" - + "{\"label\":\"..\",\"send\":\"cd ..\",\"style\":\"up\",\"enter\":true," - + "\"hint\":\"~/repos/home\"}," - + "{\"label\":\".claude\",\"send\":\"cd .claude\",\"style\":\"dir\",\"enter\":true}," - + "{\"label\":\"client\",\"send\":\"cd client\",\"style\":\"dir\",\"enter\":true}," - + "{\"label\":\"docs\",\"send\":\"cd docs\",\"style\":\"dir\",\"enter\":true}," - + "{\"label\":\"packaging\",\"send\":\"cd packaging\",\"style\":\"dir\",\"enter\":true}," - + "{\"label\":\"server\",\"send\":\"cd server\",\"style\":\"git\",\"enter\":true}]}," - + "{\"name\":\"projects\",\"actions\":[" - + "{\"label\":\"docker-infra\",\"send\":\"cd x\",\"style\":\"fav\",\"enter\":true," - + "\"hint\":\"~/repos/intruforce/docker-infra · 34×\"}," - + "{\"label\":\"cve-analyzer\",\"send\":\"cd x\",\"style\":\"fav\",\"enter\":true}," - + "{\"label\":\"go-services\",\"send\":\"cd x\",\"style\":\"fav\",\"enter\":true}," - + "{\"label\":\"k8s\",\"send\":\"cd x\",\"style\":\"fav\",\"enter\":true}," - + "{\"label\":\"ai-agents\",\"send\":\"cd x\",\"style\":\"fav\",\"enter\":true}," - + "{\"label\":\"pentax\",\"send\":\"cd x\",\"style\":\"fav\",\"enter\":true}," - + "{\"label\":\"home/terminal\",\"send\":\"cd x\",\"style\":\"fav\",\"enter\":true}," - + "{\"label\":\"shared\",\"send\":\"cd x\",\"style\":\"fav\",\"enter\":true}]}," - + "{\"name\":\"run\",\"actions\":[" - + "{\"label\":\"claude\",\"send\":\"claude\",\"style\":\"cmd\",\"enter\":true}," - + "{\"label\":\"codex\",\"send\":\"codex\",\"style\":\"cmd\",\"enter\":true}," - + "{\"label\":\"ls -la\",\"send\":\"ls -la\",\"style\":\"cmd\",\"enter\":true}," - + "{\"label\":\"^R\",\"send\":\"\\u0012\",\"style\":\"key\",\"enter\":false," - + "\"hint\":\"history search\"}," - + "{\"label\":\"git status\",\"send\":\"git status\",\"style\":\"cmd\",\"enter\":true}," - + "{\"label\":\"git diff\",\"send\":\"git diff\",\"style\":\"cmd\",\"enter\":true}]}]}"; - - /** With Claude Code in front: its own keys on the right, its skills on the left. */ - private static final String CLAUDE_CONTEXT = "{" - + "\"cwd_label\":\"~/repos/home/linux-terminal\"," - + "\"tool\":\"claude\",\"tool_name\":\"claude\"," - + "\"git\":{\"branch\":\"concept/panel-look\",\"dirty\":4}," - + "\"groups\":[" - + "{\"name\":\"claude\",\"actions\":[" - + "{\"label\":\"⇧Tab\",\"send\":\"\\u001b[Z\",\"style\":\"key\",\"enter\":false," - + "\"hint\":\"cycle permission mode\"}," - + "{\"label\":\"1\",\"send\":\"1\",\"style\":\"key\",\"enter\":false}," - + "{\"label\":\"2\",\"send\":\"2\",\"style\":\"key\",\"enter\":false}," - + "{\"label\":\"3\",\"send\":\"3\",\"style\":\"key\",\"enter\":false}," - + "{\"label\":\"Esc Esc\",\"send\":\"\\u001b\\u001b\",\"style\":\"warn\",\"enter\":false}," - + "{\"label\":\"^T\",\"send\":\"\\u0014\",\"style\":\"key\",\"enter\":false}," - + "{\"label\":\"^O\",\"send\":\"\\u000f\",\"style\":\"key\",\"enter\":false}," - + "{\"label\":\"^B\",\"send\":\"\\u0002\",\"style\":\"key\",\"enter\":false}," - + "{\"label\":\"^S\",\"send\":\"\\u0013\",\"style\":\"key\",\"enter\":false}," - + "{\"label\":\"/\",\"send\":\"/\",\"style\":\"key\",\"enter\":false}," - + "{\"label\":\"!\",\"send\":\"!\",\"style\":\"key\",\"enter\":false}," - + "{\"label\":\"#\",\"send\":\"#\",\"style\":\"key\",\"enter\":false}," - + "{\"label\":\"/plan\",\"send\":\"/plan\",\"style\":\"cmd\",\"enter\":true}," - + "{\"label\":\"/rewind\",\"send\":\"/rewind\",\"style\":\"cmd\",\"enter\":true}," - + "{\"label\":\"/context\",\"send\":\"/context\",\"style\":\"cmd\",\"enter\":true}," - + "{\"label\":\"/review\",\"send\":\"/review\",\"style\":\"cmd\",\"enter\":true}," - + "{\"label\":\"/compact\",\"send\":\"/compact\",\"style\":\"cmd\",\"enter\":true}," - + "{\"label\":\"/usage\",\"send\":\"/usage\",\"style\":\"cmd\",\"enter\":true}," - + "{\"label\":\"/clear\",\"send\":\"/clear\",\"style\":\"warn\",\"enter\":true," - + "\"hint\":\"wipe the conversation\"}]}," - + "{\"name\":\"skills\",\"actions\":[" - + "{\"label\":\"/diagnose\",\"send\":\"/diagnose \",\"style\":\"skill\",\"enter\":false}," - + "{\"label\":\"/headset\",\"send\":\"/headset \",\"style\":\"skill\",\"enter\":false}," - + "{\"label\":\"/install\",\"send\":\"/install \",\"style\":\"skill\",\"enter\":false}," - + "{\"label\":\"/new-feature\",\"send\":\"/new-feature \",\"style\":\"skill\",\"enter\":false}," - + "{\"label\":\"/pairing\",\"send\":\"/pairing \",\"style\":\"skill\",\"enter\":false}," - + "{\"label\":\"/release\",\"send\":\"/release \",\"style\":\"skill\",\"enter\":false}," - + "{\"label\":\"/minimalist-ui\",\"send\":\"/minimalist-ui \",\"style\":\"skill-global\"," - + "\"enter\":false}," - + "{\"label\":\"/infraq\",\"send\":\"/infraq \",\"style\":\"skill-global\",\"enter\":false}]}]}"; - - private static final Skin[] SKINS = - {Skin.capped(), Skin.outline(), Skin.machined(), Skin.tactile(), Skin.console()}; - private static final String[] SKIN_NAMES = - {"capped", "outline", "machined", "tactile", "console"}; - - // The font switcher is gone: DejaVu won, and it is what docs/readability.md - // measured with. JetBrains Mono stays in assets until the headset says which - // it prefers; if DejaVu holds, the other should be deleted rather than kept. - private LinearLayout root; - private int chosen; - private boolean claude; - - @Override - protected void onCreate(Bundle state) { - super.onCreate(state); - root = new LinearLayout(this); - root.setOrientation(LinearLayout.VERTICAL); - setContentView(root); - build(); - } - - /** - * Everything is rebuilt on every switch, deliberately. - * - *

{@link Buttons} reads the skin while it builds a view, so an existing bar - * keeps the colours it was born with. Rebuilding is also the honest test: it is - * exactly what the bar does whenever the foreground process changes. - */ - private void build() { - Buttons.applySkin(SKINS[chosen]); - Fonts.choose(Fonts.DEJAVU); - root.removeAllViews(); - root.setBackgroundColor(Buttons.skin().bg); - - root.addView(switcher(), new LinearLayout.LayoutParams( - LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT)); - - // Every skin, console included, is the real ContextBar. The console one - // rearranges the same parts rather than replacing them, so what is looked - // at here is what would ship. - ContextBar bar = new ContextBar(this, this); - try { - bar.setContext(new JSONObject(claude ? CLAUDE_CONTEXT : SHELL_CONTEXT)); - } catch (JSONException e) { - bar.setStatus("bad mock context: " + e.getMessage()); - } - View panel = bar; - root.addView(panel, new LinearLayout.LayoutParams( - LinearLayout.LayoutParams.MATCH_PARENT, 0, 1f)); - } - - /** The one row that is not part of the concept: what to draw, and in which skin. */ - private LinearLayout switcher() { - Typeface icons = Typeface.createFromAsset(getAssets(), "MaterialIcons-Regular.ttf"); - Buttons buttons = new Buttons(this, icons); - - LinearLayout row = new LinearLayout(this); - row.setOrientation(LinearLayout.HORIZONTAL); - row.setGravity(Gravity.CENTER_VERTICAL); - row.setBackgroundColor(Buttons.skin().bgPanel); - row.setPadding(buttons.inset(), buttons.dp(6), buttons.inset(), buttons.dp(6)); - - row.addView(caption(buttons, "SKIN")); - for (int i = 0; i < SKINS.length; i++) { - final int which = i; - row.addView(choice(buttons, SKIN_NAMES[i], chosen == i, v -> { - chosen = which; - build(); - })); - } - - row.addView(caption(buttons, "CONTEXT")); - row.addView(choice(buttons, "shell", !claude, v -> { - claude = false; - build(); - })); - row.addView(choice(buttons, "claude", claude, v -> { - claude = true; - build(); - })); - - TextView note = new TextView(this); - note.setText("debug · mocked context · lenses not simulated"); - note.setTextColor(Buttons.MUTED); - note.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 12); - LinearLayout.LayoutParams fill = new LinearLayout.LayoutParams( - 0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f); - fill.setMarginStart(buttons.dp(16)); - note.setGravity(Gravity.END); - row.addView(note, fill); - - return row; - } - - private TextView caption(Buttons buttons, String text) { - TextView view = new TextView(this); - view.setText(text); - view.setTextColor(Buttons.MUTED); - view.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 10); - view.setLetterSpacing(0.12f); - view.setPadding(buttons.dp(14), 0, buttons.dp(8), 0); - return view; - } - - private View choice(Buttons buttons, String label, boolean active, View.OnClickListener click) { - View view = buttons.key(label, active ? Buttons.COMMAND : Buttons.KEY, null, click); - LinearLayout.LayoutParams params = new LinearLayout.LayoutParams( - LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT); - params.setMarginEnd(buttons.gap()); - view.setLayoutParams(params); - return view; - } - - // ------------------------------------------------------- nothing is connected - - @Override - public void onAction(String send, boolean enter) { - } - - @Override - public void onDictate() { - } - - @Override - public void onStopDictating() { - } - - @Override - public void onKeyboard() { - } - - @Override - public void onFontStep(int delta) { - } - - @Override - public void onScroll(int rows) { - } -} From 74eb5d5a429e9cfae50275d369e32111e9c283c9 Mon Sep 17 00:00:00 2001 From: butschster Date: Wed, 12 Aug 2026 18:07:56 +0400 Subject: [PATCH 13/15] The bar offers what a shell can run, and the tray icon comes back MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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//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. --- docs/service.md | 77 +++++++++++++++++++++++++++++++++++++ server/actions.go | 73 +++++++++++++++++++++++++++++++++-- server/go.mod | 6 +-- server/main.go | 23 +++++++++-- server/tray.go | 97 ++++++++++++++++++++++++++++++++++++++++++++--- 5 files changed, 260 insertions(+), 16 deletions(-) create mode 100644 docs/service.md diff --git a/docs/service.md b/docs/service.md new file mode 100644 index 0000000..113f2ee --- /dev/null +++ b/docs/service.md @@ -0,0 +1,77 @@ +# The server as a user service, and what that costs + +`make install` puts the server behind `systemd --user`, which is the right place +for it: the shells it opens are yours, they inherit your environment, and nothing +about it wants root. The cost is that a service's environment is **not** a +terminal's environment, and two things that worked when the server was started by +hand quietly stopped working when it was installed. + +Both were found the same way, and it is worth doing that way again: + +```sh +tr '\0' '\n' < /proc/$(pgrep -f linux-terminal-server | head -1)/environ +systemctl --user show-environment +``` + +The first prints what the running server actually has. The second prints what +`systemd --user` has **now**. When those two disagree, something started before +the desktop did. + +## The service's PATH is not the shell's PATH + +A service enabled with `WantedBy=default.target` starts at login with the bare +system PATH — `/usr/local/bin`, `/usr/bin`, and so on. Nothing in `~/.bashrc` has +run, so `~/.local/bin` is not on it. + +That is where the interesting tools live. `claude`, `codex`, and this machine's +own `cch` and `cgit` are all installed there, and `runActions` in +`server/actions.go` offers a button only for a command that exists — a button +that answers with `command not found` is worse than no button. + +So the bar dropped four of its six commands, and only when the server was +installed properly. Started from a terminal for a day's work, the same binary +offered all of them, because a terminal's PATH is the one `.bashrc` built. + +The fix is to ask the shell rather than to ask this process: + +``` +$SHELL -ic 'printf "\n%s\n" "$PATH"' +``` + +Interactive and **not** a login shell, because that is exactly what `pty.Start` +opens for a session. On this machine the difference is real: `-lc` reads +`.profile` and returns a PATH without `~/.nvm` and half the rest; `-ic` reads +`.bashrc` and returns what a session will actually search. It is read once and +cached, because it forks a shell. + +## The tray icon needs a bus, not a display + +`trayAvailable` used to require `DISPLAY` or `WAYLAND_DISPLAY`. Neither is in a +user service's environment at the moment it starts, and neither ever appears in +it afterwards — the graphical session imports them into `systemd --user` after +the service is already running, which is why `systemctl --user show-environment` +lists `WAYLAND_DISPLAY` while `/proc//environ` of the server does not. + +The result was a server that had been running for three days on a desktop machine +and had never once tried to show an icon. + +What the tray actually needs is somebody willing to hold one, and that is a name +on the session bus: + +```sh +busctl --user list | grep StatusNotifierWatcher +``` + +So the server now waits for `org.kde.StatusNotifierWatcher` to be owned and only +then hands the goroutine to systray. Waiting is free — that goroutine would be +blocked inside the library anyway — and it means the icon appears when you log in +rather than never. `fyne.io/systray` already re-registers by itself if the +desktop restarts later; what it cannot do is start on a desktop we refused to +look for. + +To see whether the icon is registered, and which process owns it: + +```sh +busctl --user list | grep StatusNotifierItem +busctl --user status :1.1458 | grep -E '^(PID|Comm)=' +``` diff --git a/server/actions.go b/server/actions.go index ac7354b..9959a0d 100644 --- a/server/actions.go +++ b/server/actions.go @@ -1,10 +1,15 @@ package main import ( + "context" "fmt" + "log" + "os" "os/exec" + "path/filepath" "strings" "sync" + "time" ) // Action is one button. The client draws label, colours it by style, and on a press @@ -266,11 +271,73 @@ func installed(name string) bool { if cached, ok := installedCache.Load(name); ok { return cached.(bool) } - _, err := exec.LookPath(name) - installedCache.Store(name, err == nil) - return err == nil + found := false + for _, dir := range sessionPath() { + if dir == "" { + continue + } + // Stat, not LookPath: the search list is the shell's, not this process's, + // and LookPath can only be told about the second. Following the symlink is + // wanted — every one of these tools installs itself as one. + info, err := os.Stat(filepath.Join(dir, name)) + if err == nil && !info.IsDir() && info.Mode()&0o111 != 0 { + found = true + break + } + } + installedCache.Store(name, found) + return found } +// sessionShell is the shell sessions are opened with. Set once at start-up, +// because the button tables have to ask the same shell the session will get. +var sessionShell = "/bin/bash" + +// sessionPath is the PATH a shell opened here would search, which is emphatically +// not the PATH this process has. +// +// This was `exec.LookPath`, and it was wrong in the one place it matters. The +// server usually runs as a `systemd --user` service, whose PATH is the bare +// system one — /usr/bin and friends. Every tool this machine's owner actually +// installed lives in ~/.local/bin, which is put on the PATH by their .bashrc. +// So `claude`, `codex` and the rest were plainly there, plainly runnable in the +// session the button would have typed into, and the bar refused to offer them +// because the *service* could not see them. Started from a terminal instead, +// the same server offered all of them — which is why this survived so long. +// +// Asked the way a session is opened: interactive, not a login shell. That is what +// pty.Start gives you, and on this machine it is the difference between a PATH +// with ~/.local/bin on it and one without. +// +// Read once. A shell is forked for it, so it must not happen per refresh — and a +// tool installed while the server runs still needs a restart to appear, which is +// the same trade the cache above already made. +var sessionPath = sync.OnceValue(func() []string { + own := filepath.SplitList(os.Getenv("PATH")) + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + // The leading newline is deliberate: an interactive shell may greet, and the + // answer is whatever the last line is. + cmd := exec.CommandContext(ctx, sessionShell, "-ic", `printf '\n%s\n' "$PATH"`) + cmd.Env = shellEnv() + out, err := cmd.Output() + if err != nil { + log.Printf("cannot read the shell's PATH (%v) — offering only what this process can see", err) + return own + } + + lines := strings.Split(strings.TrimSpace(string(out)), "\n") + answer := strings.TrimSpace(lines[len(lines)-1]) + if !strings.Contains(answer, string(filepath.ListSeparator)) { + // Not a PATH. A shell whose printf takes a list rather than a string — + // fish — lands here, and the process's own PATH is a better answer than + // a fragment of one. + return own + } + return filepath.SplitList(answer) +}) + func trim(text string, limit int) string { if len(text) <= limit { return text diff --git a/server/go.mod b/server/go.mod index 948fe7a..61e8cad 100644 --- a/server/go.mod +++ b/server/go.mod @@ -5,9 +5,7 @@ go 1.26.5 require ( fyne.io/systray v1.12.2 github.com/creack/pty v1.1.24 + github.com/godbus/dbus/v5 v5.1.0 ) -require ( - github.com/godbus/dbus/v5 v5.1.0 // indirect - golang.org/x/sys v0.15.0 // indirect -) +require golang.org/x/sys v0.15.0 // indirect diff --git a/server/main.go b/server/main.go index 9a4e73c..948d645 100644 --- a/server/main.go +++ b/server/main.go @@ -114,6 +114,9 @@ func main() { if shell == "" { shell = "/bin/bash" } + // The button tables ask this shell what a session's PATH would be; see + // sessionPath in actions.go. + sessionShell = shell name := *flagName if name == "" { @@ -179,13 +182,25 @@ func main() { // systray owns the goroutine it is given and never returns it, so the tray // takes the main one and accepting moves aside. - if *flagTray && trayAvailable() { + // + // Waiting for the desktop happens on this side of the fence rather than + // inside the library: a user service starts at login, before anything is + // offering to show an icon, and the wait is what makes the icon appear a few + // seconds later instead of never. Sessions are served throughout — accepting + // has already moved to its own goroutine by then. + if *flagTray && trayPossible() { go accept() - server.runTray() - return + if awaitTrayHost() { + server.runTray() + return + } + // The bus is unusable, so there will be no icon — but the shells this + // process is serving are not the tray's to end. + log.Printf("running without a tray icon") + select {} } if *flagTray { - log.Printf("no desktop session here — running without a tray icon") + log.Printf("no session bus here — running without a tray icon") } accept() } diff --git a/server/tray.go b/server/tray.go index c39643f..4ad8726 100644 --- a/server/tray.go +++ b/server/tray.go @@ -11,6 +11,7 @@ import ( "time" "fyne.io/systray" + "github.com/godbus/dbus/v5" ) // An indicator in the desktop's tray, for when the server runs on a machine you @@ -66,11 +67,97 @@ func (s *Server) trayStatus() trayState { return state } -// trayAvailable reports whether there is a desktop to put an icon on. Without -// this the library waits for a bus that will never answer. -func trayAvailable() bool { - return os.Getenv("DBUS_SESSION_BUS_ADDRESS") != "" && - (os.Getenv("WAYLAND_DISPLAY") != "" || os.Getenv("DISPLAY") != "") +// The name a desktop takes on the session bus when it is willing to host tray +// icons. GNOME's AppIndicator extension owns it; so does KDE, and so does any +// other implementation of the same specification. +const trayWatcher = "org.kde.StatusNotifierWatcher" + +// trayPossible reports whether there is a session bus at all — which is the one +// thing that cannot appear later. Without a bus the library would wait for an +// answer that will never come. +func trayPossible() bool { + return os.Getenv("DBUS_SESSION_BUS_ADDRESS") != "" +} + +// awaitTrayHost blocks until a desktop is willing to hold an icon, and reports +// false only if the bus itself cannot be used. +// +// This replaced a check for DISPLAY or WAYLAND_DISPLAY, and the difference is +// the whole bug. A user service is started by `systemd --user` at login, which +// is BEFORE the graphical session has imported those variables into it — so the +// server's own environment has neither, forever, no matter how many screens are +// plugged in. `systemctl --user show-environment` on this machine lists +// WAYLAND_DISPLAY while /proc//environ of a server started three days ago +// does not, which is exactly that ordering. The old check therefore answered +// "no desktop here" on a desktop machine and the icon was never even attempted. +// +// Asking the bus is both correct and later-proof: the question is not whether an +// X server exists, it is whether anything is offering to show an icon, and that +// is a name on the bus. Waiting for it costs nothing — this runs on a goroutine +// that would otherwise be blocked inside systray anyway — and it means the icon +// appears at login on a machine where the server was started before there was a +// session, which is the ordinary case for a service enabled with `WantedBy= +// default.target`. +func awaitTrayHost() bool { + conn, err := dbus.SessionBusPrivate() + if err != nil { + log.Printf("tray: no session bus: %v", err) + return false + } + // Private, not shared: systray opens its own connection and closing a shared + // one under it would take the icon with it. + defer conn.Close() + if err := conn.Auth(nil); err != nil { + log.Printf("tray: session bus refused us: %v", err) + return false + } + if err := conn.Hello(); err != nil { + log.Printf("tray: session bus refused us: %v", err) + return false + } + + if trayHosted(conn) { + return true + } + + signals := make(chan *dbus.Signal, 8) + conn.Signal(signals) + if err := conn.AddMatchSignal( + dbus.WithMatchObjectPath("/org/freedesktop/DBus"), + dbus.WithMatchInterface("org.freedesktop.DBus"), + dbus.WithMatchSender("org.freedesktop.DBus"), + dbus.WithMatchMember("NameOwnerChanged"), + dbus.WithMatchArg(0, trayWatcher), + ); err != nil { + log.Printf("tray: cannot watch for a desktop: %v", err) + return false + } + + // Asked again after subscribing, because a desktop that arrived between the + // first question and the match rule would otherwise never be noticed. + if trayHosted(conn) { + return true + } + + log.Printf("tray: waiting for a desktop to offer a tray") + for signal := range signals { + // NameOwnerChanged carries [name, old owner, new owner]; a non-empty new + // owner is somebody taking the name. + if len(signal.Body) < 3 { + continue + } + if owner, ok := signal.Body[2].(string); ok && owner != "" { + return true + } + } + return false +} + +func trayHosted(conn *dbus.Conn) bool { + var owned bool + err := conn.BusObject().Call("org.freedesktop.DBus.NameHasOwner", 0, trayWatcher). + Store(&owned) + return err == nil && owned } // runTray blocks — systray owns the goroutine it is given. From 0fcfdc3af170ad6e258ef53144c765cdcbfd7371 Mon Sep 17 00:00:00 2001 From: butschster Date: Wed, 12 Aug 2026 18:08:13 +0400 Subject: [PATCH 14/15] The commands stay on the panel, and the panel's look is a setting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 `tactile` 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. --- .../butschster/linuxterminal/Appearance.java | 104 +++++++++++ .../butschster/linuxterminal/ContextBar.java | 162 +++++++++++++++++- 2 files changed, 258 insertions(+), 8 deletions(-) create mode 100644 client/app/src/main/java/dev/butschster/linuxterminal/Appearance.java diff --git a/client/app/src/main/java/dev/butschster/linuxterminal/Appearance.java b/client/app/src/main/java/dev/butschster/linuxterminal/Appearance.java new file mode 100644 index 0000000..8ec4a02 --- /dev/null +++ b/client/app/src/main/java/dev/butschster/linuxterminal/Appearance.java @@ -0,0 +1,104 @@ +package dev.butschster.linuxterminal; + +import android.content.Context; +import android.content.SharedPreferences; + +/** + * Which panel the bar is drawn as, and remembering the answer. + * + *

The five skins were built to be argued with — a decision is only a decision + * once it has been compared against something — and that comparison used to live + * in a debug gallery that shipped in no release. The gallery is gone, and taking + * it away removed the only way to see the alternatives at all. This is the same + * choice offered where it belongs: inside the product, to the person wearing it, + * and not to a build variant. + * + *

The choice is stored rather than held, because a panel that forgets is worse + * than a panel with one look. The bar is recreated whenever the shell restarts it + * — resized, reopened, or after the headset sleeps — and a look that lasted until + * then would read as the app losing its settings. + * + *

Whether the alternatives should exist at all is a separate question from + * whether they should be reachable. CONSOLE remains the default and the shipped + * arrangement; see {@link Buttons#skin()}. + */ +final class Appearance { + + private static final String PREFS = "appearance"; + private static final String KEY = "skin"; + + /** One entry in the menu: what it is called and how to build it. */ + static final class Choice { + final String id; + final String name; + final String note; + + Choice(String id, String name, String note) { + this.id = id; + this.name = name; + this.note = note; + } + + Skin skin() { + switch (id) { + case "capped": + return Skin.capped(); + case "outline": + return Skin.outline(); + case "machined": + return Skin.machined(); + case "tactile": + return Skin.tactile(); + default: + return Skin.console(); + } + } + } + + /** + * In the order they were built, which is also the order they get further from + * a flat interface and closer to a panel. + */ + static final Choice[] CHOICES = { + new Choice("console", "Console", "read left, press middle, rail right"), + new Choice("tactile", "Tactile", "moulded caps on a dark deck"), + new Choice("machined", "Machined", "milled plate, deep screens"), + new Choice("outline", "Outline", "no material at all"), + new Choice("capped", "Capped", "the first shallow cap"), + }; + + private Appearance() { + } + + static String chosen(Context context) { + return prefs(context).getString(KEY, CHOICES[0].id); + } + + static Choice current(Context context) { + String id = chosen(context); + for (Choice choice : CHOICES) { + if (choice.id.equals(id)) return choice; + } + return CHOICES[0]; + } + + /** + * Put the stored choice into effect. Called before anything is drawn, from + * every entry point that builds views, because {@link Buttons} reads the skin + * at build time and there is no going back over a view that already exists. + */ + static void restore(Context context) { + Buttons.applySkin(current(context).skin()); + } + + /** Remember a choice and apply it. The caller redraws. */ + static void choose(Context context, Choice choice) { + prefs(context).edit().putString(KEY, choice.id).apply(); + Buttons.applySkin(choice.skin()); + } + + private static SharedPreferences prefs(Context context) { + return context.getApplicationContext() + .getSharedPreferences(PREFS, Context.MODE_PRIVATE); + } +} diff --git a/client/app/src/main/java/dev/butschster/linuxterminal/ContextBar.java b/client/app/src/main/java/dev/butschster/linuxterminal/ContextBar.java index b4da618..7486549 100644 --- a/client/app/src/main/java/dev/butschster/linuxterminal/ContextBar.java +++ b/client/app/src/main/java/dev/butschster/linuxterminal/ContextBar.java @@ -1,5 +1,6 @@ package dev.butschster.linuxterminal; +import android.app.Activity; import android.content.Context; import android.graphics.Typeface; import android.text.TextUtils; @@ -7,6 +8,7 @@ import android.view.Gravity; import android.view.View; import android.widget.LinearLayout; +import android.widget.PopupWindow; import android.widget.ScrollView; import android.widget.TextView; @@ -61,6 +63,8 @@ public interface Host { private static final String ICON_MIC = "\ue029"; private static final String ICON_KEYBOARD = "\ue312"; private static final String ICON_STOP = "\ue047"; + /** Material's "palette": the only thing on the bar that changes the bar. */ + private static final String ICON_APPEARANCE = "\ue40a"; /** * The only bytes this file sends on its own. Everything else it types came @@ -87,6 +91,7 @@ public interface Host { private FlowLayout projects; private View browserBox; private View projectBox; + private View commandBox; private LinearLayout chipColumn; private LinearLayout shortcuts; @@ -104,6 +109,10 @@ public interface Host { public ContextBar(Context context, Host host) { super(context); this.host = host; + // Before the first drawable is asked for: every background on this panel is + // built from the skin as the view is constructed, so a skin restored one + // line later would apply to nothing that has already been made. + Appearance.restore(context); Typeface icons = Typeface.createFromAsset(context.getAssets(), "MaterialIcons-Regular.ttf"); this.buttons = new Buttons(context, icons); @@ -303,6 +312,8 @@ public ContextBar(Context context, Host host) { buttons.half("A+", "larger text", v -> host.onFontStep(2)), buttons.half("A\u2212", "smaller text", v -> host.onFontStep(-2)))); + addControl(controls, appearanceKey()); + // Descriptions ride on the buttons themselves. A hint printed somewhere // else — a line under the keys, the path at the top of another panel — is a // hint in the one place the eye is not while it is aiming. @@ -361,6 +372,97 @@ private View pageKey(Glyphs.Kind kind, String bytes, String hint) { return view; } + /** + * The one control on this panel that is about the panel. + * + *

It sits on the strip with the other things that are the client's own and + * are never sent to the shell, and it is on that strip in both + * arrangements — the console rail and the older skins' controls. A way out of a + * skin has to exist inside every skin, or choosing one of them once is choosing + * it forever. + */ + private View appearanceKey() { + return buttons.icon(ICON_APPEARANCE, Buttons.DESTINATION, + "how this panel looks", this::showAppearance); + } + + /** + * The skins, offered where the person wearing the headset is. + * + *

Drawn from this panel's own parts rather than as a {@code PopupMenu}: the + * system menu is a light rectangle with a system typeface, and at half a metre + * it arrives as a piece of another application landing on top of this one. + * + *

Placed at the centre of the bar rather than under the button that opened + * it. The strip it is opened from is a few keys wide and at the edge of the + * window, so a dropdown either overflows the window or is squeezed into the + * strip's width; the panel's middle is somewhere the ray is already pointing. + */ + private void showAppearance(View anchor) { + Skin skin = Buttons.skin(); + + LinearLayout menu = new LinearLayout(getContext()); + menu.setOrientation(VERTICAL); + menu.setBackground(buttons.section(true)); + int pad = buttons.dp(12); + menu.setPadding(pad, pad, pad, pad); + + TextView caption = label("APPEARANCE", skin.heading, 12); + caption.setLetterSpacing(0.10f); + caption.setPadding(buttons.dp(8), 0, 0, buttons.dp(8)); + menu.addView(caption); + + PopupWindow popup = new PopupWindow(menu, buttons.dp(320), + LayoutParams.WRAP_CONTENT, true); + // Dismissed by pressing away from it: a menu with no way out but a choice + // is a menu that cannot be opened out of curiosity. + popup.setOutsideTouchable(true); + + String current = Appearance.chosen(getContext()); + for (Appearance.Choice choice : Appearance.CHOICES) { + menu.addView(appearanceRow(choice, choice.id.equals(current), popup), + new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); + } + + popup.showAtLocation(this, Gravity.CENTER, 0, 0); + } + + private View appearanceRow(Appearance.Choice choice, boolean active, PopupWindow popup) { + Skin skin = Buttons.skin(); + + LinearLayout row = new LinearLayout(getContext()); + row.setOrientation(VERTICAL); + row.setPadding(buttons.dp(8), buttons.dp(8), buttons.dp(8), buttons.dp(8)); + row.setClickable(true); + row.setBackground(buttons.rowSelection()); + + // The mark, not a tick in a second column: the listing on this panel already + // says "this one" with a caret in front of the name, and one language for + // "you are here" is worth more than a prettier tick. + TextView name = label((active ? "▸ " : " ") + choice.name, + active ? skin.accentGo : Buttons.TEXT, 16); + name.setTypeface(Fonts.mono(getContext())); + row.addView(name); + + TextView note = label(" " + choice.note, Buttons.MUTED, 12); + note.setTypeface(Fonts.mono(getContext())); + row.addView(note); + + row.setOnClickListener(v -> { + popup.dismiss(); + if (active) return; + Appearance.choose(getContext(), choice); + // The whole panel is built from the skin as its views are constructed, + // so there is nothing to repaint — the window is made again instead. + // A terminal window already up keeps the look it was built with until + // it is next opened, which is the honest consequence of building the + // look once rather than watching for it. + Context context = getContext(); + if (context instanceof Activity) ((Activity) context).recreate(); + }); + return row; + } + private void addControl(LinearLayout strip, View view) { LayoutParams params = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT); int half = buttons.gap() / 2; @@ -539,7 +641,7 @@ private void buildConsole(Context context) { // on the panel, which is where a label belongs. LinearLayout browser = captioned(context, "directory browser", consoleScreen(context)); - addSection(reading, browser, 0f); + addElastic(reading, browser); browserBox = browser; projects = new FlowLayout(context, 0); @@ -552,17 +654,28 @@ private void buildConsole(Context context) { projectScroll.addView(projects, new ScrollView.LayoutParams( LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); LinearLayout projectBox = captioned(context, "projects", projectScroll); - addSection(reading, projectBox, 0f); + addElastic(reading, projectBox); this.projectBox = projectBox; + // Last in the column and the last thing to give up room, which is the + // opposite of how it was built. + // + // The commands had the weight and the two listings above them did not, so + // the listings took what their contents asked for — up to 230dp each — and + // the commands were handed whatever was left. In a column 460dp tall there + // was nothing left: `claude`, `codex`, `git status` and the rest measured to + // zero height and simply were not on the panel. They had not been removed, + // they had been pushed off the bottom by two directories. LinearLayout commandBox = captioned(context, "commands", null); chipColumn = new LinearLayout(context); chipColumn.setOrientation(VERTICAL); - ScrollView chipScroll = new ScrollView(context); + ScrollView chipScroll = elasticScroll(context, 200); chipScroll.addView(chipColumn, new ScrollView.LayoutParams( LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); - commandBox.addView(chipScroll, new LayoutParams(LayoutParams.MATCH_PARENT, 0, 1f)); - addSection(reading, commandBox, 1f); + commandBox.addView(chipScroll, new LayoutParams( + LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); + addSection(reading, commandBox, 0f); + this.commandBox = commandBox; addView(gap(context, 12), new LayoutParams(buttons.dp(12), LayoutParams.MATCH_PARENT)); @@ -638,14 +751,22 @@ private View consoleScreen(Context context) { * should have had anyway. */ private ScrollView elasticScroll(Context context) { - ScrollView scroll = new ScrollView(context) { + return elasticScroll(context, 230); + } + + /** + * The same, with the ceiling stated. The commands take a lower one: a chip is + * shorter than a listing row, so 200dp is already five or six rows of them, and + * anything past that is a wall of buttons rather than a section of the panel. + */ + private ScrollView elasticScroll(Context context, int ceilingDp) { + return new ScrollView(context) { @Override protected void onMeasure(int widthSpec, int heightSpec) { super.onMeasure(widthSpec, MeasureSpec.makeMeasureSpec( - buttons.dp(230), MeasureSpec.AT_MOST)); + buttons.dp(ceilingDp), MeasureSpec.AT_MOST)); } }; - return scroll; } /** @@ -701,6 +822,9 @@ private View consoleRail(Context context) { buttons.half("A+", "larger text", v -> host.onFontStep(2)), buttons.half("A\u2212", "smaller text", v -> host.onFontStep(-2)))); + rail.addView(appearanceKey(), new LayoutParams( + LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); + hint(rail, null); return rail; } @@ -733,6 +857,25 @@ private View railRocker(Context context, View top, View bottom) { * against the next one for exactly that reason. Routing every add through here * means the gap is applied where it survives. */ + /** + * A section that is as tall as its contents, and is the one that gives way when + * the column is too short for everything on it. + * + *

WRAP_CONTENT with a weight, which is a combination that looks like + * a contradiction and is not: the weight applies to what is left over after + * every child has been measured, and when that leftover is negative — the column + * is over-full — it is taken back from the children that carry weight. So the + * listings stay content-driven while there is room, and are the ones squeezed + * when there is not. A section added with {@link #addSection} keeps its height + * either way. + */ + private void addElastic(LinearLayout column, View section) { + LayoutParams params = new LayoutParams( + LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT, 1f); + params.bottomMargin = buttons.dp(12); + column.addView(section, params); + } + private void addSection(LinearLayout column, View section, float weight) { LayoutParams params = weight > 0 ? new LayoutParams(LayoutParams.MATCH_PARENT, 0, weight) @@ -861,6 +1004,9 @@ private void fillConsole(JSONArray groups) { boolean anyProjects = projects.getChildCount() > 0; browserBox.setVisibility(anywhereToGo ? VISIBLE : GONE); projectBox.setVisibility(anyProjects ? VISIBLE : GONE); + // And by the same rule the section that holds them: a caption over nothing + // is a section that failed to load, which is not what an empty one means. + commandBox.setVisibility(chipColumn.getChildCount() > 0 ? VISIBLE : GONE); hint(chipColumn, where); } From 9d50bffab1288e13325687ce464dbbdd65c9bbb9 Mon Sep 17 00:00:00 2001 From: butschster Date: Wed, 12 Aug 2026 18:17:03 +0400 Subject: [PATCH 15/15] Two more traps written down: the service's environment, and the vanishing glyph MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- CLAUDE.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index b833ae8..14f49de 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -274,6 +274,28 @@ REG_WAIT timeout, including boots that never froze; Sunshine was already on `vaapi` and had quit beforehand; and the Quest is not an MTP device in adb mode, so gvfs never touches it. +**A service's environment is not a terminal's, and two features died of it.** +`systemd --user` starts the server at login with the bare system PATH and with +no `DISPLAY` or `WAYLAND_DISPLAY` — the graphical session imports those *after* +it is already running. So `installed()` could not see `claude`, `codex` or +anything else in `~/.local/bin` and the bar stopped offering them, and the tray +decided there was no desktop and never showed an icon at all. Both worked +perfectly when the server was started from a terminal, which is how they +survived. Ask the shell for the PATH (`$SHELL -ic`, the mode a session is opened +in) and ask the bus for a tray host, not the environment. +[`docs/service.md`](docs/service.md) has the commands that tell the two apart. + +**A drawn glyph lives in the key's background, and swapping that background +takes it with it.** `Buttons.centre()` layers a `Glyphs` drawable over whatever +background the key has, because a compound drawable is laid out beside the text +and lands off-centre on a key with no text. Anything that later replaces the +background — the rocker halves do — empties the key. `Buttons` therefore keeps +the glyph in a `WeakHashMap` so a replacement can put it back. + +The obvious fix is a foreground, and it does not work: `setForeground` on these +`TextView`s is simply not drawn, which emptied every arrow and the backspace key +on the keypad while looking like the correct answer. Do not try it again. + **Signal dispositions survive `exec`.** An earlier server set `SIGCHLD` to `SIG_IGN` to avoid zombies; every descendant that reaps its own children then got `ECHILD` from `waitpid`, and Claude Code's hooks failed with exactly that.