Skip to content

Commit dfd06d0

Browse files
committed
Fix GUI terminal newline and release gate
1 parent 364a4df commit dfd06d0

10 files changed

Lines changed: 373 additions & 19 deletions

File tree

.beads/issues.jsonl

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
{"id":"pythonos-69y","title":"Fix GUI terminal CRLF and release workflow","description":"Repair desktop terminal carriage-return/newline behavior, add low-risk GUI bridge performance optimizations including KVM auto acceleration where available, and add a nanolang-style validation/release workflow that validates local sources and CI before tagging a release.","status":"in_progress","priority":1,"issue_type":"bug","assignee":"Jordan Hubbard","owner":"jordanhubbard@gmail.com","created_at":"2026-05-05T23:21:41Z","created_by":"Jordan Hubbard","updated_at":"2026-05-05T23:21:55Z","started_at":"2026-05-05T23:21:55Z","dependency_count":0,"dependent_count":0,"comment_count":0}
12
{"id":"pythonos-cka","title":"Repair desktop Terminal editing and output","description":"Desktop Terminal windows display initial shell text but interactive command output and line editing/history do not behave correctly. Screenshot shows prompt redraw/control bytes rendered into the window, duplicate prompt artifacts, and multiple Terminal instances receiving or corrupting input. Fix TextWin/Terminal so linenoise redraw sequences are interpreted inside the terminal window, only the focused terminal receives keyboard input, output is visible, and history/arrows work.","status":"closed","priority":1,"issue_type":"bug","assignee":"Jordan Hubbard","owner":"jordanhubbard@gmail.com","created_at":"2026-05-05T22:49:22Z","created_by":"Jordan Hubbard","updated_at":"2026-05-05T22:58:36Z","started_at":"2026-05-05T22:49:48Z","closed_at":"2026-05-05T22:58:36Z","close_reason":"Fixed TextWin async line-edit key mapping, applied linenoise CSI redraw commands inside the terminal surface, and focused newly-created compositor windows so keyboard input routes to the visible terminal.","dependency_count":0,"dependent_count":0,"comment_count":0}
23
{"id":"pythonos-ykr","title":"Fix native TCP GUI performance and Terminal routing","description":"Native TCP run-gui reaches the desktop, but interactive graphics are still slow because TCP/IP path logging may be going to serial in hot paths. The desktop Terminal app also routes I/O to the kernel console session instead of confining input/output to the Terminal window. Disable noisy TCP/IP serial debug output on the GUI bridge path and repair Terminal app I/O routing.","status":"closed","priority":1,"issue_type":"bug","assignee":"Jordan Hubbard","owner":"jordanhubbard@gmail.com","created_at":"2026-05-05T19:53:04Z","created_by":"Jordan Hubbard","updated_at":"2026-05-05T19:56:17Z","started_at":"2026-05-05T19:53:21Z","closed_at":"2026-05-05T19:56:17Z","close_reason":"Gated TCP/IP packet logs behind trace flags and fixed Terminal linenoise raw output so window I/O no longer falls back to serial.","dependency_count":0,"dependent_count":0,"comment_count":0}
34
{"id":"pythonos-trh","title":"Refactor run-gui to supervised TCP bridge","description":"run-gui currently starts QEMU and pythonos_bridge as separate peers over a private Unix socket, then injects py_desktop() into the guest shell. This can boot without creating a host window and does not support running the host API proxy on another machine. Refactor the run-gui/bridge path toward a supervised TCP transport so the bridge can run locally or remotely and the guest side can connect explicitly instead of relying on shell injection.","status":"closed","priority":1,"issue_type":"feature","assignee":"Jordan Hubbard","owner":"jordanhubbard@gmail.com","created_at":"2026-05-05T19:14:52Z","created_by":"Jordan Hubbard","updated_at":"2026-05-05T19:29:44Z","started_at":"2026-05-05T19:16:35Z","closed_at":"2026-05-05T19:29:44Z","close_reason":"Implemented run-gui supervised TCP bridge, removed REPL py_desktop injection, added fw_cfg guest auto-start, and replaced invalid arm64 second-PL011 bridge path with virtio-console.","dependency_count":0,"dependent_count":0,"comment_count":0}
@@ -74,5 +75,5 @@
7475
{"id":"pythonos-bjr.7","title":"Run examples/pthread_coverage.py on arm64 (no skip needed)","description":"examples/pthread_coverage.py was written for the x86 SMP no-GIL stack. Once arm64 supports pthread_create/join via this epic's other children, the script should run on arm64 with no arch gating. If a separate, immediate workaround adds a 'skip on arm64' helper to the script (a reasonable pre-arm64-substrate stop-gap), this issue is the cleanup that removes the helper once arm64 lands.","acceptance_criteria":"examples/pthread_coverage.py runs on arm64 with the same six sections and reports 'pthread coverage done passed=6/6' under the arm64 smoke runner introduced in the sibling beads.","status":"closed","priority":3,"issue_type":"task","owner":"jordanh@nvidia.com","created_at":"2026-04-30T00:51:55Z","created_by":"Jordan Hubbard","updated_at":"2026-04-30T01:36:32Z","closed_at":"2026-04-30T01:36:32Z","close_reason":"arm64 boot smoke test (tests/smoke_test_arm64.py) verifies the substrate end-to-end via serial markers: with -smp 2, the kernel reaches Py_Initialize done, kernel.boot main loop, and PL011 serial input ready. pthread_create on arm64 is no longer arch-conditional and would dispatch to the AP brought up by smp_init. Running examples/pthread_coverage.py as a scripted interactive Python program on arm64 is gated on having a host-driveable Python entry point — arm64 currently has only a PL011 serial REPL (no PCI -\u003e no TCP REPL). That follow-up tracked separately if/when arm64 networking lands.","dependencies":[{"issue_id":"pythonos-bjr.7","depends_on_id":"pythonos-bjr","type":"parent-child","created_at":"2026-04-29T17:51:54Z","created_by":"Jordan Hubbard","metadata":"{}"},{"issue_id":"pythonos-bjr.7","depends_on_id":"pythonos-bjr.3","type":"blocks","created_at":"2026-04-29T17:52:12Z","created_by":"Jordan Hubbard","metadata":"{}"},{"issue_id":"pythonos-bjr.7","depends_on_id":"pythonos-bjr.4","type":"blocks","created_at":"2026-04-29T17:52:13Z","created_by":"Jordan Hubbard","metadata":"{}"},{"issue_id":"pythonos-bjr.7","depends_on_id":"pythonos-bjr.6","type":"blocks","created_at":"2026-04-29T17:52:14Z","created_by":"Jordan Hubbard","metadata":"{}"},{"issue_id":"pythonos-bjr.7","depends_on_id":"pythonos-hyg","type":"blocks","created_at":"2026-04-29T18:20:05Z","created_by":"Jordan Hubbard","metadata":"{}"}],"dependency_count":4,"dependent_count":0,"comment_count":0}
7576
{"id":"pythonos-bjr.6","title":"arm64 smoke test runner (qemu-system-aarch64 + TCP REPL)","description":"tests/smoke_test.py is hardcoded to qemu-system-x86_64 and the x86 ISO. The project has no automated arm64 smoke test; arm64 verification is interactive via 'make run-arm64'. Once arm64 has a TCP REPL or equivalent host-driven entry point, add a sibling host-side runner so the same Python expression / example assertions are validated on both archs in CI. The arm64 ELF is already produced by 'make arm64'.","acceptance_criteria":"A 'make test-arm64' target boots the arm64 ELF under QEMU virt, drives a host-controlled REPL or scripted command stream, and asserts the same critical markers tests/smoke_test.py asserts (boot, _hal selftest if present, ls / cat / examples). Pythonos-bjr child issues that need an arm64 verification path can land assertions here.","status":"closed","priority":3,"issue_type":"task","assignee":"Jordan Hubbard","owner":"jordanh@nvidia.com","created_at":"2026-04-30T00:51:54Z","created_by":"Jordan Hubbard","updated_at":"2026-04-30T01:21:29Z","started_at":"2026-04-30T00:55:52Z","closed_at":"2026-04-30T01:21:29Z","close_reason":"tests/smoke_test_arm64.py + 'make test-arm64' target landed. Connects to TCP REPL on port 5556, runs the curated subset of expression cases that work on arm64 (no HDA, no PCI), plus pthread/linenoise selftests. The runner already correctly detects the pythonos-hyg hang.","dependencies":[{"issue_id":"pythonos-bjr.6","depends_on_id":"pythonos-bjr","type":"parent-child","created_at":"2026-04-29T17:51:53Z","created_by":"Jordan Hubbard","metadata":"{}"}],"dependency_count":0,"dependent_count":1,"comment_count":0}
7677
{"id":"pythonos-bjr.5","title":"Drop ARCH_ARM64 skip in _hal.pthread_attr_selftest","description":"src/hal/hal.c's pthread_attr_selftest() skips the create-with-attr / join-with-attr cases under '#ifdef ARCH_ARM64' because pthread_create returns ENOSYS there. Once arm64 pthread_create is functional, drop the gate so all 22 cases run on both archs.","acceptance_criteria":"src/hal/hal.c contains no ARCH_ARM64 conditional inside py_pthread_attr_selftest. Both archs report cases=22 from _hal.pthread_attr_selftest().","status":"closed","priority":3,"issue_type":"task","assignee":"Jordan Hubbard","owner":"jordanh@nvidia.com","created_at":"2026-04-30T00:51:53Z","created_by":"Jordan Hubbard","updated_at":"2026-04-30T01:21:28Z","started_at":"2026-04-30T01:04:35Z","closed_at":"2026-04-30T01:21:28Z","close_reason":"src/hal/hal.c py_pthread_attr_selftest no longer carries the ARCH_ARM64 skip around pthread_create+join. Both archs run the full 22-case matrix once pythonos-hyg unblocks arm64 boot.","dependencies":[{"issue_id":"pythonos-bjr.5","depends_on_id":"pythonos-bjr","type":"parent-child","created_at":"2026-04-29T17:51:52Z","created_by":"Jordan Hubbard","metadata":"{}"},{"issue_id":"pythonos-bjr.5","depends_on_id":"pythonos-bjr.3","type":"blocks","created_at":"2026-04-29T17:52:12Z","created_by":"Jordan Hubbard","metadata":"{}"}],"dependency_count":1,"dependent_count":0,"comment_count":0}
77-
{"_type":"memory","key":"kernel-cpython-3-14-dynamic-compile-of-compound","value":"kernel CPython 3.14 dynamic compile() of compound statements (def/class/for/if/import/async) fails non-deterministically with 'cannot delete function call' or 'invalid syntax'. Frozen modules work because their bytecode is pre-serialized. Workaround: kernel/shell.py:_try_precompiled_script short-circuits /examples/*.py and /bin/*.py to __import__(frozen_module). User scripts in /home blocked. See pythonos-0ta + tools/probe_compile.py reproducer."}
7878
{"_type":"memory","key":"hard-won-lesson-src-libc-string-c-stubs","value":"Hard-won lesson: src/libc/string.c stubs are exercised by every CPython codepath. A subtly wrong strncmp (returning byte-after-n diff instead of 0 on full prefix match) silently broke ALL keyword recognition because the parser table comparisons always matched 'X\\0' vs 'X\u003cnext-source-char\u003e'. Symptom was 'cannot delete function call' / 'identifier field can't represent True constant'. Verify libc string functions against glibc test suite if anything weird happens in CPython internals."}
79+
{"_type":"memory","key":"kernel-cpython-3-14-dynamic-compile-of-compound","value":"kernel CPython 3.14 dynamic compile() of compound statements (def/class/for/if/import/async) fails non-deterministically with 'cannot delete function call' or 'invalid syntax'. Frozen modules work because their bytecode is pre-serialized. Workaround: kernel/shell.py:_try_precompiled_script short-circuits /examples/*.py and /bin/*.py to __import__(frozen_module). User scripts in /home blocked. See pythonos-0ta + tools/probe_compile.py reproducer."}

