Copy-mode with scrollback search + config hot-reload

- alt+c enters copy-mode on the focused terminal: arrows/PgUp/PgDn/
  Home/End scroll the history, / searches (matches highlighted in the
  view), n/N jump between hits, q/Esc exits back to the bottom.
  Status line shows position and key help
- config.toml, panel files and the theme file are watched (1s mtime
  poll): edits reload theme/panels/keybinds live without restarting —
  windows and focus are untouched
- Integration suite runs the daemon in an isolated config dir and
  covers copy-mode search and panel hot-reload — 27/27 passing

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-15 19:43:57 +02:00
parent b85345be99
commit b1424da4fb
5 changed files with 323 additions and 6 deletions

View File

@@ -21,6 +21,7 @@ Miljö: TUI_WM_BIN (default target/debug/tui-wm)
import json
import os
import shutil
import re
import socket
import struct
@@ -116,8 +117,20 @@ def main():
env = dict(os.environ, XDG_RUNTIME_DIR=tmp)
sock_path = os.path.join(tmp, "tui-wm.sock")
# Egen konfig-katalog så testerna (t.ex. hot-reload) kan skriva i
# konfigfiler utan att röra repot.
cfgdir = os.path.join(tmp, "cfg")
os.makedirs(cfgdir)
shutil.copy(os.path.join(REPO, "config.toml"), cfgdir)
shutil.copytree(os.path.join(REPO, "panels"), os.path.join(cfgdir, "panels"))
shutil.copytree(os.path.join(REPO, "themes"), os.path.join(cfgdir, "themes"))
# bakgrundsbilden finns inte i test-miljön — ta bort raden
cfg = open(os.path.join(cfgdir, "config.toml")).read()
cfg = "\n".join(l for l in cfg.splitlines() if not l.startswith("background_image"))
open(os.path.join(cfgdir, "config.toml"), "w").write(cfg)
daemon = subprocess.Popen(
[TUI_WM_BIN, "-d"], cwd=REPO, env=env,
[TUI_WM_BIN, "-d"], cwd=cfgdir, env=env,
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
)
try:
@@ -306,6 +319,35 @@ def main():
f"geometri: {maxed}")
app.send({"type": "close_window", "window_id": sw_open["id"]})
# ── 4f. Copy-mode: alt+c, sök, avsluta ───────────────────────────
app.send({"type": "spawn_window", "command": "seq 1 200; exec cat",
"request_id": "w-copy"})
cm_open = app.recv_until("window_opened")
disp.frame_text(timeout=5.0, contains="200")
disp.send(key_event(ch="c", modifiers="ALT"))
text = disp.frame_text(timeout=5.0, contains="KOPIERA")
check("alt+c öppnar copy-mode", "KOPIERA" in text)
# sök efter rad 42
disp.send(key_event(ch="/"))
for ch in "42\n":
if ch == "\n":
disp.send(key_event(code="Enter"))
else:
disp.send(key_event(ch=ch))
text = disp.frame_text(timeout=5.0, contains="42")
check("copy-mode-sökning hittar träffen", "42" in text)
disp.send(key_event(ch="q"))
text = disp.frame_text(timeout=5.0, contains="200")
check("q lämnar copy-mode och hoppar till botten", "200" in text)
app.send({"type": "close_window", "window_id": cm_open["id"]})
# ── 4g. Hot-reload: ändra panelfilen → panelen uppdateras ────────
with open(os.path.join(cfgdir, "panels", "topbar.toml"), "a") as f:
f.write('\n[[item]]\nlabel = "HOTRELOAD"\naction = { type = "no_op" }\n')
text = disp.frame_text(timeout=6.0, contains="HOTRELOAD")
check("hot-reload plockar upp panel-ändring", "HOTRELOAD" in text,
f"frame-tail: {text[:150]!r}")
# ── 5. Popup + popup_result via Enter ───────────────────────────
app.send({
"type": "spawn_popup", "message": "Integrationstest?",