(require 'seq) (require 'subr-x) ;;makes sure elfeed reads index from disk before launching (defvar fscotto/elfeed-initial-update-done nil "Non-nil once Elfeed has triggered its first automatic update this session.") (defvar fscotto/elfeed-update-timer nil "Timer used for periodic Elfeed updates.") (defun fscotto/elfeed-auto-update () "Refresh Elfeed feeds in the background." (when (require 'elfeed nil 'noerror) (require 'elfeed-db) (elfeed-db-load) (elfeed-update))) (defun fscotto/elfeed-start-auto-update () "Start the periodic Elfeed update timer if needed." (unless (timerp fscotto/elfeed-update-timer) (setq fscotto/elfeed-update-timer (run-at-time "15 min" (* 15 60) #'fscotto/elfeed-auto-update)))) (defun fscotto/elfeed-load-db-and-open () "Open Elfeed, updating once per session and then every 15 minutes. Based on https://pragmaticemacs.wordpress.com/2016/08/17/read-your-rss-feeds-in-emacs-with-elfeed/ Created: 2016-08-17 Updated: 2025-06-13" (interactive) (require 'elfeed) (require 'elfeed-db) (elfeed-db-load) (elfeed) (fscotto/elfeed-start-auto-update) (unless fscotto/elfeed-initial-update-done (setq fscotto/elfeed-initial-update-done t) (message "Updating Elfeed feeds...") (elfeed-update))) (defun fscotto/project-root () "Return projectile project root or fallback to default-directory." (if (featurep 'projectile) (or (projectile-project-root) default-directory) default-directory)) (defun fscotto/project-vterm () "Open vterm in project root." (interactive) (let ((default-directory (fscotto/project-root))) (vterm))) (defun fscotto/open-multi-vterm-in (directory) "Open a new multi-vterm buffer in DIRECTORY." (let ((default-directory (file-name-as-directory directory))) (multi-vterm))) (defun fscotto/home-multi-vterm () "Open a new multi-vterm buffer in HOME." (interactive) (fscotto/open-multi-vterm-in (expand-file-name "~/"))) (defun fscotto/project-multi-vterm () "Open a new multi-vterm buffer in project root." (interactive) (fscotto/open-multi-vterm-in (fscotto/project-root))) (defconst fscotto/opencode-db-path (expand-file-name "~/.local/share/opencode/opencode.db") "Path to the OpenCode SQLite database.") (defun fscotto/launch-external-terminal (&optional command directory) "Launch external terminal in DIRECTORY, optionally running COMMAND." (let* ((default-directory (file-name-as-directory (or directory (fscotto/project-root)))) (terminal-program (or (executable-find fscotto/external-terminal-program) fscotto/external-terminal-program)) (args (append (list fscotto/external-terminal-working-directory-option default-directory) (when command (append (list fscotto/external-terminal-execute-option) command))))) (unless (file-executable-p terminal-program) (user-error "External terminal not found: %s" fscotto/external-terminal-program)) (apply #'start-process "fscotto-external-terminal" nil terminal-program args))) (defun fscotto/opencode-session-candidates (directory) "Return OpenCode session candidates for DIRECTORY. Each entry is a cons cell of display string and session id." (let ((sqlite3-program (executable-find "sqlite3")) (session-directory (directory-file-name (expand-file-name directory)))) (unless sqlite3-program (user-error "sqlite3 not found")) (unless (file-exists-p fscotto/opencode-db-path) (user-error "OpenCode database not found: %s" fscotto/opencode-db-path)) (mapcar (lambda (line) (pcase-let* ((fields (split-string line "\t")) (`(,session-id ,title ,updated-at) fields) (title (if (and title (not (string= title ""))) title "Untitled session")) (updated-seconds (/ (string-to-number updated-at) 1000)) (updated-label (format-time-string "%Y-%m-%d %H:%M" (seconds-to-time updated-seconds))) (short-id (if (> (length session-id) 12) (substring session-id 0 12) session-id))) (cons (format "%s | %s | %s" title updated-label short-id) session-id))) (process-lines sqlite3-program "-tabs" "-noheader" fscotto/opencode-db-path (format (concat "select id, title, time_updated " "from session " "where directory = '%s' and time_archived is null " "order by time_updated desc;") (replace-regexp-in-string "'" "''" session-directory)))))) (defun fscotto/project-opencode-latest-session-id () "Return the latest saved OpenCode session id for the current project." (cdr (car (fscotto/opencode-session-candidates (fscotto/project-root))))) (defun fscotto/project-agent-dwim () "Choose an agent for the current project and launch it externally." (interactive) (let ((agent (completing-read "Agent: " '("Claude" "Codex" "Gemini" "OpenCode") nil t))) (pcase agent ("Claude" (fscotto/launch-external-terminal '("claude" "--continue"))) ("OpenCode" (let ((session-id (fscotto/project-opencode-latest-session-id))) (if session-id (fscotto/launch-external-terminal (list "opencode" "--session" session-id) (fscotto/project-root)) (fscotto/project-opencode)))) ("Codex" (fscotto/launch-external-terminal '("codex" "resume" "--last"))) ("Gemini" (fscotto/launch-external-terminal '("gemini" "--resume" "latest") (fscotto/project-root)))))) (defun fscotto/project-opencode-session () "Resume a saved OpenCode session for the current project." (interactive) (let* ((project-directory (fscotto/project-root)) (candidates (fscotto/opencode-session-candidates project-directory))) (unless candidates (user-error "No OpenCode sessions found for %s" project-directory)) (let* ((selection (completing-read "OpenCode session: " candidates nil t)) (session-id (cdr (assoc selection candidates)))) (fscotto/launch-external-terminal (list "opencode" "--session" session-id) project-directory)))) (defun fscotto/gemini-session-candidates (directory) "Return Gemini session candidates for DIRECTORY. Each entry is a cons cell of display string and session index. Tries JSON output first, falls back to text parsing if unavailable." (let* ((default-directory (file-name-as-directory directory)) (json-output (shell-command-to-string "gemini --list-sessions --output-format json 2>/dev/null"))) (cond ((string-match "^{" json-output) (ignore-errors (require 'json) (let* ((parsed (json-parse-string json-output)) (sessions (gethash "sessions" parsed))) (when (vectorp sessions) (seq-map-indexed (lambda (s idx) (let* ((idx-str (number-to-string (1+ idx))) (msg (if (hash-table-p s) (or (gethash "firstUserMessage" s) "Session") "Session")) (ts (and (hash-table-p s) (ignore-errors (gethash "lastUpdated" s)) (when (stringp it) (string-trim it)))) (label (if ts (format "%s [%s]" msg ts) msg))) (cons label idx-str))) sessions))))) (t (let* ((output (shell-command-to-string "gemini --list-sessions")) (lines (seq-filter (lambda (s) (string-match "\\S-" s)) (split-string output "\n" t))) (data-lines (seq-drop lines 1)) (candidates nil)) (dolist (line data-lines) (let ((trimmed (string-trim line))) (when (string-match (rx (group (one-or-more digit)) (one-or-more whitespace) (group (one-or-more nonl))) trimmed) (push (cons (match-string 2 trimmed) (match-string 1 trimmed)) candidates)))) (nreverse candidates)))))) (defun fscotto/project-gemini-session () "Choose and resume a Gemini session for the current project." (interactive) (let* ((project-directory (fscotto/project-root)) (candidates (fscotto/gemini-session-candidates project-directory))) (unless candidates (user-error "No Gemini sessions found for %s" project-directory)) (let* ((selection (completing-read "Gemini session: " candidates nil t)) (session-idx (cdr (assoc selection candidates)))) (fscotto/launch-external-terminal (list "gemini" "--resume" session-idx) project-directory)))) (defun fscotto/project-agent-session () "Choose an agent and resume a saved session for the current project." (interactive) (let ((agent (completing-read "Agent session: " '("Claude" "Codex" "Gemini" "OpenCode") nil t))) (pcase agent ("Claude" (fscotto/launch-external-terminal '("claude" "--resume"))) ("OpenCode" (fscotto/project-opencode-session)) ("Codex" (fscotto/launch-external-terminal '("codex" "resume"))) ("Gemini" (fscotto/project-gemini-session))))) (defun fscotto/project-external-terminal () "Open the external terminal in project root." (interactive) (fscotto/launch-external-terminal)) (defun fscotto/project-opencode () "Open the external terminal in project root and run opencode." (interactive) (fscotto/launch-external-terminal '("opencode"))) (defun fscotto/project-magit-status () "Open magit-status in project root." (interactive) (let ((default-directory (fscotto/project-root))) (magit-status))) (defun fscotto/magit-dispatch () "Load Magit if necessary and open magit-dispatch." (interactive) (require 'magit) (call-interactively #'magit-dispatch)) (defun fscotto/disable-c-formatting () (setq-local lsp-enable-on-type-formatting nil)) ;; Golang development support functions (defun fscotto/go-format-on-save () "Format Go buffers on save using gofmt." (add-hook 'before-save-hook #'lsp-format-buffer nil t)) (defun fscotto/go-mod-tidy () "Esegue go mod tidy nella root del progetto." (interactive) (let ((default-directory (project-root (project-current t)))) (compile "go mod tidy"))) (defun fscotto/go-mod-download () "Scarica i moduli Go." (interactive) (let ((default-directory (project-root (project-current t)))) (compile "go mod download"))) (defun fscotto/go-mod-after-save () (when (and (eq major-mode 'go-mod-ts-mode) (lsp-workspaces)) (lsp-restart-workspace))) (defun fscotto/go-test-package () "Run `go test` in the current package." (interactive) (let ((default-directory (project-root (project-current t)))) (compile "go test"))) (defun fscotto/go-test-module () "Run `go test ./...` in the current Go module." (interactive) (let ((default-directory (project-root (project-current t)))) (compile "go test ./..."))) (defun fscotto/go-test-current-test () "Run `go test -run` for the test at point." (interactive) (let* ((test-name (thing-at-point 'symbol t)) (default-directory (project-root (project-current t)))) (unless test-name (user-error "No test name at point")) (compile (format "go test -run '^%s$'" test-name)))) (defun fscotto/go-test-at-point () "Return Go test name at point." (let ((sym (thing-at-point 'symbol t))) (unless (and sym (string-prefix-p "Test" sym)) (user-error "No Go test at point")) sym)) (defun fscotto/sudo-edit (&optional arg) "Reopen current file as root via TRAMP. If ARG is provided (C-u), prompts for file path." (interactive "P") (if arg (find-file (concat "/sudo:root@localhost:" (read-file-name "File: "))) (if buffer-file-name (find-alternate-file (concat "/sudo:root@localhost:" buffer-file-name)) (user-error "This buffer is not associated with a file"))))