dotemacs

My Emacs configuration
git clone git://git.entf.net/dotemacs
Log | Files | Refs | LICENSE

sesman.el (41268B)


      1 ;;; sesman.el --- Generic Session Manager -*- lexical-binding: t -*-
      2 ;;
      3 ;; Copyright (C) 2018, Vitalie Spinu
      4 ;; Author: Vitalie Spinu
      5 ;; URL: https://github.com/vspinu/sesman
      6 ;; Keywords: process
      7 ;; Version: 0.3.2
      8 ;; Package-Requires: ((emacs "25"))
      9 ;; Keywords: processes, tools, vc
     10 ;;
     11 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
     12 ;;
     13 ;; This file is *NOT* part of GNU Emacs.
     14 ;;
     15 ;; This program is free software; you can redistribute it and/or
     16 ;; modify it under the terms of the GNU General Public License as
     17 ;; published by the Free Software Foundation; either version 3, or
     18 ;; (at your option) any later version.
     19 ;;
     20 ;; This program is distributed in the hope that it will be useful,
     21 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
     22 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
     23 ;; General Public License for more details.
     24 ;;
     25 ;; You should have received a copy of the GNU General Public License
     26 ;; along with this program; see the file COPYING.  If not, write to
     27 ;; the Free Software Foundation, Inc., 51 Franklin Street, Fifth
     28 ;; Floor, Boston, MA 02110-1301, USA.
     29 ;;
     30 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
     31 ;;
     32 ;;; Commentary:
     33 ;;
     34 ;; Sesman provides facilities for session management and interactive session
     35 ;; association with the current contexts (project, directory, buffers etc).  See
     36 ;; project's readme for more details.
     37 ;;
     38 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
     39 ;;
     40 ;;; Code:
     41 
     42 (require 'cl-generic)
     43 (require 'seq)
     44 (require 'subr-x)
     45 
     46 (defgroup sesman nil
     47   "Generic Session Manager."
     48   :prefix "sesman-"
     49   :group 'tools
     50   :link '(url-link :tag "GitHub" "https://github.com/vspinu/sesman"))
     51 
     52 (defface sesman-project-face
     53   '((default (:inherit font-lock-doc-face)))
     54   "Face used to mark projects."
     55   :group 'sesman)
     56 
     57 (defface sesman-directory-face
     58   '((default (:inherit font-lock-type-face)))
     59   "Face used to mark directories."
     60   :group 'sesman)
     61 
     62 (defface sesman-buffer-face
     63   '((default (:inherit font-lock-preprocessor-face)))
     64   "Face used to mark buffers."
     65   :group 'sesman)
     66 
     67 (defcustom sesman-use-friendly-sessions t
     68   "If non-nil consider friendly sessions when searching for the current sessions.
     69 The definition of friendly sessions is system dependent but usually means
     70 sessions running in dependent projects."
     71   :group 'sesman
     72   :type 'boolean
     73   :package-version '(sesman . "0.3.2"))
     74 
     75 ;; (defcustom sesman-disambiguate-by-relevance t
     76 ;;   "If t choose most relevant session in ambiguous situations, otherwise ask.
     77 ;; Ambiguity arises when multiple sessions are associated with current context.  By
     78 ;; default only projects could be associated with multiple sessions.  See
     79 ;; `sesman-single-link-contexts' in order to change that.  Relevance is decided by
     80 ;; system's implementation, see `sesman-more-relevant-p'."
     81 ;;   :group 'sesman
     82 ;;   :type 'boolean)
     83 
     84 (defcustom sesman-single-link-context-types '(buffer)
     85   "List of context types to which at most one session can be linked."
     86   :group 'sesman
     87   :type '(repeat symbol)
     88   :package-version '(sesman . "0.1.0"))
     89 
     90 ;; FIXME:
     91 ;; (defcustom sesman-abbreviate-paths 2
     92 ;;   "Abbreviate paths to that many parents.
     93 ;; When set to nil, don't abbreviate directories."
     94 ;;   :group 'sesman
     95 ;;   :type '(choice number
     96 ;;                  (const :tag "Don't abbreviate" nil)))
     97 
     98 (defvar sesman-sessions-hashmap (make-hash-table :test #'equal)
     99   "Hash-table of all sesman sessions.
    100 Key is a cons (system-name . session-name).")
    101 
    102 (defvar sesman-links-alist nil
    103   "An alist of all sesman links.
    104 Each element is of the form (key cxt-type cxt-value) where
    105 \"key\" is of the form (system-name . session-name). system-name
    106 and cxt-type must be symbols.")
    107 
    108 (defvar-local sesman-system nil
    109   "Name of the system managed by `sesman'.
    110 Can be either a symbol, or a function returning a symbol.")
    111 (put 'sesman-system 'permanent-local 't)
    112 
    113 
    114 
    115 ;; Internal Utilities
    116 
    117 (defun sesman--on-C-u-u-sessions (system which)
    118   (cond
    119    ((null which)
    120     (let ((ses (sesman-current-session system)))
    121       (when ses
    122         (list ses))))
    123    ((or (equal which '(4)) (eq which 'linked))
    124     (sesman--linked-sessions system 'sort))
    125    ((or (equal which '(16)) (eq which 'all) (eq which t))
    126     (sesman--all-system-sessions system 'sort))
    127    ;; session itself
    128    ((and (listp which)
    129          (or (stringp (car which))
    130              (symbolp (car which))))
    131     (list which))
    132    ;; session name
    133    ((or (stringp which)
    134         (symbolp which)
    135         (gethash (cons system which) sesman-sessions-hashmap)))
    136    (t (error "Invalid which argument (%s)" which))))
    137 
    138 (defun sesman--cap-system-name (system)
    139   (let ((name (symbol-name system)))
    140     (if (string-match-p "^[[:upper:]]" name)
    141         name
    142       (capitalize name))))
    143 
    144 (defun sesman--least-specific-context (system)
    145   (seq-some (lambda (ctype)
    146               (when-let (val (sesman-context ctype system))
    147                 (cons ctype val)))
    148             (reverse (sesman-context-types system))))
    149 
    150 (defun sesman--link-session-interactively (session cxt-type cxt-val)
    151   (let ((system (sesman--system)))
    152     (unless cxt-type
    153       (let ((cxt (sesman--least-specific-context system)))
    154         (setq cxt-type (car cxt)
    155               cxt-val (cdr cxt))))
    156     (let ((cxt-name (symbol-name cxt-type)))
    157       (if (member cxt-type (sesman-context-types system))
    158           (let ((session (or session
    159                              (sesman-ask-for-session
    160                               system
    161                               (format "Link with %s %s: "
    162                                       cxt-name (sesman--abbrev-path-maybe
    163                                                 (sesman-context cxt-type system)))
    164                               (sesman--all-system-sessions system 'sort)
    165                               'ask-new))))
    166             (sesman-link-session system session cxt-type cxt-val))
    167         (error (format "%s association not allowed for this system (%s)"
    168                        (capitalize cxt-name)
    169                        system))))))
    170 
    171 (defun sesman--expand-path-maybe (obj)
    172   (if (stringp obj)
    173       (expand-file-name obj)
    174     obj))
    175 
    176 ;; FIXME: incorporate `sesman-abbreviate-paths'
    177 (defun sesman--abbrev-path-maybe (obj)
    178   (if (stringp obj)
    179       (abbreviate-file-name obj)
    180     obj))
    181 
    182 (defun sesman--system-in-buffer (&optional buffer)
    183   (with-current-buffer (or buffer (current-buffer))
    184     (if (functionp sesman-system)
    185         (funcall sesman-system)
    186       sesman-system)))
    187 
    188 (defun sesman--system ()
    189   (if sesman-system
    190       (if (functionp sesman-system)
    191           (funcall sesman-system)
    192         sesman-system)
    193     (error "No `sesman-system' in buffer `%s'" (current-buffer))))
    194 
    195 (defun sesman--linked-sessions (system &optional sort cxt-types)
    196   (let* ((system (or system (sesman--system)))
    197          (cxt-types (or cxt-types (sesman-context-types system))))
    198     ;; just in case some links are lingering due to user errors
    199     (sesman--clear-links)
    200     (delete-dups
    201      (mapcar (lambda (assoc)
    202                (gethash (car assoc) sesman-sessions-hashmap))
    203              (sesman-current-links system nil sort cxt-types)))))
    204 
    205 (defun sesman--friendly-sessions (system &optional sort)
    206   (let ((sessions (seq-filter (lambda (ses) (sesman-friendly-session-p system ses))
    207                               (sesman--all-system-sessions system))))
    208     (if sort
    209         (sesman--sort-sessions system sessions)
    210       sessions)))
    211 
    212 (defun sesman--all-system-sessions (&optional system sort)
    213   "Return a list of sessions registered with SYSTEM.
    214 If SORT is non-nil, sort in relevance order."
    215   (let ((system (or system (sesman--system)))
    216         sessions)
    217     (maphash
    218      (lambda (k s)
    219        (when (eql (car k) system)
    220          (push s sessions)))
    221      sesman-sessions-hashmap)
    222     (if sort
    223         (sesman--sort-sessions system sessions)
    224       sessions)))
    225 
    226 ;; FIXME: make this a macro
    227 (defun sesman--link-lookup-fn (&optional system ses-name cxt-type cxt-val x)
    228   (let ((system (or system (caar x)))
    229         (ses-name (or ses-name (cdar x)))
    230         (cxt-type (or cxt-type (nth 1 x)))
    231         (cxt-val (or cxt-val (nth 2 x))))
    232     (lambda (el)
    233       (and (or (null system) (eq (caar el) system))
    234            (or (null ses-name) (equal (cdar el) ses-name))
    235            (or (null cxt-type)
    236                (if (listp cxt-type)
    237                    (member (nth 1 el) cxt-type)
    238                  (eq (nth 1 el) cxt-type)))
    239            (or (null cxt-val) (equal (nth 2 el) cxt-val))))))
    240 
    241 (defun sesman--unlink (x)
    242   (setq sesman-links-alist
    243         (seq-remove (sesman--link-lookup-fn nil nil nil nil x)
    244                     sesman-links-alist)))
    245 
    246 (defun sesman--clear-links ()
    247   (setq sesman-links-alist
    248         (seq-filter (lambda (x)
    249                       (gethash (car x) sesman-sessions-hashmap))
    250                     sesman-links-alist)))
    251 
    252 (defun sesman--format-session-objects (system session &optional sep)
    253   (let ((info (sesman-session-info system session)))
    254     (if (and (listp info)
    255              (keywordp (car info)))
    256         (let ((ses-name (car session))
    257               (sep (or sep " "))
    258               (strings (or (plist-get info :strings)
    259                            (mapcar (lambda (x) (format "%s" x))
    260                                    (plist-get info :objects)))))
    261           (mapconcat (lambda (str)
    262                        (replace-regexp-in-string ses-name "%%s" str nil t))
    263                      strings sep))
    264       (format "%s" info))))
    265 
    266 (defun sesman--format-session (system ses &optional prefix)
    267   (format (propertize "%s%s [%s] linked-to %s" 'face 'bold)
    268           (or prefix "")
    269           (propertize (car ses) 'face 'bold)
    270           (propertize (sesman--format-session-objects system ses ", ") 'face 'italic)
    271           (sesman-grouped-links system ses t t)))
    272 
    273 (defun sesman--format-link (link)
    274   (let* ((system (sesman--lnk-system-name link))
    275          (session (gethash (car link) sesman-sessions-hashmap)))
    276     (format "%s(%s) -> %s [%s]"
    277             (sesman--lnk-context-type link)
    278             (propertize (sesman--abbrev-path-maybe (sesman--lnk-value link)) 'face 'bold)
    279             (propertize (sesman--lnk-session-name link) 'face 'bold)
    280             (if session
    281                 (sesman--format-session-objects system session)
    282               "invalid"))))
    283 
    284 (defun sesman--ask-for-link (prompt links &optional ask-all)
    285   (let* ((name.keys (mapcar (lambda (link)
    286                               (cons (sesman--format-link link) link))
    287                             links))
    288          (name.keys (append name.keys
    289                             (when (and ask-all (> (length name.keys) 1))
    290                               '(("*all*")))))
    291          (nms (mapcar #'car name.keys))
    292          (sel (completing-read prompt nms nil t nil nil (car nms))))
    293     (cond ((string= sel "*all*")
    294            links)
    295           (ask-all
    296            (list (cdr (assoc sel name.keys))))
    297           (t
    298            (cdr (assoc sel name.keys))))))
    299 
    300 (defun sesman--sort-sessions (system sessions)
    301   (seq-sort (lambda (x1 x2)
    302               (sesman-more-relevant-p system x1 x2))
    303             sessions))
    304 
    305 (defun sesman--sort-links (system links)
    306   (seq-sort (lambda (x1 x2)
    307               (sesman-more-relevant-p system
    308                                       (gethash (car x1) sesman-sessions-hashmap)
    309                                       (gethash (car x2) sesman-sessions-hashmap)))
    310             links))
    311 
    312 ;; link data structure accessors
    313 (defun sesman--lnk-system-name (lnk)
    314   (caar lnk))
    315 (defun sesman--lnk-session-name (lnk)
    316   (cdar lnk))
    317 (defun sesman--lnk-context-type (lnk)
    318   (cadr lnk))
    319 (defun sesman--lnk-value (lnk)
    320   (nth 2 lnk))
    321 
    322 
    323 ;;; User Interface
    324 
    325 (defun sesman-post-command-hook nil
    326   "Normal hook ran after every state-changing Sesman command.")
    327 
    328 ;;;###autoload
    329 (defun sesman-start ()
    330   "Start a Sesman session."
    331   (interactive)
    332   (let ((system (sesman--system)))
    333     (message "Starting new %s session ..." system)
    334     (prog1 (sesman-start-session system)
    335       (run-hooks 'sesman-post-command-hook))))
    336 
    337 ;;;###autoload
    338 (defun sesman-restart (&optional which)
    339   "Restart sesman session.
    340 When WHICH is nil, restart the current session; when a single universal
    341 argument or 'linked, restart all linked sessions; when a double universal
    342 argument, t or 'all, restart all sessions. For programmatic use, WHICH can also
    343 be a session or a name of the session, in which case that session is restarted."
    344   (interactive "P")
    345   (let* ((system (sesman--system))
    346          (sessions (sesman--on-C-u-u-sessions system which)))
    347     (if (null sessions)
    348         (message "No %s sessions found" system)
    349       (with-temp-message (format "Restarting %s %s %s"  system
    350                                  (if (= 1 (length sessions)) "session" "sessions")
    351                                  (mapcar #'car sessions))
    352         (mapc (lambda (s)
    353                 (sesman-restart-session system s))
    354               sessions))
    355       ;; restarting is not guaranteed to finish here, but what can we do?
    356       (run-hooks 'sesman-post-command-hook))))
    357 
    358 ;;;###autoload
    359 (defun sesman-quit (&optional which)
    360   "Terminate a Sesman session.
    361 When WHICH is nil, kill only the current session; when a single universal
    362 argument or 'linked, kill all linked sessions; when a double universal argument,
    363 t or 'all, kill all sessions. For programmatic use, WHICH can also be a session
    364 or a name of the session, in which case that session is killed."
    365   (interactive "P")
    366   (let* ((system (sesman--system))
    367          (sessions (sesman--on-C-u-u-sessions system which)))
    368     (if (null sessions)
    369         (message "No %s sessions found" system)
    370       (with-temp-message (format "Killing %s %s %s"  system
    371                                  (if (= 1 (length sessions)) "session" "sessions")
    372                                  (mapcar #'car sessions))
    373         (mapc (lambda (s)
    374                 (sesman-unregister system s)
    375                 (sesman-quit-session system s))
    376               sessions))
    377       (run-hooks 'sesman-post-command-hook))))
    378 
    379 ;;;###autoload
    380 (defun sesman-info (&optional all)
    381   "Display info for all current sessions (`sesman-current-sessions').
    382 In the resulting minibuffer display linked sessions are numbered and the
    383 other (friendly) sessions are not. When ALL is non-nil, show info for all
    384 sessions."
    385   (interactive "P")
    386   (let* ((system (sesman--system))
    387          (i 1)
    388          (sessions (if all
    389                        (sesman-sessions system t)
    390                      (sesman-current-sessions system)))
    391          (empty-prefix (if (> (length sessions) 1) "  " "")))
    392     (if sessions
    393         (message (mapconcat (lambda (ses)
    394                               (let ((prefix (if (sesman-relevant-session-p system ses)
    395                                                 (prog1 (format "%d " i)
    396                                                   (setq i (1+ i)))
    397                                               empty-prefix)))
    398                                 (sesman--format-session system ses prefix)))
    399                             sessions
    400                             "\n"))
    401       (message "No %s%s sessions"
    402                (if all "" "current ")
    403                system))))
    404 
    405 ;;;###autoload
    406 (defun sesman-link-with-buffer (&optional buffer session)
    407   "Ask for SESSION and link with BUFFER.
    408 BUFFER defaults to current buffer. On universal argument, or if BUFFER is 'ask,
    409 ask for buffer."
    410   (interactive "P")
    411   (let ((buf (if (or (eq buffer 'ask)
    412                      (equal buffer '(4)))
    413                  (let ((this-system (sesman--system)))
    414                    (read-buffer "Link buffer: " (current-buffer) t
    415                                 (lambda (buf-cons)
    416                                   (equal this-system
    417                                          (sesman--system-in-buffer (cdr buf-cons))))))
    418                (or buffer (current-buffer)))))
    419     (sesman--link-session-interactively session 'buffer buf)))
    420 
    421 ;;;###autoload
    422 (defun sesman-link-with-directory (&optional dir session)
    423   "Ask for SESSION and link with DIR.
    424 DIR defaults to `default-directory'. On universal argument, or if DIR is 'ask,
    425 ask for directory."
    426   (interactive "P")
    427   (let ((dir (if (or (eq dir 'ask)
    428                      (equal dir '(4)))
    429                  (read-directory-name "Link directory: ")
    430                (or dir default-directory))))
    431     (sesman--link-session-interactively session 'directory dir)))
    432 
    433 ;;;###autoload
    434 (defun sesman-link-with-project (&optional project session)
    435   "Ask for SESSION and link with PROJECT.
    436 PROJECT defaults to current project. On universal argument, or if PROJECT is
    437 'ask, ask for the project. SESSION defaults to the current session."
    438   (interactive "P")
    439   (let* ((system (sesman--system))
    440          (project (expand-file-name
    441                    (if (or (eq project 'ask)
    442                            (equal project '(4)))
    443                        ;; FIXME: should be a completion over all known projects for this system
    444                        (read-directory-name "Project: " (sesman-project system))
    445                      (or project (sesman-project system))))))
    446     (sesman--link-session-interactively session 'project project)))
    447 
    448  ;;;###autoload
    449 (defun sesman-link-with-least-specific (&optional session)
    450   "Ask for SESSION and link with the least specific context available.
    451 Normally the least specific context is the project. If not in a project, link
    452 with the `default-directory'. If `default-directory' is nil, link with current
    453 buffer."
    454   (interactive "P")
    455   (sesman--link-session-interactively session nil nil))
    456 
    457 ;;;###autoload
    458 (defun sesman-unlink ()
    459   "Break any of the previously created links."
    460   (interactive)
    461   (let* ((system (sesman--system))
    462          (links (or (sesman-current-links system)
    463                     (user-error "No %s links found" system))))
    464     (mapc #'sesman--unlink
    465           (sesman--ask-for-link "Unlink: " links 'ask-all)))
    466   (run-hooks 'sesman-post-command-hook))
    467 
    468 (declare-function sesman-browser "sesman-browser")
    469 ;;;###autoload (autoload 'sesman-map "sesman" "Session management prefix keymap." t 'keymap)
    470 (defvar sesman-map
    471   (let (sesman-map)
    472     (define-prefix-command 'sesman-map)
    473     (define-key sesman-map (kbd "C-i") #'sesman-info)
    474     (define-key sesman-map (kbd   "i") #'sesman-info)
    475     (define-key sesman-map (kbd "C-w") #'sesman-browser)
    476     (define-key sesman-map (kbd   "w") #'sesman-browser)
    477     (define-key sesman-map (kbd "C-s") #'sesman-start)
    478     (define-key sesman-map (kbd   "s") #'sesman-start)
    479     (define-key sesman-map (kbd "C-r") #'sesman-restart)
    480     (define-key sesman-map (kbd   "r") #'sesman-restart)
    481     (define-key sesman-map (kbd "C-q") #'sesman-quit)
    482     (define-key sesman-map (kbd   "q") #'sesman-quit)
    483     (define-key sesman-map (kbd "C-l") #'sesman-link-with-least-specific)
    484     (define-key sesman-map (kbd   "l") #'sesman-link-with-least-specific)
    485     (define-key sesman-map (kbd "C-b") #'sesman-link-with-buffer)
    486     (define-key sesman-map (kbd   "b") #'sesman-link-with-buffer)
    487     (define-key sesman-map (kbd "C-d") #'sesman-link-with-directory)
    488     (define-key sesman-map (kbd   "d") #'sesman-link-with-directory)
    489     (define-key sesman-map (kbd "C-p") #'sesman-link-with-project)
    490     (define-key sesman-map (kbd   "p") #'sesman-link-with-project)
    491     (define-key sesman-map (kbd "C-u") #'sesman-unlink)
    492     (define-key sesman-map (kbd "  u") #'sesman-unlink)
    493     sesman-map)
    494   "Session management prefix keymap.")
    495 
    496 (defvar sesman-menu
    497   '("Sesman"
    498     ["Show Session Info" sesman-info]
    499     "--"
    500     ["Start" sesman-start]
    501     ["Restart" sesman-restart :active (sesman-connected-p)]
    502     ["Quit" sesman-quit :active (sesman-connected-p)]
    503     "--"
    504     ["Link with Buffer" sesman-link-with-buffer :active (sesman-connected-p)]
    505     ["Link with Directory" sesman-link-with-directory :active (sesman-connected-p)]
    506     ["Link with Project" sesman-link-with-project :active (sesman-connected-p)]
    507     "--"
    508     ["Unlink" sesman-unlink :active (sesman-connected-p)])
    509   "Sesman Menu.")
    510 
    511 (defun sesman-install-menu (map)
    512   "Install `sesman-menu' into MAP."
    513   (easy-menu-do-define 'seman-menu-open
    514                        map
    515                        (get 'sesman-menu 'variable-documentation)
    516                        sesman-menu))
    517 
    518 
    519 ;;; System Generic
    520 
    521 (cl-defgeneric sesman-start-session (system)
    522   "Start and return SYSTEM SESSION.")
    523 
    524 (cl-defgeneric sesman-quit-session (system session)
    525   "Terminate SYSTEM SESSION.")
    526 
    527 (cl-defgeneric sesman-restart-session (system session)
    528   "Restart SYSTEM SESSION.
    529 By default, calls `sesman-quit-session' and then
    530 `sesman-start-session'."
    531   (let ((old-name (car session)))
    532     (sesman-quit-session system session)
    533     (let ((new-session (sesman-start-session system)))
    534       (setcar new-session old-name))))
    535 
    536 (cl-defgeneric sesman-session-info (_system session)
    537   "Return a plist with :objects key containing user \"visible\" objects.
    538 Optional :strings value is a list of string representations of objects. Optional
    539 :map key is a local keymap to place on every object in the session browser.
    540 Optional :buffers is a list of buffers which will be used for navigation from
    541 the session browser. If :buffers is missing, buffers from :objects are used
    542 instead."
    543   (list :objects (cdr session)))
    544 
    545 (cl-defgeneric sesman-project (_system)
    546   "Retrieve project root for SYSTEM in directory DIR.
    547 DIR defaults to `default-directory'. Return a string or nil if no project has
    548 been found."
    549   nil)
    550 
    551 (cl-defgeneric sesman-more-relevant-p (_system session1 session2)
    552   "Return non-nil if SESSION1 should be sorted before SESSION2.
    553 By default, sort by session name. Systems should overwrite this method to
    554 provide a more meaningful ordering. If your system objects are buffers you can
    555 use `sesman-more-recent-p' utility in this method."
    556   (not (string-greaterp (car session1) (car session2))))
    557 
    558 (cl-defgeneric sesman-friendly-session-p (_system _session)
    559   "Return non-nil if SESSION is a friendly session in current context.
    560 The \"friendship\" is system dependent but usually means sessions running in
    561 dependent projects. Unless SYSTEM has defined a method for this generic, there
    562 are no friendly sessions."
    563   nil)
    564 
    565 (cl-defgeneric sesman-context-types (_system)
    566   "Return a list of context types understood by SYSTEM.
    567 Contexts must be sorted from most specific to least specific."
    568   '(buffer directory project))
    569 
    570 
    571 ;;; System API
    572 
    573 (defun sesman-session (system session-name)
    574   "Retrieve SYSTEM's session with SESSION-NAME from global hash."
    575   (let ((system (or system (sesman--system))))
    576     (gethash (cons system session-name) sesman-sessions-hashmap)))
    577 
    578 (defun sesman-sessions (system &optional sort type cxt-types)
    579   "Return a list of sessions registered with SYSTEM.
    580 When TYPE is either 'all or nil return all sessions registered with the SYSTEM,
    581 when 'linked, only linked to the current context sessions, when 'friendly - only
    582 friendly sessions. If SORT is non-nil, sessions are sorted in the relevance
    583 order with linked sessions leading the list. CXT-TYPES is a list of context
    584 types to consider for linked sessions."
    585   (let ((system (or system (sesman--system))))
    586     (cond
    587      ((eq type 'linked)
    588       (sesman--linked-sessions system sort cxt-types))
    589      ((eq type 'friendly)
    590       (sesman--friendly-sessions system sort))
    591      ((memq type '(all nil))
    592       (if sort
    593           (delete-dups
    594            (append (sesman--linked-sessions system 'sort cxt-types)
    595                    (sesman--all-system-sessions system 'sort)))
    596         (sesman--all-system-sessions system)))
    597      (t (error "Invalid session TYPE argument %s" type)))))
    598 
    599 (defun sesman-current-sessions (system &optional cxt-types)
    600   "Return a list of SYSTEM sessions active in the current context.
    601 Sessions are ordered by the relevance order and linked sessions come first. If
    602 `sesman-use-friendly-sessions' current sessions consist of linked and friendly
    603 sessions, otherwise only of linked sessions. CXT-TYPES is a list of context
    604 types to consider. Defaults to the list returned from `sesman-context-types'."
    605   (if sesman-use-friendly-sessions
    606       (delete-dups
    607        (append (sesman--linked-sessions system 'sort cxt-types)
    608                (sesman--friendly-sessions system 'sort)))
    609     (sesman--linked-sessions system 'sort cxt-types)))
    610 
    611 (defun sesman-has-sessions-p (system)
    612   "Return t if there is at least one session registered with SYSTEM."
    613   (let ((system (or system (sesman--system)))
    614         (found))
    615     (condition-case nil
    616         (maphash (lambda (k _)
    617                    (when (eq (car k) system)
    618                      (setq found t)
    619                      (throw 'found nil)))
    620                  sesman-sessions-hashmap)
    621       (error))
    622     found))
    623 
    624 (defvar sesman--select-session-history nil)
    625 (defun sesman-ask-for-session (system prompt &optional sessions ask-new ask-all)
    626   "Ask for a SYSTEM session with PROMPT.
    627 SESSIONS defaults to value returned from `sesman-sessions'.  If
    628 ASK-NEW is non-nil, offer *new* option to start a new session.  If
    629 ASK-ALL is non-nil offer *all* option.  If ASK-ALL is non-nil,
    630 return a list of sessions, otherwise a single session."
    631   (let* ((sessions (or sessions (sesman-sessions system)))
    632          (name.syms (mapcar (lambda (s)
    633                               (let ((name (car s)))
    634                                 (cons (if (symbolp name) (symbol-name name) name)
    635                                       name)))
    636                             sessions))
    637          (nr (length name.syms))
    638          (syms (if (and (not ask-new) (= nr 0))
    639                    (error "No %s sessions found" system)
    640                  (append name.syms
    641                          (when ask-new '(("*new*")))
    642                          (when (and ask-all (> nr 1))
    643                            '(("*all*"))))))
    644          (def (caar syms))
    645          ;; (def (if (assoc (car sesman--select-session-history) syms)
    646          ;;          (car sesman--select-session-history)
    647          ;;        (caar syms)))
    648          (sel (completing-read
    649                prompt (mapcar #'car syms) nil t nil 'sesman--select-session-history def)))
    650     (cond
    651      ((string= sel "*new*")
    652       (let ((ses (sesman-start-session system)))
    653         (message "Started %s" (car ses))
    654         (if ask-all (list ses) ses)))
    655      ((string= sel "*all*")
    656       sessions)
    657      (t
    658       (let* ((sym (cdr (assoc sel syms)))
    659              (ses (assoc sym sessions)))
    660         (if ask-all (list ses) ses))))))
    661 
    662 (defun sesman-current-session (system &optional cxt-types)
    663   "Get the most relevant current session for the SYSTEM.
    664 CXT-TYPES is a list of context types to consider."
    665   (or (car (sesman--linked-sessions system 'sort cxt-types))
    666       (car (sesman--friendly-sessions system 'sort))))
    667 
    668 (defun sesman-ensure-session (system &optional cxt-types)
    669   "Get the most relevant linked session for SYSTEM or throw if none exists.
    670 CXT-TYPES is a list of context types to consider."
    671   (or (sesman-current-session system cxt-types)
    672       (user-error "No linked %s sessions" system)))
    673 
    674 (defvar sesman--cxt-abbrevs '(buffer "buf" project "proj" directory "dir"))
    675 (defun sesman--format-context (cxt-type cxt-val extra-face)
    676   (let* ((face (intern (format "sesman-%s-face" cxt-type)))
    677          (short-type (propertize (or (plist-get sesman--cxt-abbrevs cxt-type)
    678                                      (symbol-value cxt-type))
    679                                  'face (list (if (facep face)
    680                                                  face
    681                                                'font-lock-function-name-face)
    682                                              extra-face))))
    683     (concat short-type
    684             (propertize (format "(%s)" cxt-val)
    685                         'face extra-face))))
    686 
    687 (defun sesman-grouped-links (system session &optional current-first as-string)
    688   "Retrieve all links for SYSTEM's SESSION from the global `sesman-links-alist'.
    689 Return an alist of the form
    690 
    691    ((buffer buffers..)
    692     (directory directories...)
    693     (project projects...)).
    694 
    695 When `CURRENT-FIRST' is non-nil, a cons of two lists as above is returned with
    696 car containing links relevant in current context and cdr all other links. If
    697 AS-STRING is non-nil, return an equivalent string representation."
    698   (let* ((system (or system (sesman--system)))
    699          (session (or session (sesman-current-session system)))
    700          (ses-name (car session))
    701          (links (thread-last sesman-links-alist
    702                   (seq-filter (sesman--link-lookup-fn system ses-name))
    703                   (sesman--sort-links system)
    704                   (reverse)))
    705          (out (mapcar (lambda (x) (list x))
    706                       (sesman-context-types system)))
    707          (out-rel (when current-first
    708                     (copy-alist out))))
    709     (mapc (lambda (link)
    710             (let* ((type (sesman--lnk-context-type link))
    711                    (entry (if (and current-first
    712                                    (sesman-relevant-link-p link))
    713                               (assoc type out-rel)
    714                             (assoc type out))))
    715               (when entry
    716                 (setcdr entry (cons link (cdr entry))))))
    717           links)
    718     (let ((out (delq nil (mapcar (lambda (el) (and (cdr el) el)) out)))
    719           (out-rel (delq nil (mapcar (lambda (el) (and (cdr el) el)) out-rel))))
    720       (if as-string
    721           (let ((fmt-fn (lambda (typed-links)
    722                           (let* ((type (car typed-links)))
    723                             (mapconcat (lambda (lnk)
    724                                          (let ((val (sesman--abbrev-path-maybe
    725                                                      (sesman--lnk-value lnk))))
    726                                            (sesman--format-context type val 'italic)))
    727                                        (cdr typed-links)
    728                                        ", ")))))
    729             (if out-rel
    730                 (concat (mapconcat fmt-fn out-rel ", ")
    731                         (when out " | ")
    732                         (mapconcat fmt-fn out ", "))
    733               (mapconcat fmt-fn out ", ")))
    734         (if current-first
    735             (cons out-rel out)
    736           out)))))
    737 
    738 (defun sesman-link-session (system session &optional cxt-type cxt-val)
    739   "Link SYSTEM's SESSION to context give by CXT-TYPE and CXT-VAL.
    740 If CXT-TYPE is nil, use the least specific type available in the current
    741 context. If CXT-TYPE is non-nil, and CXT-VAL is not given, retrieve it with
    742 `sesman-context'. See also `sesman-link-with-project',
    743 `sesman-link-with-directory' and `sesman-link-with-buffer'."
    744   (let* ((ses-name (or (car-safe session)
    745                        (error "SESSION must be a headed list")))
    746          (cxt-val (or cxt-val
    747                       (sesman--expand-path-maybe
    748                        (or (if cxt-type
    749                                (sesman-context cxt-type system)
    750                              (let ((cxt (sesman--least-specific-context system)))
    751                                (setq cxt-type (car cxt))
    752                                (cdr cxt)))
    753                            (error "No local context of type %s" cxt-type)))))
    754          (key (cons system ses-name))
    755          (link (list key cxt-type cxt-val)))
    756     (if (member cxt-type sesman-single-link-context-types)
    757         (thread-last sesman-links-alist
    758           (seq-remove (sesman--link-lookup-fn system nil cxt-type cxt-val))
    759           (cons link)
    760           (setq sesman-links-alist))
    761       (unless (seq-filter (sesman--link-lookup-fn system ses-name cxt-type cxt-val)
    762                           sesman-links-alist)
    763         (setq sesman-links-alist (cons link sesman-links-alist))))
    764     (run-hooks 'sesman-post-command-hook)
    765     link))
    766 
    767 (defun sesman-links (system &optional session-or-name cxt-types sort)
    768   "Retrieve all links for SYSTEM, SESSION-OR-NAME and CXT-TYPES.
    769 SESSION-OR-NAME can be either a session or a name of the session. If SORT is
    770 non-nil links are sorted in relevance order and `sesman-current-links' lead the
    771 list, otherwise links are returned in the creation order."
    772   (let* ((ses-name (if (listp session-or-name)
    773                        (car session-or-name)
    774                      session-or-name))
    775          (lfn (sesman--link-lookup-fn system ses-name cxt-types)))
    776     (if sort
    777         (delete-dups (append
    778                       (sesman-current-links system ses-name)
    779                       (sesman--sort-links system (seq-filter lfn sesman-links-alist))))
    780       (seq-filter lfn sesman-links-alist))))
    781 
    782 (defun sesman-current-links (system &optional session-or-name sort cxt-types)
    783   "Retrieve all active links in current context for SYSTEM and SESSION-OR-NAME.
    784 SESSION-OR-NAME can be either a session or a name of the session. CXT-TYPES is a
    785 list of context types to consider. Returned links are a subset of
    786 `sesman-links-alist' sorted in order of relevance if SORT is non-nil."
    787   ;; mapcan is a built-in in 26.1; don't want to require cl-lib for one function
    788   (let ((ses-name (if (listp session-or-name)
    789                       (car session-or-name)
    790                     session-or-name)))
    791     (seq-mapcat
    792      (lambda (cxt-type)
    793        (let* ((lfn (sesman--link-lookup-fn system ses-name cxt-type))
    794               (links (seq-filter (lambda (l)
    795                                    (and (funcall lfn l)
    796                                         (sesman-relevant-context-p cxt-type (sesman--lnk-value l))))
    797                                  sesman-links-alist)))
    798          (if sort
    799              (sesman--sort-links system links)
    800            links)))
    801      (or cxt-types (sesman-context-types system)))))
    802 
    803 (defun sesman-has-links-p (system &optional cxt-types)
    804   "Return t if there is at least one linked session.
    805 CXT-TYPES defaults to `sesman-context-types' for current SYSTEM."
    806   (let ((cxt-types (or cxt-types (sesman-context-types system)))
    807         (found))
    808     (condition-case nil
    809         (mapc (lambda (l)
    810                 (when (eq system (sesman--lnk-system-name l))
    811                   (let ((cxt (sesman--lnk-context-type l)))
    812                     (when (and (member cxt cxt-types)
    813                                (sesman-relevant-context-p cxt (sesman--lnk-value l)))
    814                       (setq found t)
    815                       (throw 'found nil)))))
    816               sesman-links-alist)
    817       (error))
    818     found))
    819 
    820 (defun sesman-register (system session)
    821   "Register SESSION into `sesman-sessions-hashmap' and `sesman-links-alist'.
    822 SYSTEM defaults to current system.  If a session with same name is already
    823 registered in `sesman-sessions-hashmap', change the name by appending \"#1\",
    824 \"#2\" ... to the name.  This function should be called by system-specific
    825 connection initializers (\"run-xyz\", \"xyz-jack-in\" etc.)."
    826   (let* ((system (or system (sesman--system)))
    827          (ses-name (car session))
    828          (ses-name0 (car session))
    829          (i 1))
    830     (while (sesman-session system ses-name)
    831       (setq ses-name (format "%s#%d" ses-name0 i)
    832             i (1+ i)))
    833     (setq session (cons ses-name (cdr session)))
    834     (puthash (cons system ses-name) session sesman-sessions-hashmap)
    835     (sesman-link-session system session)
    836     session))
    837 
    838 (defun sesman-unregister (system session)
    839   "Unregister SESSION.
    840 SYSTEM defaults to current system.  Remove session from
    841 `sesman-sessions-hashmap' and `sesman-links-alist'."
    842   (let ((ses-key (cons system (car session))))
    843     (remhash ses-key sesman-sessions-hashmap)
    844     (sesman--clear-links)
    845     session))
    846 
    847 (defun sesman-add-object (system session-name object &optional allow-new)
    848   "Add (destructively) OBJECT to session SESSION-NAME of SYSTEM.
    849 If ALLOW-NEW is nil and session with SESSION-NAME does not exist
    850 throw an error, otherwise register a new session with
    851 session (list SESSION-NAME OBJECT)."
    852   (let* ((system (or system (sesman--system)))
    853          (session (sesman-session system session-name)))
    854     (if session
    855         (setcdr session (cons object (cdr session)))
    856       (if allow-new
    857           (sesman-register system (list session-name object))
    858         (error "%s session '%s' does not exist"
    859                (sesman--cap-system-name system) session-name)))))
    860 
    861 (defun sesman-remove-object (system session-name object &optional auto-unregister no-error)
    862   "Remove (destructively) OBJECT from session SESSION-NAME of SYSTEM.
    863 If SESSION-NAME is nil, retrieve the session with
    864 `sesman-session-for-object'.  If OBJECT is the last object in sesman
    865 session, `sesman-unregister' the session.  If AUTO-UNREGISTER is non-nil
    866 unregister sessions of length 0 and remove all the links with the session.
    867 If NO-ERROR is non-nil, don't throw an error if OBJECT is not found in any
    868 session.  This is useful if there are several \"concurrent\" parties which
    869 can remove the object."
    870   (let* ((system (or system (sesman--system)))
    871          (session (if session-name
    872                       (sesman-session system session-name)
    873                     (sesman-session-for-object system object no-error)))
    874          (new-session (delete object session)))
    875     (cond ((null new-session))
    876           ((= (length new-session) 1)
    877            (when auto-unregister
    878              (sesman-unregister system session)))
    879           (t
    880            (puthash (cons system (car session)) new-session sesman-sessions-hashmap)))))
    881 
    882 (defun sesman-session-for-object (system object &optional no-error)
    883   "Retrieve SYSTEM session which contains OBJECT.
    884 When NO-ERROR is non-nil, don't throw an error if OBJECT is not part of any
    885 session.  In such case, return nil."
    886   (let* ((system (or system (sesman--system)))
    887          (sessions (sesman--all-system-sessions system)))
    888     (or (seq-find (lambda (ses)
    889                     (seq-find (lambda (x) (equal object x)) (cdr ses)))
    890                   sessions)
    891         (unless no-error
    892           (error "%s is not part of any %s sessions"
    893                  object system)))))
    894 
    895 (defun sesman-session-name-for-object (system object &optional no-error)
    896   "Retrieve the name of the SYSTEM's session containing OBJECT.
    897 When NO-ERROR is non-nil, don't throw an error if OBJCECT is not part of
    898 any session.  In such case, return nil."
    899   (car (sesman-session-for-object system object no-error)))
    900 
    901 (defun sesman-more-recent-p (bufs1 bufs2)
    902   "Return t if BUFS1 is more recent than BUFS2.
    903 BUFS1 and BUFS2 are either buffers or lists of buffers.  When lists of
    904 buffers, most recent buffers from each list are considered.  To be used
    905 primarily in `sesman-more-relevant-p' methods when session objects are
    906 buffers."
    907   (let ((bufs1 (if (bufferp bufs1) (list bufs1) bufs1))
    908         (bufs2 (if (bufferp bufs2) (list bufs2) bufs2)))
    909     (eq 1 (seq-some (lambda (b)
    910                       (if (member b bufs1)
    911                           1
    912                         (when (member b bufs2)
    913                           -1)))
    914                     (buffer-list)))))
    915 
    916 
    917 ;;; Contexts
    918 
    919 (defvar sesman--path-cache (make-hash-table :test #'equal))
    920 ;; path caching because file-truename is very slow
    921 (defun sesman--expand-path (path)
    922   (or (gethash path sesman--path-cache)
    923       (puthash path (file-truename path) sesman--path-cache)))
    924 
    925 (cl-defgeneric sesman-context (_cxt-type _system)
    926   "Given SYSTEM and context type CXT-TYPE return the context.")
    927 (cl-defmethod sesman-context ((_cxt-type (eql buffer)) _system)
    928   "Return current buffer."
    929   (current-buffer))
    930 (cl-defmethod sesman-context ((_cxt-type (eql directory)) _system)
    931   "Return current directory."
    932   (sesman--expand-path default-directory))
    933 (cl-defmethod sesman-context ((_cxt-type (eql project)) system)
    934   "Return current project."
    935   (let ((proj (or
    936                (sesman-project (or system (sesman--system)))
    937                ;; Normally we would use (project-roots (project-current)) but currently
    938                ;; project-roots fails on nil and doesn't work on custom `('foo .
    939                ;; "path/to/project"). So, use vc as a fallback and don't use project.el at
    940                ;; all for now.
    941                (vc-root-dir))))
    942     (when proj
    943       (sesman--expand-path proj))))
    944 
    945 (cl-defgeneric sesman-relevant-context-p (_cxt-type cxt)
    946   "Non-nil if context CXT is relevant to current context of type CXT-TYPE.")
    947 (cl-defmethod sesman-relevant-context-p ((_cxt-type (eql buffer)) buf)
    948   "Non-nil if BUF is `current-buffer'."
    949   (eq (current-buffer) buf))
    950 (cl-defmethod sesman-relevant-context-p ((_cxt-type (eql directory)) dir)
    951   "Non-nil if DIR is the parent or equals the `default-directory'."
    952   (when (and dir default-directory)
    953     (string-match-p (concat "^" (sesman--expand-path dir))
    954                     (sesman--expand-path default-directory))))
    955 (cl-defmethod sesman-relevant-context-p ((_cxt-type (eql project)) proj)
    956   "Non-nil if PROJ is the parent or equal to the `default-directory'."
    957   (when (and proj default-directory)
    958     (string-match-p (concat "^" (sesman--expand-path proj))
    959                     (sesman--expand-path default-directory))))
    960 
    961 (defun sesman-relevant-link-p (link &optional cxt-types)
    962   "Return non-nil if LINK is relevant to the current context.
    963 If CXT-TYPES is non-nil, only check relevance for those contexts."
    964   (when (or (null cxt-types)
    965             (member (sesman--lnk-context-type link) cxt-types))
    966     (sesman-relevant-context-p
    967      (sesman--lnk-context-type link)
    968      (sesman--lnk-value link))))
    969 
    970 (defun sesman-relevant-session-p (system session &optional cxt-types)
    971   "Return non-nil if SYSTEM's SESSION is relevant to the current context.
    972 If CXT-TYPES is non-nil, only check relevance for those contexts."
    973   (seq-some #'sesman-relevant-link-p
    974             (sesman-links system session cxt-types)))
    975 
    976 (provide 'sesman)
    977 
    978 ;;; sesman.el ends here