From a88e8fe7d6b2658013fd9c448691a8d3b76a9a9d Mon Sep 17 00:00:00 2001 From: Jiri Jakes Date: Sun, 15 Mar 2026 11:53:01 -0300 Subject: [PATCH] ppq: Display Opencode releases --- ppq-status.el | 169 +++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 148 insertions(+), 21 deletions(-) diff --git a/ppq-status.el b/ppq-status.el index afbaee5..13b72b7 100644 --- a/ppq-status.el +++ b/ppq-status.el @@ -77,25 +77,144 @@ (when secret (funcall (plist-get (car secret) :secret))))) -;; --- Table 1: Fruits --- +;; --- Table 1: Releases --- -(defvar ppq-fruits-mode-map +(defvar ppq--releases-data nil + "Cached releases data from GitHub.") + +(defvar ppq--releases-projects + '(("Opencode" . "anomalyco/opencode")) + "List of projects to fetch releases for. +Each element is a cons cell (DISPLAY-NAME . REPO-STRING).") + +(defun ppq--releases-get-buffer-name () + "Get the releases buffer name." + "*Dashboard:Releases*") + +(defvar ppq--releases-active-processes nil + "List of active release fetch processes.") + +(defun ppq--releases-process-sentinel (process _event) + "Process sentinel for gh release list. +PROCESS is the process object, _EVENT is the status change string." + (setq ppq--releases-active-processes + (delq process ppq--releases-active-processes)) + (when (memq (process-status process) '(exit signal)) + (let ((stdout-buf (process-buffer process)) + (stderr-buf (process-get process 'ppq-stderr-buffer)) + (exit-code (process-exit-status process)) + (project-name (process-get process 'ppq-project-name))) + (if (zerop exit-code) + (let ((output (with-current-buffer stdout-buf + (buffer-string)))) + (condition-case nil + (let* ((json (json-parse-string (string-trim output) :object-type 'alist)) + (releases (if (vectorp json) (append json nil) json))) + (push (cons project-name releases) ppq--releases-data) + (ppq--releases-refresh-buffer)) + (error nil))) + (let ((stderr-output (with-current-buffer stderr-buf + (buffer-string)))) + (message "ppq releases: failed for %s: %s" + project-name (string-trim stderr-output)))) + (when (buffer-live-p stdout-buf) + (kill-buffer stdout-buf)) + (when (buffer-live-p stderr-buf) + (kill-buffer stderr-buf))))) + +(defun ppq--releases-fetch-async (project-name repo) + "Fetch releases for REPO asynchronously using gh CLI. +PROJECT-NAME is the display name for the project." + (let* ((stdout-buf (generate-new-buffer " *gh-releases-stdout*")) + (stderr-buf (generate-new-buffer " *gh-releases-stderr*")) + (process-environment (append '("PAGER=cat" "GH_PAGER=cat" "GIT_PAGER=cat" "NO_COLOR=1") + process-environment)) + (process (make-process + :name "gh-releases" + :buffer stdout-buf + :command (list "gh" "api" + (format "repos/%s/releases?per_page=5" repo)) + :stderr stderr-buf + :sentinel #'ppq--releases-process-sentinel + :noquery t))) + (process-put process 'ppq-project-name project-name) + (process-put process 'ppq-stderr-buffer stderr-buf) + (push process ppq--releases-active-processes))) + +(defun ppq--releases-fetch-all-async () + "Fetch releases for all configured projects." + (setq ppq--releases-data nil) + (dolist (project ppq--releases-projects) + (ppq--releases-fetch-async (car project) (cdr project)))) + +(defun ppq--releases-format-date (iso-date) + "Format ISO-DATE string to human-readable date." + (when iso-date + (condition-case nil + (format-time-string + "%Y-%m-%d" + (encode-time (parse-time-string iso-date))) + (error iso-date)))) + +(defun ppq--releases-insert-content () + "Insert releases content into current buffer." + (let ((inhibit-read-only t)) + (erase-buffer) + (insert "* Software Releases\n\n") + (if ppq--releases-data + (dolist (project-data ppq--releases-data) + (let ((project-name (car project-data)) + (releases (cdr project-data))) + (insert (format "** %s\n" project-name)) + (if releases + (dolist (release releases) + (let* ((tag (cdr (assq 'tag_name release))) + (name (cdr (assq 'name release))) + (published (cdr (assq 'published_at release))) + (body (cdr (assq 'body release))) + (display-name (if (and name (not (string= name ""))) + name + tag))) + (insert (format "*** %s (%s)\n" + display-name + (ppq--releases-format-date published))) + ;; Insert changelog body if present + (when (and body (not (eq body :null)) (not (string= body ""))) + (insert "\n#+begin_src markdown\n") + (insert body) + (insert "\n#+end_src\n")))) + (insert "- No releases found\n")) + (insert "\n"))) + (insert "Loading releases...\n")) + (goto-char (point-min)) + ;; Fold to show release versions (level 3) but hide changelog content + (org-content 3))) + +(defun ppq--releases-refresh-buffer () + "Refresh the releases buffer if it exists." + (when-let ((buf (get-buffer (ppq--releases-get-buffer-name)))) + (with-current-buffer buf + (ppq--releases-insert-content)))) + +(defun ppq-releases-mode () + "Major mode for displaying GitHub releases." + (interactive) + (kill-all-local-variables) + (org-mode) + (org-indent-mode 1) + (setq major-mode 'ppq-releases-mode + mode-name "Releases") + (use-local-map (let ((map (copy-keymap org-mode-map))) + (define-key map "q" #'ppq-quit) + map)) + (setq buffer-read-only t) + (ppq--releases-insert-content)) + +(defvar ppq-releases-mode-map (let ((map (make-sparse-keymap))) - (set-keymap-parent map tabulated-list-mode-map) (define-key map "q" #'ppq-quit) map)) -(define-derived-mode ppq-fruits-mode tabulated-list-mode "Fruits" - "A table of fruits." - (setq tabulated-list-format [("Fruit" 15 t) ("Color" 10 t) ("Taste" 10 t)]) - (setq tabulated-list-entries - '((1 ["Apple" "Red" "Sweet"]) - (2 ["Banana" "Yellow" "Sweet"]) - (3 ["Lemon" "Yellow" "Sour"]) - (4 ["Grape" "Purple" "Sweet"]))) - (tabulated-list-init-header) - (tabulated-list-print)) - ;; --- Table 2: Models --- (defvar ppq--models-data nil @@ -301,11 +420,18 @@ (defun ppq-quit () "Quit the dashboard, restore previous window configuration, and kill buffers." (interactive) - (dolist (buf (list (get-buffer "*Dashboard:Fruits*") + ;; Kill any active release fetch processes + (dolist (proc ppq--releases-active-processes) + (when (process-live-p proc) + (delete-process proc))) + (setq ppq--releases-active-processes nil) + ;; Kill buffers + (dolist (buf (list (get-buffer "*Dashboard:Releases*") (get-buffer "*Dashboard:Models*") (get-buffer "*Dashboard:Info*"))) (when buf (kill-buffer buf))) (setq ppq--models-data nil) + (setq ppq--releases-data nil) (when ppq--window-config (set-window-configuration ppq--window-config) (setq ppq--window-config nil))) @@ -318,12 +444,12 @@ (interactive) (setq ppq--window-config (current-window-configuration)) - (let ((fruits-buf (get-buffer-create "*Dashboard:Fruits*")) + (let ((releases-buf (get-buffer-create "*Dashboard:Releases*")) (models-buf (get-buffer-create "*Dashboard:Models*")) (org-buf (get-buffer-create "*Dashboard:Info*"))) - (with-current-buffer fruits-buf - (ppq-fruits-mode)) + (with-current-buffer releases-buf + (ppq-releases-mode)) (with-current-buffer models-buf (ppq-models-mode)) @@ -337,7 +463,7 @@ (insert "Welcome to the *PayPerQ* dashboard!\n\n") (insert "** Notes\n") (insert "- Press ~q~ in any table to quit\n") - (insert "- Table 1 :: Fruits of the world\n") + (insert "- Table 1 :: Recent Releases (loading...)\n") (insert "- Table 2 :: PayPerQ Models (loading...)\n\n") (insert "** Balance\n\n") (insert "** Topup (BTC-LN)\n") @@ -352,12 +478,13 @@ (let* ((top-left (selected-window)) (top-right (split-window-right)) (bottom (split-window-below (/ (window-height) 2)))) - (set-window-buffer top-left fruits-buf) + (set-window-buffer top-left releases-buf) (set-window-buffer top-right models-buf) (set-window-buffer bottom org-buf)) - (select-window (get-buffer-window fruits-buf))) + (select-window (get-buffer-window releases-buf))) + (ppq--releases-fetch-all-async) (ppq--fetch-models-async) (ppq--fetch-balance-async))