flake-update-20260505
  1;;; project-func.el --- -*- lexical-binding: t -*-
  2;;; Commentary:
  3;;; Code:
  4(require 'project)
  5(require 'json)
  6(require 'vc)
  7
  8;; Declare vterm functions and variables for byte-compiler
  9(declare-function vterm "vterm")
 10(declare-function vterm-mode "vterm")
 11(declare-function vterm-send-string "vterm")
 12(declare-function vterm-send-return "vterm")
 13(defvar vterm-kill-buffer-on-exit)
 14(defvar vterm-shell)
 15
 16;; Declare eat functions for byte-compiler
 17(declare-function eat "eat")
 18
 19;; Declare magit functions for byte-compiler
 20(declare-function magit-status "magit")
 21
 22;; Project local identifier variable
 23(defvar vde/project-local-identifier nil
 24  "Identifier for local projects (file name or list of file names).")
 25
 26(defun in-git-repo-p ()
 27  "Check if current directory is in a git repository."
 28  (eq (vc-backend (or buffer-file-name default-directory))
 29      'Git))
 30
 31(defun is-github-repo-p ()
 32  "Check if current git repository has a GitHub remote."
 33  (when (in-git-repo-p)
 34    (string-match-p "github\\.com"
 35                    (shell-command-to-string "git remote -v"))))
 36
 37(defun fetch-github-prs ()
 38  "Fetch GitHub PRs synchronously."
 39  (let* ((output (shell-command-to-string "gh pr list --limit=5000 --json number,title,author,url,baseRefName,labels,isDraft"))
 40         (prs (json-read-from-string output)))
 41    prs))
 42
 43(defun format-pr-draft (isDraft)
 44  "Return (draft) if `pr' is a draft, otherwise returns an empty string"
 45  (cond ((eq isDraft :json-false) "")
 46	(t "🚧 draft")))
 47
 48(defun format-pr-candidates (prs)
 49  "Format PR data into candidates for completion."
 50  (mapcar (lambda (pr)
 51            (let-alist pr
 52              (cons (format "#%d %s (by @%s) on %s %s" .number .title .author.login .baseRefName (format-pr-draft .isDraft))
 53                    .number)))
 54          prs))
 55
 56
 57
 58;;;###autoload
 59(defun checkout-github-pr ()
 60  "Interactive function to select and checkout a GitHub PR."
 61  (interactive)
 62  (cond
 63   ((not (in-git-repo-p))
 64    (message "Not in a Git repository"))
 65   ((not (is-github-repo-p))
 66    (message "Not a GitHub repository"))
 67   (t
 68    (let* ((prs (fetch-github-prs))
 69           (candidates (format-pr-candidates prs))
 70           (selected (if candidates
 71                         (cdr (assoc (completing-read "Checkout PR: " candidates)
 72                                     candidates))
 73                       nil)))
 74      (if selected
 75          (shell-command (format "gh pr checkout %d" selected))
 76        (message "No pull requests found"))))))
 77
 78;;;###autoload
 79(defun vde-project--project-current ()
 80  "Return directory from `project-current' based on Emacs version."
 81  (if (>= emacs-major-version 29)
 82      (project-root (project-current))
 83    (cdr (project-current))))
 84
 85;;;###autoload
 86(defun vde-project--project-root-or-default-directory ()
 87  "Return path to the project root *or* the default-directory."
 88  (cond
 89   ((and (featurep 'project) (project-current))
 90    (project-root (project-current)))
 91   (t default-directory)))
 92
 93;;;##autoload
 94(defun vde/project-run-in-vterm (command &optional directory)
 95  "Run the given `COMMAND' in a new vterm buffer in `project-root' or the
 96given `DIRECTORY'.
 97
 98This is similar to `compile' but with vterm.
 99One reason for this is to be able to run commands that needs a TTY."
100  (interactive "sCommand: ")
101  (let* ((cwd (or directory (vde-project--project-root-or-default-directory)))
102	 (default-directory cwd)
103	 (buffer-name (format "*vterm %s: %s*" cwd command))
104         (buffer (get-buffer buffer-name))
105         (vterm-kill-buffer-on-exit nil)
106	 (vterm-shell (concat "bash -c '" command ";exit'")))
107    (when buffer
108      (kill-buffer buffer))
109    (let ((buffer (generate-new-buffer buffer-name)))
110      (pop-to-buffer buffer)
111      (with-current-buffer buffer
112        (vterm-mode)))))
113
114;;;###autoload
115(defun vde/open-readme ()
116  "Open a README file in the current project.
117It will search for README.org, README.md or README in that order"
118  (interactive)
119  (let* ((default-directory (vde-project--project-current)))
120    (cond ((file-exists-p (expand-file-name "README.org" default-directory))
121	   (find-file "README.org"))
122	  ((file-exists-p (expand-file-name "README.md" default-directory))
123	   (find-file "README.md"))
124	  ((file-exists-p (expand-file-name "README" default-directory))
125	   (find-file "README")))))
126
127;;;###autoload
128(defun vde/project-try-local (dir)
129  "Determine if DIR is a non-VC project."
130  (if-let ((root (if (listp vde/project-local-identifier)
131                     (seq-some (lambda (n)
132                                 (locate-dominating-file dir n))
133                               vde/project-local-identifier)
134                   (locate-dominating-file dir vde/project-local-identifier))))
135      (cons 'local root)))
136
137;;;###autoload
138(defun vde/project-vterm (&optional command)
139  "Run `vterm' on project.
140If a buffer already exists for running a vterm shell in the project's root,
141switch to it. Otherwise, create a new vterm shell."
142  (interactive)
143  (let* ((default-directory (vde-project--project-current))
144         (default-project-vterm-name (or (and (project-current)
145                                              (project-prefixed-buffer-name "vterm"))
146                                         (format "*vterm-%s*" default-directory)))
147         (vterm-buffer (get-buffer default-project-vterm-name)))
148    (if (and vterm-buffer (not current-prefix-arg))
149        (pop-to-buffer-same-window vterm-buffer)
150      (let* ((cd-cmd (concat " cd " (shell-quote-argument default-directory))))
151        (vterm default-project-vterm-name)
152        (with-current-buffer vterm-buffer
153          (vterm-send-string cd-cmd)
154          (vterm-send-return))))
155    (when command
156      (vterm-send-string command)
157      (vterm-send-return))))
158
159;;;###autoload
160(defun vde/project-eat ()
161  "Run Eat term in the current project's root directory.
162If a buffer already exists for running Eshell in the project's root,
163switch to it.  Otherwise, create a new Eshell buffer.
164With \\[universal-argument] prefix arg, create a new Eshell buffer even
165if one already exists."
166  (interactive)
167  (defvar eat-buffer-name)
168  (let* ((default-directory (project-root (project-current t)))
169	 (eat-buffer-name (project-prefixed-buffer-name "eat"))
170	 (eat-buffer (get-buffer eat-buffer-name))
171	 (shell (if (file-remote-p default-directory)
172		    ;; For TRAMP, find zsh on remote host or fall back to /bin/zsh
173		    (or (executable-find "zsh" t)
174			"/bin/zsh")
175		  ;; For local, use shell-file-name
176		  shell-file-name)))
177    (if (and eat-buffer (not current-prefix-arg))
178	(pop-to-buffer eat-buffer (bound-and-true-p display-comint-buffer-action))
179      (eat shell))))
180
181;;;###autoload
182(defun vde/project-magit-status ()
183  "Run `magit-status' on project."
184  (interactive)
185  (magit-status (vde-project--project-current)))
186
187(provide 'project-func)
188;;; project-func.el ends here