emacs-humanities
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

Re: [emacs-humanities] Emacs "Projects" management?


From: Göktuğ Kayaalp
Subject: Re: [emacs-humanities] Emacs "Projects" management?
Date: Wed, 06 Oct 2021 03:53:29 +0300

> The very word "Project" is fuzzy, imprecise in usage: I have seen it used
> in the sense of a writing project, or without boundaries; yet in the
> tutorials and guides for, for example, "projectile" project manager
> package, it seems to focus on programming projects.
>
> I would like to ask about how others, in the "emacs-humanities" community,
> use such project managers for various kinds of projects [... snip ...]

I have my personal solution for this, and it’s pretty simple.

For definitions, ‘project’ for me is a version control repository, which
in my case these days is a Git repository or in some cases an RCS
repository [1].

In Emacs a ‘project’ for me is a frame that has a few parameters:
project name, project directory’s path, the name for a shell-mode buffer
to be associated with the project, and a symbol identifying the version
control system type.

My "dashboard" view of a project is a dired buffer at project root on
the left window, and a magit (or vc.el, if not git) buffer on the right
window. I have a basic pop-up shell binding to summon the project’s
shell buffer.

The relevant code is found at:
https://github.com/cadadr/configuration/blob/fc79fbc39490f77c4bb8c262434abef7d2330417/emacs.d/init.el#L1346-L1525

There, I bind C-c C-p to gk-open-project, which opens the project in a
new frame (or w/ C-u, appropriates the current frame), and Home is bound
to gk-home, which, if the frame is a project frame, returns to that
dashboard view, and if not, displays initial-buffer-choice if non-nil,
or *scratch*. C-c ] is bound to gk-pop-shell which toggles a shell in a
bottom side-window.

This is what a project basically is for me. Whether it’s my master’s
thesis (which I’ll hopefully start writing one day), or is it ~/Notes
which contains all my notes and my bibliographies, or is it my dotfiles,
websites, or the few programming projects.

The way I work with this is, I don’t always open projects. E.g. if it’s
tweaking a dotfile, or adding to one of my org-mode notebooks, likely
I’ll just use plain old C-x C-f. I know where those files are at.  I’ll
open a project to (1) do version control actions, (2) do stuff that
involves many files, and (3) work on a project other than my ~/Notes or
my dotfiles.  I’ll often jump to files from magit or dired, do my edits,
use xref (and eglot) to jump around if necessary, and if I feel like
lost, I’ll hit Home to, well, go back to the home view.

As for how helpful this is, I can say I really enjoy it. It’s basically
formalising how I like to deal with projects. I tried project.el and
looked into projectile, and they’re way bigger and compels for my
tastes.  I’m not really a software minimalist or anything, but I’m
losing tolerance for stuff that confuses me as of late. I’ve moved from
pretty heavily customised desktops to Linux Mint, from Qutebrowser to
Ungoogled Chromium, and similar, because frankly the heyday of those
setups feel like they’re over, and you have to fight against Linux
desktop trends, which honestly I don’t have the energy to do.

If you want to incorporate this into your setup, feel free.  It should
have a couple dependencies on the rest of my config, but not too many.
I extracted a version that seems to work on "emacs -Q", and appended it
to the end of this message. You’ll probably need a somewhat recent
emacs, and you’ll need f.el [2].  Optionally, it’ll use magit when
available, for git repositories.


Hope it helps!

Cheers,

        -gk.




[1] RCS is old and deprecated but if you’re working with single local
files that are not tied to each other and that won’t be shared as source
code, it’s still useful.

[2] https://github.com/rejeep/f.el




;; My project management setup.

