|
;;; vui-btop-improved.el --- VUI btop, restructured for granular updates -*- lexical-binding: t; -*- |
|
|
|
;; EXPERIMENT: same btop UI as vui-btop-state-prototype.el (the gist), |
|
;; same rendered output, but structured the way a vui app is meant to be |
|
;; structured: per-band memoized components and memoized derived data, |
|
;; so a state change only re-renders the parts that depend on it. |
|
;; |
|
;; Differences from the gist version: |
|
;; |
|
;; 1. The UI is split into three keyed band components (cpu, lower, |
|
;; footer), each `:memo t'. A selection change re-renders only the |
|
;; lower band; cpu graphs and footer bail out on shallow prop |
|
;; comparison. |
|
;; 2. The filtered+sorted process list is computed once per render via |
|
;; `vui-use-memo' (the gist sorted it twice per render: once in the |
|
;; app render, once again inside the process panel). |
|
;; 3. The non-process lower-panel lines (memory, disks, network) are |
|
;; cached with `vui-use-memo' keyed on (snapshot width height boxes), |
|
;; so moving the selection reuses them as-is. |
|
;; 4. Keymaps are built once; keys dispatch through a ref that the |
|
;; render refreshes, instead of re-running `define-key' every render. |
|
;; 5. The per-line memoized wrapper component is gone. It could never |
|
;; hit for process rows anyway: their :on-click closures are fresh |
|
;; every render, so the props never compare equal. |
|
;; |
|
;; Pure helpers (parsing, graphs, panel string drawing) are reused from |
|
;; the gist file, which must be on `load-path'. |
|
;; |
|
;; Run: |
|
;; emacs -Q -L /path/to/vui.el -L /path/to/gist -l vui-btop-improved.el |
|
|
|
;;; Code: |
|
|
|
(require 'cl-lib) |
|
(require 'subr-x) |
|
(require 'vui) |
|
(require 'vui-btop-state-prototype) |
|
|
|
(defconst vui-btop-improved--buffer-name "*VUI btop IMPROVED*") |
|
|
|
;;; Process panel (takes the already-ordered list; the gist re-sorted here) |
|
|
|
(defun vui-btop-improved--process-panel |
|
(processes width height process-index sort-index reversed details paused |
|
select-function row-keymap) |
|
"Return responsive process panel lines for ordered PROCESSES and UI state." |
|
(let* ((maximum (max 0 (1- (length processes)))) |
|
(selected (min process-index maximum)) |
|
(process (nth selected processes)) |
|
(detail-height (if (and details process) 6 0)) |
|
(fixed (+ 3 detail-height)) |
|
(slots (max 1 (- height fixed))) |
|
(start (max 0 (min (- (length processes) |
|
(min slots (length processes))) |
|
(- selected (/ slots 2))))) |
|
(visible (cl-subseq processes start |
|
(min (length processes) (+ start slots)))) |
|
(sort (aref [cpu memory pid] sort-index)) |
|
(attributes (and process (process-attributes (plist-get process :pid)))) |
|
(threads (or (alist-get 'thcount attributes) 0)) |
|
lines) |
|
(push (vui-btop-state-prototype--text |
|
(vui-btop-state-prototype--title-line |
|
width "4" "proc" |
|
(format "%s%s%s" sort (if reversed " reverse" "") |
|
(if paused " paused" "")) |
|
vui-btop-state-prototype--process-face)) |
|
lines) |
|
(when (and details process) |
|
(dolist (line |
|
(list |
|
(format " Status: %-8s Elapsed: %s" |
|
(plist-get process :state) (plist-get process :etime)) |
|
(format " PID: %-8d User: %-10s Threads: %d" |
|
(plist-get process :pid) (plist-get process :user) threads) |
|
(format " CPU: %.1f%% Memory: %d MiB" |
|
(float (plist-get process :cpu)) (plist-get process :mem)) |
|
"" (format " %s" (plist-get process :command)))) |
|
(push (vui-btop-state-prototype--text |
|
(vui-btop-state-prototype--inside-line width line)) |
|
lines)) |
|
(push (vui-btop-state-prototype--text |
|
(vui-btop-state-prototype--separator-line |
|
width "proc filter per-core reverse tree" |
|
vui-btop-state-prototype--process-face)) |
|
lines)) |
|
(push (vui-btop-state-prototype--text |
|
(vui-btop-state-prototype--inside-line |
|
width (vui-btop-state-prototype--process-header (max 1 (- width 2)))) |
|
vui-btop-state-prototype--process-face) |
|
lines) |
|
(let ((index start)) |
|
(dolist (row visible) |
|
(push (vui-btop-state-prototype--process-row |
|
row index width (= index selected) select-function row-keymap) |
|
lines) |
|
(setq index (1+ index)))) |
|
(dotimes (_ (- slots (length visible))) |
|
(push (vui-btop-state-prototype--text |
|
(vui-btop-state-prototype--inside-line width "")) |
|
lines)) |
|
(push (vui-btop-state-prototype--text |
|
(vui-btop-state-prototype--bottom-line |
|
width (format "%d/%d" (if process (1+ selected) 0) |
|
(length processes)) |
|
vui-btop-state-prototype--process-face)) |
|
lines) |
|
(nreverse lines))) |
|
|
|
;;; Non-process lower lines (pure; result is cached by the lower band) |
|
|
|
(defun vui-btop-improved--static-lower-lines (snapshot width height boxes) |
|
"Return the non-process lower lines for SNAPSHOT, or nil when proc-only." |
|
(let* ((mem (memq 'mem boxes)) |
|
(net (memq 'net boxes)) |
|
(left-visible (or mem net)) |
|
(proc (memq 'proc boxes))) |
|
(cond |
|
((and (>= width 108) left-visible proc) |
|
(let ((left (max 46 (/ (* width 45) 100)))) |
|
(vui-btop-state-prototype--left-column snapshot left height boxes))) |
|
((and left-visible proc) |
|
(let* ((left-count (+ (if mem 1 0) (if net 1 0))) |
|
(small (max 6 (/ height (+ left-count 2)))) |
|
lines) |
|
(when mem |
|
(setq lines (append lines |
|
(vui-btop-state-prototype--memory-group |
|
snapshot width small)))) |
|
(when net |
|
(setq lines (append lines |
|
(vui-btop-state-prototype--network-panel |
|
snapshot width small)))) |
|
lines)) |
|
(left-visible |
|
(vui-btop-state-prototype--left-column snapshot width height boxes)) |
|
(proc nil) |
|
(t |
|
(vui-btop-state-prototype--simple-panel |
|
width height nil "btop" "" |
|
'(" No boxes visible. Press 1, 2, 3, or 4.") |
|
vui-btop-state-prototype--muted-face))))) |
|
|
|
;;; Band components |
|
|
|
(vui-defcomponent vui-btop-improved-cpu (snapshot width height) |
|
"Full-width CPU panel. Skips re-render unless the snapshot changes." |
|
:memo t |
|
:render |
|
(apply #'vui-vstack :spacing 0 |
|
(vui-btop-state-prototype--cpu-panel snapshot width height))) |
|
|
|
(vui-defcomponent vui-btop-improved-lower |
|
(snapshot processes width height boxes process-index sort-index reversed |
|
details paused select-fn row-keymap) |
|
"Lower band: memory, disks, network, and the process table." |
|
:memo t |
|
:render |
|
(let* ((mem (memq 'mem boxes)) |
|
(net (memq 'net boxes)) |
|
(left-visible (or mem net)) |
|
(proc-visible (memq 'proc boxes)) |
|
(wide (and (>= width 108) left-visible proc-visible)) |
|
;; Cached: selection moves reuse these nodes untouched. |
|
(static-lines |
|
(vui-use-memo (snapshot width height boxes) |
|
(vui-btop-improved--static-lower-lines snapshot width height boxes))) |
|
(proc-lines |
|
(when proc-visible |
|
(let ((proc-width |
|
(if wide (- width (max 46 (/ (* width 45) 100))) width)) |
|
(proc-height |
|
(cond |
|
(wide height) |
|
(left-visible |
|
(let* ((left-count (+ (if mem 1 0) (if net 1 0))) |
|
(small (max 6 (/ height (+ left-count 2))))) |
|
(max 6 (- height (* small left-count))))) |
|
(t height)))) |
|
(vui-btop-improved--process-panel |
|
processes proc-width proc-height process-index sort-index |
|
reversed details paused select-fn row-keymap))))) |
|
(apply #'vui-vstack :spacing 0 |
|
(cond |
|
(wide (vui-btop-state-prototype--combine-lines |
|
static-lines proc-lines)) |
|
((and left-visible proc-visible) (append static-lines proc-lines)) |
|
(left-visible static-lines) |
|
(proc-visible proc-lines) |
|
(t static-lines))))) |
|
|
|
(vui-defcomponent vui-btop-improved-footer (paused width) |
|
"Key hint line." |
|
:memo t |
|
:render |
|
(vui-btop-state-prototype--text |
|
(vui-btop-state-prototype--fit |
|
(format |
|
"↑↓ select Enter details ←→ sort / filter r reverse p %s 1-4 boxes q quit" |
|
(if paused "resume" "pause")) |
|
width) |
|
vui-btop-state-prototype--muted-face)) |
|
|
|
;;; Keymaps: built once, dispatching through a ref the render refreshes |
|
|
|
(defun vui-btop-improved--command (actions-ref key) |
|
"Return a command running the KEY action stored in ACTIONS-REF." |
|
(lambda () |
|
(interactive) |
|
(funcall (plist-get (car actions-ref) key)))) |
|
|
|
(defun vui-btop-improved--ensure-keymaps (keymap-ref row-keymap-ref actions-ref) |
|
"Populate KEYMAP-REF and ROW-KEYMAP-REF once, dispatching via ACTIONS-REF." |
|
(unless (car keymap-ref) |
|
(let ((map (make-sparse-keymap)) |
|
(row (make-sparse-keymap))) |
|
(dolist (key '("j" "\C-n" [down] [wheel-down] [mouse-5])) |
|
(define-key map key (vui-btop-improved--command actions-ref :down))) |
|
(dolist (key '("k" "\C-p" [up] [wheel-up] [mouse-4])) |
|
(define-key map key (vui-btop-improved--command actions-ref :up))) |
|
(define-key map (kbd "RET") |
|
(vui-btop-improved--command actions-ref :details)) |
|
(define-key row (kbd "RET") |
|
(vui-btop-improved--command actions-ref :details)) |
|
(define-key map "p" (vui-btop-improved--command actions-ref :pause)) |
|
(define-key map [left] (vui-btop-improved--command actions-ref :sort-left)) |
|
(define-key map [right] (vui-btop-improved--command actions-ref :sort-right)) |
|
(define-key map "r" (vui-btop-improved--command actions-ref :reverse)) |
|
(define-key map "/" (vui-btop-improved--command actions-ref :filter)) |
|
(define-key map "1" (vui-btop-improved--command actions-ref :box-1)) |
|
(define-key map "2" (vui-btop-improved--command actions-ref :box-2)) |
|
(define-key map "3" (vui-btop-improved--command actions-ref :box-3)) |
|
(define-key map "4" (vui-btop-improved--command actions-ref :box-4)) |
|
(define-key map "q" (vui-btop-improved--command actions-ref :quit)) |
|
(setcar keymap-ref map) |
|
(setcar row-keymap-ref row)))) |
|
|
|
;;; App |
|
|
|
(vui-defcomponent vui-btop-improved-app () |
|
:state ((snapshot (vui-btop-state-prototype--empty-snapshot)) |
|
(process-index 0) |
|
(sort-index 0) |
|
(reversed nil) |
|
(filter "") |
|
(paused nil) |
|
(details t) |
|
(boxes '(cpu mem net proc)) |
|
(viewport nil)) |
|
:render |
|
(let* ((process-ref (vui-use-ref nil)) |
|
(started-ref (vui-use-ref 0.0)) |
|
(network-ref (vui-use-ref nil)) |
|
(keymap-ref (vui-use-ref nil)) |
|
(row-keymap-ref (vui-use-ref nil)) |
|
(actions-ref (vui-use-ref nil)) |
|
(select-ref (vui-use-ref nil)) |
|
(finished |
|
(vui-async-callback (process _event) |
|
(when (memq (process-status process) '(exit signal)) |
|
(let* ((output (process-buffer process)) |
|
(status (process-exit-status process)) |
|
(text (and (buffer-live-p output) |
|
(with-current-buffer output (buffer-string))))) |
|
(when (buffer-live-p output) (kill-buffer output)) |
|
(setcar process-ref nil) |
|
(when (and (= status 0) text) |
|
(vui-set-state |
|
:snapshot |
|
(lambda (previous) |
|
(let ((parsed |
|
(vui-btop-state-prototype--parse-output |
|
text previous (car network-ref) |
|
(* 1000.0 |
|
(- (float-time) (car started-ref)))))) |
|
(setcar network-ref (cdr parsed)) |
|
(car parsed))))))))) |
|
(sample |
|
(vui-with-async-context |
|
(unless (process-live-p (car process-ref)) |
|
(let ((output (generate-new-buffer " *vui-btop-sample*"))) |
|
(setcar started-ref (float-time)) |
|
(setcar process-ref |
|
(make-process |
|
:name "vui-btop-sample" |
|
:buffer output |
|
:command vui-btop-state-prototype--sample-command |
|
:coding 'utf-8-unix |
|
:noquery t |
|
:connection-type 'pipe |
|
:sentinel finished)))))) |
|
(size (or viewport (vui-btop-state-prototype--window-size))) |
|
(width (car size)) |
|
;; Sorted and filtered once per render; the gist did this twice. |
|
(processes |
|
(vui-use-memo (snapshot sort-index reversed filter) |
|
(vui-btop-state-prototype--ordered-processes |
|
snapshot sort-index reversed filter))) |
|
(maximum (max 0 (1- (length processes)))) |
|
(select-process |
|
(or (car select-ref) |
|
(setcar select-ref |
|
(lambda (index) (vui-set-state :process-index index))))) |
|
(move |
|
(lambda (amount) |
|
(vui-set-state |
|
:process-index |
|
(max 0 (min maximum |
|
(+ (min maximum process-index) amount)))))) |
|
(toggle-box |
|
(lambda (box) |
|
(vui-set-state :boxes |
|
(lambda (current-boxes) |
|
(vui-btop-state-prototype--toggle |
|
box current-boxes))))) |
|
(body-height (max 12 (1- (cdr size)))) |
|
(cpu-height (if (memq 'cpu boxes) |
|
(max 8 (min 14 (/ body-height 3))) |
|
0)) |
|
(lower-height (max 8 (- body-height cpu-height)))) |
|
(setcar actions-ref |
|
(list |
|
:down (vui-with-async-context (funcall move 1)) |
|
:up (vui-with-async-context (funcall move -1)) |
|
:details (vui-with-async-context |
|
(vui-set-state :details (not details))) |
|
:pause (vui-with-async-context |
|
(vui-set-state :paused (not paused))) |
|
:sort-left (vui-with-async-context |
|
(vui-batch |
|
(vui-set-state :sort-index (mod (1- sort-index) 3)) |
|
(vui-set-state :process-index 0))) |
|
:sort-right (vui-with-async-context |
|
(vui-batch |
|
(vui-set-state :sort-index (mod (1+ sort-index) 3)) |
|
(vui-set-state :process-index 0))) |
|
:reverse (vui-with-async-context |
|
(vui-batch |
|
(vui-set-state :reversed (not reversed)) |
|
(vui-set-state :process-index 0))) |
|
:filter (vui-with-async-context |
|
(vui-batch |
|
(vui-set-state :filter |
|
(if (string-empty-p filter) "emacs" "")) |
|
(vui-set-state :process-index 0))) |
|
:box-1 (vui-with-async-context (funcall toggle-box 'cpu)) |
|
:box-2 (vui-with-async-context (funcall toggle-box 'mem)) |
|
:box-3 (vui-with-async-context (funcall toggle-box 'net)) |
|
:box-4 (vui-with-async-context (funcall toggle-box 'proc)) |
|
:quit (lambda () (kill-buffer (current-buffer))))) |
|
(vui-btop-improved--ensure-keymaps keymap-ref row-keymap-ref actions-ref) |
|
(vui-use-effect (paused) |
|
(unless paused |
|
(let ((timer (run-with-timer |
|
0 vui-btop-state-prototype--interval sample))) |
|
(lambda () |
|
(cancel-timer timer) |
|
(when-let* ((process (car process-ref))) |
|
(let ((output (process-buffer process))) |
|
(when (process-live-p process) (delete-process process)) |
|
(when (buffer-live-p output) (kill-buffer output)) |
|
(setcar process-ref nil))))))) |
|
(vui-use-effect () |
|
(let ((resize |
|
(vui-with-async-context |
|
(let ((next (vui-btop-state-prototype--window-size))) |
|
(vui-set-state |
|
:viewport |
|
(lambda (current) (if (equal current next) current next))))))) |
|
(add-hook 'window-configuration-change-hook resize nil t) |
|
(lambda () |
|
(remove-hook 'window-configuration-change-hook resize t)))) |
|
(vui-vstack :spacing 0 :keymap (car keymap-ref) |
|
(when (memq 'cpu boxes) |
|
(vui-component 'vui-btop-improved-cpu :key 'cpu |
|
:snapshot snapshot :width width :height cpu-height)) |
|
(vui-component 'vui-btop-improved-lower :key 'lower |
|
:snapshot snapshot :processes processes |
|
:width width :height lower-height :boxes boxes |
|
:process-index process-index :sort-index sort-index |
|
:reversed reversed :details details :paused paused |
|
:select-fn select-process |
|
:row-keymap (car row-keymap-ref)) |
|
(vui-component 'vui-btop-improved-footer :key 'footer |
|
:paused paused :width width)))) |
|
|
|
(defun vui-btop-improved-open () |
|
"Open the improved live VUI btop experiment." |
|
(interactive) |
|
(unless (eq system-type 'darwin) |
|
(user-error "This throwaway sampler currently targets macOS")) |
|
(vui-mount (vui-component 'vui-btop-improved-app) |
|
vui-btop-improved--buffer-name) |
|
(with-current-buffer vui-btop-improved--buffer-name |
|
(setq-local truncate-lines t |
|
cursor-type nil |
|
mode-line-format nil |
|
vui-incremental-render t) |
|
(face-remap-add-relative |
|
'default '(:background "#070a0f" :foreground "#c8d3f5")))) |
|
|
|
(provide 'vui-btop-improved) |
|
|
|
;;; vui-btop-improved.el ends here |