Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
7877b48
The bar stops offering cd to a program that owns the terminal
butschster Aug 8, 2026
41f67ff
Five panels to choose between, and a font that was never chosen
butschster Aug 8, 2026
8fe49a9
A window onto the bar that needs no server, and the review it produced
butschster Aug 8, 2026
3332d5b
Green goes back to meaning go, and a heading can be read again
butschster Aug 8, 2026
6c63d8d
Keys that belong together share one cut-out, and Enter stops being a key
butschster Aug 8, 2026
5fa5b50
The hover glow fires, because it is listened for where the ray actual…
butschster Aug 8, 2026
b478501
Enter is seated in the plate, and the return mark stops being a blob
butschster Aug 8, 2026
0c5dbbf
Enter is marked like the control it is
butschster Aug 8, 2026
598c882
Enter lies in a milled slot, sized to itself
butschster Aug 8, 2026
e2b419e
Enter is a sculpted keycap, and it is made of whatever panel it sits on
butschster Aug 8, 2026
8810282
The console arrangement becomes the bar, and the bar opens as a strip…
butschster Aug 8, 2026
0467e8c
The gallery is gone: the panel is the product now
butschster Aug 12, 2026
74eb5d5
The bar offers what a shell can run, and the tray icon comes back
butschster Aug 12, 2026
0fcfdc3
The commands stay on the panel, and the panel's look is a setting
butschster Aug 12, 2026
bec4444
Merge main: the tool tables became files while this branch redrew the…
butschster Aug 12, 2026
9d50bff
Two more traps written down: the service's environment, and the vanis…
butschster Aug 12, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
22 changes: 21 additions & 1 deletion client/app/NOTICE.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
14 changes: 14 additions & 0 deletions client/app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,20 @@ android {
signingConfig = signingConfigs.findByName("release")
?: signingConfigs.getByName("debug")
}

// 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 change.
create("concept") {
initWith(getByName("debug"))
isDebuggable = true
signingConfig = signingConfigs.findByName("release")
?: signingConfigs.getByName("debug")
}
}

compileOptions {
Expand Down
16 changes: 16 additions & 0 deletions client/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,22 @@
android:label="terminal bar"
android:windowSoftInputMode="adjustResize|stateHidden"
android:configChanges="density|keyboard|keyboardHidden|navigation|orientation|screenLayout|screenSize|uiMode">
<!-- Tall enough to open on a layout that needs the height, and with a
floor low enough that the bar can still be squeezed into a strip
when that is what the moment wants.

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. -->
<layout
android:defaultWidth="1150dp"
android:defaultHeight="460dp"
Expand Down
Binary file added client/app/src/main/assets/DejaVuSansMono.ttf
Binary file not shown.
Binary file not shown.
104 changes: 104 additions & 0 deletions client/app/src/main/java/dev/butschster/linuxterminal/Appearance.java
Original file line number Diff line number Diff line change
@@ -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.
*
* <p>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.
*
* <p>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.
*
* <p>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);
}
}
Loading
Loading