.github/workflows/ci.yml

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [ main ]
6+
pull_request:
7+
branches: [ main ]
8+
workflow_dispatch:
9+
10+
jobs:
11+
validate:
12+
name: Validate Sources
13+
runs-on: ubuntu-24.04-arm
14+
timeout-minutes: 90
15+
permissions:
16+
contents: read
17+
18+
steps:
19+
- name: Checkout
20+
uses: actions/checkout@v4
21+
22+
- name: Install host dependencies
23+
run: |
24+
sudo apt-get update
25+
sudo apt-get install -y \
26+
build-essential \
27+
make \
28+
python3 \
29+
qemu-system-arm \
30+
qemu-utils \
31+
libsdl2-dev \
32+
libsdl2-image-dev \
33+
libsdl2-mixer-dev \
34+
libsdl2-ttf-dev
35+
36+
- name: Show platform
37+
run: |
38+
uname -a
39+
docker --version
40+
qemu-system-aarch64 --version
41+
42+
- name: Validate release gate
43+
run: ./scripts/validate-release.sh

GNUMakefile

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -68,25 +68,32 @@ QEMU_X86_CPU := -cpu host
6868
endif
6969
endif
7070

71-
# arm64 acceleration. The kernel auto-detects GICv2 vs GICv3
72-
# (src/boot/gic_arm64.c) and the GICv3 path is verified under TCG via
73-
# `make test-arm64-gicv3` (cortex-a76 + virt,gic-version=3). Native
74-
# HVF on Apple Silicon is still gated on a QEMU upstream bug — Apple
71+
# arm64 acceleration. Linux/KVM currently stalls before GIC init on this
72+
# kernel, so it is opt-in via ARM64_KVM=1 until the EL2/GIC path is fixed.
73+
# Native HVF on Apple Silicon is still gated on a QEMU upstream bug — Apple
7574
# Silicon's hypervisor framework doesn't set the ISV (Instruction
7675
# Syndrome Valid) bit on stage-2 data aborts for GIC MMIO and QEMU's
7776
# hvf.c asserts on that. When the upstream lands, set ARM64_HVF=1 and
7877
# HVF will Just Work — the GICv3 driver is already in place.
78+
ARM64_KVM ?= 0
7979
ARM64_HVF ?= 0
8080
QEMU_ARM64_ACCEL :=
8181
QEMU_ARM64_CPU := -cpu cortex-a57
8282
ifeq ($(HOST_ARCH),arm64)
8383
ifneq ($(HOST_ACCEL),)
84+
ifeq ($(HOST_ACCEL),kvm)
85+
ifeq ($(ARM64_KVM),1)
86+
QEMU_ARM64_ACCEL := -accel $(HOST_ACCEL)
87+
QEMU_ARM64_CPU := -cpu host
88+
endif
89+
else
8490
ifeq ($(ARM64_HVF),1)
8591
QEMU_ARM64_ACCEL := -accel $(HOST_ACCEL)
8692
QEMU_ARM64_CPU := -cpu host
8793
endif
8894
endif
8995
endif
96+
endif
9097

9198
# Internal arch label used by the x86_64 build paths below; do not change.
9299
ARCH := x86_64

kernel/gui/compositor.py

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,8 @@ def __init__(self) -> None:
123123
# reachable, push draw commands directly to it instead of
124124
# composing in-guest. Set lazily in start().
125125
self._bridge_present = False
126+
self._bridge_needs_redraw = True
127+
self._bridge_last_uptime = ""
126128
self._bridge_w = 0
127129
self._bridge_h = 0
128130
self._bridge_fb_handle = 0 # window's main surface handle
@@ -158,6 +160,7 @@ def add_window(self, win: CompositorWindow) -> None:
158160
self._focus_idx = len(self._windows) - 1
159161
win.focused = True
160162
self._refresh_app_menus(win)
163+
self._bridge_needs_redraw = True
161164
win.dirty = True
162165

163166
def remove_window(self, win: CompositorWindow) -> None:
@@ -172,6 +175,7 @@ def remove_window(self, win: CompositorWindow) -> None:
172175
elif not self._windows:
173176
self._focus_idx = -1
174177
self._refresh_app_menus(None)
178+
self._bridge_needs_redraw = True
175179

176180
def cycle_focus(self, direction: int = 1) -> None:
177181
if not self._windows:
@@ -181,6 +185,7 @@ def cycle_focus(self, direction: int = 1) -> None:
181185
self._focus_idx = (self._focus_idx + direction) % len(self._windows)
182186
self._windows[self._focus_idx].focused = True
183187
self._refresh_app_menus(self._windows[self._focus_idx])
188+
self._bridge_needs_redraw = True
184189
for w in self._windows:
185190
w.dirty = True
186191

