wip: unified minibuffer panel, v0.9.1 Emacs dev env in ROADMAP
- Unified minibuffer slash-command panel (panel-based wizard, settings, help sub-mode stack) — channel-tui state/view changes - ROADMAP: v0.8.0 broken into atomic DONE items, v0.9.1 added with Emacs major mode + M-x command surface TODOs - Semver discipline from v0.7.1 onward (X.Y.Z)
This commit is contained in:
@@ -517,7 +517,7 @@ Respects CJK/emoji char widths via char-width."
|
||||
(when style-bits
|
||||
(remove-attributes win (get-bitmask style-bits)))
|
||||
(incf x (length text))))
|
||||
y)
|
||||
(1+ y))
|
||||
|
||||
(defun parse-markdown-blocks (text)
|
||||
"Split text at ``` code block boundaries."
|
||||
@@ -612,7 +612,7 @@ Respects CJK/emoji char widths via char-width."
|
||||
(nreverse lines)))
|
||||
#+end_src
|
||||
|
||||
* v0.8.0 — Sidebar + Palette View
|
||||
* v0.8.0 — Sidebar + Minibuffer View
|
||||
#+begin_src lisp
|
||||
(in-package :passepartout.channel-tui)
|
||||
|
||||
@@ -711,103 +711,183 @@ Respects CJK/emoji char widths via char-width."
|
||||
(refresh win)
|
||||
(- y 1)))
|
||||
|
||||
(defun palette-filter (items query)
|
||||
"Return items from categorized list whose :name or :desc contains QUERY (case-insensitive)."
|
||||
(if (or (null query) (string= query ""))
|
||||
items
|
||||
(let ((q (string-downcase query)))
|
||||
(loop for group in items
|
||||
for category = (getf group :category)
|
||||
for gitems = (getf group :items)
|
||||
for filtered = (loop for item in gitems
|
||||
when (or (search q (string-downcase (getf item :name)))
|
||||
(search q (string-downcase (or (getf item :desc) ""))))
|
||||
collect item)
|
||||
when filtered
|
||||
collect (list :category category :items filtered)))))
|
||||
(defun view-minibuffer (win)
|
||||
"Render the bottom-anchored minibuffer panel. Dispatches on :minibuffer-mode."
|
||||
(case (st :minibuffer-mode)
|
||||
(:slash-menu (view-slash-menu win))
|
||||
(:wizard (view-wizard-in-panel win))
|
||||
(t nil)))
|
||||
|
||||
(defun view-palette (win)
|
||||
"Render centered command palette overlay with filtered items, selection highlight."
|
||||
(clear win)
|
||||
(setf (color-pair win) (list (theme-color :border) (theme-color :background)))
|
||||
(box win 0 0)
|
||||
(let* ((w (or (width win) 50))
|
||||
(h (or (height win) 20))
|
||||
(y 1)
|
||||
(query (or (st :palette-filter) ""))
|
||||
(items (palette-filter (st :palette-items) query))
|
||||
(selected (st :palette-selected-idx))
|
||||
(flat-index 0)
|
||||
(visible-start (max 0 (- selected (floor (- h 6) 2)))))
|
||||
(add-string win (format nil " Command Palette ") :y y :x 2 :n (- w 4) :fgcolor (theme-color :accent))
|
||||
(incf y)
|
||||
(add-string win (format nil " > ~a" (if (> (length query) 0) query "type to filter..."))
|
||||
:y y :x 2 :n (- w 4) :fgcolor (theme-color :input) :attributes '(:underline t))
|
||||
(incf y)
|
||||
(dolist (group items)
|
||||
(let ((category (getf group :category))
|
||||
(gitems (getf group :items)))
|
||||
(when (and gitems (< y (1- h)))
|
||||
(incf y)
|
||||
(add-string win (format nil "── ~a ──" category) :y y :x 2 :n (- w 4) :fgcolor (theme-color :dim))
|
||||
(dolist (item gitems)
|
||||
(when (< y (1- h))
|
||||
(incf y)
|
||||
(let* ((name (getf item :name))
|
||||
(desc (getf item :desc))
|
||||
(shortcut (getf item :shortcut))
|
||||
(is-selected (= flat-index selected))
|
||||
(fg (if is-selected (theme-color :accent) (theme-color :agent))))
|
||||
(when is-selected
|
||||
(add-string win (make-string (- w 4) :initial-element #\Space) :y y :x 2 :n (- w 4)
|
||||
:fgcolor (theme-color :dim) :bgcolor (theme-color :accent)))
|
||||
(add-string win (format nil " ~a" name) :y y :x 3 :n (- w 6) :fgcolor fg)
|
||||
(when (and shortcut (> (- w 6) (+ 4 (length shortcut))))
|
||||
(add-string win shortcut :y y :x (- w (length shortcut) 3) :n (length shortcut) :fgcolor (theme-color :dim)))
|
||||
(incf flat-index)))))))
|
||||
(add-string win (format nil " ↑↓ Navigate Enter Execute Esc Close")
|
||||
:y (- h 1) :x 2 :n (- w 4) :fgcolor (theme-color :dim))
|
||||
(refresh win)
|
||||
(- h 1)))
|
||||
(defvar *slash-commands* nil) ; forward declaration — defined in channel-tui-main
|
||||
|
||||
(defun view-wizard (win)
|
||||
"Render setup wizard overlay: step title, prompt, input, error, progress."
|
||||
(defun view-slash-menu (win)
|
||||
"Render the slash-command menu: filter bar, filtered command list, selection highlight."
|
||||
(clear win)
|
||||
(setf (color-pair win) (list (theme-color :border) (theme-color :background)))
|
||||
(box win 0 0)
|
||||
(let* ((w (or (width win) 60))
|
||||
(h (or (height win) 15))
|
||||
(h (or (height win) 10))
|
||||
(y 1)
|
||||
(steps (passepartout.channel-tui::wizard-steps))
|
||||
(step-idx (st :wizard-step))
|
||||
(step (when (< step-idx (length steps)) (nth step-idx steps)))
|
||||
(prompt (getf step :prompt))
|
||||
(title (getf step :title))
|
||||
(total (length steps))
|
||||
(filter (or (st :minibuffer-filter) ""))
|
||||
(commands passepartout.channel-tui::*slash-commands*)
|
||||
(filtered (if (or (null filter) (string= filter ""))
|
||||
(mapcar (lambda (c) (list :index (position c commands) :cmd c)) commands)
|
||||
(let ((q (string-downcase filter)) (i 0) (r nil))
|
||||
(dolist (c commands (nreverse r))
|
||||
(when (or (search q (string-downcase (getf c :name)))
|
||||
(search q (string-downcase (or (getf c :desc) ""))))
|
||||
(push (list :index i :cmd c) r))
|
||||
(incf i)))))
|
||||
(sel (or (st :minibuffer-selected-idx) 0))
|
||||
(max-visible (- h 3)))
|
||||
;; Header: filter bar
|
||||
(add-string win (format nil " Commands") :y y :x 2 :n (- w 4) :fgcolor (theme-color :accent))
|
||||
(incf y)
|
||||
(add-string win (format nil " > ~a_" (if (> (length filter) 0) filter "/"))
|
||||
:y y :x 2 :n (- w 4) :fgcolor (theme-color :input))
|
||||
(incf y)
|
||||
;; Command list
|
||||
(if filtered
|
||||
(let* ((start (max 0 (- sel (floor max-visible 2))))
|
||||
(end (min (length filtered) (+ start max-visible)))
|
||||
(flat-i 0))
|
||||
(loop for entry across (subseq (coerce filtered 'vector) start end)
|
||||
for fi from start
|
||||
for cmd = (getf entry :cmd)
|
||||
do (let* ((name (getf cmd :name))
|
||||
(desc (getf cmd :desc))
|
||||
(selected (= fi sel))
|
||||
(fg (if selected (theme-color :highlight) (theme-color :agent))))
|
||||
(when selected
|
||||
(add-string win (make-string (- w 4) :initial-element #\Space) :y y :x 2 :n (- w 4)
|
||||
:fgcolor (theme-color :dim) :bgcolor (theme-color :highlight)))
|
||||
(let ((prefix (if selected " > " " ")))
|
||||
(add-string win (format nil "~a~a" prefix name) :y y :x 3 :n (min (- w 6) 25) :fgcolor fg)
|
||||
(when desc
|
||||
(add-string win (format nil " — ~a" desc) :y y :x 28 :n (min (- w 30) (length desc)) :fgcolor (theme-color :dim))))
|
||||
(incf y))))
|
||||
(progn
|
||||
(add-string win " (no matching commands)" :y y :x 2 :n (- w 4) :fgcolor (theme-color :dim))
|
||||
(incf y)))
|
||||
;; Footer
|
||||
(add-string win " ↑↓ Navigate Enter Execute Esc Close"
|
||||
:y (- h 0) :x 2 :n (- w 4) :fgcolor (theme-color :dim))
|
||||
(refresh win)
|
||||
(- h 0)))
|
||||
|
||||
(defun view-wizard-in-panel (win)
|
||||
"Render the setup wizard in the bottom-anchored minibuffer panel. Three modes: provider-list, key-entry, cascade-config."
|
||||
(clear win)
|
||||
(setf (color-pair win) (list (theme-color :border) (theme-color :background)))
|
||||
(box win 0 0)
|
||||
(let* ((w (or (width win) 70))
|
||||
(h (or (height win) 14))
|
||||
(y 1)
|
||||
(mode (st :wizard-mode))
|
||||
(error-msg (st :wizard-error))
|
||||
(input (or (st :wizard-input) "")))
|
||||
(selected-idx (st :wizard-selected-idx))
|
||||
(providers (passepartout.channel-tui::wizard-provider-list))
|
||||
(configured (st :wizard-providers)))
|
||||
(add-string win "Setup Wizard" :y y :x 2 :n (- w 4) :fgcolor (theme-color :accent))
|
||||
(incf y 2)
|
||||
(add-string win (format nil "Step ~d/~d" (1+ step-idx) total) :y y :x 2 :n (- w 4) :fgcolor (theme-color :dim))
|
||||
(incf y)
|
||||
(when title
|
||||
(add-string win title :y y :x 3 :n (- w 6) :fgcolor (theme-color :accent))
|
||||
(incf y))
|
||||
(when prompt
|
||||
(add-string win prompt :y y :x 3 :n (- w 6) :fgcolor (theme-color :agent))
|
||||
(incf y))
|
||||
(incf y)
|
||||
(add-string win (format nil "> ~a" input) :y y :x 3 :n (- w 6) :fgcolor (theme-color :input))
|
||||
(incf y)
|
||||
(case mode
|
||||
(:provider-list
|
||||
(let ((count (/ (length configured) 2)))
|
||||
(add-string win (format nil "Configure Providers~a"
|
||||
(if (> count 0) (format nil " — ~d configured" count) ""))
|
||||
:y y :x 2 :n (- w 4) :fgcolor (theme-color :dim))
|
||||
(incf y)
|
||||
(loop for p in providers
|
||||
for i from 0
|
||||
do (let* ((meta (passepartout.channel-tui::wizard-provider-meta p))
|
||||
(name (car meta))
|
||||
(key (getf configured p))
|
||||
(prefix (if (= i selected-idx) "> " " "))
|
||||
(suffix (if key " ✓" ""))
|
||||
(color (if (= i selected-idx)
|
||||
(theme-color :highlight)
|
||||
(theme-color :dim))))
|
||||
(add-string win (format nil "~a~a~a" prefix name suffix)
|
||||
:y y :x 3 :n (- w 6) :fgcolor color)
|
||||
(incf y)))
|
||||
(incf y)
|
||||
(add-string win " Done — configure cascade"
|
||||
:y y :x 3 :n (- w 6)
|
||||
:fgcolor (if (>= selected-idx (length providers))
|
||||
(theme-color :highlight)
|
||||
(theme-color :dim)))
|
||||
(when (>= selected-idx (length providers))
|
||||
(add-string win ">" :y y :x 1 :n 2 :fgcolor (theme-color :highlight))))
|
||||
(:key-entry
|
||||
(let* ((provider (st :wizard-current-provider))
|
||||
(meta (passepartout.channel-tui::wizard-provider-meta provider))
|
||||
(name (car meta))
|
||||
(url (cadr meta))
|
||||
(input (or (st :wizard-input) "")))
|
||||
(add-string win (format nil "API Key: ~a" name) :y y :x 2 :n (- w 4) :fgcolor (theme-color :agent))
|
||||
(incf y)
|
||||
(when url
|
||||
(add-string win (format nil "Get key at: ~a" url) :y y :x 3 :n (- w 6) :fgcolor (theme-color :dim))
|
||||
(incf y))
|
||||
(add-string win "Enter your API key." :y y :x 3 :n (- w 6) :fgcolor (theme-color :dim))
|
||||
(incf y 2)
|
||||
(add-string win (format nil "Key: > ~a" input) :y y :x 3 :n (- w 6) :fgcolor (theme-color :input))
|
||||
(incf y)
|
||||
(when error-msg
|
||||
(add-string win (format nil "! ~a" error-msg) :y y :x 3 :n (- w 6) :fgcolor (theme-color :error))
|
||||
(incf y))
|
||||
(incf y)
|
||||
(add-string win "Enter=Save Esc=Back Bksp=Edit Ctrl+U=Clear"
|
||||
:y (- h 0) :x 2 :n (- w 4) :fgcolor (theme-color :dim))
|
||||
(return-from view-wizard-in-panel)))
|
||||
(:cascade-config
|
||||
(let* ((slot (st :wizard-cascade-slot))
|
||||
(slot-providers (getf (st :wizard-cascade) slot))
|
||||
(slot-label (cadr (assoc slot passepartout.channel-tui::*wizard-cascade-labels*)))
|
||||
(count (/ (length configured) 2)))
|
||||
(add-string win (format nil "Configure Cascade — ~d provider~:p" count)
|
||||
:y y :x 2 :n (- w 4) :fgcolor (theme-color :dim))
|
||||
(incf y)
|
||||
(add-string win (or slot-label "Unknown") :y y :x 2 :n (- w 4) :fgcolor (theme-color :accent))
|
||||
(incf y)
|
||||
(let ((shown nil))
|
||||
(loop for p in providers
|
||||
for i from 0
|
||||
do (when (getf configured p)
|
||||
(let* ((meta (passepartout.channel-tui::wizard-provider-meta p))
|
||||
(name (car meta))
|
||||
(in-slot (member p slot-providers))
|
||||
(prefix (if (= i selected-idx) "> " " "))
|
||||
(mark (if in-slot " [✓]" " [ ]"))
|
||||
(color (if (= i selected-idx)
|
||||
(theme-color :highlight)
|
||||
(if in-slot (theme-color :gate-passed) (theme-color :dim)))))
|
||||
(add-string win (format nil "~a~a~a" prefix name mark)
|
||||
:y y :x 3 :n (- w 6) :fgcolor color)
|
||||
(incf y)
|
||||
(push t shown))))
|
||||
(unless shown
|
||||
(add-string win " (no providers configured)"
|
||||
:y y :x 3 :n (- w 6) :fgcolor (theme-color :dim))
|
||||
(incf y)))
|
||||
(incf y)
|
||||
(add-string win (format nil "Cascade: ~{~a~^, ~}"
|
||||
(or slot-providers '("(none)")))
|
||||
:y y :x 3 :n (- w 6) :fgcolor (theme-color :dim))))
|
||||
(when error-msg
|
||||
(add-string win (format nil "! ~a" error-msg) :y y :x 3 :n (- w 6) :fgcolor (theme-color :error))
|
||||
(incf y))
|
||||
(add-string win "Enter=Next Esc=Cancel Bksp=Edit" :y (- h 2) :x 2 :n (- w 4) :fgcolor (theme-color :dim))
|
||||
(refresh win)
|
||||
(- h 1)))
|
||||
(incf y)
|
||||
(add-string win (format nil "! ~a" error-msg) :y y :x 3 :n (- w 6) :fgcolor (theme-color :error)))
|
||||
(let ((footer (case mode
|
||||
(:provider-list "↑↓ Navigate Enter=Select Esc=Back Ctrl+D=Remove")
|
||||
(:cascade-config "↑↓ Select Enter=Toggle Tab=Next Quadrant Ctrl+S=Save Esc=Back")
|
||||
(t ""))))
|
||||
(when footer
|
||||
(add-string win footer :y (- h 0) :x 2 :n (- w 4) :fgcolor (theme-color :dim))))
|
||||
(- h 0)))))
|
||||
|
||||
#+end_src
|
||||
|
||||
* v0.8.0 Tests — Sidebar View
|
||||
* v0.8.0 Tests — Sidebar View + Minibuffer View
|
||||
#+begin_src lisp
|
||||
(in-package :passepartout-tui-view-tests)
|
||||
|
||||
@@ -831,26 +911,21 @@ Respects CJK/emoji char widths via char-width."
|
||||
(dolist (name names)
|
||||
(is (getf presets name) (format nil "~a preset should exist" name)))))
|
||||
|
||||
(test test-palette-filter-matches-substring
|
||||
"Contract v0.8.0: palette-filter returns items matching query."
|
||||
(let* ((items (list (list :category "Session" :items
|
||||
(list (list :name "/focus" :desc "Set context" :shortcut nil :action nil)
|
||||
(list :name "/scope" :desc "Change scope" :shortcut nil :action nil)))))
|
||||
(filtered (passepartout.channel-tui::palette-filter items "focus")))
|
||||
(is (= 1 (length (getf (first filtered) :items))))
|
||||
(is (string= "/focus" (getf (first (getf (first filtered) :items)) :name)))))
|
||||
(test test-minibuffer-init-state-fields
|
||||
"Contract v0.8.0: init-state includes minibuffer-mode, selected-idx, filter; excludes palette and wizard-visible."
|
||||
(passepartout.channel-tui::init-state)
|
||||
(is (null (passepartout.channel-tui::st :minibuffer-mode)))
|
||||
(is (= 0 (passepartout.channel-tui::st :minibuffer-selected-idx)))
|
||||
(is (string= "" (passepartout.channel-tui::st :minibuffer-filter)))
|
||||
(is (null (getf passepartout.channel-tui::*state* :palette-visible)))
|
||||
(is (null (getf passepartout.channel-tui::*state* :wizard-visible))))
|
||||
|
||||
(test test-palette-filter-case-insensitive
|
||||
"Contract v0.8.0: palette-filter is case-insensitive."
|
||||
(let* ((items (list (list :category "View" :items
|
||||
(list (list :name "/theme" :desc "Switch color" :shortcut nil :action nil)))))
|
||||
(filtered (passepartout.channel-tui::palette-filter items "THEME")))
|
||||
(is (= 1 (length (getf (first filtered) :items))))))
|
||||
|
||||
(test test-palette-filter-no-match-empty
|
||||
"Contract v0.8.0: palette-filter returns empty categories on no match."
|
||||
(let* ((items (list (list :category "View" :items
|
||||
(list (list :name "/theme" :desc "Colors" :shortcut nil :action nil)))))
|
||||
(filtered (passepartout.channel-tui::palette-filter items "xyznonexistent")))
|
||||
(is (null (getf (first filtered) :items)))))
|
||||
(test test-slash-commands-entry-count
|
||||
"Contract v0.8.0: *slash-commands* has at least 19 entries, each with :name, :desc, :action."
|
||||
(let ((cmds passepartout.channel-tui::*slash-commands*))
|
||||
(is (>= (length cmds) 19))
|
||||
(dolist (c cmds)
|
||||
(is (stringp (getf c :name)))
|
||||
(is (stringp (getf c :desc)))
|
||||
(is (functionp (getf c :action))))))
|
||||
#+end_src
|
||||
|
||||
Reference in New Issue
Block a user