(require 'cl-lib)
(require 'vc)
(require 'f)

;; Helper functions.

(defun gk-directory-files (directory &optional include-dotfiles relative-names)
  "Saner version of ‘directory-files’.
Exclude dot-files, don't sort, and return full paths by default."
  (directory-files
   directory
   (not include-dotfiles)
   directory-files-no-dot-files-regexp
   t))

(defmacro gk-with-new-frame (parameters &rest body)
  "Create a new frame and run BODY in it.
PARAMETERS is passed to ‘make-frame’.
The new frame is bound to the lexically scoped variable
‘new-frame’ inside BODY.
The newly created frame is centred and the mouse pointer is put
at the centre of the newly created frame.  This only happens when
‘display-graphic-p’ is truthy."
  (declare (indent defun))
  (let ((frame (gensym)))
    `(let ((,frame (make-frame ,parameters)))
       (raise-frame ,frame)
       (select-frame-set-input-focus ,frame)
       (select-window (frame-first-window ,frame))
       (when (display-graphic-p)
         ;; Center frame
         (set-frame-position
          ,frame
          (/ (- (x-display-pixel-width) (window-pixel-width)) 2)
          ;; XXX(2020-09-15): for some reason this works better than
          ;; dividing by 2 on my Linux Mint 20 with Cinnamon.
          (floor (/ (- (x-display-pixel-height) (window-pixel-height)) 2.5)))
         ;; Move mouse into the new frame
         (set-mouse-absolute-pixel-position
          (/ (x-display-pixel-width) 2)
          (/ (x-display-pixel-height) 2)))
       (let ((new-frame ,frame)) ,@body))))

(defun assoca (keyseq list)
  "Arbitrary depth multi-level alist query.
KEYSEQ is the list of keys to look up in the LIST.  The first key
from KEYSEQ is looked up in the LIST, then the next key from
KEYSEQ is looked up in the CDR of the return value of that
operation, and so on until all the KEYSEQ is exhausted.  The
resultant value is returned, or nil, in case one or more keys are
not found in the LIST.
If KEYSEQ is a symbol, then it's treated as if it were a
singleton list."
  (let ((ks (if (listp keyseq) keyseq (list keyseq)))
        (ret list))
    (dolist (k ks ret)
      (setq ret (cdr (assoc k ret))))))

(cl-defun gk-flash-current-line (&optional buffer &key (seconds 0.5))
  "Flash current line briefly for SECONDS in BUFFER.
BUFFER defaults to current buffer, and SECONDS to 1."
  (interactive)
  (unless hl-line-mode
    (let ((buf (or buffer (current-buffer))))
      (hl-line-mode +1)
      (run-with-idle-timer
       seconds nil
       (lambda ()
         (with-current-buffer buf
            (hl-line-mode -1)))))))

;; Functionality for opening and working with projects.

(defvar gk-projects-directory (expand-file-name "~/co")
  "Directory where software projects are located.")

(defvar gk-projects-use-eshell nil
  "Whether to use ‘eshell’ for project shells.
If nil, use ‘shell’ instead.")

(defvar gk-project-compile--hist nil)

(defvar gk-project-compile-default-command "make test"
  "Default command for ‘gk-project-compile’.")

(defun gk-project-compile (command)
  (interactive
   (list
    (read-shell-command
     "Run project compile command: "
     gk-project-compile-default-command
     gk-project-compile--hist)))
  (if-let* ((projbuf (get-buffer (assoca 'gk-project (frame-parameters)))))
      (with-current-buffer projbuf
        (compile command))
    (user-error "Not a project frame")))

(defun gk-create-project (name vcs parent-tree)
  "Create a new project.
NAME is the project name, and the project path is located in the
directory at PARENT-TREE + NAME.  PARENT-TREE defaults to
‘gk-projects-directory’.
If VCS is non-nil (and the name of a version control system
included in ‘vc-handled-backends’), a new repository with the
selected VCS is initialised under the new project directory.
The value of NAME is used directly in the project directory name,
so make sure it does not include unnecessary slashes or
problematic characters."
  (interactive (list (read-string "Project name (will be project path 
basename): ")
                     (vc-read-backend "VCS, empty for none: ")
                     (read-directory-name "Parent directory for project 
subtree: "
                                          (concat gk-projects-directory "/"))))
  (let ((project-tree (expand-file-name name parent-tree)))
    (condition-case e
        (make-directory project-tree)
      ('file-already-exists (message (apply #'format "%s: %s" (cdr e)))))
    (when vcs
     (let ((default-directory project-tree))
       (vc-create-repo vcs)))
    (gk-open-project project-tree)))

(defun gk-open-project (path &optional use-this-frame)
  "Open a project folder.
Dired buffer to the left, magit (or VC if not git) to the
right. Start a shell with name ‘*XXX shell*’ where XXX is the
basename of the PATH.
PATH is the path to the project.
If USE-THIS-FRAME is non-nil, or called interactively with a
non-zero prefix argument, use the current frame, instead of
creating a new one."
  (interactive
   (list
    (f-slash
     (read-directory-name
      (if current-prefix-arg
          "Project to open (*in _current_ frame*): "
        "Project to open (in new frame): ")
      (f-slash (expand-file-name "~"))
      nil t))
    (not (not current-prefix-arg))))
  (let* ((vcs
          (cond
           ((and (fboundp 'magit-status)
                 (file-exists-p (expand-file-name ".git" path)))
            #'magit-status)
           ((or (mapcar #'vc-backend (gk-directory-files path)))
            #'vc-dir)))
         ;; This should be fairly duplicate-proof...
         (project-name (concat
                        (user-login-name)
                        "@"
                        (system-name)
                        ":"
                        ;; remove trailing slash(es)
                        (replace-regexp-in-string "/+\\'" "" path)))
         (shell-name (format "*%s shell*" project-name))
         (frame-params `((fullscreen . maximized)
                         (gk-project . ,project-name)
                         (gk-project-dir . ,path)
                         (gk-project-shell . ,shell-name)
                         (gk-project-vcs . ,vcs))))
    (cond (use-this-frame
           (pcase-dolist (`(,param . ,val) frame-params)
             (set-frame-parameter nil param val))
           (gk--open-project-1 vcs path shell-name))
          (t
           (gk-with-new-frame frame-params
             (gk--open-project-1 vcs path shell-name))))))


(defun gk--open-project-1 (vcs path shell-name)
  "Subroutine of ‘gk-open-project’."
  (delete-other-windows)
  (dired path)
  (split-window-sensibly)
  (other-window 1)
  (funcall vcs path))

(defun gk-frame-parameters ()
  "Get my frame parameters."
  (cl-remove-if-not
   (lambda (pair) (s-starts-with? "gk-" (symbol-name (car pair))))
   (frame-parameters)))

;; Popup shell:
(defun gk--get-shell-for-frame (&optional arg-for-shell frame)
  "Get a shell for current frame, depending on whether it’s a project frame.
Subroutine for ‘gk-pop-shell’ and ‘gk-display-shell’."
  (save-window-excursion
    (let* ((prefix-arg arg-for-shell)
           (project-shell (frame-parameter frame 'gk-project-shell))
           (eshell-buffer-name (or project-shell
                                   eshell-buffer-name))
           (default-directory (or (frame-parameter frame 'gk-project-dir)
                                  default-directory)))
      (if gk-projects-use-eshell
          (eshell)
        (shell project-shell)))))

(defun gk-pop-shell (arg)
  "Pop a shell in a side window.
Pass arg to ‘shell’.  If already in a side window that displays a
shell, toggle the side window.
If there is a project shell associated to the frame, just show
that instead."
  (interactive "P")
  (if (and (assoca 'window-side (window-parameters))
           (equal major-mode
                  (if gk-projects-use-eshell
                      'eshell-mode
                    'shell-mode)))
      (window-toggle-side-windows)
    (when-let* ((win (display-buffer-in-side-window
                      (gk--get-shell-for-frame arg)
                      '((side . bottom)))))
      (select-window win))))

;; Home view
(defun gk-home ()
  "Take me to the home view."
  (interactive)
  ;; Close side windows off first because they can’t be the only
  ;; window.
  (when (window-with-parameter 'window-side)
    (window-toggle-side-windows))
  (delete-other-windows)
  (if (assoca 'gk-project-shell (frame-parameters))
      (let* ((fparam (frame-parameters))
             (vcs (assoca 'gk-project-vcs fparam))
             (dir (assoca 'gk-project-dir fparam)))
        (dired dir)
        (split-window-sensibly)
        (other-window 1)
        (funcall vcs dir))
    (other-window 1)
    (if initial-buffer-choice
        (ignore-errors (find-file initial-buffer-choice))
      (switch-to-buffer "*scratch*"))
    (gk-flash-current-line)))



reply via email to

[Prev in Thread] Current Thread [Next in Thread]