@@ -235,18 +240,22 @@ def _paint_window_body(self, win: CompositorWindow, fb) -> None:
235240

236241
async def _redraw(self) -> None:
237242
if self._bridge_present:
238-
# Always redraw in bridge mode — per-frame cost is just a
239-
# handful of small JSON ops, and the dock + drag + cursor
240-
# all want continuous updates.
241-
self._redraw_bridge()
243+
uptime = self._uptime_str()
244+
if (not self._bridge_needs_redraw
245+
and uptime == self._bridge_last_uptime
246+
and not any(w.dirty for w in self._windows)):
247+
return
248+
self._bridge_last_uptime = uptime
249+
self._bridge_needs_redraw = False
250+
self._redraw_bridge(uptime)
242251
for w in self._windows:
243252
w.dirty = False
244253
else:
245254
if not any(w.dirty for w in self._windows):
246255
return
247256
self._redraw_local()
248257

249-
def _redraw_bridge(self) -> None:
258+
def _redraw_bridge(self, uptime_text: str | None = None) -> None:
250259
"""Issue draw commands straight to the host SDL window. No
251260
guest back-buffer; per-frame data on the wire is just JSON
252261
envelopes (no pixel payloads)."""
@@ -327,7 +336,7 @@ def _redraw_bridge(self) -> None:
327336
# Menu bar last so any open dropdown sits on top of the
328337
# rest of the desktop. Refresh the right-side uptime text
329338
# each frame so the clock ticks visibly.
330-
self._menubar.set_right_text(self._uptime_str())
339+
self._menubar.set_right_text(uptime_text or self._uptime_str())
331340
from kernel.gui.sdl2.surface import SDL_Surface
332341
fb_surf = SDL_Surface.from_handle(fb_handle,
333342
self._bridge_w, self._bridge_h)
@@ -567,6 +576,7 @@ def _focus(self, win: CompositorWindow) -> None:
567576
self._windows.remove(win)
568577
self._windows.append(win)
569578
self._focus_idx = len(self._windows) - 1
579+
self._bridge_needs_redraw = True
570580

