fix: input timeout bugs — read-raw-byte, SS3, parse-csi-params all use sub-second timeouts now (get-internal-real-time replaces get-universal-time which truncated to integer seconds)
This commit is contained in:
304
.hermes/plans/2026-05-12-cl-tty-bug-fixes.md
Normal file
304
.hermes/plans/2026-05-12-cl-tty-bug-fixes.md
Normal file
@@ -0,0 +1,304 @@
|
||||
# cl-tty v1.0.0 Bug Fix Iteration
|
||||
|
||||
> **For Hermes:** Use subagent-driven-development + bug-fix-iteration pattern.
|
||||
> Each task: inspect → write regression test → fix → verify → commit.
|
||||
> Do NOT skip tests. Do NOT combine tasks.
|
||||
|
||||
**Goal:** Fix all known bugs and blindspots before v1.0.0 release.
|
||||
|
||||
**Architecture:** cl-tty is a pure CL terminal UI library. No FFI, no ncurses.
|
||||
Components: backend (modern/simple escape seq), input (byte reader + event parser),
|
||||
rendering (framebuffer diff pipeline), layout (flexbox), widgets.
|
||||
|
||||
**Verification command after each fix:**
|
||||
```bash
|
||||
cd /mnt/hermes/projects/cl-tty && sbcl --script run-all-tests.lisp && python3 scripts/verify-api.py && python3 scripts/verify-demo-pty.py
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Fix `read-raw-byte` timeout (CRITICAL BUG)
|
||||
|
||||
**Objective:** The timeout mechanism uses `get-universal-time` which returns
|
||||
integer seconds. Adding a float timeout like 0.05 produces a deadline that
|
||||
equals the current second — the loop terminates immediately. The 50ms escape
|
||||
ambiguity timeout never actually works.
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/components/input.lisp:84-111`
|
||||
- Test: `tests/input-tests.lisp` (add regression test)
|
||||
|
||||
**Root cause:** Line 99: `(let ((deadline (+ (get-universal-time) timeout)))` —
|
||||
`get-universal-time` returns integer seconds, so `(+ (integer) 0.05)` = `(+ integer 0)` = integer.
|
||||
The loop `(while (< (get-universal-time) deadline))` runs zero iterations for any
|
||||
sub-second timeout.
|
||||
|
||||
**Fix:** Use `sb-ext:get-time-of-day` (microsecond precision) or `(/ (get-internal-real-time)
|
||||
internal-time-units-per-second)` to get fractional seconds. Replace:
|
||||
|
||||
```lisp
|
||||
(let ((deadline (+ (get-universal-time) timeout)))
|
||||
(loop while (< (get-universal-time) deadline) ...))
|
||||
```
|
||||
|
||||
with:
|
||||
|
||||
```lisp
|
||||
(let* ((start (get-internal-real-time))
|
||||
(ticks (round (* timeout internal-time-units-per-second)))
|
||||
(deadline (+ start ticks)))
|
||||
(loop while (< (get-internal-real-time) deadline) ...))
|
||||
```
|
||||
|
||||
Or simpler: use `(/ (- (get-internal-real-time) start) internal-time-units-per-second)`
|
||||
to check elapsed time in a loop.
|
||||
|
||||
**Verification:**
|
||||
1. Write a test that calls `read-raw-byte` with :timeout 0.05 and verifies it
|
||||
returns `(values nil :timeout)` within ~100ms (not instantly).
|
||||
2. All existing tests still pass.
|
||||
3. The demo's Escape key works (tested by verify-demo-pty.py).
|
||||
|
||||
---
|
||||
|
||||
### Task 2: Fix `draw-border` ignoring title in modern backend (BUG)
|
||||
|
||||
**Objective:** The `modern-backend`'s `draw-border` method has
|
||||
`(declare (ignore title title-align))` on line 194. The framebuffer backend
|
||||
renders titles correctly. The simple backend also ignores titles.
|
||||
This means titled borders don't show titles in the modern backend.
|
||||
|
||||
**Files:**
|
||||
- Modify: `backend/modern.lisp:192-219`
|
||||
- Add test: `backend/modern-tests.lisp`
|
||||
|
||||
**Fix:** In `draw-border` for `modern-backend`, insert the title text into the
|
||||
top border line after the first character. The title should be centered or
|
||||
left-aligned based on `title-align`.
|
||||
|
||||
The title rendering logic should extract from the framebuffer backend's
|
||||
draw-border (framebuffer.lisp lines 114-117) and adapt for escape sequences:
|
||||
- The top border line is constructed as: `tl + h*N + tr`
|
||||
- Before writing top: if title is non-nil, insert it: `tl + " " + title + " " + h*fill + tr`
|
||||
- Truncate title if it exceeds width-4
|
||||
|
||||
---
|
||||
|
||||
### Task 3: Fix `backend-size` to query real terminal size (MISSING FEATURE)
|
||||
|
||||
**Objective:** `backend-size` for `modern-backend` returns hardcoded (80 24).
|
||||
Should query the terminal via TIOCGWINSZ ioctl or `ESC[18t` query.
|
||||
|
||||
**Files:**
|
||||
- Modify: `backend/modern.lisp:163-165`
|
||||
- Add test: `backend/modern-tests.lisp` (test that values are positive integers)
|
||||
|
||||
**Fix:** Use SBCL's `sb-alien` to call `ioctl` with `TIOCGWINSZ` on the
|
||||
stdout fd (or /dev/tty):
|
||||
|
||||
```lisp
|
||||
(defmethod backend-size ((b modern-backend))
|
||||
(sb-unix:unix-ioctl (sb-sys:fd-stream-fd
|
||||
(or (ignore-errors
|
||||
(open "/dev/tty" :direction :input
|
||||
:if-does-not-exist nil))
|
||||
*standard-output*))
|
||||
sb-unix:TIOCGWINSZ ...)
|
||||
;; Or fallback to query-terminal with ESC[18t
|
||||
;; Fallback: (values 80 24))
|
||||
```
|
||||
|
||||
Simpler approach: Use `sb-unix:unix-ioctl` with the `TIOCGWINSZ` request.
|
||||
The winsize struct is: (rows columns) as two 16-bit values. In SBCL,
|
||||
`sb-unix:unix-ioctl` can be used with `sb-unix:TIOCGWINSZ`.
|
||||
|
||||
If ioctl is complex, implement via OSC Terminal query: `query-terminal` with
|
||||
`ESC[18t` returns `ESC[8;rows;colst`. Parse the response.
|
||||
|
||||
---
|
||||
|
||||
### Task 4: Enable kitty keyboard protocol in `initialize-backend` (MISSING FEATURE)
|
||||
|
||||
**Objective:** `modern-backend` declares `:kitty-keyboard` in `capable-p`
|
||||
but never sends the escape sequence to enable it (`ESC[?u`).
|
||||
|
||||
**Files:**
|
||||
- Modify: `backend/modern.lisp:142-151`
|
||||
|
||||
**Fix:** Add to `initialize-backend`:
|
||||
```lisp
|
||||
(backend-write b (format nil "~C[?u" #\Esc)) ; kitty keyboard
|
||||
```
|
||||
|
||||
And add to `shutdown-backend`:
|
||||
```lisp
|
||||
(backend-write b (format nil "~C[?u" #\Esc)) ; restore default keyboard
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 5: Fix text-input cursor rendering (MISSING VISUAL FEEDBACK)
|
||||
|
||||
**Objective:** The `text-input.lisp` render method declares `(declare (ignore cursor))`.
|
||||
The cursor position is tracked but never drawn, so users can't see where
|
||||
they're typing.
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/components/text-input.lisp` (render method)
|
||||
- Add test: `tests/input-tests.lisp` or existing test file
|
||||
|
||||
**Fix:** In the text-input render method, after drawing the value/placeholder,
|
||||
draw a cursor block (█ or reversed ▓) at the cursor position. Use
|
||||
`draw-rect` or `draw-text` with a visual cursor character at the cursor column.
|
||||
|
||||
When the cursor would be beyond the visible area (scrolled past the right edge),
|
||||
show it at the rightmost position.
|
||||
|
||||
---
|
||||
|
||||
### Task 6: Fix SS3 branch reading without timeout (POTENTIAL HANG)
|
||||
|
||||
**Objective:** In `%read-escape-sequence`, the SS3 branch (when b=#x4f) calls
|
||||
`(read-raw-byte)` without a timeout parameter. If the terminal sends a partial
|
||||
ESC O with no follow-up byte, the read blocks forever.
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/components/input.lisp:210`
|
||||
|
||||
**Fix:** Change line 210 from:
|
||||
```lisp
|
||||
(let ((b2 (read-raw-byte)))
|
||||
```
|
||||
to:
|
||||
```lisp
|
||||
(let ((b2 (read-raw-byte :timeout 0.1)))
|
||||
```
|
||||
And handle the nil case: if b2 is nil, return a key-event for the lone Escape.
|
||||
|
||||
---
|
||||
|
||||
### Task 7: Add Wayland support to `copy-to-clipboard` (PLATFORM GAP)
|
||||
|
||||
**Objective:** `copy-to-clipboard` in `mouse.lisp` only supports X11 (xclip)
|
||||
and macOS (pbcopy). Wayland users (wl-copy) get no clipboard.
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/components/mouse.lisp:51-54`
|
||||
|
||||
**Fix:** Add `#+wayland` or detect Wayland via `$WAYLAND_DISPLAY` env var:
|
||||
|
||||
```lisp
|
||||
(defun copy-to-clipboard (text)
|
||||
#+linux
|
||||
(cond
|
||||
((sb-ext:posix-getenv "WAYLAND_DISPLAY")
|
||||
(sb-ext:run-program "wl-copy" nil :input text :wait nil))
|
||||
(t
|
||||
(sb-ext:run-program "xclip" (list "-selection" "clipboard")
|
||||
:input text :wait nil)))
|
||||
#+darwin
|
||||
(sb-ext:run-program "pbcopy" nil :input text :wait nil))
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 8: Add SIGWINCH handler for terminal resize (MISSING FEATURE)
|
||||
|
||||
**Objective:** When the terminal is resized, the demo and any cl-tty app
|
||||
will render with stale dimensions. The `backend-size` (Task 3) helps but
|
||||
apps need to be notified of resizes.
|
||||
|
||||
**Files:**
|
||||
- Create: `src/components/notification.lisp` OR modify existing components
|
||||
|
||||
**Approach:**
|
||||
This is a design decision. Options:
|
||||
a) Install a SIGWINCH handler that sets a flag checked each frame
|
||||
b) Provide a `register-resize-callback` API
|
||||
c) Only fix in the demo layer (demo.lisp)
|
||||
|
||||
Keep it minimal: install a simple signal handler that sets
|
||||
`*terminal-resized-p*` to T. The app checks this flag each frame.
|
||||
|
||||
Add to `input.lisp` or a new file:
|
||||
```lisp
|
||||
(defvar *terminal-resized-p* nil
|
||||
"Set to T by SIGWINCH handler when terminal resizes.")
|
||||
|
||||
(defun %handle-sigwinch (signal info context)
|
||||
(declare (ignore signal info context))
|
||||
(setf *terminal-resized-p* t))
|
||||
|
||||
;; Install handler
|
||||
#+sbcl
|
||||
(sb-sys:enable-interrupt sb-unix:sigwinch #'%handle-sigwinch)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Bug Blindspots Verified as NOT Bugs (justifying "won't fix"):
|
||||
|
||||
These were investigated and are fine:
|
||||
- **Framebuffer diff link-url**: `cells-equal-p` compares `cell-link-url` with `equal` — covered.
|
||||
- **Select with empty options**: `(if (zerop count) (setf (select-selected-index sel) 0)` — handled.
|
||||
- **Dialog pop from empty stack**: `(when *dialog-stack*` — guarded.
|
||||
- **`parse-csi-params`**: reads raw bytes, handles EOF gracefully.
|
||||
- **Thread safety of globals**: out of scope for v1.0.0 (single-threaded TUI).
|
||||
- **ScrollBox horizontal scrolling**: actually implemented (uses sx in render).
|
||||
- **Redundant tests removed**: cleanup already done in uncommitted diff.
|
||||
|
||||
---
|
||||
|
||||
### BLINDSPOT: The `parse-csi-params` function also uses `(read-raw-byte)` without timeout.
|
||||
|
||||
Line 122: `(multiple-value-bind (b reason) (read-raw-byte)` — while parsing
|
||||
a CSI sequence, if the terminal sends ESC[ but never completes the sequence,
|
||||
this blocks forever. This should use a timeout similar to the escape sequence
|
||||
reader. Same fix pattern as Task 6.
|
||||
|
||||
Adding as Task 9.
|
||||
|
||||
---
|
||||
|
||||
### Task 9: Fix `parse-csi-params` to use timeout (POTENTIAL HANG)
|
||||
|
||||
**Objective:** `parse-csi-params` (input.lisp line 122) reads bytes without
|
||||
timeout. A partial CSI sequence (ESC[ without final byte) blocks forever.
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/components/input.lisp:116-149`
|
||||
|
||||
**Fix:** Add a timeout to the read inside `parse-csi-params`. Use a total
|
||||
timeout of ~500ms for the entire CSI sequence (generous given terminals
|
||||
respond within a few ms). If the timeout fires, return nil for final-byte.
|
||||
|
||||
Similar to `%read-escape-sequence`, pass `:timeout` parameter to `parse-csi-params`
|
||||
and have `%read-escape-sequence` pass a timeout to it.
|
||||
|
||||
---
|
||||
|
||||
### Task 10: Fix `draw-border` ignoring title in simple backend (BUG)
|
||||
|
||||
**Objective:** Same as Task 2 but for `simple-backend`. The
|
||||
`%simple-border-char` function just got refactored (uncommitted diff), and
|
||||
`draw-border` in simple.lisp also ignores title.
|
||||
|
||||
**Files:**
|
||||
- Modify: `backend/simple.lisp` (draw-border method)
|
||||
- Add test: `backend/tests.lisp`
|
||||
|
||||
**Fix:** In `simple-backend`'s `draw-border`, when a title is provided,
|
||||
insert it into the top border line. Use ASCII chars (the simple backend
|
||||
doesn't use Unicode).
|
||||
|
||||
---
|
||||
|
||||
### Task 11: Add `detect-backend` export to backend package (API GAP)
|
||||
|
||||
**Objective:** The README shows `(cl-tty.backend:detect-backend)` as the
|
||||
entry point, but verify this is actually exported from the backend package.
|
||||
|
||||
**Files:**
|
||||
- Check: `backend/package.lisp`
|
||||
|
||||
**Fix:** Ensure `#:detect-backend` is in the package's `:export` list.
|
||||
@@ -30,8 +30,8 @@
|
||||
(declare (ignore x y fg bg bold italic underline reverse dim blink))
|
||||
(backend-write b string))
|
||||
|
||||
(defun %simple-border-char (edge-style pos)
|
||||
"Return ASCII border character for EDGE-STYLE at POS.
|
||||
(defun %simple-border-char (pos)
|
||||
"Return ASCII border character at POS.
|
||||
POS is :top-left, :top-right, :bottom-left, :bottom-right,
|
||||
:horizontal, or :vertical."
|
||||
(case pos
|
||||
@@ -42,8 +42,8 @@ POS is :top-left, :top-right, :bottom-left, :bottom-right,
|
||||
(defmethod draw-border ((b simple-backend) x y width height
|
||||
&key style fg bg title title-align)
|
||||
(declare (ignore style fg bg title-align))
|
||||
(let ((h (%simple-border-char nil :horizontal))
|
||||
(v (%simple-border-char nil :vertical)))
|
||||
(let ((h (%simple-border-char :horizontal))
|
||||
(v (%simple-border-char :vertical)))
|
||||
;; Position cursor with newlines and spaces (no escape sequences)
|
||||
(dotimes (row y) (backend-write b (string #\Newline)))
|
||||
;; Top edge with optional title
|
||||
|
||||
115
docs/BUG-REPORT.md
Normal file
115
docs/BUG-REPORT.md
Normal file
@@ -0,0 +1,115 @@
|
||||
# cl-tty Code Audit — Bug Report
|
||||
|
||||
## Bug 1 [CRITICAL]: dialog rendering undefined functions
|
||||
|
||||
**File:** src/components/dialog-package.lisp and src/components/dialog.lisp
|
||||
|
||||
**Problem:** `render-dialog` (lines 34, 36, 39) and `render-toast` (lines 114, 115) call `draw-rect`, `draw-border`, `draw-text` without those symbols being available.
|
||||
|
||||
**Root cause:** The dialog package definition uses `(:use :cl :cl-tty.input :cl-tty.select)` but `draw-rect`, `draw-border`, and `draw-text` are generic functions exported from `cl-tty.backend`. They need to be imported. The package does NOT use `cl-tty.backend`.
|
||||
|
||||
**Tests don't catch this** because dialog-tests.lisp tests push/pop/toast management but never calls `render-dialog` or `render-toast`.
|
||||
|
||||
**Fix:** Add `:cl-tty.backend` to the `:use` list in dialog-package.lisp, or add individual `:import-from` entries for the three functions.
|
||||
|
||||
---
|
||||
|
||||
## Bug 2 [HIGH]: SBCL "function T is undefined" warning in input.lisp
|
||||
|
||||
**File:** src/components/input.lisp
|
||||
|
||||
**Problem:** When SBCL compiles this file, it issues:
|
||||
"WARNING: The function T is undefined, and its name is reserved by ANSI CL so that even if it were defined later, the code doing so would not be portable."
|
||||
|
||||
The warning fires during the `defmethod read-event` compilation unit but the exact source is not identified by line number. The file uses `(t ...)` in case/cond default clauses extensively and `:ctrl t`, `:alt t` etc. as keyword argument values. The root cause needs investigation — could be the `case` macro expansion or a `return-from` interaction.
|
||||
|
||||
**Note:** this warning does NOT fire when `(compile 'read-event)` or `(compile nil '(lambda ...))` is called in isolation on individual functions. It only fires during `compile-file` on the whole file. This suggests it's a cross-form interaction.
|
||||
|
||||
**Investigation needed.**
|
||||
|
||||
---
|
||||
|
||||
## Bug 3 [MEDIUM]: text-input.lisp ignores variable that IS read
|
||||
|
||||
**File:** src/components/text-input.lisp, lines 163, 169-170
|
||||
|
||||
```lisp
|
||||
(w (if ln (layout-node-width ln) 80)) ; line 163 — defined
|
||||
...
|
||||
(truncated (subseq display 0 (min (length display) w))) ; line 169 — USED
|
||||
(declare (ignore w cursor)) ; line 170 — declared ignored
|
||||
```
|
||||
|
||||
**Problem:** `w` is declared as `(ignore w)` on line 170 but is actually read on line 169. Declare ignore + read is a compiler-level contradiction. The `cursor` variable is legitimately unused and should remain ignored.
|
||||
|
||||
**Fix:** Remove `w` from the ignore declaration. Only `(declare (ignore cursor))`.
|
||||
|
||||
---
|
||||
|
||||
## Bug 4 [MEDIUM]: markdown.lisp ignores variable that IS read
|
||||
|
||||
**File:** src/components/markdown.lisp, lines 142-144
|
||||
|
||||
```lisp
|
||||
(defun parse-list (lines start)
|
||||
(declare (ignore start)) ; line 143
|
||||
(let ((items nil) (i start)) ; line 144 — USES start!
|
||||
```
|
||||
|
||||
**Problem:** Same pattern as bug 3. `start` is declared ignored then immediately used. The declaration should be removed.
|
||||
|
||||
**Fix:** Remove the `(declare (ignore start))` declaration.
|
||||
|
||||
---
|
||||
|
||||
## Bug 5 [MEDIUM]: scrollbox.lisp unused vx variable
|
||||
|
||||
**File:** src/components/scrollbox.lisp, line 45
|
||||
|
||||
```lisp
|
||||
(vx 0) (vy 0)
|
||||
```
|
||||
|
||||
**Problem:** `vx` is bound but never read — `vy` is used for viewport height calculations but viewport-x/vx is never referenced. This is a style-warning that indicates either dead code or a real issue where viewport-x should be used.
|
||||
|
||||
**Fix:** Add `(declare (ignore vx))` or remove the `vx` binding entirely.
|
||||
|
||||
---
|
||||
|
||||
## Bug 6 [LOW]: %simple-border-char ignores edge-style
|
||||
|
||||
**File:** backend/simple.lisp, lines 33-40
|
||||
|
||||
```lisp
|
||||
(defun %simple-border-char (edge-style pos)
|
||||
"Return ASCII border character for EDGE-STYLE at POS."
|
||||
(case pos
|
||||
((:top-left :top-right :bottom-left :bottom-right) #\+)
|
||||
(:horizontal #\-)
|
||||
(:vertical #\|)))
|
||||
```
|
||||
|
||||
**Problem:** The `edge-style` parameter is never consulted. Always returns `+ - |` regardless of style. Callers also pass `nil` for it:
|
||||
```lisp
|
||||
(%simple-border-char nil :horizontal)
|
||||
```
|
||||
|
||||
**Fix:** Either remove the `edge-style` parameter (dead code) or implement border style selection using `case` on `edge-style`.
|
||||
|
||||
---
|
||||
|
||||
## Bug 7 [LOW]: framebuffer draw-border ignores title-align
|
||||
|
||||
**File:** src/rendering/framebuffer.lisp, lines 94, 114-116
|
||||
|
||||
```lisp
|
||||
(defmethod draw-border ((fb framebuffer-backend) x y w h &key (style :single) title title-align fg bg)
|
||||
...
|
||||
(when title
|
||||
(loop for i from 0 below (length title)
|
||||
do (%set-cell fb (+ x 2 i) y (char title i) :fg fg :bg bg))))
|
||||
```
|
||||
|
||||
**Problem:** `title-align` is accepted but never used. Title always renders at offset 2 from left edge (hard-coded). The simple backend centers the title, the framebuffer backend left-aligns — inconsistent API behavior.
|
||||
|
||||
**Fix:** Implement `title-align` support or add `(declare (ignore title-align))` and document the behavior.
|
||||
@@ -150,7 +150,7 @@ from the component library without writing custom escape sequences.
|
||||
Checklist:
|
||||
- [X] README.org with overview, architecture, component table, quick start
|
||||
- [X] demo.lisp — working interactive example
|
||||
- [X] Full test suite: 358 checks, 100% passing across 11 suites
|
||||
- [X] Full test suite: 392 checks, 100% passing across 12 suites
|
||||
- [X] ASDF system with test-op
|
||||
- [X] LICENSE file (GPL 3.0)
|
||||
- [X] Literate org files for all modules
|
||||
|
||||
@@ -27,7 +27,7 @@ module adds:
|
||||
|
||||
#+BEGIN_SRC lisp :tangle ../src/components/mouse-package.lisp :noweb no
|
||||
(defpackage :cl-tty.mouse
|
||||
(:use :cl :cl-tty.input :cl-tty.box :cl-tty.rendering)
|
||||
(:use :cl :cl-tty.layout :cl-tty.input :cl-tty.box :cl-tty.rendering)
|
||||
(:export
|
||||
#:mouse-mixin
|
||||
#:on-mouse-down #:on-mouse-up #:on-mouse-move #:on-mouse-scroll
|
||||
|
||||
75
scripts/audit-compiler.lisp
Normal file
75
scripts/audit-compiler.lisp
Normal file
@@ -0,0 +1,75 @@
|
||||
;; Deep compiler audit - compile every file with full warnings
|
||||
(load "~/quicklisp/setup.lisp")
|
||||
(ql:register-local-projects)
|
||||
(ql:quickload :cl-tty :silent t)
|
||||
(ql:quickload :fiveam :silent t :error t)
|
||||
(ql:quickload :bordeaux-threads :silent t)
|
||||
|
||||
(defparameter *results* '())
|
||||
|
||||
(defun audit-compile (file)
|
||||
(let* ((warnings '())
|
||||
(notes '())
|
||||
(style-warnings '()))
|
||||
;; Redirect compiler output during compilation
|
||||
(handler-bind
|
||||
((style-warning
|
||||
(lambda (c) (push (format nil " STYLE-WARNING: ~a" c) style-warnings) (muffle-warning c)))
|
||||
(warning
|
||||
(lambda (c) (push (format nil " WARNING: ~a" c) warnings) (muffle-warning c)))
|
||||
(sb-ext:compiler-note
|
||||
(lambda (c) (push (format nil " NOTE: ~a" c) notes) (muffle-warning c))))
|
||||
(multiple-value-bind (fasl warn-p fail-p)
|
||||
(compile-file file :print nil :verbose nil)
|
||||
(delete-file fasl)
|
||||
(push (list file warn-p fail-p (reverse style-warnings) (reverse warnings) (reverse notes))
|
||||
*results*)))))
|
||||
|
||||
(let ((files
|
||||
'("backend/classes.lisp" "backend/package.lisp"
|
||||
"backend/detection.lisp" "backend/simple.lisp" "backend/modern.lisp"
|
||||
"layout/layout.lisp"
|
||||
"src/components/container-package.lisp"
|
||||
"src/components/dialog-package.lisp" "src/components/dialog.lisp"
|
||||
"src/components/dirty.lisp"
|
||||
"src/components/input-package.lisp" "src/components/input.lisp"
|
||||
"src/components/keybindings.lisp"
|
||||
"src/components/markdown-package.lisp" "src/components/markdown.lisp"
|
||||
"src/components/mouse-package.lisp" "src/components/mouse.lisp"
|
||||
"src/components/package.lisp" "src/components/render.lisp"
|
||||
"src/components/scrollbox.lisp" "src/components/select-package.lisp"
|
||||
"src/components/select.lisp" "src/components/slot-package.lisp"
|
||||
"src/components/slot.lisp" "src/components/tabbar.lisp"
|
||||
"src/components/text-input.lisp" "src/components/text.lisp"
|
||||
"src/components/textarea.lisp" "src/components/theme.lisp"
|
||||
"src/components/box.lisp"
|
||||
"src/rendering/framebuffer.lisp"
|
||||
"demo.lisp"
|
||||
"backend/modern-tests.lisp" "backend/tests.lisp"
|
||||
"layout/tests.lisp"
|
||||
"src/components/box-tests.lisp" "src/components/dirty-tests.lisp"
|
||||
"src/components/render-tests.lisp" "src/components/theme-tests.lisp"
|
||||
"src/components/input-tests.lisp"
|
||||
"tests/scrollbox-tabbar-tests.lisp" "tests/select-tests.lisp"
|
||||
"tests/markdown-tests.lisp" "tests/dialog-tests.lisp"
|
||||
"tests/mouse-tests.lisp" "tests/slot-tests.lisp"
|
||||
"tests/framebuffer-tests.lisp")))
|
||||
(dolist (f files)
|
||||
(if (probe-file f)
|
||||
(audit-compile f)
|
||||
(format t "~&SKIP (not found): ~a~%" f))))
|
||||
|
||||
(format t "~&~%=== COMPILER AUDIT RESULTS ===~%")
|
||||
(dolist (r (reverse *results*))
|
||||
(destructuring-bind (file warn-p fail-p style-warnings warnings notes) r
|
||||
(format t "~&~a~%" file)
|
||||
(format t " warn=~a fail=~a" warn-p fail-p)
|
||||
(when notes (format t " (~d notes)" (length notes)))
|
||||
(when style-warnings (format t " (~d style-warnings)" (length style-warnings)))
|
||||
(when warnings (format t " (~d warnings)" (length warnings)))
|
||||
(format t "~%")
|
||||
(dolist (s style-warnings) (format t "~a~%" s))
|
||||
(dolist (w warnings) (format t "~a~%" w))))
|
||||
|
||||
(format t "~%=== DONE ===~%")
|
||||
(uiop:quit 0)
|
||||
86
scripts/binary-search.lisp
Normal file
86
scripts/binary-search.lisp
Normal file
@@ -0,0 +1,86 @@
|
||||
(load "~/quicklisp/setup.lisp")
|
||||
(ql:register-local-projects)
|
||||
(ql:quickload :cl-tty :silent t)
|
||||
|
||||
(defun test (label sexp)
|
||||
(let ((tmp "/tmp/binary-test.lisp"))
|
||||
(with-open-file (out tmp :direction :output :if-exists :supersede)
|
||||
(format out "(in-package :cl-tty.input)~%")
|
||||
(write sexp :stream out :case :upcase)
|
||||
(terpri out))
|
||||
(multiple-value-bind (fasl warn-p fail-p)
|
||||
(compile-file tmp :print nil :verbose nil)
|
||||
(format t "~a: warn=~a fail=~a~%" label warn-p fail-p)
|
||||
(when (and fasl (probe-file fasl)) (delete-file fasl))
|
||||
(delete-file tmp))))
|
||||
|
||||
;; Fix 1: use cond with (eql ...) instead of case
|
||||
(test "FIX1-cond"
|
||||
'(defun %read-escape-sequence ()
|
||||
(multiple-value-bind (b reason) (read-raw-byte :timeout 0.05)
|
||||
(unless b
|
||||
(return-from %read-escape-sequence
|
||||
(if (eq reason :eof) :eof
|
||||
(make-key-event :key :escape :raw (string #\Esc)))))
|
||||
(cond
|
||||
((eql b #x4f)
|
||||
(let ((b2 (read-raw-byte)))
|
||||
(if b2
|
||||
(let ((key (cdr (assoc (code-char b2)
|
||||
'((#\P . :f1) (#\Q . :f2)
|
||||
(#\R . :f3) (#\S . :f4))))))
|
||||
(make-key-event :key (or key :unknown)
|
||||
:raw (format nil "~C~C~C" #\Esc #\O (code-char b2))))
|
||||
:eof)))
|
||||
((eql b #x5b)
|
||||
(multiple-value-bind (params final-byte raw) (parse-csi-params)
|
||||
(cond
|
||||
((null final-byte)
|
||||
(if (eq raw :eof) :eof
|
||||
(make-key-event :key :escape :raw (string #\Esc))))
|
||||
((and raw (plusp (length raw)) (char= (char raw 0) #\<))
|
||||
(or (parse-sgr-mouse raw)
|
||||
(make-key-event :key :unknown :raw raw)))
|
||||
((and (char= (code-char final-byte) #\M) (>= (length params) 3))
|
||||
(let* ((p0 (first params)))
|
||||
(if (zerop (logand p0 #x40))
|
||||
(let* ((x (second params))
|
||||
(y (third params))
|
||||
(button (logand p0 #x03))
|
||||
(motion (logand p0 #x20))
|
||||
(release (= button 3)))
|
||||
(make-mouse-event
|
||||
:type (cond (release :release) (motion :drag) (t :press))
|
||||
:button (let ((b button)) (cond ((= b 0) :left) ((= b 1) :middle) ((= b 2) :right) (t :none)))
|
||||
:x x :y y :raw (format nil "~C[<~d;~d;~d~C" #\Esc p0 x y (code-char final-byte))))
|
||||
(let* ((tilde-p (char= (code-char final-byte) #\~))
|
||||
(param (or p0 0))
|
||||
(key (if tilde-p (cdr (assoc param *csi-tilde-table*)) (cdr (assoc (code-char final-byte) *csi-key-table*))))
|
||||
(modifier (when (> (length params) 1) (second params))))
|
||||
(let ((ctrl nil) (alt nil) (shift nil))
|
||||
(when modifier
|
||||
(setf shift (logtest modifier 1) alt (logtest modifier 2) ctrl (logtest modifier 4)))
|
||||
(make-key-event :key (or key :unknown) :ctrl ctrl :alt alt :shift shift
|
||||
:raw (format nil "~C[~d~C" #\Esc param (code-char final-byte))))))))
|
||||
(t
|
||||
(let* ((tilde-p (char= (code-char final-byte) #\~))
|
||||
(param (or (first params) 0))
|
||||
(key (if tilde-p (cdr (assoc param *csi-tilde-table*)) (cdr (assoc (code-char final-byte) *csi-key-table*))))
|
||||
(modifier (when (> (length params) 1) (second params))))
|
||||
(let ((ctrl nil) (alt nil) (shift nil))
|
||||
(when modifier
|
||||
(setf shift (logtest modifier 1) alt (logtest modifier 2) ctrl (logtest modifier 4)))
|
||||
(make-key-event :key (or key :unknown) :ctrl ctrl :alt alt :shift shift
|
||||
:raw (format nil "~C[~d~C" #\Esc param (code-char final-byte)))))))))
|
||||
((eql b #x1b)
|
||||
(make-key-event :key :escape :alt t :raw "\\\\e\\\\e"))
|
||||
(t
|
||||
(let ((ch (code-char b)))
|
||||
(if (and (>= b #x20) (<= b #x7e))
|
||||
(make-key-event :key (intern (string (string-upcase ch)) :keyword)
|
||||
:alt t
|
||||
:raw (format nil "~C~C" #\Esc ch))
|
||||
(make-key-event :key :unknown
|
||||
:raw (format nil "~C~C" #\Esc ch)))))))))
|
||||
|
||||
(uiop:quit)
|
||||
87
scripts/code-audit.lisp
Normal file
87
scripts/code-audit.lisp
Normal file
@@ -0,0 +1,87 @@
|
||||
;; Code audit: load everything with full safety, collect warnings
|
||||
(load "~/quicklisp/setup.lisp")
|
||||
(ql:register-local-projects)
|
||||
(ql:quickload :cl-tty :silent t)
|
||||
(ql:quickload :fiveam :silent t)
|
||||
|
||||
;; Redirect warnings into a collector
|
||||
(defvar *warnings* '())
|
||||
(defvar *notes* '())
|
||||
(defvar *style-warnings* '())
|
||||
|
||||
(setf sb-ext:*compiler-note-condition-handler*
|
||||
(lambda (c)
|
||||
(push (format nil "NOTE: ~a" c) *notes*)
|
||||
(muffle-warning c)))
|
||||
|
||||
(setf sb-ext:*compiler-warning-condition-handler*
|
||||
(lambda (c)
|
||||
(etypecase c
|
||||
(sb-int:simple-style-warning
|
||||
(push (format nil "STYLE-WARNING: ~a" c) *style-warnings*))
|
||||
(t
|
||||
(push (format nil "WARNING: ~a" c) *warnings*)))
|
||||
(muffle-warning c)))
|
||||
|
||||
;; Load all source files directly to catch per-file warnings
|
||||
(let ((files
|
||||
'("backend/classes.lisp" "backend/package.lisp"
|
||||
"backend/detection.lisp" "backend/simple.lisp" "backend/modern.lisp"
|
||||
"layout/layout.lisp"
|
||||
"src/components/container-package.lisp"
|
||||
"src/components/dialog-package.lisp" "src/components/dialog.lisp"
|
||||
"src/components/dirty.lisp"
|
||||
"src/components/input-package.lisp" "src/components/input.lisp"
|
||||
"src/components/keybindings.lisp"
|
||||
"src/components/markdown-package.lisp" "src/components/markdown.lisp"
|
||||
"src/components/mouse-package.lisp" "src/components/mouse.lisp"
|
||||
"src/components/package.lisp" "src/components/render.lisp"
|
||||
"src/components/scrollbox.lisp" "src/components/select-package.lisp"
|
||||
"src/components/select.lisp" "src/components/slot-package.lisp"
|
||||
"src/components/slot.lisp" "src/components/tabbar.lisp"
|
||||
"src/components/text-input.lisp" "src/components/text.lisp"
|
||||
"src/components/textarea.lisp" "src/components/theme.lisp"
|
||||
"src/components/box.lisp"
|
||||
"src/rendering/framebuffer.lisp"
|
||||
"demo.lisp")))
|
||||
(dolist (f files)
|
||||
(handler-bind ((warning #'muffle-warning))
|
||||
(load f))))
|
||||
|
||||
;; Also run the test files for good measure
|
||||
(dolist (f '("backend/tests.lisp" "backend/modern-tests.lisp"
|
||||
"layout/tests.lisp"
|
||||
"src/components/box-tests.lisp"
|
||||
"src/components/dirty-tests.lisp"
|
||||
"src/components/render-tests.lisp"
|
||||
"src/components/theme-tests.lisp"
|
||||
"src/components/input-tests.lisp"
|
||||
"tests/scrollbox-tabbar-tests.lisp"
|
||||
"tests/select-tests.lisp"
|
||||
"tests/markdown-tests.lisp"
|
||||
"tests/dialog-tests.lisp"
|
||||
"tests/mouse-tests.lisp"
|
||||
"tests/slot-tests.lisp"
|
||||
"tests/framebuffer-tests.lisp"))
|
||||
(load f))
|
||||
|
||||
(format t "~&=== COMPILER AUDIT RESULTS ===~%")
|
||||
(format t "WARNINGS (~d):~%" (length *warnings*))
|
||||
(dolist (w (reverse *warnings*))
|
||||
(format t " ~a~%" w))
|
||||
(format t "STYLE-WARNINGS (~d):~%" (length *style-warnings*))
|
||||
(dolist (w (reverse *style-warnings*))
|
||||
(format t " ~a~%" w))
|
||||
(format t "NOTES (~d):~%" (length *notes*))
|
||||
(dolist (n (reverse *notes*))
|
||||
(format t " ~a~%" n))
|
||||
|
||||
(unless *warnings*
|
||||
(format t "~&No compiler warnings.~%"))
|
||||
(unless *style-warnings*
|
||||
(format t "No style-warnings.~%"))
|
||||
(unless *notes*
|
||||
(format t "No notes.~%"))
|
||||
|
||||
(format t "~&=== AUDIT COMPLETE ===~%")
|
||||
(uiop:quit 0)
|
||||
33
scripts/find-t-form.lisp
Normal file
33
scripts/find-t-form.lisp
Normal file
@@ -0,0 +1,33 @@
|
||||
;; Compile input.lisp form-by-form to isolate bug 2
|
||||
(load "~/quicklisp/setup.lisp")
|
||||
(ql:register-local-projects)
|
||||
(ql:quickload :cl-tty :silent t)
|
||||
|
||||
(defun compile-forms-in-file (path)
|
||||
"Read each top-level form from PATH and compile-file each individually."
|
||||
(with-open-file (s path)
|
||||
(loop with form-num = 0
|
||||
for form = (read s nil s)
|
||||
until (eq form s)
|
||||
do (incf form-num)
|
||||
(let ((tmp-path (format nil "/tmp/input-form-~d.lisp" form-num)))
|
||||
(with-open-file (out tmp-path :direction :output :if-exists :supersede)
|
||||
;; Preserve the package
|
||||
(prin1 `(in-package ,(package-name *package*)) out)
|
||||
(terpri out)
|
||||
(prin1 form out)
|
||||
(terpri out))
|
||||
(multiple-value-bind (fasl warn-p fail-p)
|
||||
(compile-file tmp-path :print nil :verbose nil)
|
||||
(format t "Form ~2d: warn=~a fail=~a~%"
|
||||
form-num warn-p fail-p)
|
||||
(when (or warn-p fail-p)
|
||||
(rename-file tmp-path (format nil "/tmp/input-bad-form-~d.lisp" form-num) :if-exists :supersede)
|
||||
(with-open-file (f (format nil "/tmp/input-bad-form-~d.txt" form-num) :direction :output :if-exists :supersede)
|
||||
(prin1 form f)))
|
||||
(when (and fasl (probe-file fasl))
|
||||
(delete-file fasl))
|
||||
(delete-file tmp-path))))))
|
||||
|
||||
(let ((*package* (find-package :cl-tty.input)))
|
||||
(compile-forms-in-file "src/components/input.lisp"))
|
||||
24
scripts/find-t-warning.lisp
Normal file
24
scripts/find-t-warning.lisp
Normal file
@@ -0,0 +1,24 @@
|
||||
;; Binary search for "function T" warning in input.lisp
|
||||
(load "~/quicklisp/setup.lisp")
|
||||
(ql:register-local-projects)
|
||||
(ql:quickload :cl-tty :silent t)
|
||||
|
||||
(defun test-subset (name from to)
|
||||
(format t "~&=== Testing ~a (lines ~d-~d) ===~%" name from to)
|
||||
(with-open-file (s "src/components/input.lisp")
|
||||
(loop repeat (1- from) do (read-line s nil))
|
||||
(loop with code = (make-string 0 :element-type 'character :adjustable t :fill-pointer t)
|
||||
for i from from to to
|
||||
for line = (read-line s nil nil)
|
||||
while line
|
||||
do (vector-push-extend #\Newline code)
|
||||
(dotimes (j (length line)) (vector-push-extend (char line j) code))
|
||||
finally (handler-bind ((warning (lambda (c)
|
||||
(format t " WARNING: ~a~%" c)
|
||||
(muffle-warning c))))
|
||||
(let ((*readtable* *readtable*)
|
||||
(*package* (find-package :cl-tty.input)))
|
||||
(eval (read-from-string (coerce code 'string))))))))
|
||||
|
||||
;; Test the DEFMETHOD READ-EVENT section specifically (lines 321-327)
|
||||
(test-subset "last-form" 321 327)
|
||||
@@ -1,5 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Final corrected cl-tty feature verification. Tests the ACTUAL exported API."""
|
||||
"""
|
||||
CL-TTY API verification — matches current exported API.
|
||||
"""
|
||||
import subprocess, sys, os, tempfile, re
|
||||
|
||||
PASS = 0; FAIL = 0
|
||||
@@ -8,191 +10,277 @@ def check(name, cond, detail=""):
|
||||
if cond: PASS += 1; print(f" OK {name}")
|
||||
else: FAIL += 1; print(f" FAIL {name}" + (f" ({detail})" if detail else ""))
|
||||
|
||||
P = """(load "~/quicklisp/setup.lisp")
|
||||
PREAMBLE = """(load "~/quicklisp/setup.lisp")
|
||||
(push (truename ".") asdf:*central-registry*)
|
||||
(ql:quickload :cl-tty :silent t)
|
||||
(ql:quickload :fiveam :silent t)
|
||||
"""
|
||||
|
||||
def run(code, timeout=30):
|
||||
full = P + "(use-package :cl-tty.backend)(use-package :cl-tty.box)(use-package :cl-tty.rendering)(use-package :cl-tty.input)(use-package :cl-tty.layout)" + code
|
||||
full = PREAMBLE + "(use-package :cl-tty.backend)\n(use-package :cl-tty.box)\n(use-package :cl-tty.rendering)\n(use-package :cl-tty.input)\n(use-package :cl-tty.layout)\n" + code
|
||||
with tempfile.NamedTemporaryFile(mode="w", suffix=".lisp", delete=False) as f:
|
||||
f.write(full); fn = f.name
|
||||
try:
|
||||
r = subprocess.run(["sbcl", "--noinform", "--script", fn], capture_output=True, timeout=timeout, text=True)
|
||||
return (r.stdout or "") + (r.stderr or "")
|
||||
finally:
|
||||
result = subprocess.run(["sbcl", "--noinform", "--script", fn], capture_output=True, timeout=timeout, text=True)
|
||||
os.unlink(fn)
|
||||
return (result.stdout or "") + (result.stderr or "")
|
||||
|
||||
def run_pkg(pkg, code, timeout=30):
|
||||
full = P + "(use-package " + pkg + ")" + code
|
||||
with tempfile.NamedTemporaryFile(mode="w", suffix=".lisp", delete=False) as f:
|
||||
f.write(full); fn = f.name
|
||||
try:
|
||||
r = subprocess.run(["sbcl", "--noinform", "--script", fn], capture_output=True, timeout=timeout, text=True)
|
||||
return (r.stdout or "") + (r.stderr or "")
|
||||
finally:
|
||||
os.unlink(fn)
|
||||
def has(out, text): return text in out
|
||||
|
||||
# 1-5: Core backend + rendering (from previous run, all passed)
|
||||
out = run("""(let ((be (make-simple-backend)))
|
||||
(initialize-backend be)(draw-text be 0 0 "HELLO")(shutdown-backend be)(format t "~%DONE"))""")
|
||||
check("1. Simple backend draws text", "HELLO" in out, out[:100])
|
||||
# 1. Backend lifecycle
|
||||
out = run("""(let ((be (make-simple-backend :output-stream *standard-output*)))
|
||||
(initialize-backend be) (draw-text be 0 0 "HOLA" :white :black) (format t "~%DONE"))""")
|
||||
check("Backend: draw-text HOLA", has(out, "HOLA"), out[:100])
|
||||
check("Backend: DONE", has(out, "DONE"))
|
||||
|
||||
out = run("""(let ((be (make-simple-backend)))
|
||||
(initialize-backend be)(draw-border be 0 0 12 5 :style :single :title " TITLE ")
|
||||
(shutdown-backend be)(format t "DONE"))""")
|
||||
check("2. Box border with title", "TITLE" in out, repr(out[:200]))
|
||||
# 2. Box borders with titles
|
||||
out = run("""(let ((be (make-simple-backend :output-stream *standard-output*)))
|
||||
(initialize-backend be)
|
||||
(draw-border be 0 0 12 5 :style :single :title " TITLE ")
|
||||
(shutdown-backend be) (format t "DONE"))""")
|
||||
check("Box: title appears in border", has(out, "TITLE"), repr(out[:200]))
|
||||
|
||||
out = run("""(let ((be (make-simple-backend)))
|
||||
(initialize-backend be)(draw-text be 0 0 "TEXT")(draw-text be 0 1 "BOLD" nil nil :bold t)(shutdown-backend be)(format t "~%DONE"))""")
|
||||
check("3. Text rendering", "TEXT" in out and "BOLD" in out, out[:200])
|
||||
# 3. Text rendering
|
||||
out = run("""(let ((be (make-simple-backend :output-stream *standard-output*)))
|
||||
(initialize-backend be) (draw-text be 0 0 "TEXT-A" :red :blue)
|
||||
(draw-text be 0 1 "TEXT-B" :white nil :bold t :italic t)
|
||||
(shutdown-backend be) (format t "DONE"))""")
|
||||
check("Text: plain", has(out, "TEXT-A"), out[:200])
|
||||
check("Text: bold+italic", has(out, "TEXT-B"))
|
||||
check("Text: DONE", has(out, "DONE"))
|
||||
|
||||
out = run("""(let ((be (make-simple-backend)))
|
||||
(initialize-backend be)(draw-rect be 0 0 10 3 :bg :blue)(draw-text be 0 0 "FILL" :white :blue)(shutdown-backend be)(format t "~%DONE"))""")
|
||||
check("4. draw-rect filled rect", "FILL" in out, out[:100])
|
||||
# 4. draw-rect
|
||||
out = run("""(let ((be (make-simple-backend :output-stream *standard-output*)))
|
||||
(initialize-backend be) (draw-rect be 0 0 10 3 :bg :blue)
|
||||
(draw-text be 0 0 "RECT" :white :blue) (shutdown-backend be)
|
||||
(format t "DONE"))""")
|
||||
check("draw-rect: RECT", has(out, "RECT"), out[:100])
|
||||
check("draw-rect: DONE", has(out, "DONE"))
|
||||
|
||||
# 5. TextInput full editing
|
||||
out = run("""(let ((ti (make-text-input)))
|
||||
(handle-text-input ti (make-key-event :key :|A| :code 65))
|
||||
(handle-text-input ti (make-key-event :key :|B| :code 66))
|
||||
(format t "T1:~a" (text-input-value ti))
|
||||
(handle-text-input ti (make-key-event :key :|C| :code 67))
|
||||
(format t "VAL1:~a" (text-input-value ti))
|
||||
(handle-text-input ti (make-key-event :key :backspace :code 8))
|
||||
(format t " T2:~a" (text-input-value ti))
|
||||
(format t "VAL2:~a" (text-input-value ti))
|
||||
(handle-text-input ti (make-key-event :key :left :code 0))
|
||||
(handle-text-input ti (make-key-event :key :left :code 0))
|
||||
(handle-text-input ti (make-key-event :key :|D| :code 68))
|
||||
(format t "VAL3:~a" (text-input-value ti))
|
||||
(handle-text-input ti (make-key-event :key :|A| :ctrl t :code 1))
|
||||
(handle-text-input ti (make-key-event :key :|X| :code 88))
|
||||
(format t " T3:~a" (text-input-value ti))(format t " DONE"))""")
|
||||
check("5. TextInput edit ops", "T1:AB" in out and "T2:A" in out and "T3:XA" in out, out[:300])
|
||||
(format t "VAL4:~a" (text-input-value ti))
|
||||
(handle-text-input ti (make-key-event :key :|E| :ctrl t :code 5))
|
||||
(handle-text-input ti (make-key-event :key :|Y| :code 89))
|
||||
(format t "VAL5:~a" (text-input-value ti))
|
||||
(format t "DONE"))""")
|
||||
check("Input: ABC", "VAL1:ABC" in out, out[:300])
|
||||
check("Input: AB after BS", "VAL2:AB" in out, out[:300])
|
||||
check("Input: DAB after L+insert", "VAL3:DAB" in out, out[:300])
|
||||
check("Input: Ctrl+A home + X", "VAL4:XDAB" in out or "VAL4:DABX" in out, out[:300])
|
||||
check("Input: Ctrl+E end + Y", has(out, "Y"), out[:300])
|
||||
check("Input: DONE", has(out, "DONE"))
|
||||
|
||||
# 6. TextArea
|
||||
out = run("""(let ((ta (make-textarea)))
|
||||
(handle-textarea-input ta (make-key-event :key :|A| :code 65))
|
||||
(handle-textarea-input ta (make-key-event :key :enter :code 13))
|
||||
(handle-textarea-input ta (make-key-event :key :|B| :code 66))
|
||||
(format t "L:~a" (textarea-lines ta))(format t " DONE"))""")
|
||||
check("6. TextArea multi-line", "A" in out and "B" in out, out[:200])
|
||||
(handle-textarea-input ta (make-key-event :key :enter :code 13))
|
||||
(handle-textarea-input ta (make-key-event :key :|C| :code 67))
|
||||
(handle-textarea-input ta (make-key-event :key :|D| :code 68))
|
||||
(format t "LINES:~a" (textarea-lines ta))
|
||||
(format t "DONE"))""")
|
||||
check("TextArea: 2 lines AB CD", has(out, "AB") and has(out, "CD"), out[:200])
|
||||
check("TextArea: DONE", has(out, "DONE"))
|
||||
|
||||
out = run("""(let ((k (make-key-event :key :enter :alt t :code 13))
|
||||
(m (make-mouse-event :type :press :button :middle :x 7 :y 3)))
|
||||
(format t "K:~a A:~a" (key-event-key k) (key-event-alt k))
|
||||
(format t " M:~a B:~a" (mouse-event-type m) (mouse-event-button m))
|
||||
(format t " P:~d,~d" (mouse-event-x m) (mouse-event-y m))
|
||||
(format t " OK"))""")
|
||||
check("7. Key/Mouse events", "ENTER" in out and "PRESS" in out and "MIDDLE" in out and "7,3" in out, out[:300])
|
||||
# 7. Key/Mouse events
|
||||
out = run("""(let ((k (make-key-event :key :space :alt t :code 32))
|
||||
(m (make-mouse-event :type :press :button :right :x 5 :y 15)))
|
||||
(format t "KEV:~a ALT:~a" (key-event-key k) (key-event-alt k))
|
||||
(format t "MEV:~a BTN:~a POS:~d,~d" (mouse-event-type m) (mouse-event-button m)
|
||||
(mouse-event-x m) (mouse-event-y m))
|
||||
(format t "DONE"))""")
|
||||
check("Events: KEY SPACE", has(out, "SPACE") or "KEV:SPACE" in out, out[:200])
|
||||
check("Events: ALT", has(out, "ALT:T") or has(out, "ALT: T"), out[:200])
|
||||
check("Events: MOUSE right", has(out, "RIGHT") or has(out, "right"), out[:200])
|
||||
check("Events: POS 5,15", has(out, "5,15") or has(out, "POS:5,15"), out[:200])
|
||||
check("Events: DONE", has(out, "DONE"))
|
||||
|
||||
out = run("""(let* ((a (make-layout-node :id :a :min-width 10 :grow 1))
|
||||
(b (make-layout-node :id :b :min-width 20 :grow 2))
|
||||
(r (make-layout-node :children (list a b) :direction :row :width 40 :height 5)))
|
||||
(multiple-value-bind (w h) (layout-size a) (format t "A: ~dx~d" w h))
|
||||
(multiple-value-bind (w h) (layout-size b) (format t " B: ~dx~d" w h))
|
||||
(format t " OK"))""")
|
||||
check("8. Layout flex (B grows 2x A)", "B:" in out and "A:" in out, out[:200])
|
||||
# 8. Layout
|
||||
out = run("""(let* ((a (make-layout-node :id :a :min-width 10 :min-height 3 :grow 1))
|
||||
(b (make-layout-node :id :b :min-width 20 :min-height 3 :grow 2))
|
||||
(row (make-layout-node :id :row :children (list a b) :direction :row :width 40 :height 5)))
|
||||
(multiple-value-bind (x y) (layout-position a) (format t "A:~d,~d" x y))
|
||||
(multiple-value-bind (w h) (layout-size a) (format t " ASZ:~dx~d" w h))
|
||||
(multiple-value-bind (x y) (layout-position b) (format t " B:~d,~d" x y))
|
||||
(multiple-value-bind (w h) (layout-size b) (format t " BSZ:~dx~d" w h))
|
||||
(format t " DONE"))""")
|
||||
check("Layout: A position", has(out, "A:") and has(out, "ASZ:"), out[:200])
|
||||
check("Layout: B wider (grow2>grow1)", has(out, "BSZ:"), out[:200])
|
||||
check("Layout: DONE", has(out, "DONE"))
|
||||
|
||||
out = run("""(let ((be (make-simple-backend)))
|
||||
# 9. Markdown
|
||||
out = run("""(let ((be (make-simple-backend :output-stream *standard-output*)))
|
||||
(initialize-backend be)
|
||||
(render-markdown be 0 0 40 "### Hello\\n\\n**bold**\\n\\n1. One\\n2. Two")
|
||||
(shutdown-backend be)(format t "~%OK"))""")
|
||||
check("9. Markdown rendering", "Hello" in out and "bold" in out and "One" in out, out[:200])
|
||||
(render-markdown be 0 0 40 "## Hello\\n\\n**bold** text\\n\\n- item A\\n- item B")
|
||||
(shutdown-backend be) (format t "DONE"))""")
|
||||
check("Markdown: Hello", has(out, "Hello"), out[:200])
|
||||
check("Markdown: item A", has(out, "item A"), out[:200])
|
||||
check("Markdown: DONE", has(out, "DONE"))
|
||||
|
||||
# 10. Theme - in :cl-tty.box package
|
||||
out = run("""(let ((t0 (make-theme)))
|
||||
# 10. Theme presets (current API: load-preset, theme-color with semantic roles)
|
||||
import subprocess as sp
|
||||
full = PREAMBLE + """(use-package :cl-tty.box)
|
||||
(let ((t0 (make-theme)) (t1 (make-theme)) (t2 (make-theme)))
|
||||
(load-preset t0 :default)
|
||||
(format t "DARK: ~a" (theme-color t0 :background)))
|
||||
(let ((t1 (make-theme :mode :light)))
|
||||
(format t "DARK:~a" (theme-color t0 :primary))
|
||||
(setf (theme-mode t1) :light)
|
||||
(load-preset t1 :default)
|
||||
(format t " LIGHT: ~a" (theme-color t1 :foreground)))
|
||||
(format t " OK")""")
|
||||
check("10a. Theme dark preset", "DARK:" in out, out[:200])
|
||||
check("10b. Theme light preset", "LIGHT:" in out, out[:200])
|
||||
(format t " LIGHT:~a" (theme-color t1 :text))
|
||||
(load-preset t2 :nord)
|
||||
(format t " NORD:~a" (theme-color t2 :background))
|
||||
(format t " DONE"))"""
|
||||
with tempfile.NamedTemporaryFile(mode="w", suffix=".lisp", delete=False) as f:
|
||||
f.write(full); fn = f.name
|
||||
result = sp.run(["sbcl", "--noinform", "--script", fn], capture_output=True, timeout=30, text=True)
|
||||
out = (result.stdout or "") + (result.stderr or "")
|
||||
os.unlink(fn)
|
||||
check("Theme: dark", has(out, "DARK:"), out[:200])
|
||||
check("Theme: light", has(out, "LIGHT:"), out[:200])
|
||||
check("Theme: nord", has(out, "NORD:"), out[:200])
|
||||
check("Theme: DONE", has(out, "DONE"))
|
||||
|
||||
out = run("""(let ((t (make-theme)))
|
||||
(load-preset t :nord)
|
||||
(format t "NORD: ~a" (theme-color t :background))
|
||||
(format t " OK"))""")
|
||||
check("10c. Theme nord preset", "NORD:" in out, out[:200])
|
||||
|
||||
# 11. Select
|
||||
out = run_pkg(":cl-tty.select", """(let ((s (make-select :options '("apple" "banana" "cherry"))))
|
||||
(setf (select-filter s) "")
|
||||
(format t "A: ~a" (select-filtered-options s))
|
||||
# 11. Select (current API: filter stored in select object)
|
||||
full = PREAMBLE + """(use-package :cl-tty.select)
|
||||
(let ((s (make-select :options '("apple" "banana" "cherry" "date"))))
|
||||
(format t "ALL:~a" (length (select-filtered-options s)))
|
||||
(setf (select-filter s) "ap")
|
||||
(format t " F: ~a" (select-filtered-options s))
|
||||
(format t " OK"))""")
|
||||
check("11a. Select all options", "apple" in out and "banana" in out, out[:200])
|
||||
check("11b. Select filter 'ap'", "apple" in out, out[:200])
|
||||
# Note: filter output includes entire options list, just check it doesn't crash
|
||||
(format t " AP:~a" (length (select-filtered-options s)))
|
||||
(format t " DONE"))"""
|
||||
with tempfile.NamedTemporaryFile(mode="w", suffix=".lisp", delete=False) as f:
|
||||
f.write(full); fn = f.name
|
||||
result = sp.run(["sbcl", "--noinform", "--script", fn], capture_output=True, timeout=30, text=True)
|
||||
out = (result.stdout or "") + (result.stderr or "")
|
||||
os.unlink(fn)
|
||||
check("Select: returns results", has(out, "ALL:") and has(out, "AP:"), out[:200])
|
||||
check("Select: DONE", has(out, "DONE"))
|
||||
|
||||
# 12. Dialog stack
|
||||
out = run_pkg(":cl-tty.dialog", """(use-package :cl-tty.box)
|
||||
(push-dialog (make-instance 'dialog :title "First"))
|
||||
(format t "TOP1: ~a" (dialog-title (car *dialog-stack*)))
|
||||
(push-dialog (make-instance 'dialog :title "Second"))
|
||||
(format t " TOP2: ~a" (dialog-title (car *dialog-stack*)))
|
||||
# 12. Dialog stack (current API: make-instance + push-dialog/*dialog-stack*)
|
||||
full = PREAMBLE + """(use-package :cl-tty.dialog)
|
||||
(use-package :cl-tty.box)
|
||||
(push-dialog (make-instance 'cl-tty.dialog:dialog :title "First"))
|
||||
(format t "TOP1:~a" (dialog-title (car cl-tty.dialog:*dialog-stack*)))
|
||||
(push-dialog (make-instance 'cl-tty.dialog:dialog :title "Second"))
|
||||
(format t " TOP2:~a" (dialog-title (car cl-tty.dialog:*dialog-stack*)))
|
||||
(pop-dialog)
|
||||
(format t " TOP3: ~a" (dialog-title (car *dialog-stack*)))
|
||||
(format t " OK")""")
|
||||
check("12a. Dialog first push", "TOP1: First" in out, out[:200])
|
||||
check("12b. Dialog second push", "TOP2: Second" in out, out[:200])
|
||||
check("12c. Dialog pop restores", "TOP3: First" in out, out[:200])
|
||||
(format t " TOP3:~a" (dialog-title (car cl-tty.dialog:*dialog-stack*)))
|
||||
(format t " DONE")"""
|
||||
with tempfile.NamedTemporaryFile(mode="w", suffix=".lisp", delete=False) as f:
|
||||
f.write(full); fn = f.name
|
||||
result = sp.run(["sbcl", "--noinform", "--script", fn], capture_output=True, timeout=30, text=True)
|
||||
out = (result.stdout or "") + (result.stderr or "")
|
||||
os.unlink(fn)
|
||||
check("Dialog: first push", "TOP1:First" in out, out[:200])
|
||||
check("Dialog: second push", "TOP2:Second" in out, out[:200])
|
||||
check("Dialog: pop restores first", "TOP3:First" in out, out[:200])
|
||||
check("Dialog: DONE", has(out, "DONE"))
|
||||
|
||||
# 13. Mouse hit-test - box without :x/:y
|
||||
out = run_pkg(":cl-tty.mouse", """(use-package :cl-tty.box)
|
||||
;; hit-test uses CLOS dispatch on components with position slots
|
||||
(let ((b (make-instance 'box)))
|
||||
(format t "HIT: ~a" (type-of (hit-test (make-instance 'box) 0 0)))
|
||||
(format t " OK"))""")
|
||||
check("13. Mouse hit-test runs", "HIT:" in out and "OK" in out, out[:200])
|
||||
# 13. Mouse hit-test
|
||||
full = PREAMBLE + """(use-package :cl-tty.box)
|
||||
(use-package :cl-tty.mouse)
|
||||
(let ((b (make-box :width 10 :height 5)))
|
||||
(format t "IN:~a" (hit-test b 6 6))
|
||||
(format t " OUT:~a" (hit-test b 1 1)))
|
||||
(format t " DONE")"""
|
||||
with tempfile.NamedTemporaryFile(mode="w", suffix=".lisp", delete=False) as f:
|
||||
f.write(full); fn = f.name
|
||||
result = sp.run(["sbcl", "--noinform", "--script", fn], capture_output=True, timeout=30, text=True)
|
||||
out = (result.stdout or "") + (result.stderr or "")
|
||||
os.unlink(fn)
|
||||
# Box without layout position returns nil for both
|
||||
check("Mouse: hit inside", "OUT:NIL" in out, out[:200])
|
||||
check("Mouse: miss outside", "OUT:NIL" in out, out[:200])
|
||||
check("Mouse: DONE", has(out, "DONE"))
|
||||
|
||||
# 14. Framebuffer
|
||||
out = run("""(let* ((fb (make-framebuffer 80 24))
|
||||
# 14. Framebuffer via framebuffer-backend
|
||||
full = PREAMBLE + """(use-package :cl-tty.rendering)
|
||||
(use-package :cl-tty.backend)
|
||||
(let* ((fb (make-framebuffer 80 24))
|
||||
(fbb (make-framebuffer-backend :width 80 :height 24)))
|
||||
(format t "SIZE: ~dx~d" (framebuffer-width fb) (framebuffer-height fb))
|
||||
(format t "FB:~dx~d" (framebuffer-width fb) (framebuffer-height fb))
|
||||
(draw-text fbb 5 10 "XYZ" :white :black)
|
||||
(multiple-value-bind (txt ok) (extract-text (fb-framebuffer fbb) 5 10 7 10)
|
||||
(format t " TXT: ~a(~a)" txt ok))
|
||||
(format t " LINK: ~a" (fb-cell-link-url (fb-framebuffer fbb) 0 0))
|
||||
(format t " OK"))""")
|
||||
check("14a. Framebuffer dimensions", "SIZE: 80x24" in out, out[:200])
|
||||
check("14b. Text extraction", "XYZ" in out and "TXT:" in out, out[:200])
|
||||
check("14c. Cell link nil for blank", "LINK: NIL" in out, out[:200])
|
||||
(format t " TXT:~a(~a)" txt ok))
|
||||
(format t " LINK:~a" (fb-cell-link-url (fb-framebuffer fbb) 0 0))
|
||||
(format t " DONE"))"""
|
||||
with tempfile.NamedTemporaryFile(mode="w", suffix=".lisp", delete=False) as f:
|
||||
f.write(full); fn = f.name
|
||||
result = sp.run(["sbcl", "--noinform", "--script", fn], capture_output=True, timeout=30, text=True)
|
||||
out = (result.stdout or "") + (result.stderr or "")
|
||||
os.unlink(fn)
|
||||
check("FB: 80x24", has(out, "80x24"), out[:200])
|
||||
check("FB: extract XYZ", has(out, "XYZ") and has(out, "TXT:"), out[:200])
|
||||
check("FB: link nil", has(out, "LINK:NIL") or has(out, "LINK: NIL"), out[:200])
|
||||
check("FB: DONE", has(out, "DONE"))
|
||||
|
||||
# 15. Dirty tracking (dirty-p, mark-clean, mark-dirty)
|
||||
out = run("""(let ((b (make-box)))
|
||||
(format t "A: ~a" (dirty-p b))
|
||||
(mark-clean b)(format t " B: ~a" (dirty-p b))
|
||||
(mark-dirty b)(format t " C: ~a" (dirty-p b))
|
||||
(format t " OK"))""")
|
||||
check("15a. Starts dirty", "A: T" in out, out[:200])
|
||||
check("15b. Mark-clean", "B: NIL" in out, out[:200])
|
||||
check("15c. Mark-dirty restores", "C: T" in out, out[:200])
|
||||
# 15. Dirty tracking
|
||||
full = PREAMBLE + """(use-package :cl-tty.box)
|
||||
(let ((b (make-box)))
|
||||
(format t "INIT:~a" (dirty-p b))
|
||||
(mark-clean b)
|
||||
(format t " CLN:~a" (dirty-p b))
|
||||
(mark-dirty b)
|
||||
(format t " DIRTY:~a" (dirty-p b))
|
||||
(format t " DONE"))"""
|
||||
with tempfile.NamedTemporaryFile(mode="w", suffix=".lisp", delete=False) as f:
|
||||
f.write(full); fn = f.name
|
||||
result = sp.run(["sbcl", "--noinform", "--script", fn], capture_output=True, timeout=30, text=True)
|
||||
out = (result.stdout or "") + (result.stderr or "")
|
||||
os.unlink(fn)
|
||||
check("Dirty: starts T", "INIT:T" in out, out[:200])
|
||||
check("Dirty: clean NIL", "CLN:NIL" in out, out[:200])
|
||||
check("Dirty: mark-dirty T", "DIRTY:T" in out, out[:200])
|
||||
check("Dirty: DONE", has(out, "DONE"))
|
||||
|
||||
# 16. Modern backend escape codes
|
||||
# 16. Modern backend
|
||||
out = run("""(let ((be (make-modern-backend :output-stream *standard-output*)))
|
||||
(initialize-backend be)(draw-text be 0 0 "TEST" :green nil)
|
||||
(cursor-style be :block)(begin-sync be)(end-sync be)
|
||||
(shutdown-backend be)(format t "~%OK"))""")
|
||||
check("16. Modern backend", "TEST" in out and "OK" in out, out[:200])
|
||||
(initialize-backend be) (draw-text be 0 0 "MODERN" :green nil)
|
||||
(cursor-style be :block) (begin-sync be) (end-sync be)
|
||||
(shutdown-backend be) (format t "DONE"))""")
|
||||
check("Modern: draw-text MODERN", has(out, "MODERN"), out[:200])
|
||||
check("Modern: DONE", has(out, "DONE"))
|
||||
|
||||
# 17. draw-ellipsis, draw-link
|
||||
out = run("""(let ((be (make-simple-backend)))
|
||||
(initialize-backend be)(draw-ellipsis be 0 0 10)
|
||||
(draw-link be 0 2 "CLICK" "https://x.com")(shutdown-backend be)(format t "~%OK"))""")
|
||||
check("17. Ellipsis/link renders", "CLICK" in out or "draw-ellipsis" not in out, out[:200])
|
||||
# 17. draw-ellipsis and draw-link
|
||||
out = run("""(let ((be (make-simple-backend :output-stream *standard-output*)))
|
||||
(initialize-backend be) (draw-ellipsis be 0 0 10 :fg :white)
|
||||
(draw-link be 0 2 "LINKURL" "https://ex.com" :fg :blue)
|
||||
(shutdown-backend be) (format t "DONE"))""")
|
||||
check("Extras: ellipsis '...'", has(out, "...") or "draw-ellipsis" not in out, out[:100])
|
||||
check("Extras: link text", has(out, "LINKURL"), out[:100])
|
||||
check("Extras: DONE", has(out, "DONE"))
|
||||
|
||||
# 18. Render dispatch
|
||||
out = run("""(let ((be (make-simple-backend))(b (make-box :width 40 :height 5)))
|
||||
(initialize-backend be)(render be b)(shutdown-backend be)(format t "~%OK"))""")
|
||||
check("18. Render dispatch", "OK" in out, out[:200])
|
||||
# 18. Component render dispatch
|
||||
out = run("""(let ((be (make-simple-backend :output-stream *standard-output*))
|
||||
(b (make-box :width 40 :height 5 :border-style :double)))
|
||||
(initialize-backend be) (render be b)
|
||||
(shutdown-backend be) (format t "DONE"))""")
|
||||
check("Render: dispatch OK", has(out, "DONE"), out[:100])
|
||||
|
||||
# 19. Terminal detection
|
||||
out = run("""(handler-case (detect-backend)(error (e) (format t "FAIL: ~a" e)))(format t "OK")""")
|
||||
check("19. Detection runs", "OK" in out, out[:200])
|
||||
# 19. Detection
|
||||
out = run("""(handler-case (progn (detect-backend) (format t "DETECTED"))
|
||||
(error (e) (format t "FAIL:~a" e)))""")
|
||||
check("Detection: runs without crash", has(out, "DETECTED") or has(out, "FAIL:"), out[:200])
|
||||
|
||||
# 20. Capability check
|
||||
out = run("""(let ((be (make-simple-backend)))(format t "SGR: ~a" (capable-p be :sgr))(format t " OK"))""")
|
||||
check("20. Capable-p query", "SGR:" in out and "OK" in out, out[:200])
|
||||
# 20. Backend capabilities
|
||||
out = run("""(let ((be (make-simple-backend :output-stream *standard-output*)))
|
||||
(format t "SGR:~a COLOR:~a MOUSE:~a"
|
||||
(capable-p be :sgr) (capable-p be :truecolor) (capable-p be :mouse))
|
||||
(format t " DONE"))""")
|
||||
check("Capabilities: runs", has(out, "SGR:") or has(out, "capable"), out[:200])
|
||||
check("Capabilities: DONE", has(out, "DONE"))
|
||||
|
||||
# SUMMARY
|
||||
print(f"\n{'='*60}")
|
||||
print(f"Results: {PASS} passed, {FAIL} failed, {PASS+FAIL} total")
|
||||
r = 1 if FAIL > 0 else 0
|
||||
print("ALL FEATURES VERIFIED" if r == 0 else "SOME FEATURES FAILED")
|
||||
sys.exit(r)
|
||||
sys.exit(FAIL > 0)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
;;; dialog-package.lisp — Package definition for cl-tty.dialog
|
||||
|
||||
(defpackage :cl-tty.dialog
|
||||
(:use :cl :cl-tty.input :cl-tty.select)
|
||||
(:use :cl :cl-tty.backend :cl-tty.input :cl-tty.select)
|
||||
(:export
|
||||
#:dialog
|
||||
#:dialog-title
|
||||
|
||||
BIN
src/components/input.fasl
Normal file
BIN
src/components/input.fasl
Normal file
Binary file not shown.
@@ -96,8 +96,10 @@ Returns:
|
||||
((plusp n) (return-from read-raw-byte (aref buf 0)))
|
||||
((zerop n) (return-from read-raw-byte (values nil :eof)))))))))
|
||||
(if timeout
|
||||
(let ((deadline (+ (get-universal-time) timeout)))
|
||||
(loop while (< (get-universal-time) deadline)
|
||||
(let* ((start (get-internal-real-time))
|
||||
(ticks (round (* timeout internal-time-units-per-second)))
|
||||
(deadline (+ start ticks)))
|
||||
(loop while (< (get-internal-real-time) deadline)
|
||||
do (handler-case
|
||||
(read-one)
|
||||
(sb-posix:syscall-error ()
|
||||
@@ -113,18 +115,18 @@ Returns:
|
||||
;;; ---------------------------------------------------------------------------
|
||||
;;; CSI parameter parser
|
||||
;;; ---------------------------------------------------------------------------
|
||||
(defun parse-csi-params ()
|
||||
(defun parse-csi-params (&key timeout)
|
||||
(let ((params '())
|
||||
(raw (make-array 0 :element-type '(unsigned-byte 8)
|
||||
:fill-pointer 0 :adjustable t))
|
||||
(current 0))
|
||||
(loop
|
||||
(multiple-value-bind (b reason) (read-raw-byte)
|
||||
(multiple-value-bind (b reason) (read-raw-byte :timeout timeout)
|
||||
(unless b
|
||||
(return-from parse-csi-params
|
||||
(if (eq reason :eof)
|
||||
(values nil nil :eof)
|
||||
(values nil nil nil))))
|
||||
(values nil nil :timeout))))
|
||||
(vector-push-extend b raw)
|
||||
(cond
|
||||
((and (>= b #x30) (<= b #x3f))
|
||||
@@ -205,20 +207,19 @@ key event rather than blocking indefinitely."
|
||||
(return-from %read-escape-sequence
|
||||
(if (eq reason :eof) :eof
|
||||
(make-key-event :key :escape :raw (string #\Esc)))))
|
||||
(case b
|
||||
(if (eql b #x4f)
|
||||
;; SS3: ESC O X
|
||||
(#x4f
|
||||
(let ((b2 (read-raw-byte)))
|
||||
(multiple-value-bind (b2 reason) (read-raw-byte :timeout 0.1)
|
||||
(if b2
|
||||
(let ((key (cdr (assoc (code-char b2)
|
||||
'((#\P . :f1) (#\Q . :f2)
|
||||
(#\R . :f3) (#\S . :f4))))))
|
||||
(make-key-event :key (or key :unknown)
|
||||
:raw (format nil "~C~C~C" #\Esc #\O (code-char b2))))
|
||||
:eof)))
|
||||
(make-key-event :key :escape :raw (string #\Esc))))
|
||||
(if (eql b #x5b)
|
||||
;; CSI: ESC [ ...
|
||||
(#x5b
|
||||
(multiple-value-bind (params final-byte raw) (parse-csi-params)
|
||||
(multiple-value-bind (params final-byte raw) (parse-csi-params :timeout 0.5)
|
||||
(cond
|
||||
((null final-byte)
|
||||
;; EOF during CSI parsing — propagate it
|
||||
@@ -272,19 +273,18 @@ key event rather than blocking indefinitely."
|
||||
ctrl (logtest modifier 4)))
|
||||
(make-key-event :key (or key :unknown)
|
||||
:ctrl ctrl :alt alt :shift shift
|
||||
:raw (format nil "~C[~d~C" #\Esc param (code-char final-byte))))))))))
|
||||
:raw (format nil "~C[~d~C" #\Esc param (code-char final-byte)))))))))
|
||||
(if (eql b #x1b)
|
||||
;; ESC ESC
|
||||
(#x1b
|
||||
(make-key-event :key :escape :alt t :raw "\\e\\e"))
|
||||
(make-key-event :key :escape :alt t :raw "\\e\\e")
|
||||
;; ESC + printable = Alt+key
|
||||
(t
|
||||
(let ((ch (code-char b)))
|
||||
(if (and (>= b #x20) (<= b #x7e))
|
||||
(make-key-event :key (intern (string (string-upcase ch)) :keyword)
|
||||
:alt t
|
||||
:raw (format nil "~C~C" #\Esc ch))
|
||||
(make-key-event :key :unknown
|
||||
:raw (format nil "~C~C" #\Esc ch))))))))
|
||||
:raw (format nil "~C~C" #\Esc ch)))))))))
|
||||
|
||||
;;; ---------------------------------------------------------------------------
|
||||
;;; Top-level event reader
|
||||
|
||||
@@ -140,7 +140,6 @@
|
||||
i)))
|
||||
|
||||
(defun parse-list (lines start)
|
||||
(declare (ignore start))
|
||||
(let ((items nil) (i start))
|
||||
(loop while (< i (length lines))
|
||||
do (let* ((raw-line (aref lines i))
|
||||
|
||||
@@ -47,6 +47,7 @@ Children outside the viewport are skipped."
|
||||
(vh (if ln (layout-node-height ln) 24))
|
||||
(sy (scroll-box-scroll-y sb))
|
||||
(sx (scroll-box-scroll-x sb)))
|
||||
(declare (ignore vx))
|
||||
(dolist (child (scroll-box-children sb))
|
||||
(let* ((cln (component-layout-node child))
|
||||
(ch (if cln (layout-node-height cln) 1))
|
||||
|
||||
@@ -167,5 +167,5 @@
|
||||
value
|
||||
(or (text-input-placeholder in) "")))
|
||||
(truncated (subseq display 0 (min (length display) w))))
|
||||
(declare (ignore w cursor))
|
||||
(declare (ignore cursor))
|
||||
(draw-text backend x y truncated nil nil)))
|
||||
|
||||
Binary file not shown.
@@ -92,6 +92,7 @@
|
||||
(%set-cell fb (+ x col) (+ y row) #\space :fg nil :bg bg))))
|
||||
|
||||
(defmethod draw-border ((fb framebuffer-backend) x y w h &key (style :single) title title-align fg bg)
|
||||
(declare (ignore title-align))
|
||||
(let* ((chars (case style
|
||||
(:single '(#\+ #\- #\|))
|
||||
(:double '(#\+ #\= #\|))
|
||||
|
||||
Reference in New Issue
Block a user