pio-mode.el
1 ;;; pio-mode.el --- PlatformIO & Emacs integration -*- lexical-binding: t; -*- 2 3 ;; Copyright (C) 2022, 2026 Mumtahin Farabi. 4 ;; Author: Mumtahin Farabi <mfarabi619@gmail.com> 5 ;; Maintainer: Mumtahin Farabi <mfarabi619@gmail.com> 6 ;; Created: 13 Oct 2025 7 ;; 8 ;; This file is not part of GNU Emacs. 9 ;; 10 ;; Version: 0.0.1 11 ;; Package-Version: 0.0.1 12 ;; Keywords: c, hardware, processes, tools 13 ;; Package-Requires: ((platformio-core "6.1.19")) 14 ;; URL: https://github.com/MFarabi619/MFarabi619/tree/main/modules/home/programs/emacs/pio-mode 15 16 ;;; Commentary: 17 18 ;; PlatformIO is a modern alternative to the Arduino CLI, and is widely adopted in the embedded systems development ecosystem. 19 ;; This package provides a guts-out, hackable integration with the modern "pio" CLI; with the goal of being a successor to `platformio-mode'. 20 21 ;;; Change Log: Initial Release 22 23 ;;; Code: 24 25 (require 'json) 26 (require 'ansi-color) 27 (require 'subr-x) 28 (require 'seq) 29 (require 'tabulated-list) 30 (require 'transient nil t) 31 32 (defgroup pio nil 33 "PlatformIO integration." 34 :group 'tools) 35 36 (defcustom pio-executable "platformio" 37 "Executable name or absolute path for the PlatformIO CLI." 38 :type 'string 39 :group 'pio) 40 41 (defcustom pio-cache-enabled t 42 "Enable in-memory caching for stable PlatformIO JSON commands." 43 :type 'boolean 44 :group 'pio) 45 46 (defcustom pio-cache-ttl-account-show 600 47 "Seconds to cache `pio account show --json-output'." 48 :type 'integer 49 :group 'pio) 50 51 (defcustom pio-cache-ttl-org-list 600 52 "Seconds to cache `pio org list --json-output'." 53 :type 'integer 54 :group 'pio) 55 56 (defcustom pio-cache-ttl-team-list 600 57 "Seconds to cache `pio team list --json-output'." 58 :type 'integer 59 :group 'pio) 60 61 (defcustom pio-cache-ttl-boards 1800 62 "Seconds to cache `pio boards --json-output'." 63 :type 'integer 64 :group 'pio) 65 66 (defcustom pio-cache-ttl-device-list 30 67 "Seconds to cache `pio device list --json-output'." 68 :type 'integer 69 :group 'pio) 70 71 (defcustom pio-cache-ttl-remote-device-list 30 72 "Seconds to cache `pio remote device list --json-output'." 73 :type 'integer 74 :group 'pio) 75 76 77 (defconst pio-system-info-buffer-name "*PIO*") 78 79 (defconst pio-device-list-buffer-name "*PIO*") 80 81 (defconst pio-remote-device-list-buffer-name "*PIO*") 82 83 (defconst pio-run-list-targets-buffer-name "*PIO*") 84 85 (defconst pio-test-list-tests-buffer-name "*PIO*") 86 87 (defconst pio-check-buffer-name "*PIO*") 88 89 (defconst pio-project-config-buffer-name "*PIO*") 90 91 (defconst pio-board-info-buffer-name "*PIO Board Info*") 92 93 94 (defconst pio-account-show-buffer-name "*PIO*") 95 96 (defconst pio-org-list-buffer-name "*PIO*") 97 98 (defconst pio-team-list-buffer-name "*PIO*") 99 100 (defvar-local pio-system-info--json-accumulator nil) 101 102 (defvar-local pio-device-list--json-accumulator nil) 103 104 (defvar-local pio-remote-device-list--json-accumulator nil) 105 106 (defvar-local pio-check--json-accumulator nil) 107 108 (defvar-local pio-project-config--json-accumulator nil) 109 110 (defvar-local pio-boards--all nil) 111 112 (defvar-local pio-boards--filtered nil) 113 114 (defvar-local pio-boards--query "") 115 116 (defvar-local pio-boards--selected-id nil) 117 118 (defvar-local pio-boards--board-by-id nil) 119 120 121 (defvar pio--json-command-cache (make-hash-table :test 'equal) 122 "In-memory cache for JSON command results.") 123 124 (defvar pio--window-configuration nil 125 "Last non-PIO window configuration for restoring on quit.") 126 127 (defvar pio-boards--window-configuration nil 128 "Saved window configuration before opening boards fullscreen UI.") 129 130 131 (defvar pio-command-map 132 (let ((map (make-sparse-keymap))) 133 (define-key map (kbd "a") #'pio-account-show) 134 (define-key map (kbd "b") #'pio-boards) 135 (define-key map (kbd "c") #'pio-check) 136 (define-key map (kbd "d") #'pio-device-list) 137 (define-key map (kbd "g") #'pio-project-config) 138 (define-key map (kbd "k") #'pio-cache-clear) 139 (define-key map (kbd "l") #'pio-run-list-targets) 140 (define-key map (kbd "m") #'pio-mode) 141 (define-key map (kbd "o") #'pio-org-list) 142 (define-key map (kbd "r") #'pio-remote-device-list) 143 (define-key map (kbd "s") #'pio-system-info) 144 (define-key map (kbd "x") #'pio-test-list-tests) 145 (define-key map (kbd "t") #'pio-team-list) 146 map) 147 "Prefix keymap for PlatformIO commands.") 148 149 (defun pio-mode-force-refresh () 150 "Open the PlatformIO command dispatcher." 151 (interactive) 152 (pio-mode)) 153 154 (defun pio-account-show-force-refresh () 155 "Show account and bypass cache." 156 (interactive) 157 (pio-account-show t)) 158 159 (defun pio-org-list-force-refresh () 160 "Show org list and bypass cache." 161 (interactive) 162 (pio-org-list t)) 163 164 (defun pio-team-list-force-refresh () 165 "Show team list and bypass cache." 166 (interactive) 167 (pio-team-list t)) 168 169 (defun pio-boards-force-refresh () 170 "Show boards and bypass cache." 171 (interactive) 172 (pio-boards t)) 173 174 (when (featurep 'transient) 175 (transient-define-prefix pio-transient () 176 "PlatformIO command palette." 177 ["General" 178 ("m" "Command menu" pio-mode) 179 ("k" "Clear cache" pio-cache-clear)] 180 ["Project" 181 ("s" "System info" pio-system-info) 182 ("g" "Project config" pio-project-config) 183 ("d" "Device list" pio-device-list) 184 ("r" "Remote devices" pio-remote-device-list) 185 ("b" "Boards" pio-boards) 186 ("B" "Boards (force)" pio-boards-force-refresh)] 187 ["Build & Test" 188 ("l" "Run list targets" pio-run-list-targets) 189 ("x" "Test list" pio-test-list-tests) 190 ("c" "Check" pio-check)] 191 ["Cloud" 192 ("a" "Account" pio-account-show) 193 ("A" "Account (force)" pio-account-show-force-refresh) 194 ("o" "Organizations" pio-org-list) 195 ("O" "Organizations (force)" pio-org-list-force-refresh) 196 ("t" "Teams" pio-team-list) 197 ("T" "Teams (force)" pio-team-list-force-refresh)])) 198 199 (defun pio-dispatch () 200 "Open the PlatformIO command dispatcher. 201 Uses transient when available, otherwise falls back to `pio-command-map'." 202 (interactive) 203 (if (fboundp 'pio-transient) 204 (pio-transient) 205 (message "Transient is unavailable; use `C-c C-p` prefix commands."))) 206 207 (defun pio--bind-common-keys (mode-map) 208 "Bind common PlatformIO command keys into MODE-MAP." 209 (define-key mode-map (kbd "C-c p") #'pio-dispatch) 210 (define-key mode-map (kbd "C-c C-p") pio-command-map) 211 (define-key mode-map (kbd "C-c a") #'pio-account-show) 212 (define-key mode-map (kbd "C-c b") #'pio-boards) 213 (define-key mode-map (kbd "C-c c") #'pio-check) 214 (define-key mode-map (kbd "C-c d") #'pio-device-list) 215 (define-key mode-map (kbd "C-c g") #'pio-project-config) 216 (define-key mode-map (kbd "C-c k") #'pio-cache-clear) 217 (define-key mode-map (kbd "C-c l") #'pio-run-list-targets) 218 (define-key mode-map (kbd "C-c m") #'pio-mode) 219 (define-key mode-map (kbd "C-c o") #'pio-org-list) 220 (define-key mode-map (kbd "C-c r") #'pio-remote-device-list) 221 (define-key mode-map (kbd "C-c s") #'pio-system-info) 222 (define-key mode-map (kbd "C-c x") #'pio-test-list-tests) 223 (define-key mode-map (kbd "C-c t") #'pio-team-list) 224 (define-key mode-map (kbd "q") #'pio-quit-window)) 225 226 (defun pio-quit-window () 227 "Close the current PIO window cleanly." 228 (interactive) 229 (if pio-boards--window-configuration 230 (progn 231 (set-window-configuration pio-boards--window-configuration) 232 (setq pio-boards--window-configuration nil)) 233 (if pio--window-configuration 234 (progn 235 (set-window-configuration pio--window-configuration) 236 (setq pio--window-configuration nil)) 237 (if (one-window-p) 238 (quit-window) 239 (delete-window))))) 240 241 (defun pio-boards--restore-window-configuration () 242 "Restore the saved pre-boards window configuration." 243 (when pio-boards--window-configuration 244 (set-window-configuration pio-boards--window-configuration) 245 (setq pio-boards--window-configuration nil))) 246 247 (defun pio-boards--setup-fullscreen-layout (results-buffer detail-buffer) 248 "Show RESULTS-BUFFER and DETAIL-BUFFER in a fullscreen two-pane layout." 249 (setq pio-boards--window-configuration (current-window-configuration)) 250 (setq pio--window-configuration nil) 251 (delete-other-windows) 252 (let ((results-window (selected-window)) 253 (detail-window nil)) 254 (set-window-buffer results-window results-buffer) 255 (setq detail-window (split-window results-window nil 'right)) 256 (set-window-buffer detail-window detail-buffer) 257 (select-window results-window))) 258 259 (defun pio-boards--display-layout () 260 "Display boards/results in fullscreen split layout." 261 (let ((results-buffer (get-buffer-create pio-system-info-buffer-name)) 262 (detail-buffer (pio-boards--detail-buffer))) 263 (pio-boards--setup-fullscreen-layout results-buffer detail-buffer))) 264 265 (defun pio-boards-quit-layout () 266 "Quit boards UI and restore the previous window layout." 267 (interactive) 268 (if pio-boards--window-configuration 269 (progn 270 (pio-boards--restore-window-configuration) 271 (setq pio--window-configuration nil)) 272 (if (one-window-p) 273 (quit-window) 274 (delete-window)))) 275 276 (defun pio--find-visible-window () 277 "Return a visible window currently showing a PIO buffer, or nil." 278 (catch 'pio-window 279 (dolist (win (window-list nil nil)) 280 (let ((name (buffer-name (window-buffer win)))) 281 (when (and (stringp name) 282 (string-prefix-p "*PIO" name)) 283 (throw 'pio-window win)))) 284 nil)) 285 286 (defun pio--display-buffer-passive (buffer) 287 "Show BUFFER in a PIO window without changing focus. 288 If no PIO window is visible, create a regular window on the right." 289 (let ((target-window (or (get-buffer-window buffer t) 290 (pio--find-visible-window)))) 291 (if (window-live-p target-window) 292 (set-window-buffer target-window buffer) 293 (let ((origin (selected-window))) 294 (unless pio--window-configuration 295 (setq pio--window-configuration (current-window-configuration))) 296 (setq target-window (split-window origin nil 'right)) 297 (set-window-buffer target-window buffer) 298 (select-window origin))))) 299 300 (defun pio--append-ansi-process-output (process output-chunk) 301 "Append OUTPUT-CHUNK from PROCESS, applying ANSI color sequences." 302 (when-let ((process-buffer (process-buffer process))) 303 (with-current-buffer process-buffer 304 (let ((inhibit-read-only t)) 305 (goto-char (point-max)) 306 (insert (ansi-color-apply output-chunk)))))) 307 308 (defun pio-greet () 309 "Return the greeting string for pio-mode." 310 "Hellooo from pio-mode!") 311 312 (defun pio-hello () 313 (interactive) 314 (message "%s" (pio-greet))) 315 316 (defun pio-system-info--resolve-executable () 317 "Return the absolute path to the PlatformIO CLI, or signal a user error." 318 (or (executable-find pio-executable) 319 (user-error "PlatformIO CLI not found: %s" pio-executable))) 320 321 (defun pio--run-json-command-sync (&rest args) 322 "Run `platformio' with ARGS and parse JSON output." 323 (let ((raw-output (apply #'pio--run-command-sync args))) 324 (condition-case err 325 (json-parse-string raw-output :object-type 'alist :array-type 'list) 326 (error 327 (user-error "Failed to parse JSON from platformio %s: %s" 328 (string-join args " ") 329 (error-message-string err)))))) 330 331 (defun pio--run-command-sync (&rest args) 332 "Run `platformio' with ARGS and return trimmed stdout string." 333 (let ((platformio-cli (pio-system-info--resolve-executable))) 334 (with-temp-buffer 335 (let ((exit-code (apply #'process-file platformio-cli nil (current-buffer) nil args)) 336 (command-label (format "%s %s" platformio-cli (string-join args " ")))) 337 (let ((raw-output (string-trim (buffer-string)))) 338 (unless (and (integerp exit-code) (zerop exit-code)) 339 (user-error "PlatformIO command failed: %s\n%s" command-label raw-output)) 340 (when (string-empty-p raw-output) 341 (user-error "PlatformIO command returned empty output: %s" command-label)) 342 raw-output))))) 343 344 (defun pio-cache-clear () 345 "Clear the in-memory PlatformIO JSON cache." 346 (interactive) 347 (clrhash pio--json-command-cache) 348 (message "PIO cache cleared")) 349 350 (defun pio--cache-root-key () 351 "Return a cache root key based on current project context." 352 (expand-file-name 353 (or (locate-dominating-file default-directory "platformio.ini") 354 default-directory))) 355 356 (defun pio--cache-entry-fresh-p (entry ttl) 357 "Return non-nil when ENTRY is fresh for TTL seconds." 358 (and (consp entry) 359 (numberp (car entry)) 360 (< (- (float-time) (car entry)) ttl))) 361 362 (defun pio--run-json-command-cached (cache-id ttl force-refresh &rest args) 363 "Run JSON command ARGS with CACHE-ID and TTL. 364 When FORCE-REFRESH is non-nil, bypass cache." 365 (let* ((cache-key (list cache-id (pio--cache-root-key) args)) 366 (entry (gethash cache-key pio--json-command-cache)) 367 (use-cache (and pio-cache-enabled 368 (not force-refresh) 369 (pio--cache-entry-fresh-p entry ttl)))) 370 (if use-cache 371 (cdr entry) 372 (let ((value (apply #'pio--run-json-command-sync args))) 373 (when pio-cache-enabled 374 (puthash cache-key (cons (float-time) value) pio--json-command-cache)) 375 value)))) 376 377 (defun pio-boards--field (board key) 378 "Return KEY from BOARD as a normalized display string." 379 (pio-device-list--normalize-field 380 (pio-device-list--alist-get-any key board))) 381 382 (defun pio-boards--lookup (board key) 383 "Return raw KEY value from BOARD alist, preserving list values." 384 (cdr (assoc key board))) 385 386 (defun pio-boards--format-size-kib (value) 387 "Return VALUE in bytes as a human-friendly KiB string." 388 (if (numberp value) 389 (format "%d KiB" (/ value 1024)) 390 "n/a")) 391 392 (defun pio-boards--format-cpu-hz (value) 393 "Return CPU frequency VALUE in Hz as MHz text." 394 (if (numberp value) 395 (format "%.1f MHz" (/ value 1000000.0)) 396 "n/a")) 397 398 (defun pio-boards--insert-detail-line (label value) 399 "Insert one detail row with LABEL and VALUE." 400 (insert (propertize (format "%-11s" (concat label ":")) 'face 'font-lock-variable-name-face) 401 " " 402 (propertize (format "%s" value) 'face 'font-lock-string-face) 403 "\n")) 404 405 (defun pio-boards--debug-tools-summary (board) 406 "Return formatted debug tools string from BOARD." 407 (let* ((debug (pio-boards--lookup board 'debug)) 408 (tools-cell (and (listp debug) (assoc 'tools debug))) 409 (tools (cdr tools-cell)) 410 (names (mapcar (lambda (tool) 411 (if (symbolp tool) 412 (symbol-name tool) 413 (symbol-name (car tool)))) 414 tools)) 415 (default-tool 416 (catch 'found 417 (dolist (tool tools nil) 418 (when (and (listp tool) 419 (assoc 'default tool) 420 (cdr (assoc 'default tool))) 421 (throw 'found (symbol-name (car tool)))))))) 422 (if names 423 (if default-tool 424 (format "%s (default: %s)" (string-join names ", ") default-tool) 425 (string-join names ", ")) 426 "n/a"))) 427 428 (defun pio-boards--board-id (board index) 429 "Return a stable row id from BOARD and INDEX." 430 (let ((raw-id (pio-device-list--alist-get-any 'id board))) 431 (if (and raw-id (not (string-empty-p (format "%s" raw-id)))) 432 (format "%s" raw-id) 433 (format "board-%d" index)))) 434 435 (defun pio-boards--matches-query-p (board query) 436 "Return non-nil if BOARD matches QUERY." 437 (if (string-empty-p query) 438 t 439 (let ((q (downcase query))) 440 (or (string-match-p (regexp-quote q) (downcase (pio-boards--field board 'name))) 441 (string-match-p (regexp-quote q) (downcase (pio-boards--field board 'vendor))) 442 (string-match-p (regexp-quote q) (downcase (pio-boards--field board 'platform))) 443 (string-match-p (regexp-quote q) (downcase (pio-boards--field board 'id))))))) 444 445 (defun pio-boards--tabulated-entry (board index) 446 "Convert BOARD at INDEX into one tabulated entry." 447 (let ((id (pio-boards--board-id board index)) 448 (name (pio-boards--field board 'name)) 449 (vendor (pio-boards--field board 'vendor)) 450 (platform (pio-boards--field board 'platform))) 451 (puthash id board pio-boards--board-by-id) 452 (list id (vector name vendor platform)))) 453 454 (defun pio-boards--render-list () 455 "Render the boards list from local state." 456 (setq pio-boards--board-by-id (make-hash-table :test 'equal)) 457 (let ((index 0)) 458 (setq tabulated-list-entries 459 (mapcar (lambda (board) 460 (setq index (1+ index)) 461 (pio-boards--tabulated-entry board index)) 462 pio-boards--filtered))) 463 (setq header-line-format 464 (format " Boards | %d/%d | Filter: %s" 465 (length pio-boards--filtered) 466 (length pio-boards--all) 467 (if (string-empty-p pio-boards--query) "<none>" pio-boards--query))) 468 (tabulated-list-print t) 469 (goto-char (point-min)) 470 (forward-line 1) 471 (pio-boards--update-detail-at-point)) 472 473 (defun pio-boards--apply-filter (query) 474 "Apply QUERY to boards list and rerender." 475 (setq pio-boards--query (string-trim (or query "")) 476 pio-boards--filtered 477 (seq-filter (lambda (board) 478 (pio-boards--matches-query-p board pio-boards--query)) 479 pio-boards--all)) 480 (pio-boards--render-list)) 481 482 (defun pio-boards--detail-buffer () 483 "Return the board detail buffer." 484 (get-buffer-create pio-board-info-buffer-name)) 485 486 (defun pio-boards--render-board-detail (board) 487 "Render BOARD object into the detail buffer." 488 (with-current-buffer (pio-boards--detail-buffer) 489 (let ((inhibit-read-only t)) 490 (erase-buffer) 491 (pio-board-info-mode) 492 (setq-local truncate-lines t) 493 (let ((name (pio-boards--field board 'name)) 494 (board-id (pio-boards--field board 'id)) 495 (vendor (pio-boards--field board 'vendor)) 496 (platform (pio-boards--field board 'platform)) 497 (mcu (pio-boards--field board 'mcu)) 498 (frameworks (pio-boards--lookup board 'frameworks)) 499 (url (pio-boards--field board 'url)) 500 (ram (pio-boards--lookup board 'ram)) 501 (rom (pio-boards--lookup board 'rom)) 502 (f-cpu (pio-boards--lookup board 'f_cpu))) 503 (insert (propertize name 'face 'bold) "\n") 504 (insert (propertize board-id 'face 'shadow) "\n\n") 505 (insert (propertize "Overview\n" 'face 'font-lock-keyword-face)) 506 (insert "--------\n") 507 (pio-boards--insert-detail-line "Vendor" vendor) 508 (pio-boards--insert-detail-line "Platform" platform) 509 (pio-boards--insert-detail-line "MCU" mcu) 510 (pio-boards--insert-detail-line "CPU" (pio-boards--format-cpu-hz f-cpu)) 511 (pio-boards--insert-detail-line "RAM" (pio-boards--format-size-kib ram)) 512 (pio-boards--insert-detail-line "Flash" (pio-boards--format-size-kib rom)) 513 (pio-boards--insert-detail-line "Frameworks" 514 (if (listp frameworks) 515 (string-join (mapcar #'format frameworks) ", ") 516 "n/a")) 517 (pio-boards--insert-detail-line "Debug" (pio-boards--debug-tools-summary board)) 518 (insert "\n") 519 (insert (propertize "URL\n" 'face 'font-lock-keyword-face)) 520 (insert "---\n") 521 (insert-text-button url 522 'action (lambda (_) 523 (browse-url url)) 524 'follow-link t 525 'help-echo "Open board page in browser") 526 (insert "\n\n") 527 (insert (propertize "Raw Data\n" 'face 'font-lock-keyword-face)) 528 (insert "--------\n") 529 (condition-case _err 530 (let ((json-encoding-pretty-print t)) 531 (insert (json-encode board)) 532 (json-pretty-print-buffer) 533 (goto-char (point-max))) 534 (error 535 (pp board (current-buffer))))) 536 (goto-char (point-min))))) 537 538 (defun pio-boards--update-detail-at-point () 539 "Update detail pane using the current row in results buffer." 540 (when (derived-mode-p 'pio-boards-mode) 541 (let ((id (tabulated-list-get-id))) 542 (unless (equal id pio-boards--selected-id) 543 (setq pio-boards--selected-id id) 544 (when id 545 (when-let ((board (gethash id pio-boards--board-by-id))) 546 (pio-boards--render-board-detail board))))))) 547 548 (defun pio-boards-filter (query) 549 "Prompt and apply board filter QUERY." 550 (interactive (list (read-from-minibuffer 551 "Boards filter: " 552 pio-boards--query))) 553 (pio-boards--apply-filter query)) 554 555 (defun pio-boards-clear-filter () 556 "Clear board list filter." 557 (interactive) 558 (pio-boards--apply-filter "")) 559 560 (defun pio-boards-refresh (&optional force-refresh) 561 "Refresh boards data. With FORCE-REFRESH, bypass cache." 562 (interactive "P") 563 (let ((boards (pio--run-json-command-cached 564 'boards 565 pio-cache-ttl-boards 566 force-refresh 567 "boards" "--json-output"))) 568 (setq pio-boards--all boards) 569 (pio-boards--apply-filter pio-boards--query))) 570 571 (defun pio-boards--post-command-hook () 572 "Keep board detail pane synced with current line." 573 (pio-boards--update-detail-at-point)) 574 575 (define-derived-mode pio-boards-mode tabulated-list-mode "PIO-Boards" 576 "Major mode for browsing PlatformIO boards." 577 (setq tabulated-list-format 578 [("Name" 34 t) 579 ("Vendor" 22 t) 580 ("Platform" 16 t)]) 581 (setq tabulated-list-padding 2) 582 (setq tabulated-list-sort-key (cons "Name" nil)) 583 (setq tabulated-list-use-header-line t) 584 (tabulated-list-init-header) 585 (add-hook 'post-command-hook #'pio-boards--post-command-hook nil t)) 586 587 (define-derived-mode pio-board-info-mode special-mode "PIO-Board-Info" 588 "Major mode for displaying board detail JSON.") 589 590 (pio--bind-common-keys pio-boards-mode-map) 591 (pio--bind-common-keys pio-board-info-mode-map) 592 (define-key pio-boards-mode-map (kbd "g") #'pio-boards-refresh) 593 (define-key pio-boards-mode-map (kbd "G") (lambda () (interactive) (pio-boards-refresh t))) 594 (define-key pio-boards-mode-map (kbd "/") #'pio-boards-filter) 595 (define-key pio-boards-mode-map (kbd "C-c /") #'pio-boards-clear-filter) 596 (define-key pio-boards-mode-map (kbd "q") #'pio-boards-quit-layout) 597 (define-key pio-board-info-mode-map (kbd "q") #'pio-boards-quit-layout) 598 599 (defun pio-boards (&optional force-refresh) 600 "Open boards explorer with list and board detail panes. 601 With FORCE-REFRESH, bypass cache for boards data." 602 (interactive "P") 603 (let ((results-buffer (get-buffer-create pio-system-info-buffer-name))) 604 (with-current-buffer results-buffer 605 (pio-boards-mode) 606 (setq-local truncate-lines t)) 607 (pio-boards--display-layout) 608 (with-current-buffer results-buffer 609 (pio-boards-refresh force-refresh)))) 610 611 (defun pio--person-display-name (person) 612 "Return a readable display name string from PERSON alist." 613 (let* ((username (pio-device-list--alist-get-any 'username person)) 614 (firstname (pio-device-list--alist-get-any 'firstname person)) 615 (lastname (pio-device-list--alist-get-any 'lastname person)) 616 (full-name (string-trim (format "%s %s" 617 (or firstname "") 618 (or lastname ""))))) 619 (if (string-empty-p full-name) 620 (pio-device-list--normalize-field username) 621 full-name))) 622 623 (defun pio-system-info--infer-section (field-title) 624 "Infer a section label from FIELD-TITLE." 625 (cond 626 ((string-match-p "Core" field-title) "Core") 627 ((string-match-p "Python" field-title) "Python") 628 ((string-match-p "System" field-title) "System") 629 ((string-match-p "Platform" field-title) "Platform") 630 ((string-match-p "Libraries" field-title) "Libraries") 631 ((string-match-p "Tool" field-title) "Toolchains") 632 (t "Other"))) 633 634 (defun pio-system-info--format-field-title (raw-title) 635 "Return a display title for RAW-TITLE, including a section prefix." 636 (let ((section (pio-system-info--infer-section raw-title))) 637 (format "%s › %s" section raw-title))) 638 639 (defun pio-system-info--propertize-value (value) 640 "Return VALUE as a pretty, colored string for display." 641 (propertize (format "%s" value) 'face 'font-lock-string-face)) 642 643 (defun pio-system-info--tabulated-entry-from-json-pair (json-pair) 644 "Convert JSON-PAIR into a `tabulated-list-entries' row." 645 (let* ((entry-key (car json-pair)) 646 (entry-object (cdr json-pair)) 647 (field-title (alist-get 'title entry-object)) 648 (field-value (alist-get 'value entry-object)) 649 (display-title (pio-system-info--format-field-title field-title)) 650 (display-value (pio-system-info--propertize-value field-value))) 651 (list (symbol-name entry-key) 652 (vector display-title display-value)))) 653 654 (defun pio-system-info--entries-from-json (json-string) 655 "Parse JSON-STRING and return `tabulated-list-entries'." 656 (let ((parsed-json (json-parse-string json-string :object-type 'alist :array-type 'list))) 657 (mapcar #'pio-system-info--tabulated-entry-from-json-pair parsed-json))) 658 659 (defun pio-system-info--render-buffer (system-info-buffer) 660 "Initialize and render SYSTEM-INFO-BUFFER for system info output." 661 (with-current-buffer system-info-buffer 662 (pio-system-info-mode) 663 (setq tabulated-list-entries nil) 664 (setq-local pio-system-info--json-accumulator nil) 665 (tabulated-list-print))) 666 667 (defun pio-system-info--append-process-output (process output-chunk) 668 "Append OUTPUT-CHUNK from PROCESS into the current buffer accumulator." 669 (when-let ((process-buffer (process-buffer process))) 670 (with-current-buffer process-buffer 671 (setq-local pio-system-info--json-accumulator 672 (concat (or pio-system-info--json-accumulator "") output-chunk))))) 673 674 (defun pio-system-info--finalize-process (_process _event) 675 "Finalize system info output: parse JSON and refresh the tabulated list." 676 (with-current-buffer pio-system-info-buffer-name 677 (let* ((json-string (or pio-system-info--json-accumulator "{}")) 678 (tabulated-entries (pio-system-info--entries-from-json json-string))) 679 (setq tabulated-list-entries tabulated-entries 680 pio-system-info--json-accumulator nil) 681 (tabulated-list-revert)))) 682 683 (define-derived-mode pio-system-info-mode tabulated-list-mode "PIO-System" 684 "Major mode for displaying `pio system info --json-output' in a table." 685 (setq tabulated-list-format 686 [("Field" 28 t) 687 ("Value" 0 t)]) 688 (setq tabulated-list-padding 4) 689 (setq tabulated-list-sort-key (cons "Field" nil)) 690 (setq tabulated-list-use-header-line t) 691 (tabulated-list-init-header)) 692 693 (pio--bind-common-keys pio-system-info-mode-map) 694 695 (defun pio-system-info () 696 "Display `pio system info --json-output' in a grid." 697 (interactive) 698 (let* ((platformio-cli (pio-system-info--resolve-executable)) 699 (system-info-buffer (get-buffer-create pio-system-info-buffer-name))) 700 (pio-system-info--render-buffer system-info-buffer) 701 (pio--display-buffer-passive system-info-buffer) 702 (make-process 703 :name "pio-system-info" 704 :buffer system-info-buffer 705 :command (list platformio-cli "system" "info" "--json-output") 706 :filter #'pio-system-info--append-process-output 707 :sentinel #'pio-system-info--finalize-process))) 708 709 (defun pio-device-list--alist-get-any (key alist) 710 "Get KEY from ALIST, supporting symbol and string forms." 711 (or (alist-get key alist) 712 (alist-get (symbol-name key) alist nil nil #'equal))) 713 714 (defun pio-device-list--normalize-field (value) 715 "Return VALUE as a display-safe device field string." 716 (let ((as-string (string-trim (format "%s" (or value ""))))) 717 (if (string-empty-p as-string) "n/a" as-string))) 718 719 (defun pio-device-list--tabulated-entry-from-device (device index) 720 "Convert DEVICE at INDEX into a `tabulated-list-entries' row." 721 (let* ((port (pio-device-list--normalize-field 722 (pio-device-list--alist-get-any 'port device))) 723 (description (pio-device-list--normalize-field 724 (pio-device-list--alist-get-any 'description device))) 725 (hwid (pio-device-list--normalize-field 726 (pio-device-list--alist-get-any 'hwid device))) 727 (entry-id (format "%s#%d" port index))) 728 (list entry-id 729 (vector port description hwid)))) 730 731 (defun pio-device-list--entries-from-json (json-string) 732 "Parse JSON-STRING and return `tabulated-list-entries'." 733 (let ((parsed-json (json-parse-string json-string :object-type 'alist :array-type 'list)) 734 (index 0)) 735 (mapcar (lambda (device) 736 (setq index (1+ index)) 737 (pio-device-list--tabulated-entry-from-device device index)) 738 parsed-json))) 739 740 (defun pio-device-list--entries-from-json-data (parsed-json) 741 "Convert PARSED-JSON from `pio device list' into `tabulated-list-entries'." 742 (let ((index 0)) 743 (mapcar (lambda (device) 744 (setq index (1+ index)) 745 (pio-device-list--tabulated-entry-from-device device index)) 746 parsed-json))) 747 748 (defun pio-device-list--render-buffer (device-list-buffer) 749 "Initialize and render DEVICE-LIST-BUFFER for device list output." 750 (with-current-buffer device-list-buffer 751 (pio-device-list-mode) 752 (setq tabulated-list-entries nil) 753 (setq-local pio-device-list--json-accumulator nil) 754 (tabulated-list-print))) 755 756 (defun pio-device-list--append-process-output (process output-chunk) 757 "Append OUTPUT-CHUNK from PROCESS into the current buffer accumulator." 758 (when-let ((process-buffer (process-buffer process))) 759 (with-current-buffer process-buffer 760 (setq-local pio-device-list--json-accumulator 761 (concat (or pio-device-list--json-accumulator "") output-chunk))))) 762 763 (defun pio-device-list--finalize-process (_process _event) 764 "Finalize device list output: parse JSON and refresh tabulated list." 765 (when-let ((device-list-buffer (get-buffer pio-device-list-buffer-name))) 766 (with-current-buffer device-list-buffer 767 (let* ((json-string (or pio-device-list--json-accumulator "[]")) 768 (tabulated-entries (pio-device-list--entries-from-json json-string))) 769 (setq tabulated-list-entries tabulated-entries 770 pio-device-list--json-accumulator nil) 771 (tabulated-list-revert))))) 772 773 (define-derived-mode pio-device-list-mode tabulated-list-mode "PIO-Devices" 774 "Major mode for displaying `pio device list --json-output' in a table." 775 (setq tabulated-list-format 776 [("Port" 22 t) 777 ("Description" 34 t) 778 ("HWID" 0 t)]) 779 (setq tabulated-list-padding 2) 780 (setq tabulated-list-sort-key (cons "Port" nil)) 781 (setq tabulated-list-use-header-line t) 782 (tabulated-list-init-header)) 783 784 (pio--bind-common-keys pio-device-list-mode-map) 785 786 (defun pio-device-list (&optional force-refresh) 787 "Display `pio device list --json-output' in a table." 788 (interactive "P") 789 (let* ((parsed-json (pio--run-json-command-cached 790 'device-list 791 pio-cache-ttl-device-list 792 force-refresh 793 "device" "list" "--json-output")) 794 (device-list-buffer (get-buffer-create pio-device-list-buffer-name))) 795 (with-current-buffer device-list-buffer 796 (pio-device-list-mode) 797 (setq tabulated-list-entries (pio-device-list--entries-from-json-data parsed-json)) 798 (tabulated-list-print)) 799 (pio--display-buffer-passive device-list-buffer))) 800 801 (defun pio-org-list--owners-string (owners) 802 "Return a readable owners string from OWNERS list." 803 (if owners 804 (string-join (mapcar #'pio--person-display-name owners) ", ") 805 "n/a")) 806 807 (defun pio-org-list--tabulated-entry-from-org (org index) 808 "Convert ORG at INDEX into a `tabulated-list-entries' row." 809 (let* ((orgname (pio-device-list--normalize-field 810 (pio-device-list--alist-get-any 'orgname org))) 811 (displayname (pio-device-list--normalize-field 812 (pio-device-list--alist-get-any 'displayname org))) 813 (email (pio-device-list--normalize-field 814 (pio-device-list--alist-get-any 'email org))) 815 (owners (pio-org-list--owners-string 816 (pio-device-list--alist-get-any 'owners org)))) 817 (list (format "%s#%d" orgname index) 818 (vector orgname displayname email owners)))) 819 820 (defun pio-org-list--entries-from-json-data (parsed-json) 821 "Convert PARSED-JSON from `pio org list' into `tabulated-list-entries'." 822 (let ((index 0)) 823 (mapcar (lambda (org) 824 (setq index (1+ index)) 825 (pio-org-list--tabulated-entry-from-org org index)) 826 parsed-json))) 827 828 (define-derived-mode pio-org-list-mode tabulated-list-mode "PIO-Orgs" 829 "Major mode for displaying `pio org list --json-output' in a table." 830 (setq tabulated-list-format 831 [("Org" 24 t) 832 ("Display Name" 24 t) 833 ("Email" 30 t) 834 ("Owners" 0 t)]) 835 (setq tabulated-list-padding 2) 836 (setq tabulated-list-sort-key (cons "Org" nil)) 837 (setq tabulated-list-use-header-line t) 838 (tabulated-list-init-header)) 839 840 (pio--bind-common-keys pio-org-list-mode-map) 841 842 (defun pio-org-list (&optional force-refresh) 843 "Display `pio org list --json-output' in a table." 844 (interactive "P") 845 (let* ((parsed-json (pio--run-json-command-cached 846 'org-list 847 pio-cache-ttl-org-list 848 force-refresh 849 "org" "list" "--json-output")) 850 (org-list-buffer (get-buffer-create pio-org-list-buffer-name))) 851 (with-current-buffer org-list-buffer 852 (pio-org-list-mode) 853 (setq tabulated-list-entries (pio-org-list--entries-from-json-data parsed-json)) 854 (tabulated-list-print)) 855 (pio--display-buffer-passive org-list-buffer))) 856 857 (defun pio-team-list--members-string (members) 858 "Return a readable members string from MEMBERS list." 859 (if members 860 (string-join (mapcar #'pio--person-display-name members) ", ") 861 "n/a")) 862 863 (defun pio-team-list--tabulated-entry-from-team (org team index) 864 "Convert ORG TEAM at INDEX into a `tabulated-list-entries' row." 865 (let* ((team-name (pio-device-list--normalize-field 866 (pio-device-list--alist-get-any 'name team))) 867 (description (pio-device-list--normalize-field 868 (pio-device-list--alist-get-any 'description team))) 869 (members (pio-team-list--members-string 870 (pio-device-list--alist-get-any 'members team))) 871 (team-id (pio-device-list--normalize-field 872 (pio-device-list--alist-get-any 'id team)))) 873 (list (format "%s:%s#%d" org team-name index) 874 (vector org team-name description members team-id)))) 875 876 (defun pio-team-list--entries-from-json-data (parsed-json) 877 "Convert PARSED-JSON from `pio team list' into `tabulated-list-entries'." 878 (let ((entries nil) 879 (index 0)) 880 (dolist (org-pair parsed-json (nreverse entries)) 881 (let* ((org-key (car org-pair)) 882 (org-name (if (symbolp org-key) (symbol-name org-key) org-key)) 883 (teams (cdr org-pair))) 884 (dolist (team teams) 885 (setq index (1+ index)) 886 (push (pio-team-list--tabulated-entry-from-team org-name team index) entries)))))) 887 888 (define-derived-mode pio-team-list-mode tabulated-list-mode "PIO-Teams" 889 "Major mode for displaying `pio team list --json-output' in a table." 890 (setq tabulated-list-format 891 [("Org" 24 t) 892 ("Team" 16 t) 893 ("Description" 28 t) 894 ("Members" 28 t) 895 ("ID" 0 t)]) 896 (setq tabulated-list-padding 2) 897 (setq tabulated-list-sort-key (cons "Org" nil)) 898 (setq tabulated-list-use-header-line t) 899 (tabulated-list-init-header)) 900 901 (pio--bind-common-keys pio-team-list-mode-map) 902 903 (defun pio-team-list (&optional force-refresh) 904 "Display `pio team list --json-output' in a table." 905 (interactive "P") 906 (let* ((parsed-json (pio--run-json-command-cached 907 'team-list 908 pio-cache-ttl-team-list 909 force-refresh 910 "team" "list" "--json-output")) 911 (team-list-buffer (get-buffer-create pio-team-list-buffer-name))) 912 (with-current-buffer team-list-buffer 913 (pio-team-list-mode) 914 (setq tabulated-list-entries (pio-team-list--entries-from-json-data parsed-json)) 915 (tabulated-list-print)) 916 (pio--display-buffer-passive team-list-buffer))) 917 918 (defun pio-remote-device-list--tabulated-entry-from-device (host device index) 919 "Convert HOST DEVICE at INDEX into a `tabulated-list-entries' row." 920 (let* ((port (pio-device-list--normalize-field 921 (pio-device-list--alist-get-any 'port device))) 922 (description (pio-device-list--normalize-field 923 (pio-device-list--alist-get-any 'description device))) 924 (hwid (pio-device-list--normalize-field 925 (pio-device-list--alist-get-any 'hwid device))) 926 (entry-id (format "%s:%s#%d" host port index))) 927 (list entry-id 928 (vector host port description hwid)))) 929 930 (defun pio-remote-device-list--entries-from-json (json-string) 931 "Parse JSON-STRING and return `tabulated-list-entries'." 932 (let ((parsed-json (json-parse-string json-string :object-type 'alist :array-type 'list)) 933 (entries nil) 934 (index 0)) 935 (dolist (host-pair parsed-json (nreverse entries)) 936 (let* ((host-key (car host-pair)) 937 (host (pio-device-list--normalize-field 938 (if (symbolp host-key) 939 (symbol-name host-key) 940 host-key))) 941 (devices (cdr host-pair))) 942 (dolist (device devices) 943 (setq index (1+ index)) 944 (push (pio-remote-device-list--tabulated-entry-from-device host device index) 945 entries)))))) 946 947 (defun pio-remote-device-list--entries-from-json-data (parsed-json) 948 "Convert PARSED-JSON from `pio remote device list' into `tabulated-list-entries'." 949 (let ((entries nil) 950 (index 0)) 951 (dolist (host-pair parsed-json (nreverse entries)) 952 (let* ((host-key (car host-pair)) 953 (host (pio-device-list--normalize-field 954 (if (symbolp host-key) (symbol-name host-key) host-key))) 955 (devices (cdr host-pair))) 956 (dolist (device devices) 957 (setq index (1+ index)) 958 (push (pio-remote-device-list--tabulated-entry-from-device host device index) 959 entries)))))) 960 961 (defun pio-remote-device-list--render-buffer (remote-device-list-buffer) 962 "Initialize and render REMOTE-DEVICE-LIST-BUFFER for remote device output." 963 (with-current-buffer remote-device-list-buffer 964 (pio-remote-device-list-mode) 965 (setq tabulated-list-entries nil) 966 (setq-local pio-remote-device-list--json-accumulator nil) 967 (tabulated-list-print))) 968 969 (defun pio-remote-device-list--append-process-output (process output-chunk) 970 "Append OUTPUT-CHUNK from PROCESS into the current buffer accumulator." 971 (when-let ((process-buffer (process-buffer process))) 972 (with-current-buffer process-buffer 973 (setq-local pio-remote-device-list--json-accumulator 974 (concat (or pio-remote-device-list--json-accumulator "") output-chunk))))) 975 976 (defun pio-remote-device-list--finalize-process (_process _event) 977 "Finalize remote device output: parse JSON and refresh tabulated list." 978 (when-let ((remote-device-list-buffer (get-buffer pio-remote-device-list-buffer-name))) 979 (with-current-buffer remote-device-list-buffer 980 (let* ((json-string (or pio-remote-device-list--json-accumulator "{}")) 981 (tabulated-entries (pio-remote-device-list--entries-from-json json-string))) 982 (setq tabulated-list-entries tabulated-entries 983 pio-remote-device-list--json-accumulator nil) 984 (tabulated-list-revert))))) 985 986 (define-derived-mode pio-remote-device-list-mode tabulated-list-mode "PIO-Remote-Devices" 987 "Major mode for displaying `pio remote device list --json-output' in a table." 988 (setq tabulated-list-format 989 [("Host" 18 t) 990 ("Port" 22 t) 991 ("Description" 34 t) 992 ("HWID" 0 t)]) 993 (setq tabulated-list-padding 2) 994 (setq tabulated-list-sort-key (cons "Host" nil)) 995 (setq tabulated-list-use-header-line t) 996 (tabulated-list-init-header)) 997 998 (pio--bind-common-keys pio-remote-device-list-mode-map) 999 1000 (defun pio-remote-device-list (&optional force-refresh) 1001 "Display `pio remote device list --json-output' in a table." 1002 (interactive "P") 1003 (let* ((parsed-json (pio--run-json-command-cached 1004 'remote-device-list 1005 pio-cache-ttl-remote-device-list 1006 force-refresh 1007 "remote" "device" "list" "--json-output")) 1008 (remote-device-list-buffer (get-buffer-create pio-remote-device-list-buffer-name))) 1009 (with-current-buffer remote-device-list-buffer 1010 (pio-remote-device-list-mode) 1011 (setq tabulated-list-entries 1012 (pio-remote-device-list--entries-from-json-data parsed-json)) 1013 (tabulated-list-print)) 1014 (pio--display-buffer-passive remote-device-list-buffer))) 1015 1016 (define-derived-mode pio-run-list-targets-mode special-mode "PIO-Run-Targets" 1017 "Major mode for displaying `pio run --list-targets' output.") 1018 1019 (pio--bind-common-keys pio-run-list-targets-mode-map) 1020 1021 (defun pio-run-list-targets--append-process-output (process output-chunk) 1022 "Append OUTPUT-CHUNK from PROCESS directly into its process buffer." 1023 (pio--append-ansi-process-output process output-chunk)) 1024 1025 (defun pio-run-list-targets--finalize-process (process event) 1026 "Finalize PROCESS for list targets output and append EVENT if needed." 1027 (when-let ((process-buffer (process-buffer process))) 1028 (with-current-buffer process-buffer 1029 (let ((inhibit-read-only t)) 1030 (goto-char (point-max)) 1031 (unless (and (stringp event) 1032 (string-match-p "finished" event)) 1033 (insert "\n" event)))))) 1034 1035 (defun pio-run-list-targets () 1036 "Display raw output of `pio run --list-targets'." 1037 (interactive) 1038 (let* ((platformio-cli (pio-system-info--resolve-executable)) 1039 (run-targets-buffer (get-buffer-create pio-run-list-targets-buffer-name))) 1040 (with-current-buffer run-targets-buffer 1041 (let ((inhibit-read-only t)) 1042 (erase-buffer) 1043 (setq-local ansi-color-context nil) 1044 (pio-run-list-targets-mode))) 1045 (pio--display-buffer-passive run-targets-buffer) 1046 (make-process 1047 :name "pio-run-list-targets" 1048 :buffer run-targets-buffer 1049 :command (list platformio-cli "run" "--list-targets") 1050 :filter #'pio-run-list-targets--append-process-output 1051 :sentinel #'pio-run-list-targets--finalize-process))) 1052 1053 (define-derived-mode pio-test-list-tests-mode special-mode "PIO-Test-List" 1054 "Major mode for displaying `pio test --list-tests' output.") 1055 1056 (pio--bind-common-keys pio-test-list-tests-mode-map) 1057 1058 (defun pio-test-list-tests--append-process-output (process output-chunk) 1059 "Append OUTPUT-CHUNK from PROCESS directly into its process buffer." 1060 (pio--append-ansi-process-output process output-chunk)) 1061 1062 (defun pio-test-list-tests--finalize-process (process event) 1063 "Finalize PROCESS for list tests output and append EVENT if needed." 1064 (when-let ((process-buffer (process-buffer process))) 1065 (with-current-buffer process-buffer 1066 (let ((inhibit-read-only t)) 1067 (goto-char (point-max)) 1068 (unless (and (stringp event) 1069 (string-match-p "finished" event)) 1070 (insert "\n" event)))))) 1071 1072 (defun pio-test-list-tests () 1073 "Display raw output of `pio test --list-tests'." 1074 (interactive) 1075 (let* ((platformio-cli (pio-system-info--resolve-executable)) 1076 (test-list-buffer (get-buffer-create pio-test-list-tests-buffer-name))) 1077 (with-current-buffer test-list-buffer 1078 (let ((inhibit-read-only t)) 1079 (erase-buffer) 1080 (setq-local ansi-color-context nil) 1081 (pio-test-list-tests-mode))) 1082 (pio--display-buffer-passive test-list-buffer) 1083 (make-process 1084 :name "pio-test-list-tests" 1085 :buffer test-list-buffer 1086 :command (list platformio-cli "test" "--list-tests") 1087 :filter #'pio-test-list-tests--append-process-output 1088 :sentinel #'pio-test-list-tests--finalize-process))) 1089 1090 (defun pio-check--severity-face (severity) 1091 "Return a face for SEVERITY string." 1092 (pcase (downcase (format "%s" severity)) 1093 ("high" 'error) 1094 ("medium" 'warning) 1095 ("low" 'font-lock-doc-face) 1096 (_ 'default))) 1097 1098 (defun pio-check--format-location (file line) 1099 "Return a compact location string for FILE and LINE." 1100 (format "%s:%s" 1101 (abbreviate-file-name (pio-device-list--normalize-field file)) 1102 (if (numberp line) (number-to-string line) "-"))) 1103 1104 (defun pio-check--tabulated-entry-from-defect (env tool defect index) 1105 "Convert DEFECT at INDEX into a `tabulated-list-entries' row." 1106 (let* ((severity (pio-device-list--normalize-field 1107 (pio-device-list--alist-get-any 'severity defect))) 1108 (category (pio-device-list--normalize-field 1109 (pio-device-list--alist-get-any 'category defect))) 1110 (defect-id (pio-device-list--normalize-field 1111 (pio-device-list--alist-get-any 'id defect))) 1112 (message (pio-device-list--normalize-field 1113 (pio-device-list--alist-get-any 'message defect))) 1114 (file (pio-device-list--alist-get-any 'file defect)) 1115 (line (pio-device-list--alist-get-any 'line defect)) 1116 (location (pio-check--format-location file line))) 1117 (list (format "%s#%d" defect-id index) 1118 (vector env 1119 tool 1120 (propertize severity 'face (pio-check--severity-face severity)) 1121 category 1122 defect-id 1123 location 1124 message)))) 1125 1126 (defun pio-check--entries-from-json (json-string) 1127 "Parse JSON-STRING and return `tabulated-list-entries'." 1128 (let ((parsed-json (json-parse-string json-string :object-type 'alist :array-type 'list)) 1129 (entries nil) 1130 (index 0)) 1131 (dolist (report parsed-json (nreverse entries)) 1132 (let* ((env (pio-device-list--normalize-field 1133 (pio-device-list--alist-get-any 'env report))) 1134 (tool (pio-device-list--normalize-field 1135 (pio-device-list--alist-get-any 'tool report))) 1136 (defects (pio-device-list--alist-get-any 'defects report))) 1137 (dolist (defect defects) 1138 (setq index (1+ index)) 1139 (push (pio-check--tabulated-entry-from-defect env tool defect index) 1140 entries)))))) 1141 1142 (defun pio-check--render-buffer (check-buffer) 1143 "Initialize and render CHECK-BUFFER for pio check output." 1144 (with-current-buffer check-buffer 1145 (pio-check-mode) 1146 (setq tabulated-list-entries nil) 1147 (setq-local pio-check--json-accumulator nil) 1148 (tabulated-list-print))) 1149 1150 (defun pio-check--append-process-output (process output-chunk) 1151 "Append OUTPUT-CHUNK from PROCESS into the current buffer accumulator." 1152 (when-let ((process-buffer (process-buffer process))) 1153 (with-current-buffer process-buffer 1154 (setq-local pio-check--json-accumulator 1155 (concat (or pio-check--json-accumulator "") output-chunk))))) 1156 1157 (defun pio-check--finalize-process (_process _event) 1158 "Finalize pio check output: parse JSON and refresh tabulated list." 1159 (when-let ((check-buffer (get-buffer pio-check-buffer-name))) 1160 (with-current-buffer check-buffer 1161 (let* ((json-string (or pio-check--json-accumulator "[]")) 1162 (tabulated-entries 1163 (condition-case err 1164 (pio-check--entries-from-json json-string) 1165 (error 1166 (list (list "pio-check-error" 1167 (vector "-" 1168 "-" 1169 "error" 1170 "-" 1171 "parse" 1172 "-" 1173 (format "Failed to parse pio check output: %s" 1174 (error-message-string err))))))))) 1175 (setq tabulated-list-entries tabulated-entries 1176 pio-check--json-accumulator nil) 1177 (tabulated-list-revert))))) 1178 1179 (define-derived-mode pio-check-mode tabulated-list-mode "PIO-Check" 1180 "Major mode for displaying `pio check --json-output' in a table." 1181 (setq tabulated-list-format 1182 [("Env" 14 t) 1183 ("Tool" 10 t) 1184 ("Severity" 10 t) 1185 ("Category" 12 t) 1186 ("ID" 22 t) 1187 ("Location" 44 t) 1188 ("Message" 0 t)]) 1189 (setq tabulated-list-padding 2) 1190 (setq tabulated-list-sort-key (cons "Severity" nil)) 1191 (setq tabulated-list-use-header-line t) 1192 (tabulated-list-init-header)) 1193 1194 (pio--bind-common-keys pio-check-mode-map) 1195 1196 (defun pio-check () 1197 "Display `pio check --skip-packages --json-output' in a table." 1198 (interactive) 1199 (let* ((platformio-cli (pio-system-info--resolve-executable)) 1200 (check-buffer (get-buffer-create pio-check-buffer-name))) 1201 (pio-check--render-buffer check-buffer) 1202 (pio--display-buffer-passive check-buffer) 1203 (make-process 1204 :name "pio-check" 1205 :buffer check-buffer 1206 :command (list platformio-cli "check" "--skip-packages" "--json-output") 1207 :filter #'pio-check--append-process-output 1208 :sentinel #'pio-check--finalize-process))) 1209 1210 (defun pio-project-config--value-to-string (value) 1211 "Return VALUE as a readable string for project config tables." 1212 (cond 1213 ((eq value :false) "false") 1214 ((eq value t) "true") 1215 ((null value) "null") 1216 ((stringp value) (if (string-empty-p value) "" value)) 1217 ((listp value) (mapconcat #'pio-project-config--value-to-string value ", ")) 1218 (t (format "%s" value)))) 1219 1220 (defun pio-project-config--tabulated-entry-from-option (section option index) 1221 "Convert SECTION OPTION at INDEX into a `tabulated-list-entries' row." 1222 (let* ((option-key (car option)) 1223 (option-value (cadr option)) 1224 (key (pio-device-list--normalize-field option-key)) 1225 (value (pio-project-config--value-to-string option-value))) 1226 (list (format "%s:%s#%d" section key index) 1227 (vector section key value)))) 1228 1229 (defun pio-project-config--entries-from-json (json-string) 1230 "Parse JSON-STRING and return `tabulated-list-entries'." 1231 (let ((parsed-json (json-parse-string json-string :object-type 'alist :array-type 'list)) 1232 (entries nil) 1233 (index 0)) 1234 (dolist (section-pair parsed-json (nreverse entries)) 1235 (let* ((section-name (pio-device-list--normalize-field (car section-pair))) 1236 (section-options (cadr section-pair))) 1237 (dolist (option section-options) 1238 (setq index (1+ index)) 1239 (push (pio-project-config--tabulated-entry-from-option section-name option index) 1240 entries)))))) 1241 1242 (defun pio-project-config--render-buffer (project-config-buffer) 1243 "Initialize PROJECT-CONFIG-BUFFER for raw config output." 1244 (with-current-buffer project-config-buffer 1245 (pio-project-config-mode) 1246 (setq-local pio-project-config--json-accumulator nil) 1247 (let ((inhibit-read-only t)) 1248 (erase-buffer) 1249 (setq-local ansi-color-context nil)))) 1250 1251 (defun pio-project-config--append-process-output (process output-chunk) 1252 "Append OUTPUT-CHUNK from PROCESS into the buffer as-is." 1253 (pio--append-ansi-process-output process output-chunk)) 1254 1255 (defun pio-project-config--finalize-process (process event) 1256 "Finalize project config output and append EVENT if needed." 1257 (when-let ((project-config-buffer (get-buffer pio-project-config-buffer-name))) 1258 (with-current-buffer project-config-buffer 1259 (setq-local pio-project-config--json-accumulator nil) 1260 (let ((inhibit-read-only t)) 1261 (goto-char (point-max)) 1262 (unless (and (stringp event) 1263 (string-match-p "finished" event)) 1264 (insert "\n" event)))))) 1265 1266 (define-derived-mode pio-project-config-mode special-mode "PIO-Project-Config" 1267 "Major mode for displaying raw `pio project config' output.") 1268 1269 (pio--bind-common-keys pio-project-config-mode-map) 1270 1271 (defun pio-project-config () 1272 "Display raw output of `pio project config'." 1273 (interactive) 1274 (let* ((platformio-cli (pio-system-info--resolve-executable)) 1275 (project-config-buffer (get-buffer-create pio-project-config-buffer-name))) 1276 (pio-project-config--render-buffer project-config-buffer) 1277 (pio--display-buffer-passive project-config-buffer) 1278 (make-process 1279 :name "pio-project-config" 1280 :buffer project-config-buffer 1281 :command (list platformio-cli "project" "config") 1282 :filter #'pio-project-config--append-process-output 1283 :sentinel #'pio-project-config--finalize-process))) 1284 1285 (defun pio-account-show--string-value (value) 1286 "Return VALUE as a user-friendly string." 1287 (cond 1288 ((eq value :false) "false") 1289 ((null value) "-") 1290 ((stringp value) (if (string-empty-p value) "-" value)) 1291 (t (format "%s" value)))) 1292 1293 (defun pio-account-show--format-expire-at (value) 1294 "Format VALUE as a friendly expiration time string." 1295 (if (numberp value) 1296 (format-time-string "%Y-%m-%d %H:%M:%S %Z" (seconds-to-time value)) 1297 "-")) 1298 1299 (defun pio-account-show--service-title (service-value) 1300 "Return a user-facing title string for SERVICE-VALUE." 1301 (if (stringp service-value) 1302 service-value 1303 (pio-account-show--string-value 1304 (pio-device-list--alist-get-any 'title service-value)))) 1305 1306 (defun pio-account-show--service-fields (package) 1307 "Extract human-readable service strings from PACKAGE." 1308 (let (services) 1309 (dolist (pair package (nreverse services)) 1310 (let* ((raw-key (car pair)) 1311 (key (if (symbolp raw-key) (symbol-name raw-key) raw-key)) 1312 (value (cdr pair))) 1313 (when (and (stringp key) 1314 (string-prefix-p "service." key)) 1315 (push (pio-account-show--service-title value) services)))))) 1316 1317 (defun pio-account-show--insert-heading (title) 1318 "Insert TITLE as a section heading." 1319 (insert (propertize title 'face 'font-lock-keyword-face) "\n") 1320 (insert (make-string (length title) ?=) "\n\n")) 1321 1322 (defun pio-account-show--insert-labeled-line (label value) 1323 "Insert LABEL and VALUE as a single aligned line." 1324 (insert (format "%-12s %s\n" (format "%s:" label) 1325 (pio-account-show--string-value value)))) 1326 1327 (defun pio-account-show--insert-profile (profile) 1328 "Insert profile details from PROFILE alist." 1329 (pio-account-show--insert-heading "Profile") 1330 (pio-account-show--insert-labeled-line "Username" 1331 (pio-device-list--alist-get-any 'username profile)) 1332 (pio-account-show--insert-labeled-line "Email" 1333 (pio-device-list--alist-get-any 'email profile)) 1334 (pio-account-show--insert-labeled-line "First name" 1335 (pio-device-list--alist-get-any 'firstname profile)) 1336 (pio-account-show--insert-labeled-line "Last name" 1337 (pio-device-list--alist-get-any 'lastname profile)) 1338 (insert "\n")) 1339 1340 (defun pio-account-show--insert-organizations (organizations) 1341 "Insert Organizations section from ORGANIZATIONS list." 1342 (pio-account-show--insert-heading "Organizations") 1343 (if organizations 1344 (dolist (org organizations) 1345 (let* ((orgname (pio-device-list--normalize-field 1346 (pio-device-list--alist-get-any 'orgname org))) 1347 (displayname (pio-device-list--normalize-field 1348 (pio-device-list--alist-get-any 'displayname org))) 1349 (email (pio-device-list--normalize-field 1350 (pio-device-list--alist-get-any 'email org))) 1351 (owners (pio-org-list--owners-string 1352 (pio-device-list--alist-get-any 'owners org)))) 1353 (insert (propertize orgname 'face 'bold) "\n") 1354 (insert (make-string (length orgname) ?-) "\n") 1355 (pio-account-show--insert-labeled-line "Display" displayname) 1356 (pio-account-show--insert-labeled-line "Email" email) 1357 (pio-account-show--insert-labeled-line "Owners" owners) 1358 (insert "\n"))) 1359 (insert "No organizations\n\n"))) 1360 1361 (defun pio-account-show--insert-teams (teams-by-org) 1362 "Insert Teams section from TEAMS-BY-ORG alist." 1363 (pio-account-show--insert-heading "Teams") 1364 (if teams-by-org 1365 (dolist (org-pair teams-by-org) 1366 (let* ((org-key (car org-pair)) 1367 (org-name (if (symbolp org-key) (symbol-name org-key) org-key)) 1368 (teams (cdr org-pair))) 1369 (insert (propertize (format "Org: %s" org-name) 'face 'bold) "\n") 1370 (insert (make-string (+ 5 (length org-name)) ?-) "\n") 1371 (if teams 1372 (dolist (team teams) 1373 (let ((team-name (pio-device-list--normalize-field 1374 (pio-device-list--alist-get-any 'name team))) 1375 (description (pio-device-list--normalize-field 1376 (pio-device-list--alist-get-any 'description team))) 1377 (members (pio-team-list--members-string 1378 (pio-device-list--alist-get-any 'members team)))) 1379 (insert (format "- %s\n" team-name)) 1380 (pio-account-show--insert-labeled-line "Description" description) 1381 (pio-account-show--insert-labeled-line "Members" members) 1382 (insert "\n"))) 1383 (insert "No teams\n\n")))) 1384 (insert "No teams\n\n"))) 1385 1386 (defun pio-account-show--insert-package (package) 1387 "Insert PACKAGE details in a readable text layout." 1388 (let ((title (pio-account-show--string-value 1389 (pio-device-list--alist-get-any 'title package))) 1390 (description (pio-device-list--alist-get-any 'description package)) 1391 (path (pio-device-list--alist-get-any 'path package)) 1392 (services (pio-account-show--service-fields package))) 1393 (insert (propertize title 'face 'bold) "\n") 1394 (insert (make-string (length title) ?-) "\n") 1395 (insert (pio-account-show--string-value description) "\n") 1396 (pio-account-show--insert-labeled-line "Path" path) 1397 (if services 1398 (progn 1399 (insert "Services:\n") 1400 (dolist (service services) 1401 (insert (format "- %s\n" service)))) 1402 (insert "Services: -\n")) 1403 (insert "\n"))) 1404 1405 (defun pio-account-show--render-data (account-show-buffer account) 1406 "Render ACCOUNT into ACCOUNT-SHOW-BUFFER." 1407 (with-current-buffer account-show-buffer 1408 (let ((inhibit-read-only t) 1409 (profile (pio-device-list--alist-get-any 'profile account)) 1410 (packages (pio-device-list--alist-get-any 'packages account)) 1411 (user-id (pio-device-list--alist-get-any 'user_id account)) 1412 (expire-at (pio-device-list--alist-get-any 'expire_at account))) 1413 (erase-buffer) 1414 (pio-account-show-mode) 1415 (pio-account-show--insert-profile profile) 1416 (pio-account-show--insert-heading "Packages") 1417 (if packages 1418 (dolist (package packages) 1419 (pio-account-show--insert-package package)) 1420 (insert "No packages\n\n")) 1421 (pio-account-show--insert-heading "Meta") 1422 (pio-account-show--insert-labeled-line "User ID" user-id) 1423 (pio-account-show--insert-labeled-line "Expire at" 1424 (pio-account-show--format-expire-at expire-at)) 1425 (goto-char (point-min))))) 1426 1427 (define-derived-mode pio-account-show-mode special-mode "PIO-Account" 1428 "Major mode for displaying `pio account show --json-output' in readable text.") 1429 1430 (pio--bind-common-keys pio-account-show-mode-map) 1431 1432 (defun pio-account-show (&optional force-refresh) 1433 "Display `pio account show --json-output' in a readable text layout." 1434 (interactive "P") 1435 (let ((account-show-buffer (get-buffer-create pio-account-show-buffer-name))) 1436 (condition-case err 1437 (let ((account (pio--run-json-command-cached 1438 'account-show 1439 pio-cache-ttl-account-show 1440 force-refresh 1441 "account" "show" "--json-output"))) 1442 (pio-account-show--render-data account-show-buffer account) 1443 (pio--display-buffer-passive account-show-buffer)) 1444 (error 1445 (with-current-buffer account-show-buffer 1446 (pio-account-show-mode) 1447 (let ((inhibit-read-only t)) 1448 (erase-buffer) 1449 (insert (format "Failed to load account data: %s\n" (error-message-string err))))) 1450 (pio--display-buffer-passive account-show-buffer))))) 1451 1452 1453 (defun pio-mode (&optional _force-refresh) 1454 "Open the PlatformIO command dispatcher." 1455 (interactive "P") 1456 (pio-dispatch)) 1457 1458 (provide 'pio-mode) 1459 ;;; pio-mode.el ends here