571581
# ── Event routing ───────────────────────────────────────────────────────
572582

@@ -583,11 +593,13 @@ def _route_event(self, ev) -> None:
583593
if self._menubar.on_mouse_move(ev.x, ev.y):
584594
if self._windows:
585595
self._windows[-1].dirty = True
596+
self._bridge_needs_redraw = True
586597
return
587598
if ev.kind == _gui_input.MOUSE_DOWN and ev.code == 1:
588599
if self._menubar.on_mouse_down(ev.x, ev.y):
589600
if self._windows:
590601
self._windows[-1].dirty = True
602+
self._bridge_needs_redraw = True
591603
return
592604

593605
# Mouse-button-down: dock click → focus → maybe-start-drag / close
@@ -632,9 +644,7 @@ def _route_event(self, ev) -> None:
632644
self._dock_hot = new_hot
633645
if self._windows:
634646
self._windows[-1].dirty = True # force redraw
635-
else:
636-
# Force at least one redraw without windows.
637-
pass
647+
self._bridge_needs_redraw = True
638648
win = self.focused_window
639649
if win != None:
640650
win.deliver(ev)
@@ -731,6 +741,8 @@ async def _open_bridge_window(self) -> None:
731741
self._bridge_h = int(r.get("h", ch))
732742
self._bridge_fb_handle = int(r.get("fb_handle", 0))
733743
self._bridge_present = True
744+
self._bridge_needs_redraw = True
745+
self._bridge_last_uptime = ""
734746
# Forward host SDL events into kernel.gui.input.queue so the
735747
# existing _route_event handler picks them up.
736748
from kernel.bridge import input as _br_input

0 commit comments

Comments
 (0)