diff --git a/IDEAS.md b/IDEAS.md
index c79ff8f..adea24a 100644
--- a/IDEAS.md
+++ b/IDEAS.md
@@ -59,39 +59,29 @@ hält — dieselbe Liste, die heute schon die CORS-Freigabe steuert. Das ist ein
Sicherheitsentscheidung und keine Kleinigkeit; erst bauen, wenn der Umweg über
den Schreibtisch wirklich nervt.
-### 1.3 App-Schublade mit Symbolen
+### 1.3 App-Schublade mit Symbolen — ✅ halb gebaut (29.08.2026)
-**Die Idee:** ein Knopf, der eine Übersicht aller Apps aufklappt — mit Symbol.
-Die Liste kommt aus HoneyOS. Eine neue App trägt man einmal ein und sie taucht
-überall auf, ohne dass eine App angefasst wird.
+**Gebaut: die geteilte Symbolquelle.** Die Raster liegen in `web/glyphs.json`,
+der Schreibtisch lädt sie, die HoneyBar bekommt sie mit der App-Liste mit
+(siehe [README](README.md)). Damit tragen beide Leisten dieselben Symbole neben
+den App-Namen, und die doppelt abgetippten Wettersymbole sind weg.
-**Stand:** Die Hälfte steht. `honeybar.js` holt die Liste aus `/api/apps`, bringt
-seine Sitzung mit und hat ein Aufklappmenü. Es fehlt die Darstellung als Raster
-mit Symbolen — heute sind es Textknöpfe. Bei zwei Apps geht das noch, ab vier
-wird es eng, am Handy sofort.
+Dazu ein Schalter *Einstellungen → HoneyBar*: Symbole im App-Streifen an oder
+aus, für beide Leisten zugleich. In den Aufklappmenüs und auf den Kacheln
+stehen sie immer — dort ist Platz, und dort sucht man eine App, statt sie im
+Vorbeigehen wiederzuerkennen.
-**Der eigentliche Punkt: die Symbole.** Das Pixelraster lebt in `os.js`
-(`GLYPHS`), die HoneyBar kennt es nicht. Zwei Wege:
+**Offen bleibt die Schublade selbst:** ein Raster aus Symbolen statt einer
+Liste, wenn es viele Apps werden. Heute reicht der Streifen samt Menü; das ist
+erst ab sechs, sieben Apps eine echte Frage.
-- **Pixelglyphen teilen** — `GLYPHS` + `glyphSvg()` in eine eigene Datei, die
- Desktop und HoneyBar beide laden. Kein Upload, kein Speicher, passt zum Look.
- Nachteil: nur die eingebauten Symbole.
-- **Echte Bilder** — pro App eine Bildadresse oder ein Upload in HoneyOS.
- Flexibler, aber: Speicher, Größenbegrenzungen, Auslieferung, und der
- Retro-Look bekommt bunte Fremdkörper.
-
-→ **Empfehlung: Pixelglyphen teilen** — als Erstes, weil es sofort trägt.
-
-**Beschlossen als Planung (25.08.2026): eigene Logos hochladen.** Je App ein
-Bild in HoneyOS hinterlegen, statt nur aus den eingebauten Glyphen zu wählen.
-Nicht als Ersatz für die Pixelglyphen, sondern daneben: Glyphe als Rückfall,
-wenn kein Bild da ist.
-
-**Das Logo gehört ausdrücklich auch in die HoneyBar**, nicht nur auf den
-Schreibtisch — dort ist die Unterscheidung am nötigsten, weil die Knöpfe klein
-sind und nebeneinanderstehen. Damit ist die geteilte Symbolquelle aus 1.3
-Voraussetzung: Beide Seiten müssen dasselbe Bild bekommen, sonst pflegt man es
-zweimal.
+**Ebenfalls offen: eigene Logos hochladen** (beschlossen als Planung,
+25.08.2026). Je App ein Bild in HoneyOS, die Pixelglyphe als Rückfall, wenn
+keins da ist. Das Logo gehört ausdrücklich auch in die HoneyBar — dort ist die
+Unterscheidung am nötigsten, weil die Knöpfe klein sind und nebeneinanderstehen.
+Der Weg dorthin ist durch die geteilte Quelle jetzt kurz: Beide Seiten holen
+ihre Symbole schon aus derselben Antwort, es müsste nur ein Bild an die Stelle
+des Rasters treten können.
Was daran zu bauen ist, in dieser Reihenfolge:
diff --git a/README.md b/README.md
index 564e74b..d2f9ab2 100644
--- a/README.md
+++ b/README.md
@@ -114,6 +114,31 @@ Gespeichert wird die Liste als Ganzes; der Server prüft sie noch einmal und ant
dem, was er wirklich behalten hat. Eine Adresse ohne `https://` kommt als Fehlermeldung
zurück, nicht als kaputter Eintrag.
+## Ein Satz Symbole für alles
+
+Die Pixelraster stehen **an genau einer Stelle**: `web/glyphs.json`, 12×12
+Zeichen je Symbol, ein `#` ist ein Pixel. Der Schreibtisch holt die Datei
+selbst; die HoneyBar in fremden Apps bekommt die Raster **mit der App-Liste
+mitgeliefert** — sie soll eine einzelne Datei ohne zweite Anfrage bleiben, und
+`/api/apps` fragt sie ohnehin. Wer dort ein Symbol ergänzt, kann es sofort im
+Programm-Editor auswählen; abgetippt wird nichts mehr.
+
+Dass das nötig war, hatte einen Anlass: Die Wettersymbole standen einmal in
+`os.js` **und** in `honeybar.js`. Sie liefen auseinander, und niemand merkte es,
+bis das Wetter in der einen Leiste anders aussah als in der anderen.
+
+**Wo die Symbole stehen:**
+
+| | Symbol |
+|---|---|
+| App-Streifen in beiden Leisten | nach Schalter *Einstellungen → HoneyBar* |
+| Aufklappmenü beider Leisten | immer |
+| Kacheln, Schreibtischsymbole, Startmenü | immer |
+
+Der Schalter gilt für **beide** Leisten zugleich — es ist dieselbe Leiste, nur
+an zwei Orten. Er liegt wie alles andere im Server, wirkt also auf jedem Gerät
+und in jeder App, ohne dass eine App neu gebaut wird.
+
## Zwei Betriebsarten, ein Datenstand
| | Terminal-Modus | Desktop-Modus |
@@ -446,8 +471,9 @@ internal/
web/
index.html
css/style.css
+ glyphs.json die Pixelraster — die einzige Quelle, beide Leisten
js/core.js Zustand, Server, Einstellungen, Kleinkram
- js/glyphs.js die Pixelsymbole
+ js/glyphs.js lädt die Raster und baut SVGs daraus
js/os.js die beiden Ansichten, Fenster, Dialoge
js/widgets/ ein Widget je Datei
honeybar/ die HoneyBar für fremde Seiten — bewusst eine Datei
diff --git a/main.go b/main.go
index 615c0e0..1ecbe48 100644
--- a/main.go
+++ b/main.go
@@ -28,6 +28,26 @@ import (
//go:embed all:web
var webFS embed.FS
+// glyphen sind die Pixelraster aus web/glyphs.json. Der Schreibtisch holt sich
+// dieselbe Datei selbst; die HoneyBar in einer fremden App kann das nicht — sie
+// ist bewusst eine einzelne Datei ohne zweite Anfrage. Also reist das Raster
+// mit der App-Liste mit. Eine Quelle, zwei Wege, kein Auseinanderlaufen.
+var glyphen = ladeGlyphen()
+
+func ladeGlyphen() map[string][]string {
+ b, err := webFS.ReadFile("web/glyphs.json")
+ if err != nil {
+ log.Printf("glyphs.json fehlt: %v", err)
+ return nil
+ }
+ var g map[string][]string
+ if err := json.Unmarshal(b, &g); err != nil {
+ log.Printf("glyphs.json unlesbar: %v", err)
+ return nil
+ }
+ return g
+}
+
// Version ist die einzige Stelle, an der sie steht. build.sh liest sie hier aus
// und vergibt denselben Docker-Tag; die Oberfläche holt sie über /api/public.
// Vorher stand sie zusätzlich in os.js und lief mit den Image-Tags auseinander.
@@ -316,6 +336,7 @@ func (s *Server) handleApps(w http.ResponseWriter, r *http.Request, sub string)
writeJSON(w, http.StatusOK, map[string]any{
"apps": s.store.Apps(),
"settings": s.store.SettingsFor(sub),
+ "glyphs": glyphen,
})
}
diff --git a/web/css/style.css b/web/css/style.css
index 29c87b5..3f65a07 100644
--- a/web/css/style.css
+++ b/web/css/style.css
@@ -231,6 +231,13 @@ body {
white-space: nowrap;
}
.mb-app:hover { color: var(--p); border-color: var(--p-dim); }
+/* Symbol und Name in einer Zeile, wie in der HoneyBar. Das Symbol trägt die
+ Textfarbe, also stimmt es auch im umgekehrten Zustand der aktiven App. */
+.mb-app { display: inline-flex; align-items: center; gap: 6px; }
+.mb-ico,
+.sm-ico { display: block; width: 12px; height: 12px; flex: 0 0 auto; }
+.mb-ico svg,
+.sm-ico svg { width: 12px; height: 12px; display: block; }
.mb-app[aria-current="true"] {
color: var(--bg);
background: var(--p);
@@ -816,6 +823,10 @@ body {
text-align: left;
padding: 6px 9px;
cursor: pointer;
+ /* Symbol links, Name daneben — in den Menüs stehen die Symbole immer. */
+ display: flex;
+ align-items: center;
+ gap: 8px;
}
.startmenu button:hover { background: var(--p); color: var(--bg); text-shadow: none; }
.startmenu hr { border: 0; border-top: 1px solid var(--p-faint); margin: 3px 0; }
diff --git a/web/glyphs.json b/web/glyphs.json
new file mode 100644
index 0000000..c4dbf29
--- /dev/null
+++ b/web/glyphs.json
@@ -0,0 +1,212 @@
+{
+ "sonne": [
+ "............",
+ ".....##.....",
+ "..#..##..#..",
+ "...#....#...",
+ "....####....",
+ "..##....##..",
+ "..##....##..",
+ "....####....",
+ "...#....#...",
+ "..#..##..#..",
+ ".....##.....",
+ "............"
+ ],
+ "heiter": [
+ "............",
+ "...##.......",
+ "..####..#...",
+ "...##..###..",
+ "......#####.",
+ "...####...#.",
+ "..#....##.#.",
+ ".##........#",
+ "#..........#",
+ ".##########.",
+ "............",
+ "............"
+ ],
+ "wolke": [
+ "............",
+ "....####....",
+ "...#....#...",
+ "..#......##.",
+ ".##........#",
+ "##.........#",
+ "#..........#",
+ "#..........#",
+ ".##########.",
+ "............",
+ "............",
+ "............"
+ ],
+ "nebel": [
+ "............",
+ "....####....",
+ "..##....##..",
+ ".##.......#.",
+ "#..........#",
+ ".##########.",
+ "............",
+ ".#########..",
+ "............",
+ "..#########.",
+ "............",
+ ".#########.."
+ ],
+ "regen": [
+ "............",
+ "....####....",
+ "...#....#...",
+ "..#......##.",
+ ".##........#",
+ "##.........#",
+ ".##########.",
+ "............",
+ "..#..#..#...",
+ ".#..#..#....",
+ "..#..#..#...",
+ ".#..#..#...."
+ ],
+ "schnee": [
+ "............",
+ "....####....",
+ "...#....#...",
+ "..#......##.",
+ ".##........#",
+ "##.........#",
+ ".##########.",
+ "............",
+ "..#...#...#.",
+ ".#.#.#.#.#..",
+ "..#...#...#.",
+ "............"
+ ],
+ "gewitter": [
+ "............",
+ "....####....",
+ "...#....#...",
+ "..#......##.",
+ ".##........#",
+ "##.........#",
+ ".##########.",
+ "......##....",
+ ".....##.....",
+ "....####....",
+ "......##....",
+ ".....##....."
+ ],
+ "cloud": [
+ "............",
+ "....####....",
+ "...#....#...",
+ "..#......##.",
+ ".##........#",
+ "##.........#",
+ "#..........#",
+ "#..........#",
+ ".##########.",
+ "............",
+ "............",
+ "............"
+ ],
+ "note": [
+ "..########..",
+ "..#......#..",
+ "..#.####.#..",
+ "..#......#..",
+ "..#.####.#..",
+ "..#......#..",
+ "..#.####.#..",
+ "..#......#..",
+ "..#.##...#..",
+ "..#......#..",
+ "..########..",
+ "............"
+ ],
+ "box": [
+ "............",
+ ".##########.",
+ ".#........#.",
+ ".#........#.",
+ ".#...##...#.",
+ ".#..####..#.",
+ ".#..####..#.",
+ ".#...##...#.",
+ ".#........#.",
+ ".#........#.",
+ ".##########.",
+ "............"
+ ],
+ "chip": [
+ "............",
+ "..#.#.#.#...",
+ ".##########.",
+ ".#........#.",
+ "##.######.##",
+ ".#.#....#.#.",
+ "##.#....#.##",
+ ".#.######.#.",
+ ".#........#.",
+ ".##########.",
+ "..#.#.#.#...",
+ "............"
+ ],
+ "gear": [
+ "....####....",
+ "..#.####.#..",
+ "..########..",
+ ".####..####.",
+ "###......###",
+ "##........##",
+ "##........##",
+ "###......###",
+ ".####..####.",
+ "..########..",
+ "..#.####.#..",
+ "....####...."
+ ],
+ "globe": [
+ "............",
+ "...######...",
+ "..#..#...#..",
+ ".#...#....#.",
+ "############",
+ "#....#.....#",
+ "#....#.....#",
+ "############",
+ ".#...#....#.",
+ "..#..#...#..",
+ "...######...",
+ "............"
+ ],
+ "disk": [
+ "............",
+ "############",
+ "#..######..#",
+ "#..#....#..#",
+ "#..#....#..#",
+ "#..######..#",
+ "#..........#",
+ "#.########.#",
+ "#.#......#.#",
+ "#.#......#.#",
+ "############",
+ "............"
+ ],
+ "folder": [
+ "............",
+ ".####.......",
+ "#....#......",
+ "#.....######",
+ "#..........#",
+ "#..........#",
+ "#..........#",
+ "#..........#",
+ "#..........#",
+ "############",
+ "............",
+ "............"
+ ]
+}
diff --git a/web/honeybar/honeybar.js b/web/honeybar/honeybar.js
index 88749b3..a3090a1 100644
--- a/web/honeybar/honeybar.js
+++ b/web/honeybar/honeybar.js
@@ -56,6 +56,7 @@
let apps = DEFAULT_APPS;
let settings = null;
+ let glyphs = {};
// Was die Seite oben schon hatte, bevor die HoneyBar sie heruntergeschoben hat.
let urspruenglichesPolster = null;
@@ -69,22 +70,13 @@
const SAFE = "env(safe-area-inset-top, 0px)";
const HOEHE = "calc(" + BAR_H + "px + " + SAFE + ")";
- /* Wettersymbole als Pixelraster, dieselben Namen wie in weather.go.
- Sie stehen auch in os.js — bis die geteilte Symbolquelle (1.3) gebaut ist,
- ist das die kleinere Sünde: Die HoneyBar soll eine einzelne Datei bleiben,
- die eine fremde App mit einer Zeile einbindet. */
- const WX = {
- sonne: "............;.....##.....;..#..##..#..;...#....#...;....####....;..##....##..;..##....##..;....####....;...#....#...;..#..##..#..;.....##.....;............",
- heiter: "............;...##.......;..####..#...;...##..###..;......#####.;...####...#.;..#....##.#.;.##........#;#..........#;.##########.;............;............",
- wolke: "............;....####....;...#....#...;..#......##.;.##........#;##.........#;#..........#;#..........#;.##########.;............;............;............",
- nebel: "............;....####....;..##....##..;.##.......#.;#..........#;.##########.;............;.#########..;............;..#########.;............;.#########..",
- regen: "............;....####....;...#....#...;..#......##.;.##........#;##.........#;.##########.;............;..#..#..#...;.#..#..#....;..#..#..#...;.#..#..#....",
- schnee: "............;....####....;...#....#...;..#......##.;.##........#;##.........#;.##########.;............;..#...#...#.;.#.#.#.#.#..;..#...#...#.;............",
- gewitter: "............;....####....;...#....#...;..#......##.;.##........#;##.........#;.##########.;......##....;.....##.....;....####....;......##....;.....##.....",
- };
-
- function wxSvg(name) {
- const grid = (WX[name] || WX.wolke).split(";");
+ /* Alle Pixelraster kommen mit der App-Liste vom Server (web/glyphs.json).
+ Frueher standen die Wettersymbole hier noch einmal abgetippt — genau das
+ war schon einmal auseinandergelaufen. Diese Datei bleibt trotzdem eine
+ einzelne ohne zweite Anfrage: Sie fragt ohnehin nach der App-Liste, und
+ die bringt die Raster einfach mit. */
+ function rasterSvg(grid) {
+ if (!grid) return "";
let d = "";
grid.forEach((row, y) => {
let run = 0;
@@ -93,7 +85,7 @@
else if (run) { d += `
gilt für den App-Streifen hier und in jeder App
+