From 386ad9d7a6c1f5f183c461d59f7b81a239761865 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 7 Aug 2026 15:39:45 +0000 Subject: [PATCH 1/2] fix(kiosk): let left clicks reach the editor again The Openbox mouse policy bound a bare Left press in the Frame context. That one line swallowed every left click in VS Code while the right and middle buttons kept working, because Openbox grabs the two window contexts with different pointer modes (openbox/mouse.c, mouse_grab_for_client): Frame grab on the frame window, GrabModeAsync Client grab on the client window, GrabModeSync Only the synchronous grab is replayed - mouse_replay_pointer() calls XAllowEvents(ReplayPointer) "to send the event through to the client", and mouse.c arms it for the client context alone. The asynchronous grab on the frame consumes the press. On top of that the frame is the client window's parent, and X activates the outermost of two competing passive grabs, so the Frame binding also stopped the Client one from ever firing. Drop the Frame context. Click-to-focus already came from the Client bindings, which are what upstream's rc.xml uses bare buttons for; upstream binds only modifier combinations (A-Left and friends) under Frame, for exactly this reason. The comment left behind says why, so it does not get added back. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01K67j2gUu6CKtsw7RisXrFc --- rootfs-common/etc/skel/.config/openbox/rc.xml | 31 ++++++++++++++----- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/rootfs-common/etc/skel/.config/openbox/rc.xml b/rootfs-common/etc/skel/.config/openbox/rc.xml index aa9c996..f9e9cc7 100644 --- a/rootfs-common/etc/skel/.config/openbox/rc.xml +++ b/rootfs-common/etc/skel/.config/openbox/rc.xml @@ -100,14 +100,31 @@ 8 200 0 - - - - - - - + + From 2b64a3a09e0698b74bf4a289f05ac7cc026e6fa0 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 7 Aug 2026 15:39:57 +0000 Subject: [PATCH 2/2] feat(shell): give the power button a real power symbol The tray's power button drew $(circle-slash), a circle with a diagonal bar through it - a "no entry" sign, not a power symbol. None of codicon's 753 icons is one, so the glyph now comes from a one-glyph font of the shell's own, registered through contributes.icons and used as $(vscodeos-power). It is IEC 5009: a ring broken at the top with a bar rising through the gap. media/icons/build-font.py generates it to codicon's own metrics - 300 units per em, a 19-unit stroke, optically centred on the 16 px box - so it sits at the same size, weight and baseline as the $(plug) and $(volume) glyphs beside it. The woff is 956 bytes and is committed; the generator needs fonttools and only runs if the glyph itself changes, and it pins the head timestamps so a rebuild of an unchanged glyph is a byte-identical file. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01K67j2gUu6CKtsw7RisXrFc --- extension/README.md | 11 +- extension/media/icons/build-font.py | 183 ++++++++++++++++++++++ extension/media/icons/vscodeos-icons.woff | Bin 0 -> 956 bytes extension/package.json | 11 +- extension/src/statusbar/index.ts | 8 +- scripts/build-extension.sh | 4 + 6 files changed, 212 insertions(+), 5 deletions(-) create mode 100644 extension/media/icons/build-font.py create mode 100644 extension/media/icons/vscodeos-icons.woff diff --git a/extension/README.md b/extension/README.md index 2ea03fa..5d7d787 100644 --- a/extension/README.md +++ b/extension/README.md @@ -63,9 +63,18 @@ src/ media/ src/ one TypeScript entry point per page, shared code in src/lib css/ one stylesheet, all colours from --vscode-* variables - icons/ container and panel icons + icons/ container and panel icons, plus vscodeos-icons.woff ``` +`vscodeos-icons.woff` exists because **none of codicon's 753 icons is a power +symbol** — the nearest is `circle-slash`, a "no entry" sign — so the tray's power +button draws IEC 5009 from a one-glyph font of our own, registered under +`contributes.icons` and used as `$(vscodeos-power)`. It is generated by +`media/icons/build-font.py` to codicon's metrics (300 units per em, 19-unit +stroke) so it sits level with the `$(plug)` and `$(volume)` glyphs beside it. The +font is committed; the generator needs `fonttools` and only runs if the glyph +changes. + `src/webview/protocol.ts` is imported by both sides, so a change to a message shape is a compile error in the webview that consumes it. diff --git a/extension/media/icons/build-font.py b/extension/media/icons/build-font.py new file mode 100644 index 0000000..5f27435 --- /dev/null +++ b/extension/media/icons/build-font.py @@ -0,0 +1,183 @@ +#!/usr/bin/env python3 +"""Generate vscodeos-icons.woff, the shell's own icon font. + +VS Code's codicon set has no power symbol - the closest thing in all 753 names +is `circle-slash`, a "no entry" sign - so the tray's power button draws its +glyph from this font instead, registered through `contributes.icons` in +package.json. VS Code accepts woff, woff2 or ttf there. + +The glyph is IEC 5009: a ring broken at the top with a vertical bar rising +through the gap. It is drawn to codicon's own metrics so it sits level with the +`$(plug)` and `$(volume)` glyphs beside it in the status bar: + + units per em 300 (a 16 px icon box, so 18.75 units = 1 px) + ascender 300, descender 0 + advance width 300 + stroke 19 units, ~1 px, matching codicon's outlines + bounding box x 24..276, y 14..286, optically centred on (150, 150) + +Arcs are approximated with quadratic Beziers in 15 degree steps, which puts the +worst-case radial error at 0.005 units - four thousandths of a pixel. + +Run after changing anything above; the woff is committed, so a normal build +never needs this script or Python at all: + + pip install fonttools brotli + python3 extension/media/icons/build-font.py +""" + +from __future__ import annotations + +import math +from pathlib import Path + +from fontTools.fontBuilder import FontBuilder +from fontTools.pens.ttGlyphPen import TTGlyphPen + +UPM = 300 +CENTER_X = 150.0 +RING_CENTER_Y = 140.0 +STROKE = 19.0 +R_OUT = 126.0 +R_IN = R_OUT - STROKE +R_MID = (R_OUT + R_IN) / 2 +CAP = STROKE / 2 + +# Half-width of the gap at 12 o'clock. The bar subtends about 5 degrees at the +# inner radius, so 21 leaves a clear 1 px of daylight on either side of it. +GAP_HALF_DEG = 21.0 + +BAR_BOTTOM = 145.0 # centreline; the round cap reaches 9.5 further down +BAR_TOP = 276.5 # ...and 9.5 further up, to 286, clear of the ring at 266 + +STEP_DEG = 15.0 # maximum sweep of a single quadratic segment + +# 2024-01-01, in the seconds-since-1904 that TrueType's head table counts in. +EPOCH = 1704067200 + 2082844800 + + +def arc(pen_points: list, cx: float, cy: float, radius: float, + start_deg: float, end_deg: float) -> None: + """Append a circular arc as (off-curve, on-curve) quadratic segments. + + The control point of a segment sits where the two end tangents meet, at + radius / cos(half the sweep) - the standard quadratic arc approximation. + """ + sweep = end_deg - start_deg + steps = max(1, math.ceil(abs(sweep) / STEP_DEG)) + delta = sweep / steps + half = math.radians(delta / 2) + control_radius = radius / math.cos(half) + + for i in range(steps): + a0 = math.radians(start_deg + delta * i) + a1 = math.radians(start_deg + delta * (i + 1)) + mid = (a0 + a1) / 2 + pen_points.append(((cx + control_radius * math.cos(mid), + cy + control_radius * math.sin(mid)), False)) + pen_points.append(((cx + radius * math.cos(a1), + cy + radius * math.sin(a1)), True)) + + +def broken_ring() -> list: + """The ring: outer edge round, a round cap, the inner edge back, a cap.""" + start = 90 + GAP_HALF_DEG + end = 90 - GAP_HALF_DEG + 360 + + points: list = [] + # Outer edge, anticlockwise from one side of the gap to the other. + points.append(((CENTER_X + R_OUT * math.cos(math.radians(start)), + RING_CENTER_Y + R_OUT * math.sin(math.radians(start))), True)) + arc(points, CENTER_X, RING_CENTER_Y, R_OUT, start, end) + + # Round the end: a half turn about the point on the centreline, taking the + # outline from the outer edge round to the inner one. + end_x = CENTER_X + R_MID * math.cos(math.radians(end)) + end_y = RING_CENTER_Y + R_MID * math.sin(math.radians(end)) + arc(points, end_x, end_y, CAP, end, end + 180) + + # Inner edge, back the way we came. + arc(points, CENTER_X, RING_CENTER_Y, R_IN, end, start) + + # ...and round the start the same way. + start_x = CENTER_X + R_MID * math.cos(math.radians(start)) + start_y = RING_CENTER_Y + R_MID * math.sin(math.radians(start)) + arc(points, start_x, start_y, CAP, start + 180, start + 360) + + return points + + +def bar() -> list: + """The vertical stroke: a stadium, drawn anticlockwise like the ring.""" + points: list = [((CENTER_X + CAP, BAR_BOTTOM), True), + ((CENTER_X + CAP, BAR_TOP), True)] + arc(points, CENTER_X, BAR_TOP, CAP, 0, 180) + points.append(((CENTER_X - CAP, BAR_BOTTOM), True)) + arc(points, CENTER_X, BAR_BOTTOM, CAP, 180, 360) + return points + + +def draw(pen: TTGlyphPen, contour: list) -> None: + """Feed one closed contour of (point, on_curve) pairs to a TrueType pen.""" + pen.moveTo(contour[0][0]) + segment: list = [] + for point, on_curve in contour[1:]: + segment.append(point) + if on_curve: + if len(segment) == 1: + pen.lineTo(segment[0]) + else: + pen.qCurveTo(*segment) + segment = [] + if segment: + pen.qCurveTo(*segment, None) + pen.closePath() + + +def main() -> None: + pen = TTGlyphPen(None) + draw(pen, broken_ring()) + draw(pen, bar()) + power = pen.glyph() + + blank = TTGlyphPen(None).glyph() + + builder = FontBuilder(UPM, isTTF=True) + builder.setupGlyphOrder(['.notdef', 'power']) + builder.setupCharacterMap({0xE000: 'power'}) + builder.setupGlyf({'.notdef': blank, 'power': power}) + + # A glyph's left side bearing has to be its own xMin: rasterisers phase the + # outline by the difference between the two, so getting this wrong slides + # the icon sideways rather than failing outright. + glyf = builder.font['glyf'] + builder.setupHorizontalMetrics({ + name: (UPM, getattr(glyf[name], 'xMin', 0)) for name in ('.notdef', 'power') + }) + builder.setupHorizontalHeader(ascent=UPM, descent=0, lineGap=0) + builder.setupNameTable({ + 'familyName': 'VS Code OS Icons', + 'styleName': 'Regular', + 'psName': 'VSCodeOSIcons-Regular', + 'version': '1.0', + 'copyright': 'MIT licensed, part of VS Code OS', + }) + builder.setupOS2(sTypoAscender=UPM, sTypoDescender=0, sTypoLineGap=0, + usWinAscent=UPM, usWinDescent=0, achVendID='VSOS') + # Format 2.0, so the file names its own glyph and stays inspectable. + builder.setupPost(keepGlyphNames=True) + + # head stamps the build time by default, which would make every run produce + # a different binary for the same glyph. Pin it, so re-running this script + # after an unrelated edit shows up as no diff at all. + head = builder.font['head'] + head.created = head.modified = EPOCH + + builder.font.flavor = 'woff' + out = Path(__file__).with_name('vscodeos-icons.woff') + builder.save(out) + print(f'{out} ({out.stat().st_size} bytes)') + + +if __name__ == '__main__': + main() diff --git a/extension/media/icons/vscodeos-icons.woff b/extension/media/icons/vscodeos-icons.woff new file mode 100644 index 0000000000000000000000000000000000000000..92124cb38c75319352e2c09b2524d6cb3bc9d006 GIT binary patch literal 956 zcmXT-cXMN4WB>x@Jq%nRnl%T)2eDCsf3Ut00|R3MP*e+u6O7C~HYDdJ768SjK*dZL zcpe0&=TxQv#ZCaleljpHvE7>e!8ao{F$Ku~0>ow@th{kyKn73{s7?pSmjPlGrbtHD zjNFn6pjZ!(&j!Mb&I}AW`N<%42jF7N3=Fx66(D;(fMQ}mtiZs^uqQ7uHx+2#5g`9A zP%iuRv0XO{@{3D=ep>=nr&Cay^$6@gpr4zTTyTizxB1Gz40Z#h2fno9^o<}{4czSqNGzc>ApJG@9l}kvv!Fq%zK}?E) zLFhfhAqKFXv@3~o4jedj;E)50fJ0I{>u+{r0~-b>H(3FBr$r2`Y-|iWpRptWT?`5d zafqITgrtOo1hynWg_9i1A|*r`Ot=M@e)3EF``De%6=BzN@;=Cokf=ekfdd6351MjB~grT0D>4tUc2>nCBeh6;mbEiEIm+ zN;v|1&aJ)`#iMD|{Y2-GOt3?C=-b#HmrWDH3aafBQunOVmKQG0n`iE+b|}G{Mdj3| zH)b;}UH6-5JaqE#j3`t$pEiXGeW@ruS~47|z(#K&H8qYCV=M6__+>OLYA~|Bu`;Ka=kn zD!UdW$UVQi^Zhx41G4!lJg2v?`&9&(f9Q}Fn|M6tqfF%?W&Kx^Uq_d&Pnjs}yU$&& z@VxtzjcRtv{uPh1=jN@oo3ig^t?Aa}i*wi9jeM7LHSC^IxuW;!Z7eqGWS;jxn zlSz5!KF@Wosb+L+T=9Y_`W{vl+MzYHN97C z)1S7lQ7x|Zp7C=o$G`6CZ`c3x-ifzX0Q)aFA?W~r!>1mfHLO3Mu5@K!k7Tl9U;qGg CZ&W4# literal 0 HcmV?d00001 diff --git a/extension/package.json b/extension/package.json index 7eaaeaa..1e71729 100644 --- a/extension/package.json +++ b/extension/package.json @@ -31,6 +31,15 @@ } }, "contributes": { + "icons": { + "vscodeos-power": { + "description": "Power (IEC 5009)", + "default": { + "fontPath": "./media/icons/vscodeos-icons.woff", + "fontCharacter": "\\E000" + } + } + }, "viewsContainers": { "activitybar": [ { @@ -70,7 +79,7 @@ "command": "vscodeos.power.menu", "title": "Power…", "category": "VS Code OS", - "icon": "$(circle-slash)" + "icon": "$(vscodeos-power)" }, { "command": "vscodeos.power.shutdown", diff --git a/extension/src/statusbar/index.ts b/extension/src/statusbar/index.ts index de67773..a3375cc 100644 --- a/extension/src/statusbar/index.ts +++ b/extension/src/statusbar/index.ts @@ -54,9 +54,11 @@ export class StatusBar implements vscode.Disposable { this.clock = this.create('clock', PRIORITY.clock, 'vscodeos.calendar.show', 'Date and time'); this.power = this.create('power', PRIORITY.power, 'vscodeos.power.menu', 'Power'); - // No codicon is a power symbol; circle-slash is the closest that is - // guaranteed to exist in every VS Code build. - this.power.text = '$(circle-slash)'; + // No codicon is a power symbol - the nearest of the 753 is circle-slash, + // which is a "no entry" sign - so this one comes from the extension's own + // icon font, registered under contributes.icons in package.json. It is + // drawn to codicon's metrics, so it sits level with its neighbours. + this.power.text = '$(vscodeos-power)'; this.power.tooltip = new vscode.MarkdownString('**Power** — sleep, restart or shut down'); this.music.on('change', (state: NowPlaying | undefined) => this.renderMedia(state)); diff --git a/scripts/build-extension.sh b/scripts/build-extension.sh index 865daf8..031a804 100755 --- a/scripts/build-extension.sh +++ b/scripts/build-extension.sh @@ -117,7 +117,11 @@ install -m 0644 "${SRC_DIR}"/dist/*.js "${TARGET}/dist/" install -d -m 0755 "${TARGET}/media/dist" "${TARGET}/media/css" "${TARGET}/media/icons" install -m 0644 "${SRC_DIR}"/media/dist/*.js "${TARGET}/media/dist/" install -m 0644 "${SRC_DIR}"/media/css/*.css "${TARGET}/media/css/" +# The svg files are container/panel icons; the woff is the icon font behind +# $(vscodeos-power). build-font.py, which generates it, is deliberately not +# shipped - the font is committed and a build never regenerates it. install -m 0644 "${SRC_DIR}"/media/icons/*.svg "${TARGET}/media/icons/" +install -m 0644 "${SRC_DIR}"/media/icons/*.woff "${TARGET}/media/icons/" version="$(node -p "require('${TARGET}/package.json').version")" msg "VsCodeOsCore ${version} staged in ${TARGET} ($(du -sh "${TARGET}" | cut -f1))"