Created
August 10, 2026 22:57
-
-
Save yibie/67d2bfeedda162751a3016eef220a4aa to your computer and use it in GitHub Desktop.
VUI btop-like state and incremental-rendering prototype from the TextUI experiments (GPL-3.0-or-later)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| ;;; vui-btop-state-prototype.el --- VUI btop state experiment -*- lexical-binding: t; -*- | |
| ;; PROTOTYPE QUESTION: Does VUI materially reduce the state, async, and | |
| ;; lifecycle plumbing needed by a live btop-like Emacs UI? | |
| ;; | |
| ;; PARITY TARGET: examples/textui-btop-prototype.el. This version keeps the | |
| ;; same live data, four responsive boxes, process interactions, keyboard and | |
| ;; mouse controls, pause behavior, and cleanup. VUI horizontal layouts assume | |
| ;; single-line children, so side-by-side multi-line panels are combined one row | |
| ;; at a time; that compatibility cost intentionally remains visible here. | |
| ;; | |
| ;; Run: | |
| ;; emacs -Q -L /path/to/vui.el -l examples/vui-btop-state-prototype.el | |
| ;;; Code: | |
| (require 'cl-lib) | |
| (require 'subr-x) | |
| (require 'vui) | |
| (defconst vui-btop-state-prototype--buffer-name | |
| "*VUI btop STATE PROTOTYPE*") | |
| (defconst vui-btop-state-prototype--sample-command | |
| '("/bin/sh" "-c" | |
| "/bin/ps -axo pid=,pcpu=,rss=,user=,state=,etime=,comm=; printf '\n__VUI_VM__\n'; /usr/bin/vm_stat; printf '\n__VUI_NET__\n'; /usr/sbin/netstat -ibn; printf '\n__VUI_SYSTEM__\n'; /usr/sbin/sysctl -n hw.memsize; /usr/sbin/sysctl -n machdep.cpu.brand_string")) | |
| (defconst vui-btop-state-prototype--interval 1.0) | |
| (defconst vui-btop-state-prototype--border-face | |
| '(:foreground "#4d8f87")) | |
| (defconst vui-btop-state-prototype--accent-face | |
| '(:foreground "#9ece6a" :weight bold)) | |
| (defconst vui-btop-state-prototype--memory-face | |
| '(:foreground "#ff5d8f" :weight bold)) | |
| (defconst vui-btop-state-prototype--disk-face | |
| '(:foreground "#ffc777" :weight bold)) | |
| (defconst vui-btop-state-prototype--net-face | |
| '(:foreground "#65d1ff" :weight bold)) | |
| (defconst vui-btop-state-prototype--upload-face | |
| '(:foreground "#c099ff" :weight bold)) | |
| (defconst vui-btop-state-prototype--process-face | |
| '(:foreground "#ffc777" :weight bold)) | |
| (defconst vui-btop-state-prototype--muted-face | |
| '(:foreground "#7f8ea3")) | |
| (defconst vui-btop-state-prototype--selected-face | |
| '(:background "#365f9d" :foreground "#ffffff" :weight bold)) | |
| (vui-defcomponent vui-btop-state-prototype-line (node keymap) | |
| "Render one memoized frame line for experimental incremental updates." | |
| :memo t | |
| :render (vui-vstack :keymap keymap node)) | |
| (defun vui-btop-state-prototype--empty-snapshot () | |
| "Return initial live-data state." | |
| (list :tick 0 | |
| :cpu-history nil :download-history nil :upload-history nil | |
| :processes nil :cpu 0.0 :cpu-model "sampling…" | |
| :load-average '(0.0 0.0 0.0) | |
| :memory-total 0 :memory-used 0 :memory-free 0 :memory-cached 0 | |
| :disk-total 0 :disk-used 0 :disk-free 0 | |
| :download-rate 0.0 :upload-rate 0.0 | |
| :download-total 0 :upload-total 0 | |
| :sample-ms 0.0 :sampled-at "waiting for first sample")) | |
| (defun vui-btop-state-prototype--fit (string width) | |
| "Return STRING truncated and padded to WIDTH display cells." | |
| (let* ((width (max 0 width)) | |
| (value (truncate-string-to-width string width nil nil "…"))) | |
| (concat value (make-string (max 0 (- width (string-width value))) ?\s)))) | |
| (defun vui-btop-state-prototype--vm-pages (text label) | |
| "Return page count for LABEL in vm_stat TEXT." | |
| (if (string-match | |
| (format "^%s:[[:space:]]+\\([0-9]+\\)\\." | |
| (regexp-quote label)) | |
| text) | |
| (string-to-number (match-string 1 text)) | |
| 0)) | |
| (defun vui-btop-state-prototype--parse-processes (text) | |
| "Return (CPU . PROCESSES) parsed from ps TEXT." | |
| (let ((cpu-total 0.0) | |
| rows) | |
| (dolist (line (split-string text "\n" t)) | |
| (when (string-match | |
| "^[[:space:]]*\\([0-9]+\\)[[:space:]]+\\([0-9.]+\\)[[:space:]]+\\([0-9]+\\)[[:space:]]+\\([^[:space:]]+\\)[[:space:]]+\\([^[:space:]]+\\)[[:space:]]+\\([^[:space:]]+\\)[[:space:]]+\\(.+\\)$" | |
| line) | |
| (let* ((cpu (string-to-number (match-string 2 line))) | |
| (command (string-trim (match-string 7 line))) | |
| (name (file-name-nondirectory command))) | |
| (setq cpu-total (+ cpu-total cpu)) | |
| (push (list :pid (string-to-number (match-string 1 line)) | |
| :cpu cpu | |
| :mem (round (/ (string-to-number (match-string 3 line)) | |
| 1024.0)) | |
| :user (match-string 4 line) | |
| :state (match-string 5 line) | |
| :etime (match-string 6 line) | |
| :command command | |
| :name (if (string-empty-p name) command name)) | |
| rows)))) | |
| (cons (min 100.0 (/ cpu-total (max 1 (num-processors)))) | |
| (nreverse rows)))) | |
| (defun vui-btop-state-prototype--parse-network (text previous) | |
| "Return network values parsed from TEXT relative to PREVIOUS totals." | |
| (let ((line | |
| (cl-find-if | |
| (lambda (candidate) | |
| (and (string-prefix-p "en0 " candidate) | |
| (string-match-p "<Link#" candidate))) | |
| (split-string text "\n" t)))) | |
| (if (not line) | |
| (list :rate-down 0.0 :rate-up 0.0 :total-down 0 :total-up 0 | |
| :sample previous) | |
| (let* ((fields (split-string line "[[:space:]]+" t)) | |
| (download (string-to-number (or (nth 6 fields) "0"))) | |
| (upload (string-to-number (or (nth 9 fields) "0"))) | |
| (now (float-time)) | |
| (elapsed (and previous (- now (plist-get previous :time)))) | |
| (rate-down | |
| (if (and elapsed (> elapsed 0)) | |
| (max 0.0 (/ (- download (plist-get previous :download)) | |
| elapsed)) | |
| 0.0)) | |
| (rate-up | |
| (if (and elapsed (> elapsed 0)) | |
| (max 0.0 (/ (- upload (plist-get previous :upload)) | |
| elapsed)) | |
| 0.0))) | |
| (list :rate-down rate-down :rate-up rate-up | |
| :total-down download :total-up upload | |
| :sample (list :time now :download download :upload upload)))))) | |
| (defun vui-btop-state-prototype--parse-output | |
| (text previous previous-network sample-ms) | |
| "Return (SNAPSHOT . NETWORK-SAMPLE) parsed from sampler TEXT. | |
| PREVIOUS supplies graph history, PREVIOUS-NETWORK supplies rate totals, and | |
| SAMPLE-MS is the sampling duration." | |
| (let* ((vm-marker "\n__VUI_VM__\n") | |
| (net-marker "\n__VUI_NET__\n") | |
| (system-marker "\n__VUI_SYSTEM__\n") | |
| (vm-start (string-match (regexp-quote vm-marker) text)) | |
| (net-start (and vm-start | |
| (string-match (regexp-quote net-marker) text | |
| (+ vm-start (length vm-marker))))) | |
| (system-start (and net-start | |
| (string-match (regexp-quote system-marker) text | |
| (+ net-start (length net-marker)))))) | |
| (unless (and vm-start net-start system-start) | |
| (error "VUI btop sampler returned incomplete output")) | |
| (let* ((process-data | |
| (vui-btop-state-prototype--parse-processes | |
| (substring text 0 vm-start))) | |
| (vm (substring text (+ vm-start (length vm-marker)) net-start)) | |
| (net (substring text (+ net-start (length net-marker)) system-start)) | |
| (system (split-string | |
| (substring text (+ system-start (length system-marker))) | |
| "\n" t)) | |
| (memory-total (string-to-number (or (nth 0 system) "0"))) | |
| (page-size | |
| (if (string-match "page size of \\([0-9]+\\) bytes" vm) | |
| (string-to-number (match-string 1 vm)) | |
| 0)) | |
| (free-pages | |
| (+ (vui-btop-state-prototype--vm-pages vm "Pages free") | |
| (vui-btop-state-prototype--vm-pages vm "Pages inactive") | |
| (vui-btop-state-prototype--vm-pages vm "Pages speculative"))) | |
| (memory-free (min memory-total (* page-size free-pages))) | |
| (memory-cached | |
| (* page-size | |
| (vui-btop-state-prototype--vm-pages vm "File-backed pages"))) | |
| (network | |
| (vui-btop-state-prototype--parse-network net previous-network)) | |
| (disk (file-system-info "/")) | |
| (disk-total (or (nth 0 disk) 0)) | |
| (disk-free (or (nth 1 disk) 0)) | |
| (cpu (car process-data)) | |
| (down (plist-get network :rate-down)) | |
| (up (plist-get network :rate-up))) | |
| (cons | |
| (list :tick (1+ (plist-get previous :tick)) | |
| :cpu-history | |
| (vui-btop-state-prototype--append-sample | |
| (plist-get previous :cpu-history) cpu) | |
| :download-history | |
| (vui-btop-state-prototype--append-sample | |
| (plist-get previous :download-history) down) | |
| :upload-history | |
| (vui-btop-state-prototype--append-sample | |
| (plist-get previous :upload-history) up) | |
| :processes (cdr process-data) | |
| :cpu cpu :cpu-model (or (nth 1 system) "unknown CPU") | |
| :load-average (load-average t) | |
| :memory-total memory-total | |
| :memory-used (max 0 (- memory-total memory-free)) | |
| :memory-free memory-free :memory-cached memory-cached | |
| :disk-total disk-total | |
| :disk-used (max 0 (- disk-total disk-free)) | |
| :disk-free disk-free | |
| :download-rate down :upload-rate up | |
| :download-total (plist-get network :total-down) | |
| :upload-total (plist-get network :total-up) | |
| :sample-ms sample-ms | |
| :sampled-at (format-time-string "%H:%M:%S")) | |
| (plist-get network :sample))))) | |
| (defun vui-btop-state-prototype--append-sample (history value) | |
| "Append VALUE to HISTORY, retaining its newest 120 values." | |
| (let ((next (append history (list value)))) | |
| (if (> (length next) 120) (cdr next) next))) | |
| (defun vui-btop-state-prototype--graph-lines | |
| (values width height face &optional maximum) | |
| "Render VALUES as WIDTH by HEIGHT graph lines using FACE. | |
| FACE may be a top-to-bottom vector. MAXIMUM defaults to 100." | |
| (let* ((width (max 1 width)) | |
| (height (max 1 height)) | |
| (maximum (max 1.0 (or maximum 100.0))) | |
| (visible (last values (min width (length values)))) | |
| (visible (append (make-list (- width (length visible)) 0) visible))) | |
| (cl-loop | |
| for row below height | |
| collect | |
| (let ((row-face | |
| (if (vectorp face) | |
| (aref face (min (1- (length face)) | |
| (/ (* row (length face)) height))) | |
| face)) | |
| chars) | |
| (dolist (value visible) | |
| (let* ((filled (round (* (max 0.0 (min maximum value)) | |
| height 8.0 (/ 1.0 maximum)))) | |
| (level (max 0 (min 8 (- filled (* (- height row 1) 8)))))) | |
| (push (aref " ▁▂▃▄▅▆▇█" level) chars))) | |
| (propertize (concat (nreverse chars)) 'face row-face))))) | |
| (defun vui-btop-state-prototype--bar (value width face) | |
| "Return a VALUE-percent bar of WIDTH using FACE." | |
| (let* ((width (max 1 width)) | |
| (filled (min width (round (* width (/ value 100.0)))))) | |
| (concat (propertize (make-string filled ?█) 'face face) | |
| (propertize (make-string (- width filled) ?░) | |
| 'face vui-btop-state-prototype--muted-face)))) | |
| (defun vui-btop-state-prototype--percent (part total) | |
| "Return PART as a percentage of TOTAL." | |
| (if (> total 0) (min 100.0 (* 100.0 (/ (float part) total))) 0.0)) | |
| (defun vui-btop-state-prototype--window-size () | |
| "Return current target window size as (WIDTH . HEIGHT)." | |
| (let ((window (or (get-buffer-window (current-buffer) t) | |
| (selected-window)))) | |
| (cons (max 1 (1- (window-body-width window))) | |
| (window-body-height window)))) | |
| (defun vui-btop-state-prototype--title-line (width key title right face) | |
| "Return btop-style top border using KEY, TITLE, RIGHT, and FACE." | |
| (let* ((inside (max 0 (- width 2))) | |
| (left (format "─%s%s" (if key (format "%s" key) "") title)) | |
| (right (if (string-empty-p right) "" (format "%s─" right))) | |
| (room (max 0 (- inside (string-width right)))) | |
| (left (truncate-string-to-width left room nil nil "…"))) | |
| (propertize | |
| (concat "╭" left (make-string (max 0 (- room (string-width left))) ?─) | |
| right "╮") | |
| 'face face))) | |
| (defun vui-btop-state-prototype--inside-line (width text) | |
| "Return TEXT inside a WIDTH frame." | |
| (concat (propertize "│" 'face vui-btop-state-prototype--border-face) | |
| (vui-btop-state-prototype--fit text (max 0 (- width 2))) | |
| (propertize "│" 'face vui-btop-state-prototype--border-face))) | |
| (defun vui-btop-state-prototype--separator-line (width label face) | |
| "Return a framed separator containing LABEL." | |
| (let* ((inside (max 0 (- width 2))) | |
| (label (truncate-string-to-width (format "─%s" label) inside))) | |
| (propertize | |
| (concat "├" label (make-string (- inside (string-width label)) ?─) "┤") | |
| 'face face))) | |
| (defun vui-btop-state-prototype--bottom-line (width suffix face) | |
| "Return WIDTH bottom border ending in SUFFIX with FACE." | |
| (let* ((inside (max 0 (- width 2))) | |
| (suffix (if (string-empty-p suffix) "" (format "%s─" suffix))) | |
| (suffix (truncate-string-to-width suffix inside))) | |
| (propertize | |
| (concat "╰" (make-string (- inside (string-width suffix)) ?─) suffix "╯") | |
| 'face face))) | |
| (defun vui-btop-state-prototype--text (value &optional face key) | |
| "Return a VUI text node showing VALUE with optional FACE and KEY." | |
| (vui-text value :face face :key key)) | |
| (defun vui-btop-state-prototype--combine-lines (left right) | |
| "Combine equally tall LEFT and RIGHT line nodes horizontally." | |
| (cl-mapcar (lambda (a b) (vui-hstack :spacing 0 a b)) left right)) | |
| (defun vui-btop-state-prototype--cpu-panel (snapshot width height) | |
| "Return live CPU panel lines for SNAPSHOT at WIDTH by HEIGHT." | |
| (let* ((inside (max 1 (- width 2))) | |
| (slots (max 1 (- height 2))) | |
| (stats-width (if (>= width 90) (min 31 (/ width 4)) 20)) | |
| (graph-width (max 8 (- inside stats-width 1))) | |
| (cpu (or (car (last (plist-get snapshot :cpu-history))) 0)) | |
| (graphs | |
| (vui-btop-state-prototype--graph-lines | |
| (plist-get snapshot :cpu-history) graph-width slots | |
| (vector vui-btop-state-prototype--accent-face | |
| '(:foreground "#e0af68" :weight bold) | |
| '(:foreground "#9ece6a" :weight bold)))) | |
| (top-processes | |
| (sort (copy-sequence (plist-get snapshot :processes)) | |
| (lambda (left right) | |
| (> (plist-get left :cpu) (plist-get right :cpu))))) | |
| lines) | |
| (push (vui-btop-state-prototype--text | |
| (vui-btop-state-prototype--title-line | |
| width "1" "cpu" "1000ms +" vui-btop-state-prototype--accent-face)) | |
| lines) | |
| (dotimes (row slots) | |
| (let* ((process (and (>= row 2) (nth (- row 2) top-processes))) | |
| (load (plist-get snapshot :load-average)) | |
| (stats | |
| (cond | |
| ((= row 0) | |
| (format " %s %d cores" | |
| (plist-get snapshot :cpu-model) (num-processors))) | |
| ((= row 1) (format " CPU %5.1f%%" cpu)) | |
| ((= row (1- slots)) | |
| (format " Load AVG: %.2f %.2f %.2f" | |
| (or (nth 0 load) 0.0) (or (nth 1 load) 0.0) | |
| (or (nth 2 load) 0.0))) | |
| (process | |
| (format " %-16s %5.1f%%" | |
| (plist-get process :name) (plist-get process :cpu))) | |
| (t "")))) | |
| (push (vui-btop-state-prototype--text | |
| (vui-btop-state-prototype--inside-line | |
| width | |
| (concat (nth row graphs) " " | |
| (vui-btop-state-prototype--fit stats stats-width)))) | |
| lines))) | |
| (push (vui-btop-state-prototype--text | |
| (vui-btop-state-prototype--bottom-line | |
| width | |
| (format "sample %d: %.0fms" (plist-get snapshot :tick) | |
| (plist-get snapshot :sample-ms)) | |
| vui-btop-state-prototype--border-face)) | |
| lines) | |
| (nreverse lines))) | |
| (defun vui-btop-state-prototype--simple-panel | |
| (width height key title right content face) | |
| "Return a framed panel of CONTENT lines at WIDTH by HEIGHT." | |
| (let ((slots (max 0 (- height 2))) | |
| lines) | |
| (push (vui-btop-state-prototype--text | |
| (vui-btop-state-prototype--title-line | |
| width key title right face)) | |
| lines) | |
| (dotimes (row slots) | |
| (push (vui-btop-state-prototype--text | |
| (vui-btop-state-prototype--inside-line | |
| width (or (nth row content) ""))) | |
| lines)) | |
| (push (vui-btop-state-prototype--text | |
| (vui-btop-state-prototype--bottom-line width "" face)) | |
| lines) | |
| (nreverse lines))) | |
| (defun vui-btop-state-prototype--memory-lines (snapshot width) | |
| "Return memory data from SNAPSHOT fitted for WIDTH." | |
| (let* ((bar-width (max 5 (- width 13))) | |
| (used (vui-btop-state-prototype--percent | |
| (plist-get snapshot :memory-used) | |
| (plist-get snapshot :memory-total))) | |
| (free (vui-btop-state-prototype--percent | |
| (plist-get snapshot :memory-free) | |
| (plist-get snapshot :memory-total)))) | |
| (list (format " Total: %s" | |
| (file-size-human-readable (plist-get snapshot :memory-total))) | |
| (format " Used: %s" | |
| (file-size-human-readable (plist-get snapshot :memory-used))) | |
| (concat " " (vui-btop-state-prototype--bar | |
| used bar-width vui-btop-state-prototype--memory-face) | |
| (format " %.0f%%" used)) | |
| (format " Available: %s" | |
| (file-size-human-readable (plist-get snapshot :memory-free))) | |
| (concat " " (vui-btop-state-prototype--bar | |
| free bar-width vui-btop-state-prototype--disk-face) | |
| (format " %.0f%%" free)) | |
| (format " Cached: %s" | |
| (file-size-human-readable (plist-get snapshot :memory-cached))) | |
| (format " Free: %s" | |
| (file-size-human-readable (plist-get snapshot :memory-free)))))) | |
| (defun vui-btop-state-prototype--disk-lines (snapshot width) | |
| "Return disk data from SNAPSHOT fitted for WIDTH." | |
| (let* ((bar-width (max 5 (- width 15))) | |
| (used (vui-btop-state-prototype--percent | |
| (plist-get snapshot :disk-used) | |
| (plist-get snapshot :disk-total)))) | |
| (list (format " / %s" | |
| (file-size-human-readable (plist-get snapshot :disk-total))) | |
| (concat " " (vui-btop-state-prototype--bar | |
| used bar-width vui-btop-state-prototype--memory-face) | |
| (format " %.0f%%" used)) | |
| (format " Used: %s" | |
| (file-size-human-readable (plist-get snapshot :disk-used))) | |
| (format " Free: %s" | |
| (file-size-human-readable (plist-get snapshot :disk-free))) | |
| " Filesystem: apfs" | |
| " Source: file-system-info" | |
| ""))) | |
| (defun vui-btop-state-prototype--memory-group (snapshot width height) | |
| "Return memory and disk panel lines inside WIDTH by HEIGHT." | |
| (if (>= width 62) | |
| (let* ((memory-width (/ width 2)) | |
| (disk-width (- width memory-width))) | |
| (vui-btop-state-prototype--combine-lines | |
| (vui-btop-state-prototype--simple-panel | |
| memory-width height "2" "mem" "" | |
| (vui-btop-state-prototype--memory-lines snapshot memory-width) | |
| vui-btop-state-prototype--memory-face) | |
| (vui-btop-state-prototype--simple-panel | |
| disk-width height nil "disks" "io" | |
| (vui-btop-state-prototype--disk-lines snapshot disk-width) | |
| vui-btop-state-prototype--disk-face))) | |
| (vui-btop-state-prototype--simple-panel | |
| width height "2" "mem + disks" "" | |
| (append (cl-subseq (vui-btop-state-prototype--memory-lines snapshot width) | |
| 0 4) | |
| (cl-subseq (vui-btop-state-prototype--disk-lines snapshot width) | |
| 0 3)) | |
| vui-btop-state-prototype--memory-face))) | |
| (defun vui-btop-state-prototype--network-lines (snapshot width height) | |
| "Return HEIGHT live network graph lines from SNAPSHOT at WIDTH." | |
| (let* ((inside (max 1 (- width 2))) | |
| (stats-width (if (>= width 48) 22 14)) | |
| (graph-width (max 5 (- inside stats-width 1))) | |
| (half (max 1 (/ height 2))) | |
| (down-history (plist-get snapshot :download-history)) | |
| (up-history (plist-get snapshot :upload-history)) | |
| (down-graph | |
| (vui-btop-state-prototype--graph-lines | |
| down-history graph-width half vui-btop-state-prototype--net-face | |
| (if down-history (apply #'max down-history) 1.0))) | |
| (up-graph | |
| (vui-btop-state-prototype--graph-lines | |
| up-history graph-width (- height half) | |
| vui-btop-state-prototype--upload-face | |
| (if up-history (apply #'max up-history) 1.0))) | |
| (graphs (append down-graph up-graph))) | |
| (cl-loop | |
| for row below height | |
| collect | |
| (let ((stats | |
| (cond | |
| ((= row 0) "▼ download") | |
| ((= row 1) | |
| (format "▼ %s/s" | |
| (file-size-human-readable | |
| (plist-get snapshot :download-rate)))) | |
| ((= row (1- half)) | |
| (format "▼ Total: %s" | |
| (file-size-human-readable | |
| (plist-get snapshot :download-total)))) | |
| ((= row half) "▲ upload") | |
| ((= row (1+ half)) | |
| (format "▲ %s/s" | |
| (file-size-human-readable | |
| (plist-get snapshot :upload-rate)))) | |
| ((= row (1- height)) | |
| (format "▲ Total: %s" | |
| (file-size-human-readable | |
| (plist-get snapshot :upload-total)))) | |
| (t "")))) | |
| (concat (nth row graphs) " " | |
| (vui-btop-state-prototype--fit stats stats-width)))))) | |
| (defun vui-btop-state-prototype--network-panel (snapshot width height) | |
| "Return live network panel lines from SNAPSHOT at WIDTH by HEIGHT." | |
| (vui-btop-state-prototype--simple-panel | |
| width height "3" "net" "en0" | |
| (vui-btop-state-prototype--network-lines snapshot width (max 0 (- height 2))) | |
| vui-btop-state-prototype--net-face)) | |
| (defun vui-btop-state-prototype--ordered-processes | |
| (snapshot sort-index reversed filter) | |
| "Return filtered and sorted SNAPSHOT processes for UI state." | |
| (let* ((filtered | |
| (if (string-empty-p filter) | |
| (copy-sequence (plist-get snapshot :processes)) | |
| (cl-remove-if-not | |
| (lambda (process) | |
| (string-match-p (regexp-quote filter) | |
| (downcase (plist-get process :name)))) | |
| (plist-get snapshot :processes)))) | |
| (key (aref [cpu memory pid] sort-index)) | |
| (sorted | |
| (sort filtered | |
| (lambda (left right) | |
| (> (pcase key | |
| ('cpu (plist-get left :cpu)) | |
| ('memory (plist-get left :mem)) | |
| (_ (plist-get left :pid))) | |
| (pcase key | |
| ('cpu (plist-get right :cpu)) | |
| ('memory (plist-get right :mem)) | |
| (_ (plist-get right :pid)))))))) | |
| (if reversed (nreverse sorted) sorted))) | |
| (defun vui-btop-state-prototype--process-header (width) | |
| "Return process table header fitted to WIDTH." | |
| (if (>= width 72) | |
| (vui-btop-state-prototype--fit | |
| " PID Program Command State User MemB Cpu%" | |
| width) | |
| (vui-btop-state-prototype--fit | |
| " PID Program MemB Cpu%" width))) | |
| (defun vui-btop-state-prototype--process-label (process width) | |
| "Return one responsive PROCESS row for WIDTH." | |
| (if (>= width 72) | |
| (format "%6d %-18s %-28s %-5s %-9s %5d %5.1f" | |
| (plist-get process :pid) (plist-get process :name) | |
| (plist-get process :command) (plist-get process :state) | |
| (plist-get process :user) (plist-get process :mem) | |
| (float (plist-get process :cpu))) | |
| (format "%6d %-23s %5d %5.1f" | |
| (plist-get process :pid) (plist-get process :name) | |
| (plist-get process :mem) (float (plist-get process :cpu))))) | |
| (defun vui-btop-state-prototype--process-row | |
| (process index width selected select-function row-keymap) | |
| "Return clickable PROCESS row at INDEX in WIDTH." | |
| (vui-button | |
| (vui-btop-state-prototype--inside-line | |
| width | |
| (vui-btop-state-prototype--process-label process (max 1 (- width 2)))) | |
| :key (plist-get process :pid) | |
| :no-decoration t | |
| :help-echo nil | |
| :keymap row-keymap | |
| :face (if selected vui-btop-state-prototype--selected-face 'default) | |
| :on-click (lambda () (funcall select-function index)))) | |
| (defun vui-btop-state-prototype--process-panel | |
| (snapshot width height process-index sort-index reversed filter details paused | |
| select-function row-keymap) | |
| "Return responsive process panel lines for SNAPSHOT and UI state." | |
| (let* ((processes | |
| (vui-btop-state-prototype--ordered-processes | |
| snapshot sort-index reversed filter)) | |
| (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))) | |
| (defun vui-btop-state-prototype--left-column (snapshot width height boxes) | |
| "Return visible memory/network panel lines at WIDTH by HEIGHT." | |
| (let ((mem (memq 'mem boxes)) | |
| (net (memq 'net boxes))) | |
| (cond | |
| ((and mem net) | |
| (let ((top (/ height 2))) | |
| (append (vui-btop-state-prototype--memory-group snapshot width top) | |
| (vui-btop-state-prototype--network-panel | |
| snapshot width (- height top))))) | |
| (mem (vui-btop-state-prototype--memory-group snapshot width height)) | |
| (net (vui-btop-state-prototype--network-panel snapshot width height)) | |
| (t nil)))) | |
| (defun vui-btop-state-prototype--lower-layout | |
| (snapshot width height boxes process-index sort-index reversed filter details | |
| paused select-function row-keymap) | |
| "Return responsive lower btop lines for SNAPSHOT and UI state." | |
| (let ((left-visible (or (memq 'mem boxes) (memq 'net boxes))) | |
| (proc-visible (memq 'proc boxes))) | |
| (cond | |
| ((and (>= width 108) left-visible proc-visible) | |
| (let* ((left (max 46 (/ (* width 45) 100))) | |
| (right (- width left))) | |
| (vui-btop-state-prototype--combine-lines | |
| (vui-btop-state-prototype--left-column snapshot left height boxes) | |
| (vui-btop-state-prototype--process-panel | |
| snapshot right height process-index sort-index reversed filter details | |
| paused select-function row-keymap)))) | |
| ((and left-visible proc-visible) | |
| (let* ((mem (memq 'mem boxes)) | |
| (net (memq 'net boxes)) | |
| (left-count (+ (if mem 1 0) (if net 1 0))) | |
| (small (max 6 (/ height (+ left-count 2)))) | |
| (used (* small left-count)) | |
| 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)))) | |
| (append lines | |
| (vui-btop-state-prototype--process-panel | |
| snapshot width (max 6 (- height used)) process-index sort-index | |
| reversed filter details paused select-function row-keymap)))) | |
| (left-visible | |
| (vui-btop-state-prototype--left-column snapshot width height boxes)) | |
| (proc-visible | |
| (vui-btop-state-prototype--process-panel | |
| snapshot width height process-index sort-index reversed filter details | |
| paused select-function row-keymap)) | |
| (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))))) | |
| (defun vui-btop-state-prototype--frame-lines | |
| (snapshot width visible-height boxes process-index sort-index reversed filter | |
| details paused select-function row-keymap) | |
| "Return the complete functionally equivalent btop VUI frame." | |
| (let* ((body-height (max 12 (1- visible-height))) | |
| (cpu-height (if (memq 'cpu boxes) | |
| (max 8 (min 14 (/ body-height 3))) | |
| 0)) | |
| (lower-height (max 8 (- body-height cpu-height))) | |
| lines) | |
| (when (memq 'cpu boxes) | |
| (setq lines | |
| (vui-btop-state-prototype--cpu-panel snapshot width cpu-height))) | |
| (setq lines | |
| (append | |
| lines | |
| (vui-btop-state-prototype--lower-layout | |
| snapshot width lower-height boxes process-index sort-index reversed | |
| filter details paused select-function row-keymap) | |
| (list | |
| (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)))) | |
| lines)) | |
| (defun vui-btop-state-prototype--toggle (item items) | |
| "Return ITEMS with ITEM toggled, preserving the canonical panel order." | |
| (if (memq item items) | |
| (delq item (copy-sequence items)) | |
| (cl-remove-if-not (lambda (candidate) (memq candidate (cons item items))) | |
| '(cpu mem net proc)))) | |
| (defun vui-btop-state-prototype--interactive-command (callback) | |
| "Return an interactive command invoking zero-argument CALLBACK." | |
| (lambda () (interactive) (funcall callback))) | |
| (vui-defcomponent vui-btop-state-prototype-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)) | |
| (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)) | |
| (processes | |
| (vui-btop-state-prototype--ordered-processes | |
| snapshot sort-index reversed filter)) | |
| (maximum (max 0 (1- (length processes)))) | |
| (select-process | |
| (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)))))) | |
| (down (vui-with-async-context (funcall move 1))) | |
| (up (vui-with-async-context (funcall move -1))) | |
| (toggle-details | |
| (vui-with-async-context | |
| (vui-set-state :details (not details)))) | |
| (toggle-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)))) | |
| (toggle-reverse | |
| (vui-with-async-context | |
| (vui-batch | |
| (vui-set-state :reversed (not reversed)) | |
| (vui-set-state :process-index 0)))) | |
| (toggle-filter | |
| (vui-with-async-context | |
| (vui-batch | |
| (vui-set-state :filter (if (string-empty-p filter) "emacs" "")) | |
| (vui-set-state :process-index 0)))) | |
| (toggle-box | |
| (lambda (box) | |
| (vui-set-state :boxes | |
| (lambda (current-boxes) | |
| (vui-btop-state-prototype--toggle | |
| box current-boxes))))) | |
| (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)))) | |
| (resize | |
| (vui-with-async-context | |
| (let ((next (vui-btop-state-prototype--window-size))) | |
| (unless (equal next viewport) | |
| (vui-set-state :viewport next))))) | |
| (keymap (or (car keymap-ref) | |
| (setcar keymap-ref (make-sparse-keymap)))) | |
| (row-keymap (or (car row-keymap-ref) | |
| (setcar row-keymap-ref (make-sparse-keymap))))) | |
| (dolist (key '("j" "\C-n" [down] [wheel-down] [mouse-5])) | |
| (define-key keymap key | |
| (vui-btop-state-prototype--interactive-command down))) | |
| (dolist (key '("k" "\C-p" [up] [wheel-up] [mouse-4])) | |
| (define-key keymap key | |
| (vui-btop-state-prototype--interactive-command up))) | |
| (define-key keymap (kbd "RET") | |
| (vui-btop-state-prototype--interactive-command toggle-details)) | |
| (define-key row-keymap (kbd "RET") | |
| (vui-btop-state-prototype--interactive-command toggle-details)) | |
| (define-key keymap "p" | |
| (vui-btop-state-prototype--interactive-command toggle-pause)) | |
| (define-key keymap [left] | |
| (vui-btop-state-prototype--interactive-command sort-left)) | |
| (define-key keymap [right] | |
| (vui-btop-state-prototype--interactive-command sort-right)) | |
| (define-key keymap "r" | |
| (vui-btop-state-prototype--interactive-command toggle-reverse)) | |
| (define-key keymap "/" | |
| (vui-btop-state-prototype--interactive-command toggle-filter)) | |
| (define-key keymap "1" | |
| (vui-btop-state-prototype--interactive-command box-1)) | |
| (define-key keymap "2" | |
| (vui-btop-state-prototype--interactive-command box-2)) | |
| (define-key keymap "3" | |
| (vui-btop-state-prototype--interactive-command box-3)) | |
| (define-key keymap "4" | |
| (vui-btop-state-prototype--interactive-command box-4)) | |
| (define-key keymap "q" | |
| (vui-btop-state-prototype--interactive-command quit)) | |
| (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 (viewport) | |
| (add-hook 'window-configuration-change-hook resize nil t) | |
| (lambda () | |
| (remove-hook 'window-configuration-change-hook resize t))) | |
| (apply | |
| #'vui-vstack :spacing 0 | |
| (cl-loop | |
| for node in | |
| (vui-btop-state-prototype--frame-lines | |
| snapshot width (cdr size) boxes process-index sort-index reversed filter | |
| details paused select-process row-keymap) | |
| for index from 0 | |
| collect (vui-component 'vui-btop-state-prototype-line | |
| :key index :node node :keymap keymap))))) | |
| (defun vui-btop-state-prototype-open () | |
| "Open the live VUI btop state experiment." | |
| (interactive) | |
| (unless (eq system-type 'darwin) | |
| (user-error "This throwaway sampler currently targets macOS")) | |
| (vui-mount (vui-component 'vui-btop-state-prototype-app) | |
| vui-btop-state-prototype--buffer-name) | |
| (with-current-buffer vui-btop-state-prototype--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")))) | |
| (unless noninteractive | |
| (set-frame-size (selected-frame) 112 43) | |
| (set-frame-name "VUI — btop state experiment") | |
| (vui-btop-state-prototype-open)) | |
| (provide 'vui-btop-state-prototype) | |
| ;;; vui-btop-state-prototype.el ends here |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment