From: Andreas Gerler Date: Thu, 11 May 2023 15:51:23 +0000 (+0200) Subject: add detached X-Git-Url: http://git.bundesbrandschatzamt.de/?a=commitdiff_plain;h=fc168048d7f3a81d23cfd6a6d4ff49abf8fa9af0;p=emacsconfig.git add detached Signed-off-by: Andreas Gerler --- diff --git a/elpa/detached-20221129.1430/detached-autoloads.el b/elpa/detached-20221129.1430/detached-autoloads.el new file mode 100644 index 00000000..9178a7dd --- /dev/null +++ b/elpa/detached-20221129.1430/detached-autoloads.el @@ -0,0 +1,357 @@ +;;; detached-autoloads.el --- automatically extracted autoloads -*- lexical-binding: t -*- +;; +;;; Code: + +(add-to-list 'load-path (directory-file-name + (or (file-name-directory #$) (car load-path)))) + + +;;;### (autoloads nil "detached" "detached.el" (0 0 0 0)) +;;; Generated autoloads from detached.el + +(autoload 'detached-shell-command "detached" "\ +Execute COMMAND with `detached'. + +Optionally SUPPRESS-OUTPUT if prefix-argument is provided. + +\(fn COMMAND &optional SUPPRESS-OUTPUT)" t nil) + +(autoload 'detached-open-session "detached" "\ +Open a `detached' SESSION. + +\(fn SESSION)" t nil) + +(autoload 'detached-compile-session "detached" "\ +Compile SESSION. + +The session is compiled by opening its output and enabling +`compilation-minor-mode'. + +\(fn SESSION)" t nil) + +(autoload 'detached-edit-session-annotation "detached" "\ +Edit SESSION's annotation. + +\(fn SESSION)" t nil) + +(autoload 'detached-edit-and-run-session "detached" "\ +Edit and re-run SESSION at point. + +Optionally TOGGLE-SESSION-MODE. + +\(fn SESSION &optional TOGGLE-SESSION-MODE)" t nil) + +(autoload 'detached-rerun-session "detached" "\ +Re-run SESSION at point. + +Optionally TOGGLE-SESSION-MODE. + +\(fn SESSION &optional TOGGLE-SESSION-MODE)" t nil) + +(autoload 'detached-describe-session "detached" "\ +Describe current session." t nil) + +(autoload 'detached-describe-duration "detached" "\ +Describe the SESSION's duration statistics. + +\(fn SESSION)" t nil) + +(autoload 'detached-attach-session "detached" "\ +Attach to SESSION. + +\(fn SESSION)" t nil) + +(autoload 'detached-copy-session-output "detached" "\ +Copy SESSION's output. + +\(fn SESSION)" t nil) + +(autoload 'detached-copy-session-command "detached" "\ +Copy SESSION's command. + +\(fn SESSION)" t nil) + +(autoload 'detached-insert-session-command "detached" "\ +Insert SESSION's command. + +\(fn SESSION)" t nil) + +(autoload 'detached-delete-session "detached" "\ +Delete SESSION. + +\(fn SESSION)" t nil) + +(autoload 'detached-kill-session "detached" "\ +Send a TERM signal to SESSION. + +Optionally DELETE the session if prefix-argument is provided. + +\(fn SESSION &optional DELETE)" t nil) + +(autoload 'detached-view-session "detached" "\ +View the SESSION. + +\(fn SESSION)" t nil) + +(autoload 'detached-diff-session "detached" "\ +Diff SESSION1 with SESSION2. + +\(fn SESSION1 SESSION2)" t nil) + +(autoload 'detached-open-session-directory "detached" "\ +Open SESSION's log directory. + +\(fn SESSION)" t nil) + +(autoload 'detached-detach-session "detached" "\ +Detach from session in current buffer. + +This command is only activated if `detached-buffer-session' is an +active session. For sessions created with `detached-compile' or +`detached-shell-command', the command will also kill the window." t nil) + +(autoload 'detached-delete-sessions "detached" "\ +Delete `detached' sessions which belong to the current host, unless ALL-HOSTS. + +\(fn &optional ALL-HOSTS)" t nil) + +(autoload 'detached-initialize-sessions "detached" "\ +Initialize `detached' sessions from the database." nil nil) + +(autoload 'detached-shell-mode "detached" "\ +Integrate `detached' in `shell-mode'. + +This is a minor mode. If called interactively, toggle the +`detached-shell mode' mode. If the prefix argument is positive, +enable the mode, and if it is zero or negative, disable the mode. + +If called from Lisp, toggle the mode if ARG is `toggle'. Enable +the mode if ARG is nil, omitted, or is a positive number. +Disable the mode if ARG is a negative number. + +To check whether the minor mode is enabled in the current buffer, +evaluate `detached-shell-mode'. + +The mode's hook is called both when the mode is enabled and when +it is disabled. + +\(fn &optional ARG)" t nil) + +(autoload 'detached-log-mode "detached" "\ +Major mode for `detached' logs. + +\(fn)" t nil) + +(register-definition-prefixes "detached" '("detached-")) + +;;;*** + +;;;### (autoloads nil "detached-compile" "detached-compile.el" (0 +;;;;;; 0 0 0)) +;;; Generated autoloads from detached-compile.el + +(autoload 'detached-compile "detached-compile" "\ +Run COMMAND through `compile' but in a `detached' session. +Optionally enable COMINT if prefix-argument is provided. + +\(fn COMMAND &optional COMINT)" t nil) + +(autoload 'detached-compile-recompile "detached-compile" "\ +Re-compile by running `compile' but in a `detached' session. +Optionally EDIT-COMMAND. + +\(fn &optional EDIT-COMMAND)" t nil) + +(autoload 'detached-compile-attach "detached-compile" "\ +Attach to SESSION with `compile'. + +\(fn SESSION)" nil nil) + +(autoload 'detached-compile-start-session "detached-compile" "\ +Start SESSION with `detached-compile'. + +\(fn SESSION)" nil nil) + +(autoload 'detached-compile--start "detached-compile" "\ +Run in `compilation-start-hook' if `detached-enabled'. + +\(fn _)" nil nil) + +(autoload 'detached-compilation-mode "detached-compile" "\ +Major mode for `detached' compilation. + +\(fn)" t nil) + +(register-definition-prefixes "detached-compile" '("detached-compil")) + +;;;*** + +;;;### (autoloads nil "detached-consult" "detached-consult.el" (0 +;;;;;; 0 0 0)) +;;; Generated autoloads from detached-consult.el + +(autoload 'detached-consult-session "detached-consult" "\ +Enhanced `detached-open-session' command." t nil) + +(register-definition-prefixes "detached-consult" '("detached-consult-")) + +;;;*** + +;;;### (autoloads nil "detached-dired" "detached-dired.el" (0 0 0 +;;;;;; 0)) +;;; Generated autoloads from detached-dired.el + +(autoload 'detached-dired-do-shell-command "detached-dired" "\ +Ensure `detached' is used before running DIRED-DO-SHELL-COMMAND with ARGS. + +\(fn DIRED-DO-SHELL-COMMAND &rest ARGS)" nil nil) + +;;;*** + +;;;### (autoloads nil "detached-eshell" "detached-eshell.el" (0 0 +;;;;;; 0 0)) +;;; Generated autoloads from detached-eshell.el + +(autoload 'detached-eshell-external-command "detached-eshell" "\ +Advice ORIG-FUN to optionally use `detached' on ARGS. + +\(fn ORIG-FUN &rest ARGS)" nil nil) + +(autoload 'detached-eshell-mode "detached-eshell" "\ +Integrate `detached' in `eshell-mode'. + +This is a minor mode. If called interactively, toggle the +`detached-eshell mode' mode. If the prefix argument is positive, +enable the mode, and if it is zero or negative, disable the mode. + +If called from Lisp, toggle the mode if ARG is `toggle'. Enable +the mode if ARG is nil, omitted, or is a positive number. +Disable the mode if ARG is a negative number. + +To check whether the minor mode is enabled in the current buffer, +evaluate `detached-eshell-mode'. + +The mode's hook is called both when the mode is enabled and when +it is disabled. + +\(fn &optional ARG)" t nil) + +(register-definition-prefixes "detached-eshell" '("detached-eshell-")) + +;;;*** + +;;;### (autoloads nil "detached-extra" "detached-extra.el" (0 0 0 +;;;;;; 0)) +;;; Generated autoloads from detached-extra.el + +(autoload 'detached-extra-projectile-run-compilation "detached-extra" "\ +If CMD is a string execute it with `detached-compile'. + +Optionally USE-COMINT-MODE + +\(fn CMD &optional USE-COMINT-MODE)" nil nil) + +(autoload 'detached-extra-dired-rsync "detached-extra" "\ +Run COMMAND with `detached'. + +\(fn COMMAND DETAILS)" nil nil) + +(autoload 'detached-extra-alert-notification "detached-extra" "\ +Send an `alert' notification when SESSION becomes inactive. + +\(fn SESSION)" nil nil) + +;;;*** + +;;;### (autoloads nil "detached-init" "detached-init.el" (0 0 0 0)) +;;; Generated autoloads from detached-init.el + +(autoload 'detached-init "detached-init" "\ +Initialize `detached' integration with all packages." nil nil) + +(register-definition-prefixes "detached-init" '("detached-")) + +;;;*** + +;;;### (autoloads nil "detached-list" "detached-list.el" (0 0 0 0)) +;;; Generated autoloads from detached-list.el + +(autoload 'detached-list-sessions "detached-list" "\ +Open list of `detached'." t nil) + +(register-definition-prefixes "detached-list" '("detached-")) + +;;;*** + +;;;### (autoloads nil "detached-org" "detached-org.el" (0 0 0 0)) +;;; Generated autoloads from detached-org.el + +(autoload 'detached-org-babel-sh "detached-org" "\ +Modify ARGS before calling ORG-BABEL-SH-EVALUATE-FUN. + +This function modifies the full-body in ARGS and replaces it with a +`detached' command. The functionality is enabled by setting a header +property of :detached t in the org babel src block. + +\(fn ORG-BABEL-SH-EVALUATE-FUN &rest ARGS)" nil nil) + +(register-definition-prefixes "detached-org" '("detached-org-session-action")) + +;;;*** + +;;;### (autoloads nil "detached-shell" "detached-shell.el" (0 0 0 +;;;;;; 0)) +;;; Generated autoloads from detached-shell.el + +(autoload 'detached-shell-override-history "detached-shell" "\ +Override history in ORIG-FUN with ARGS. + +This function also makes sure that the HISTFILE is disabled for local shells. + +\(fn ORIG-FUN &rest ARGS)" nil nil) + +(autoload 'detached-shell-save-history-on-kill "detached-shell" "\ +Add hook to save history when killing `shell' buffer." nil nil) + +(register-definition-prefixes "detached-shell" '("detached-shell-")) + +;;;*** + +;;;### (autoloads nil "detached-vterm" "detached-vterm.el" (0 0 0 +;;;;;; 0)) +;;; Generated autoloads from detached-vterm.el + +(autoload 'detached-vterm-mode "detached-vterm" "\ +Integrate `detached' in `vterm'. + +This is a minor mode. If called interactively, toggle the +`detached-vterm mode' mode. If the prefix argument is positive, +enable the mode, and if it is zero or negative, disable the mode. + +If called from Lisp, toggle the mode if ARG is `toggle'. Enable +the mode if ARG is nil, omitted, or is a positive number. +Disable the mode if ARG is a negative number. + +To check whether the minor mode is enabled in the current buffer, +evaluate `detached-vterm-mode'. + +The mode's hook is called both when the mode is enabled and when +it is disabled. + +\(fn &optional ARG)" t nil) + +(register-definition-prefixes "detached-vterm" '("detached-vterm-")) + +;;;*** + +;;;### (autoloads nil nil ("detached-pkg.el") (0 0 0 0)) + +;;;*** + +;; Local Variables: +;; version-control: never +;; no-byte-compile: t +;; no-update-autoloads: t +;; coding: utf-8 +;; End: +;;; detached-autoloads.el ends here diff --git a/elpa/detached-20221129.1430/detached-compile.el b/elpa/detached-20221129.1430/detached-compile.el new file mode 100644 index 00000000..0d3fade3 --- /dev/null +++ b/elpa/detached-20221129.1430/detached-compile.el @@ -0,0 +1,200 @@ +;;; detached-compile.el --- Detached integration for compile -*- lexical-binding: t -*- + +;; Copyright (C) 2022 Free Software Foundation, Inc. + +;; This file is part of GNU Emacs. + +;; This program is free software; you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation, either version 3 of the License, or +;; (at your option) any later version. + +;; This program is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. + +;; You should have received a copy of the GNU General Public License +;; along with this program. If not, see . + +;;; Commentary: + +;; This is a `detached' extension which provides integration for `compile'. + +;;; Code: + +;;;; Requirements + +(require 'compile) +(require 'detached) + +(declare-function ansi-color-compilation-filter "ansi-color") + +;;;; Variables + +(defcustom detached-compile-session-action + '(:attach detached-compile-attach + :view detached-compile-session + :run detached-compile-start-session) + "Actions for a session created with `detached-compile'." + :group 'detached + :type 'plist) + +;;;; Commands + +;;;###autoload +(defun detached-compile (command &optional comint) + "Run COMMAND through `compile' but in a `detached' session. +Optionally enable COMINT if prefix-argument is provided." + (interactive + (list + (let ((command (eval compile-command t))) + (if (or compilation-read-command current-prefix-arg) + (compilation-read-command command) + command)) + (consp current-prefix-arg))) + (let* ((detached-enabled t) + (detached-session-origin (or detached-session-origin 'compile)) + (detached-session-action (or detached-session-action + detached-compile-session-action)) + (detached-session-mode (or detached-session-mode 'attached))) + (compile command comint))) + +;;;###autoload +(defun detached-compile-recompile (&optional edit-command) + "Re-compile by running `compile' but in a `detached' session. +Optionally EDIT-COMMAND." + (interactive "P") + (detached-with-session detached-buffer-session + (let* ((detached-enabled t) + (detached-session-mode 'attached) + (detached-session-command + (if edit-command + (compilation-read-command + (detached-session-command detached-buffer-session)) + (detached-session-command detached-buffer-session))) + (detached-current-session (detached-create-session detached-session-command))) + (apply #'compilation-start `(,detached-session-command))))) + +(defun detached-compile-kill () + "Kill a `detached' session." + (interactive) + (detached-kill-session detached-buffer-session)) + +;;;;; Functions + +;;;###autoload +(defun detached-compile-attach (session) + "Attach to SESSION with `compile'." + (when (detached-valid-session session) + (detached-with-session session + (let* ((detached-enabled t) + (detached-session-mode 'attached)) + (compilation-start `(,detached-session-command)))))) + +;;;###autoload +(defun detached-compile-start-session (session) + "Start SESSION with `detached-compile'." + (detached-with-session session + (detached-compile detached-session-command))) + +;;;;; Support functions + +;;;###autoload +(defun detached-compile--start (_) + "Run in `compilation-start-hook' if `detached-enabled'." + (when detached-enabled + (setq-local default-directory (detached-session-working-directory detached-current-session)) + (setq detached-buffer-session detached-current-session) + (setq compile-command (detached-session-command detached-current-session)) + (setq compilation-arguments nil) + (detached-compile--replace-modesetter) + (when detached-filter-ansi-sequences + (add-hook 'compilation-filter-hook #'ansi-color-compilation-filter 0 t)) + (add-hook 'comint-preoutput-filter-functions #'detached--env-message-filter 0 t) + (add-hook 'comint-preoutput-filter-functions #'detached--dtach-eof-message-filter 0 t))) + +(defun detached-compile--compilation-start (compilation-start &rest args) + "Create a `detached' session before running COMPILATION-START with ARGS." + (if detached-enabled + (pcase-let* ((`(,command ,mode ,name-function ,highlight-regexp) args) + (detached-session-environment + (or detached-session-environment + `(:compilation-args ,(list (or mode 'detached-compilation-mode) + name-function + highlight-regexp)))) + (detached-current-session + (or detached-current-session + (detached-create-session command))) + (default-directory (if (detached--session-local-p detached-current-session) + (detached-session-directory detached-current-session) + (detached-session-working-directory detached-current-session)))) + (if (eq detached-session-mode 'detached) + (detached--start-session-process detached-current-session + (detached-session-start-command + detached-current-session + :type 'string)) + (unless (detached-session-active-p detached-current-session) + (detached-watch-session detached-current-session)) + (apply compilation-start `(,(if (detached-session-active-p detached-current-session) + (detached-session-attach-command detached-current-session + :type 'string) + (detached-session-start-command detached-current-session + :type 'string)) + ,@(detached-session-environment-property detached-current-session :compilation-args))))) + (apply compilation-start args))) + +(defun detached-compile--replace-modesetter () + "Replace the modsetter inserted by `compilation-start'." + (save-excursion + (let ((inhibit-read-only t) + (regexp (rx (regexp "^dtach ") (or "-c" "-a") (regexp ".*\.socket.*$")))) + (goto-char (point-min)) + (when (re-search-forward regexp nil t) + (delete-region (match-beginning 0) (match-end 0)) + (insert (detached-session-command detached-current-session)))))) + +(defun detached-compile--compilation-detached-filter () + "Filter to modify the output in a compilation buffer." + (let ((begin compilation-filter-start) + (end (copy-marker (point)))) + (save-excursion + (goto-char begin) + (when (re-search-forward "\n?Detached session.*\n?" end t) + (delete-region (match-beginning 0) (match-end 0)))))) + +(defun detached-compile--compilation-eof-filter () + "Filter to modify the output in a compilation buffer." + (let ((begin compilation-filter-start) + (end (copy-marker (point)))) + (save-excursion + (goto-char begin) + (when (re-search-forward (format "\n?%s\n" detached--dtach-eof-message) end t) + (delete-region (match-beginning 0) (match-end 0)))))) + +(cl-defmethod detached--detach-session ((_mode (derived-mode detached-compilation-mode))) + "Detach from session when MODE is `detached-compilation-mode'." + (detached--detach-from-comint-process) + (detached--quit-session-buffer)) + +;;;;; Major modes + +(defvar detached-compilation-mode-map + (let ((map (make-sparse-keymap))) + (define-key map (kbd "C-c C-k") #'detached-compile-kill) + (define-key map (kbd "C-c C-.") #'detached-describe-session) + (define-key map (kbd detached-detach-key) #'detached-detach-session) + map) + "Keymap for `detached-compilation-mode'.") + +;;;###autoload +(define-derived-mode detached-compilation-mode compilation-mode "Detached Compilation" + "Major mode for `detached' compilation." + (add-hook 'compilation-filter-hook #'detached-compile--compilation-eof-filter 0 t) + (add-hook 'compilation-filter-hook #'detached-compile--compilation-detached-filter 0 t)) + +(advice-add #'compilation-start :around #'detached-compile--compilation-start) + +(provide 'detached-compile) + +;;; detached-compile.el ends here diff --git a/elpa/detached-20221129.1430/detached-consult.el b/elpa/detached-20221129.1430/detached-consult.el new file mode 100644 index 00000000..5c6bf89c --- /dev/null +++ b/elpa/detached-20221129.1430/detached-consult.el @@ -0,0 +1,206 @@ +;;; detached-consult.el --- Detached interface using Consult multi sources -*- lexical-binding: t -*- + +;; Copyright (C) 2021-2022 Free Software Foundation, Inc. + +;; This file is part of GNU Emacs. + +;; This program is free software; you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation, either version 3 of the License, or +;; (at your option) any later version. + +;; This program is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. + +;; You should have received a copy of the GNU General Public License +;; along with this program. If not, see . + +;;; Commentary: + +;; This package integrates `detached' with `consult'[1]. The package +;; provides a command `detached-consult-session' which provides multiple session sources. +;; +;; [1] https://github.com/minad/consult + +;;; Code: + +;;;; Requirements + +(require 'detached) + +(declare-function consult--multi "consult") + +;;;; Variables + +(defcustom detached-consult-hidden-predicates nil + "Predicates for sessions that should be hidden." + :type '(repeat function) + :group 'detached) + +(defcustom detached-consult-sources + '(detached-consult--source-session + detached-consult--source-active-session + detached-consult--source-inactive-session + detached-consult--source-hidden-session + detached-consult--source-success-session + detached-consult--source-failure-session + detached-consult--source-local-session + detached-consult--source-remote-session + detached-consult--source-current-session) + "Sources used by `detached-consult-session'. + +See `consult-multi' for a description of the source values." + :type '(repeat symbol) + :group 'detached) + +(defvar detached-consult--source-session + `(:category detached + :annotate detached-session-annotation + :action (lambda (x) (detached-open-session (detached--decode-session x))) + :items + ,(lambda () + (mapcar #'car + (seq-remove + (lambda (x) + (seq-find (lambda (predicate) + (apply predicate `(,(cdr x)))) + detached-consult-hidden-predicates)) + (detached-session-candidates (detached-get-sessions)))))) + "All `detached' sessions as a source for `consult'.") + +(defvar detached-consult--source-hidden-session + `(:narrow (?\s . "Hidden") + :hidden t + :category detached + :annotate detached-session-annotation + :action (lambda (x) (detached-open-session (detached--decode-session x))) + :items + ,(lambda () + (mapcar #'car + (seq-filter + (lambda (x) + (seq-find (lambda (predicate) + (apply predicate `(,(cdr x)))) + detached-consult-hidden-predicates)) + (detached-session-candidates (detached-get-sessions)))))) + "Active `detached' sessions as a source for `consult'.") + +(defvar detached-consult--source-active-session + `(:narrow (?a . "Active") + :hidden t + :category detached + :annotate detached-session-annotation + :action (lambda (x) (detached-open-session (detached--decode-session x))) + :items + ,(lambda () + (mapcar #'car + (thread-last (detached-session-candidates (detached-get-sessions)) + (seq-map #'cdr) + (seq-filter #'detached-session-active-p))))) + "Active `detached' sessions as a source for `consult'.") + +(defvar detached-consult--source-inactive-session + `(:narrow (?i . "Inactive") + :hidden t + :category detached + :annotate detached-session-annotation + :action (lambda (x) (detached-open-session (detached--decode-session x))) + :items + ,(lambda () + (mapcar #'car + (thread-last (detached-session-candidates (detached-get-sessions)) + (seq-map #'cdr) + (seq-filter #'detached-session-inactive-p))))) + "Inactive `detached' sessions as a source for `consult'.") + +(defvar detached-consult--source-failure-session + `(:narrow (?f . "Failure") + :hidden t + :category detached + :annotate detached-session-annotation + :action (lambda (x) (detached-open-session (detached--decode-session x))) + :items + ,(lambda () + (mapcar #'car + (thread-last (detached-session-candidates (detached-get-sessions)) + (seq-map #'cdr) + (seq-filter #'detached-session-failed-p))))) + "Failed `detached' sessions as a source for `consult'.") + +(defvar detached-consult--source-success-session + `(:narrow (?s . "Success") + :hidden t + :category detached + :annotate detached-session-annotation + :action (lambda (x) (detached-open-session (detached--decode-session x))) + :items + ,(lambda () + (mapcar #'car + (thread-last (detached-session-candidates (detached-get-sessions)) + (seq-map #'cdr) + (seq-remove #'detached-session-failed-p))))) + "Successful `detached' sessions as a source for `consult'.") + +(defvar detached-consult--source-local-session + `(:narrow (?l . "Local Host") + :hidden t + :category detached + :annotate detached-session-annotation + :action (lambda (x) (detached-open-session (detached--decode-session x))) + :items + ,(lambda () + (mapcar #'car + (thread-last (detached-session-candidates (detached-get-sessions)) + (seq-map #'cdr) + (seq-filter #'detached-session-localhost-p)))) + "Local host `detached' sessions as a source for `consult'.")) + +(defvar detached-consult--source-remote-session + `(:narrow (?r . "Remote Host") + :hidden t + :category detached + :annotate detached-session-annotation + :action (lambda (x) (detached-open-session (detached--decode-session x))) + :items + ,(lambda () + (mapcar #'car + (thread-last (detached-session-candidates (detached-get-sessions)) + (seq-map #'cdr) + (seq-filter #'detached-session-remotehost-p))))) + "Remote host `detached' sessions as a source for `consult'.") + +(defvar detached-consult--source-current-session + `(:narrow (?c . "Current Host") + :hidden t + :category detached + :annotate detached-session-annotation + :action (lambda (x) (detached-open-session (detached--decode-session x))) + :items + ,(lambda () + (let ((host-name (car (detached--host)))) + (mapcar #'car + (thread-last (detached-session-candidates (detached-get-sessions)) + (seq-map #'cdr) + (seq-filter + (lambda (x) + (string= (detached-session-host-name x) host-name)))))))) + "Current host `detached' sessions as a source for `consult'.") + +;;;; Commands + +;;;###autoload +(defun detached-consult-session () + "Enhanced `detached-open-session' command." + (interactive) + (unless (require 'consult nil 'noerror) + (error "Install Consult to use detached-consult")) + (consult--multi detached-consult-sources + :prompt "Select session: " + :require-match t + :sort nil)) + +(provide 'detached-consult) + +;;; detached-consult.el ends here diff --git a/elpa/detached-20221129.1430/detached-dired.el b/elpa/detached-20221129.1430/detached-dired.el new file mode 100644 index 00000000..b20becbe --- /dev/null +++ b/elpa/detached-20221129.1430/detached-dired.el @@ -0,0 +1,48 @@ +;;; detached-dired.el --- Detached integration for dired -*- lexical-binding: t -*- + +;; Copyright (C) 2022 Free Software Foundation, Inc. + +;; This file is part of GNU Emacs. + +;; This program is free software; you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation, either version 3 of the License, or +;; (at your option) any later version. + +;; This program is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. + +;; You should have received a copy of the GNU General Public License +;; along with this program. If not, see . + +;;; Commentary: + +;; This package integrates `detached' with `dired'. + +;;; Code: + +;;;; Requirements + +(require 'dired) +(require 'detached) + +;;;; Functions + +;;;###autoload +(defun detached-dired-do-shell-command (dired-do-shell-command &rest args) + "Ensure `detached' is used before running DIRED-DO-SHELL-COMMAND with ARGS." + (cl-letf* ((detached-session-origin 'dired) + ((symbol-function #'dired-run-shell-command) + (lambda (command) + (let ((session (detached-create-session command))) + (detached-start-shell-command-session session)) + nil))) + (pcase-let* ((`(,command ,arg ,file-list) args) + (modified-args `(,(string-remove-suffix " &" command) ,arg ,file-list))) + (apply dired-do-shell-command modified-args)))) + +(provide 'detached-dired) + +;;; detached-dired.el ends here diff --git a/elpa/detached-20221129.1430/detached-eshell.el b/elpa/detached-20221129.1430/detached-eshell.el new file mode 100644 index 00000000..2c078f87 --- /dev/null +++ b/elpa/detached-20221129.1430/detached-eshell.el @@ -0,0 +1,141 @@ +;;; detached-eshell.el --- Detached integration for eshell -*- lexical-binding: t -*- + +;; Copyright (C) 2021-2022 Free Software Foundation, Inc. + +;; This file is part of GNU Emacs. + +;; This program is free software; you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation, either version 3 of the License, or +;; (at your option) any later version. + +;; This program is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. + +;; You should have received a copy of the GNU General Public License +;; along with this program. If not, see . + +;;; Commentary: + +;; This is a `detached' extension which provides integration for `eshell'. + +;;; Code: + +;;;; Requirements + +(require 'detached) +(require 'eshell) +(require 'esh-mode) +(require 'esh-ext) +(require 'em-hist) + +;;;; Variables + +(defcustom detached-eshell-session-action + '(:attach detached-shell-command-attach-session + :view detached-view-dwim + :run detached-start-shell-command-session) + "Actions for a session created with `detached-eshell'." + :group 'detached + :type 'plist) + +;;;; Functions + +;;;###autoload +(defun detached-eshell-external-command (orig-fun &rest args) + "Advice ORIG-FUN to optionally use `detached' on ARGS." + (let* ((detached-session-action detached-eshell-session-action) + (command (string-trim-right (string-join (flatten-list args) " "))) + (session (detached-create-session command)) + (command + (detached-session-start-command session + :type 'list))) + (advice-remove #'eshell-external-command #'detached-eshell-external-command) + (setq detached-buffer-session session) + (setq detached-enabled nil) + (apply orig-fun `(,(seq-first command) ,(seq-rest command))))) + +;;;; Commands + +(defun detached-eshell-send-input (&optional detached) + "Start a `detached-session' and attach to it, unless DETACHED." + (interactive "P") + (let* ((detached-session-origin 'eshell) + (detached-session-mode (if detached 'detached 'attached)) + (detached-enabled t) + (detached-current-session nil)) + (advice-add #'eshell-external-command :around #'detached-eshell-external-command) + (call-interactively #'eshell-send-input))) + +(defun detached-eshell-attach-session (session) + "Attach to SESSION." + (interactive + (list (detached-select-host-session))) + (when (detached-valid-session session) + (if (detached-session-active-p session) + (cl-letf* ((input + (detached-session-attach-command session + :type 'string)) + ((symbol-function #'eshell-add-to-history) #'ignore)) + (let ((kill-ring nil)) + (eshell-kill-input)) + ;; Hide the input from the user + (let ((begin (point)) + (end)) + (insert input) + (setq end (point)) + (overlay-put (make-overlay begin end) 'invisible t) + (overlay-put (make-overlay end end) 'before-string "[attached]") + (insert " ")) + (setq detached-buffer-session session) + (call-interactively #'eshell-send-input)) + (detached-open-session session)))) + +;;;; Support functions + +(defun detached-eshell--get-dtach-process () + "Return `eshell' process if `detached' is running." + (when-let* ((process (and eshell-process-list (caar eshell-process-list)))) + (and (string= (process-name process) "dtach") + process))) + +(cl-defmethod detached--detach-session ((_mode (derived-mode eshell-mode))) + "Detach from session when MODE is `eshell-mode'." + (when-let ((active-session (detached-session-active-p + (alist-get (detached-session-id detached-buffer-session) + detached--sessions))) + (dtach-process (detached-eshell--get-dtach-process))) + (setq detached-buffer-session nil) + (process-send-string dtach-process + detached--dtach-detach-character))) + +;;;; Minor mode + +(defvar detached-eshell-mode-map + (let ((map (make-sparse-keymap))) + (define-key map (kbd "") #'detached-eshell-send-input) + (define-key map (kbd "") #'detached-eshell-attach-session) + (define-key map (kbd "C-c C-.") #'detached-describe-session) + (define-key map (kbd detached-detach-key) #'detached-detach-session) + map) + "Keymap for `detached-eshell-mode'.") + +;;;###autoload +(define-minor-mode detached-eshell-mode + "Integrate `detached' in `eshell-mode'." + :lighter " detached-eshell" + :keymap (let ((map (make-sparse-keymap))) + map) + (make-local-variable 'eshell-preoutput-filter-functions) + (if detached-eshell-mode + (progn + (add-hook 'eshell-preoutput-filter-functions #'detached--env-message-filter) + (add-hook 'eshell-preoutput-filter-functions #'detached--dtach-eof-message-filter)) + (remove-hook 'eshell-preoutput-filter-functions #'detached--env-message-filter) + (remove-hook 'eshell-preoutput-filter-functions #'detached--dtach-eof-message-filter))) + +(provide 'detached-eshell) + +;;; detached-eshell.el ends here diff --git a/elpa/detached-20221129.1430/detached-extra.el b/elpa/detached-20221129.1430/detached-extra.el new file mode 100644 index 00000000..cb3e3f48 --- /dev/null +++ b/elpa/detached-20221129.1430/detached-extra.el @@ -0,0 +1,73 @@ +;;; detached-extra.el --- Detached integration for external packages -*- lexical-binding: t -*- + +;; Copyright (C) 2022 Free Software Foundation, Inc. + +;; This file is part of GNU Emacs. + +;; This program is free software; you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation, either version 3 of the License, or +;; (at your option) any later version. + +;; This program is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. + +;; You should have received a copy of the GNU General Public License +;; along with this program. If not, see . + +;;; Commentary: + +;; This package provides a collection of functionality to integrate `detached' with external packages. + +;;; Code: + +;;;; Requirements + +(declare-function detached-compile "detached") +(declare-function detached-start-session "detached") +(declare-function detached-session-command "detached") + +(declare-function alert "alert") + +(defvar detached-session-origin) +(defvar detached-local-session) + +;;;; Functions + +;;;###autoload +(defun detached-extra-projectile-run-compilation (cmd &optional use-comint-mode) + "If CMD is a string execute it with `detached-compile'. + +Optionally USE-COMINT-MODE" + (if (functionp cmd) + (funcall cmd) + (let ((detached-session-origin 'projectile)) + (detached-compile cmd use-comint-mode)))) + +;;;###autoload +(defun detached-extra-dired-rsync (command _details) + "Run COMMAND with `detached'." + (let* ((detached-local-session t) + (detached-session-origin 'rsync) + (detached-session-mode 'detached) + (session (detached-create-session command))) + (detached-start-session session))) + +;;;###autoload +(defun detached-extra-alert-notification (session) + "Send an `alert' notification when SESSION becomes inactive." + (let ((status (detached-session-status session)) + (host (detached-session-host-name session))) + (alert (detached-session-command session) + :title (pcase status + ('success (format "Detached finished [%s]" host)) + ('failure (format "Detached failed [%s]" host))) + :severity (pcase status + ('success 'moderate) + ('failure 'high))))) + +(provide 'detached-extra) + +;;; detached-extra.el ends here diff --git a/elpa/detached-20221129.1430/detached-init.el b/elpa/detached-20221129.1430/detached-init.el new file mode 100644 index 00000000..aa8e4ded --- /dev/null +++ b/elpa/detached-20221129.1430/detached-init.el @@ -0,0 +1,198 @@ +;;; detached-init.el --- Initialize detached -*- lexical-binding: t -*- + +;; Copyright (C) 2022 Free Software Foundation, Inc. + +;; This file is part of GNU Emacs. + +;; This program is free software; you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation, either version 3 of the License, or +;; (at your option) any later version. + +;; This program is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. + +;; You should have received a copy of the GNU General Public License +;; along with this program. If not, see . + +;;; Commentary: + +;; This is intended to aid users in configuring `detached's integration with other packages. + +;;; Code: + +;;;; Requirements + +(require 'subr-x) + +(declare-function detached-attach-session "detached") +(declare-function detached-compile-session "detached") +(declare-function detached-delete-session "detached") +(declare-function detached-insert-session-command "detached") +(declare-function detached-open-session-directory "detached") +(declare-function detached-kill-session "detached") +(declare-function detached-rerun-session "detached") +(declare-function detached-view-session "detached") +(declare-function detached-copy-session-command "detached") +(declare-function detached-copy-session-output "detached") +(declare-function detached-diff-session "detached") +(declare-function detached-initialize-sessions "detached") +(declare-function detached-shell-mode "detached") + +(declare-function detached-compile--start "detached-compile") +(declare-function detached-dired-do-shell-command "detached-dired") +(declare-function detached-eshell-mode "detached-eshell") +(declare-function detached-extra-projectile-run-compilation "detached-extra") +(declare-function detached-extra-dired-rsync "detached-extra") +(declare-function detached-list--db-update "detached-list") +(declare-function detached-list--apply-filter "detached-list") +(declare-function detached-org-babel-sh "detached-org") +(declare-function detached-shell-override-history "detached-shell") +(declare-function detached-shell-save-history-on-kill "detached-shell") +(declare-function detached-vterm-mode "detached-vterm") + +(declare-function org-babel-sh-evaluate "ob-shell") +(declare-function dired-rsync--do-run "dired-rsync") +(declare-function dired-rsync "dired-rsync") +(declare-function projectile "projectile") +(declare-function vterm "vterm") + +(defvar detached-list-filters) + +(defvar embark-general-map) +(defvar embark-keymap-alist) +(defvar nano-modeline-mode-formats) + +;;;; Variables + +(defcustom detached-init-block-list nil + "A list of blocked packages." + :group 'detached + :type 'list) + +(defcustom detached-init-allow-list + '(compile dired dired-rsync embark eshell nano-modeline org projectile shell vterm) + "A list of allowed packages." + :group 'detached + :type 'list) + +(defvar detached-embark-action-map + (let ((map (make-sparse-keymap))) + (define-key map "a" #'detached-attach-session) + (define-key map "c" #'detached-compile-session) + (define-key map "d" #'detached-delete-session) + (define-key map "i" #'detached-insert-session-command) + (define-key map "f" #'detached-open-session-directory) + (define-key map "k" #'detached-kill-session) + (define-key map "r" #'detached-rerun-session) + (define-key map "v" #'detached-view-session) + (define-key map "w" #'detached-copy-session-command) + (define-key map "W" #'detached-copy-session-output) + (define-key map "=" #'detached-diff-session) + map)) + +(defvar detached-init-package-integration '((compile . detached-init--compile) + (dired . detached-init--dired) + (dired-rsync . detached-init--dired-rsync) + (embark . detached-init--embark) + (eshell . detached-init--eshell) + (nano-modeline . detached-init--nano-modeline) + (org . detached-init--org) + (projectile . detached-init--projectile) + (shell . detached-init--shell) + (vterm . detached-init--vterm)) + "Alist which contain names of packages and their initialization function.") + +;;;; Functions + +;;;###autoload +(defun detached-init () + "Initialize `detached' integration with all packages." + (detached-init--detached) + (detached-init--detached-list) + (let ((init-functions + (thread-last detached-init-package-integration + (seq-filter (lambda (it) + (member (car it) detached-init-allow-list))) + (seq-remove (lambda (it) + (member (car it) detached-init-block-list))) + (seq-map #'cdr)))) + (dolist (init-function init-functions) + (funcall init-function)))) + +;;;; Support functions + +(defun detached-init--shell () + "Initialize integration with `shell'." + (advice-add 'shell :around #'detached-shell-override-history) + (add-hook 'shell-mode-hook #'detached-shell-save-history-on-kill)) + +(defun detached-init--compile () + "Initialize integration with `compile'." + (add-hook 'compilation-start-hook #'detached-compile--start) + (add-hook 'compilation-shell-minor-mode-hook #'detached-shell-mode)) + +(defun detached-init--eshell () + "Initialize integration with `eshell'." + (add-hook 'eshell-mode-hook #'detached-eshell-mode)) + +(defun detached-init--org () + "Initialize integration with `org'." + (advice-add 'org-babel-sh-evaluate + :around #'detached-org-babel-sh)) + +(defun detached-init--dired () + "Initialize integration with `dired'." + (advice-add 'dired-do-shell-command + :around #'detached-dired-do-shell-command)) + +(defun detached-init--dired-rsync () + "Initialize integration with `dired-rsync'." + (advice-add 'dired-rsync--do-run + :override #'detached-extra-dired-rsync)) + +(defun detached-init--projectile () + "Initialize integration with `projectile'." + (advice-add 'projectile-run-compilation + :override #'detached-extra-projectile-run-compilation)) + +(defun detached-init--vterm () + "Initialize integration with `vterm'." + (with-eval-after-load 'vterm + (add-hook 'vterm-mode-hook #'detached-vterm-mode))) + +(defun detached-init--embark () + "Initialize integration with `embark'." + (with-eval-after-load 'embark + (defvar embark-detached-map (make-composed-keymap detached-embark-action-map embark-general-map)) + (add-to-list 'embark-keymap-alist '(detached . embark-detached-map)))) + +(defun detached-init--nano-modeline () + "Initialize integration with `nano-modeline'." + (with-eval-after-load 'nano-modeline + (push `(detached-list-mode + :mode-p (lambda () (derived-mode-p 'detached-list-mode)) + :format (lambda () (nano-modeline-render nil (detached-list--mode-line-indicator) "" ""))) + nano-modeline-mode-formats))) + +(defun detached-init--detached-list () + "Initialize `detached-list'." + ;; Trigger initialization of sessions upon load of `detached-list' + (with-eval-after-load 'detached-list + (detached-list--apply-filter + (cdr (car detached-list-filters))) + (add-hook 'detached-update-db-hooks #'detached-list--db-update))) + +(defun detached-init--detached () + "Initialize `detached'." + ;; Trigger initialization of sessions upon load of `detached' + (with-eval-after-load 'detached + (detached-initialize-sessions)) + ;; Required for `detached-shell-command' which is always provided + (add-hook 'shell-mode-hook #'detached-shell-mode)) + +(provide 'detached-init) + +;;; detached-init.el ends here diff --git a/elpa/detached-20221129.1430/detached-list.el b/elpa/detached-20221129.1430/detached-list.el new file mode 100644 index 00000000..66ceafd0 --- /dev/null +++ b/elpa/detached-20221129.1430/detached-list.el @@ -0,0 +1,943 @@ +;;; detached-list.el --- Manage detached sessions -*- lexical-binding: t -*- + +;; Copyright (C) 2022 Free Software Foundation, Inc. + +;; This file is part of GNU Emacs. + +;; This program is free software; you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation, either version 3 of the License, or +;; (at your option) any later version. + +;; This program is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. + +;; You should have received a copy of the GNU General Public License +;; along with this program. If not, see . + +;;; Commentary: + +;; This is an interface to manage `detached' sessions. + +;;; Code: + +;;;; Requirements + +(require 'detached) +(require 'hl-line) +(require 'imenu) +(require 'tabulated-list) + +;;;; Variables + +(defcustom detached-list-config + `((:name "Command" :function detached-list--command-str :length 90) + (:name "Status" :function detached-list--status-str :length 10) + (:name "Host" :function detached--host-str :length 15 :face detached-host-face) + (:name "Directory" :function detached--working-dir-str :length 40 :face detached-working-dir-face) + (:name "Metadata" :function detached--metadata-str :length 30 :face detached-metadata-face) + (:name "Duration" :function detached--duration-str :length 10 :face detached-duration-face) + (:name "Created" :function detached--creation-str :length 20 :face detached-creation-face)) + "Configuration for `detached' list mode." + :type '(repeat (plist :options ((:name symbol) + (:function symbol) + (:length symbol) + (:face symbol)))) + :group 'detached) + +(defcustom detached-list-display-buffer-action + '(display-buffer-same-window + (inhibit-same-window . nil)) + "The action used to display the detached list buffer." + :group 'detached + :type 'sexp) + +(defcustom detached-list-open-session-display-buffer-action + '(display-buffer-same-window + (inhibit-same-window . nil)) + "The action used to display a detached session." + :group 'detached + :type 'sexp) + +(defcustom detached-list-filters nil + "An alist of custom filters that can be applied. + +The filters are built using the different narrow functions that +detached list implements." + :group 'detached + :type '(alist :key-type string)) + +(defcustom detached-list-session-identifier-function + #'detached-session-identifier + "The function to use for identifying a session." + :group 'detached + :type 'sexp) + +(defcustom detached-list-state-symbols + '((unknown . "-") + (active . "*") + (failure . "!") + (success . " ") + (initially-attached . "o") + (initially-detached . " ")) + "An alist of symbols to use to communicate different states." + :group 'detached + :type '(alist :key-type symbol :value-type string)) + +;;;; Private + +(defvar detached-list--marked-sessions nil + "A list of marked session ids.") +(defvar detached-list--narrow-criteria nil + "A list of criteria to apply when displaying the sessions.") + +;;;; Functions + +(defun detached-list-imenu-index () + "Create an `imenu' index for `detached-list'." + (let ((index)) + (goto-char (point-min)) + (while (not (eobp)) + (let ((session (tabulated-list-get-id))) + (push `(,(detached-session-command session) . ,(point)) + index)) + (forward-line 1)) + (seq-reverse index))) + +(defun detached-list-eldoc (_callback) + "A member of `eldoc-documentation-functions', for signatures." + (let ((session (tabulated-list-get-id))) + (when (detached-session-p session) + (let ((strs `(,(when-let ((annotation (detached--session-annotation session))) + (propertize annotation 'face 'detached-annotation-face)) + ,(detached-session-command session)))) + (string-join (seq-remove #'null strs) "\n"))))) + +;;;; Commands + +(defun detached-list-initialize-session-directory (&optional all) + "Initialize a session-directory. + +Optionally initialize ALL session-directories." + (interactive "P") + (if-let* ((uninitialized-directories + (thread-last (detached-get-sessions) + (seq-filter #'detached-session-uninitialized-p) + (seq-map #'detached-session-directory) + (seq-uniq)))) + (if all + (seq-do #'detached-list--initialize-directory uninitialized-directories) + (when-let ((directory (completing-read "Initialize directory: " uninitialized-directories))) + (detached-list--initialize-directory directory))) + (message "All session directories have been initialized"))) + +(defun detached-list-quit () + "Quit command." + (interactive) + (if (= (length (window-list)) 1) + (bury-buffer) + (delete-window))) + +(defun detached-list-remove-narrow-criterion () + "Remove narrow criterion." + (interactive) + (if detached-list--narrow-criteria + (detached-list-narrow-sessions + (butlast detached-list--narrow-criteria)) + (message "No criterion to remove"))) + +(defun detached-list-widen () + "Remove all narrowing restrictions." + (interactive) + (when detached-list--narrow-criteria + (detached-list-narrow-sessions nil))) + +(defun detached-list-detach-from-session (session) + "Detach from SESSION at point." + (interactive + (list (tabulated-list-get-id))) + (when-let* ((buffer (detached-list--attached-p session))) + (unless (get-buffer-window buffer) + (pop-to-buffer buffer)) + (with-selected-window (get-buffer-window buffer) + (detached-detach-session)))) + +(defun detached-list-jump-to-directory (session) + "Jump to SESSION at point's directory." + (interactive + (list (tabulated-list-get-id))) + (detached-open-session-directory session)) + +(defun detached-list-kill-session () + "Send a TERM signal to sessions at point, or all marked sessions. + +Optionally DELETE the session if prefix-argument is provided." + (interactive) + (when (y-or-n-p (if detached-list--marked-sessions + "Kill all marked sessions? " + "Kill session at point? ")) + (seq-do + (lambda (session) + (detached-list--unmark-session session) + (detached-kill-session session current-prefix-arg)) + (detached-list--get-marked-or-current-sessions)) + (detached-list-revert))) + +(defun detached-list-view-session (session) + "View SESSION." + (interactive + (list (detached-session-in-context))) + (let ((detached-open-session-display-buffer-action + detached-list-open-session-display-buffer-action)) + (detached-view-dwim session))) + +(defun detached-list-edit-and-run-session (session &optional toggle-suppress-output) + "Edit and run SESSION at point. + +Optionally TOGGLE-SUPPRESS-OUTPUT." + (interactive + (list (tabulated-list-get-id) + current-prefix-arg)) + (let ((detached-session-mode + (if toggle-suppress-output + (if (eq 'detached (detached--session-initial-mode session)) + 'attached + 'detached) + (detached--session-initial-mode session)))) + (unless (eq detached-session-mode 'detached) + (when-let ((single-window (> (length (window-list)) 1)) + (buffer (current-buffer))) + (delete-window (get-buffer-window)) + (bury-buffer buffer))) + (detached-edit-and-run-session session))) + +(defun detached-list-rerun-session (session &optional toggle-session-mode) + "Rerun SESSION at point. + +Optionally TOGGLE-SESSION-MODE." + (interactive + (list (tabulated-list-get-id) + current-prefix-arg)) + (let ((detached-session-mode + (if toggle-session-mode + (if (eq 'attached (detached--session-initial-mode session)) + 'attached + 'detached) + (detached--session-initial-mode session)))) + (unless (eq detached-session-mode 'detached) + (when-let ((single-window (> (length (window-list)) 1)) + (buffer (current-buffer))) + (delete-window (get-buffer-window)) + (bury-buffer buffer))) + (detached-rerun-session session))) + +(defun detached-list-diff-marked-sessions () + "Diff two sessions." + (interactive) + (if (= (length detached-list--marked-sessions) 2) + (progn + (when-let ((single-window (> (length (window-list)) 1)) + (buffer (current-buffer))) + (delete-window (get-buffer-window)) + (bury-buffer buffer)) + (apply #'detached-diff-session detached-list--marked-sessions)) + (message "Mark two sessions"))) + +(defun detached-list-open-session () + "View session." + (interactive) + (let ((session (tabulated-list-get-id)) + (detached-open-session-display-buffer-action + detached-list-open-session-display-buffer-action)) + (when-let ((single-window (> (length (window-list)) 1)) + (buffer (current-buffer))) + (delete-window (get-buffer-window)) + (bury-buffer buffer)) + (detached-open-session session))) + +(defun detached-list-narrow-unique () + "Narrow to unique sessions." + (interactive) + (when detached-list-session-identifier-function + (detached-list-narrow-sessions + `(,@detached-list--narrow-criteria + ("Unique" . + ,(lambda (sessions) + (thread-last sessions + (seq-group-by detached-list-session-identifier-function) + (seq-map (lambda (it) + (pcase-let ((`(,_identifier . ,duplicate-sessions) it)) + (car duplicate-sessions)))) + (seq-sort-by #'detached-session-start-time #'>)))))))) + +(defun detached-list-narrow-after-time (time-threshold) + "Narrow to session's created after TIME-THRESHOLD." + (interactive + (list + (read-string "Enter time: "))) + (when time-threshold + (if-let ((parsed-threshold (detached--list-parse-time time-threshold))) + (detached-list-narrow-sessions + `(,@detached-list--narrow-criteria + (,(format "+%s" time-threshold) . + ,(lambda (sessions) + (let ((current-time (time-to-seconds (current-time)))) + (seq-filter (lambda (it) + (< (- current-time + (detached-session-start-time it)) + parsed-threshold)) + sessions)))))) + (message "Cannot parse time")))) + +(defun detached-list-narrow-before-time (time-threshold) + "Narrow to session's created before TIME-THRESHOLD." + (interactive + (list + (read-string "Enter time: "))) + (when time-threshold + (if-let ((parsed-threshold (detached--list-parse-time time-threshold))) + (detached-list-narrow-sessions + `(,@detached-list--narrow-criteria + (,(format "-%s" time-threshold) . + ,(lambda (sessions) + (let ((current-time (time-to-seconds (current-time)))) + (seq-filter (lambda (it) + (> (- current-time + (detached-session-start-time it)) + parsed-threshold)) + sessions)))))) + (message "Cannot parse time")))) + +(defun detached-list-narrow-host (hostname) + "Narrow to sessions from a selected HOSTNAME." + (interactive + (list + (when-let* ((hostnames + (thread-last (detached-list--get-narrowed-sessions) + (seq-map #'detached-session-host-name) + (seq-uniq)))) + (completing-read + "Select host: " + hostnames)))) + (when hostname + (detached-list-narrow-sessions + `(,@detached-list--narrow-criteria + (,(concat "Host: " hostname) . + ,(lambda (sessions) + (seq-filter (lambda (it) + (string-match hostname + (detached-session-host-name it))) + sessions))))))) + +(defun detached-list-narrow-output (regexp) + "Narrow to sessions which output contain REGEXP." + (interactive + (list (read-regexp + "Filter session outputs containing (regexp): "))) + (when regexp + (detached-list-narrow-sessions + `(,@detached-list--narrow-criteria + (,(concat "Output: " regexp) . + ,(lambda (sessions) + (detached--grep-sesssions-output sessions regexp))))))) + +(defun detached-list-narrow-command (regexp) + "Narrow to sessions which command match REGEXP." + (interactive + (list + (if current-prefix-arg + (regexp-quote + (detached-session-command + (detached-session-in-context))) + (read-regexp + "Filter session commands containing (regexp): ")))) + (when regexp + (detached-list-narrow-sessions + `(,@detached-list--narrow-criteria + (,(concat "Command: " regexp) . + ,(lambda (sessions) + (seq-filter (lambda (it) + (string-match regexp + (detached-session-command it))) + sessions))))))) + +(defun detached-list-narrow-working-directory (regexp) + "Narrow to sessions with working directory matching REGEXP." + (interactive + (list + (if current-prefix-arg + (regexp-quote + (detached-session-working-directory + (detached-session-in-context))) + (read-regexp + "Filter session working directories containing (regexp): ")))) + (when regexp + (detached-list-narrow-sessions + `(,@detached-list--narrow-criteria + (,(concat "Working-Directory: " regexp) . + ,(lambda (sessions) + (seq-filter (lambda (it) + (string-match regexp + (detached-session-working-directory it))) + sessions))))))) + +(defun detached-list-narrow-session-directory (session-directory) + "Narrow to sessions with SESSION-DIRECTORY." + (interactive + (list + (when-let* ((directories + (thread-last (detached-list--get-narrowed-sessions) + (seq-map #'detached-session-directory) + (seq-uniq)))) + (completing-read + "Select session directory: " + directories)))) + (when session-directory + (detached-list-narrow-sessions + `(,@detached-list--narrow-criteria + (,(concat "Session-Directory: " session-directory) . + ,(lambda (sessions) + (seq-filter (lambda (it) + (string-match session-directory + (detached-session-directory it))) + sessions))))))) + +(defun detached-list-narrow-annotation (regexp) + "Narrow to sessions which annotation match REGEXP." + (interactive + (list (read-regexp + "Filter session annotations containing (regexp): "))) + (when regexp + (detached-list-narrow-sessions + `(,@detached-list--narrow-criteria + (,(concat "Annotation: " regexp) . + ,(lambda (sessions) + (seq-filter (lambda (it) + (when-let ((annotation (detached--session-annotation it))) + (string-match regexp annotation))) + sessions))))))) + +(defun detached-list-narrow-localhost () + "Narrow to local-host sessions." + (interactive) + (detached-list-narrow-sessions + `(,@detached-list--narrow-criteria + ("Localhost" . + ,(lambda (sessions) + (seq-filter #'detached-session-localhost-p sessions)))))) + +(defun detached-list-narrow-remotehost () + "Narrow to remote-host sessions." + (interactive) + (detached-list-narrow-sessions + `(,@detached-list--narrow-criteria + ("Remotehost" . + ,(lambda (sessions) + (seq-filter #'detached-session-remotehost-p sessions)))))) + +(defun detached-list-narrow-currenthost () + "Narrow to current-host sessions." + (interactive) + (detached-list-narrow-sessions + `(,@detached-list--narrow-criteria + ("Currenthost" . + ,(lambda (sessions) + (let ((current-host (car (detached--host)))) + (seq-filter (lambda (it) + (string= (detached-session-host-name it) current-host)) + sessions))))))) + +(defun detached-list-select-filter () + "Select filter from `detached-list-filter' to apply." + (interactive) + (when-let* ((metadata `(metadata + (category . detached) + (display-sort-function . identity))) + (collection (lambda (string predicate action) + (if (eq action 'metadata) + metadata + (complete-with-action action detached-list-filters string predicate)))) + (filter-name + (completing-read "Select filter: " collection nil t))) + (detached-list--apply-filter + (alist-get filter-name detached-list-filters nil nil #'string=)))) + +(defun detached-list-narrow-origin (origin) + "Narrow to sessions with a specific ORIGIN." + (interactive + (list + (when-let ((origins + (thread-last (detached-list--get-narrowed-sessions) + (seq-map #'detached--session-origin) + (seq-uniq) + (seq-remove #'null) + (seq-map #'symbol-name)))) + (completing-read + "Select origin: " + origins)))) + (when origin + (detached-list-narrow-sessions + `(,@detached-list--narrow-criteria + (,(concat "Origin: " origin) . + ,(lambda (sessions) + (seq-filter + (lambda (it) + (string-match origin + (symbol-name (detached--session-origin it)))) + sessions))))))) + +(defun detached-list-narrow-active () + "Narrow to active sessions." + (interactive) + (detached-list-narrow-sessions + `(,@detached-list--narrow-criteria + ("Active" . + ,(lambda (sessions) + (seq-filter #'detached-session-active-p sessions)))))) + +(defun detached-list-narrow-inactive () + "Narrow to inactive sessions." + (interactive) + (detached-list-narrow-sessions + `(,@detached-list--narrow-criteria + ("Inactive" . + ,(lambda (sessions) + (seq-remove #'detached-session-active-p sessions)))))) + +(defun detached-list-narrow-success () + "Narrow to successful sessions." + (interactive) + (detached-list-narrow-sessions + `(,@detached-list--narrow-criteria + ("Success" . + ,(lambda (sessions) + (seq-remove #'detached-session-failed-p sessions)))))) + +(defun detached-list-narrow-failure () + "Narrow to failed sessions." + (interactive) + (detached-list-narrow-sessions + `(,@detached-list--narrow-criteria + ("Failure" . + ,(lambda (sessions) + (seq-filter #'detached-session-failed-p sessions)))))) + +(defun detached-list-mark-regexp (regexp) + "Mark sessions which command match REGEXP. + +If prefix-argument is provided unmark instead of mark." + (interactive + (list (read-regexp + (concat (if current-prefix-arg "Unmark" "Mark") + " session commands containing (regexp): ")))) + (save-excursion + (goto-char (point-min)) + (while (not (eobp)) + (let ((session (tabulated-list-get-id))) + (when (string-match regexp (detached-session-command session)) + (if current-prefix-arg + (detached-list--unmark-session session) + (detached-list--mark-session session)))) + (forward-line)))) + +(defun detached-list-delete-session () + "Delete session at point, or all marked sessions." + (interactive) + (when (y-or-n-p (if detached-list--marked-sessions + "Delete all marked sessions? " + "Delete session at point? ")) + (let ((detached--update-database nil)) + (seq-do + (lambda (session) + (detached-list--unmark-session session) + (detached-delete-session session)) + (detached-list--get-marked-or-current-sessions))) + (detached--db-update-sessions) + (detached-list-revert))) + +(defun detached-list-mark-session () + "Mark session at point and advance to next session." + (interactive) + (let* ((session (tabulated-list-get-id))) + (detached-list--mark-session session) + (forward-line))) + +(defun detached-list-unmark-session () + "Unmark session at point and advance to next session." + (interactive) + (let* ((session (tabulated-list-get-id))) + (detached-list--unmark-session session) + (forward-line))) + +(defun detached-list-unmark-sessions () + "Unmark all sessions." + (interactive) + (setq detached-list--marked-sessions nil) + (detached-list-revert)) + +(defun detached-list-toggle-mark-session () + "Toggle mark on session at point." + (interactive) + (let* ((session (tabulated-list-get-id))) + (if (detached-list--marked-session-p session) + (detached-list--unmark-session session) + (detached-list--mark-session session)))) + +(defun detached-list-toggle-sessions () + "Toggle mark on all sessions." + (interactive) + (let* ((sessions (seq-map #'car tabulated-list-entries)) + (unmarked-sessions + (seq-remove + (lambda (session) + (seq-find + (lambda (marked-session) + (eq (detached-session-id marked-session) + (detached-session-id session))) + detached-list--marked-sessions)) + sessions))) + (setq detached-list--marked-sessions unmarked-sessions) + (detached-list-revert))) + +(defun detached-list-revert () + "Update content in buffer." + (interactive) + (tabulated-list-revert) + (detached-list--restore-marks) + (when hl-line-mode (hl-line-highlight))) + +;;;###autoload +(defun detached-list-sessions () + "Open list of `detached'." + (interactive) + (let* ((current-directory default-directory) + (buffer (get-buffer-create "*detached-list*")) + (window + (or + (get-buffer-window buffer) + (display-buffer buffer detached-list-display-buffer-action)))) + (select-window window) + (setq-local default-directory current-directory) + (unless (eq major-mode 'detached-list-mode) + (detached-list-mode) + (setq tabulated-list-entries + (seq-map #'detached-list--get-entry + (detached-list--get-narrowed-sessions))) + (tabulated-list-print t)))) + +(defun detached-list-narrow-sessions (criteria) + "Narrow session(s) based on CRITERIA." + (setq detached-list--narrow-criteria criteria) + (when (eq major-mode 'detached-list-mode) + (setq tabulated-list-entries + (seq-map #'detached-list--get-entry + (detached-list--get-narrowed-sessions))) + (tabulated-list-print t) + (detached-list-revert))) + +;;;; Support functions + +(defun detached-list--apply-filter (filter) + "Apply FILTER." + (setq detached-list--narrow-criteria nil) + (seq-do (lambda (criteria) (apply criteria)) filter)) + +(defun detached--revert-selection-change (&rest _) + "Revert function to add to `window-selection-change-functions'." + (detached-list-revert)) + +(defun detached-list--initialize-directory (directory) + "Initialize sessions in DIRECTORY." + (thread-last (detached-get-sessions) + (seq-filter (lambda (it) (string= directory (detached-session-directory it)))) + (seq-do #'detached--initialize-session))) + +(defun detached--list-parse-time (time) + "Return a value in seconds based on TIME." + (when time + (cond ((string-match (rx (group (one-or-more digit)) "h") time) + (* 60 60 (string-to-number (match-string 1 time)))) + ((string-match (rx (group (one-or-more digit)) space "day" (zero-or-more "s")) time) + (* 60 60 24 (string-to-number (match-string 1 time)))) + ((string-match (rx (group (one-or-more digit)) space "week" (zero-or-more "s")) time) + (* 60 60 24 7 (string-to-number (match-string 1 time)))) + ((string-match (rx (group (one-or-more digit)) space "month" (zero-or-more "s")) time) + (* 60 60 24 7 30 (string-to-number (match-string 1 time)))) + ('t nil)))) + +(defun detached-list--db-update () + "Function to run when the database is updated." + (when-let ((detached-list-buffer (detached-list--get-list-mode-buffer))) + (with-current-buffer detached-list-buffer + (detached-list-revert)))) + +(defun detached-list--get-list-mode-buffer () + "Return buffer with `detached-list-mode'." + (seq-find (lambda (buffer) + (with-current-buffer buffer + (eq major-mode 'detached-list-mode))) + (buffer-list))) + +(defun detached-list--revert-sessions () + "Recompute `tabulated-list-entries'." + (setq tabulated-list-entries + (seq-map #'detached-list--get-entry + (detached-list--get-narrowed-sessions)))) + +(defun detached-list--get-entry (session) + "Return list entry based on SESSION." + `(,session + ,(cl-loop for config in detached-list-config + vconcat `(, + (let ((str (funcall (plist-get config ':function) session))) + (if-let ((face (plist-get config :face))) + (propertize str 'face face) + str)))))) + +(defun detached-list--get-format () + "Return the format for `detached-list'." + (cl-loop for config in detached-list-config + vconcat `((,(plist-get config ':name) + ,(plist-get config ':length) + ,(plist-get config ':sort))))) + +(defun detached-list--marked-session-p (session) + "Return t if SESSION is marked." + (seq-find (lambda (it) + (eq (detached-session-id it) + (detached-session-id session))) + detached-list--marked-sessions)) + +(defun detached-list--attached-p (session) + "Return t if Emacs is attached to SESSION." + (let ((id (detached-session-id session))) + (seq-find + (lambda (buffer) + (with-current-buffer buffer + (when-let ((buffer-session detached-buffer-session) + (buffer-session-id (detached-session-id buffer-session))) + (eq buffer-session-id id)))) + (buffer-list)))) + +(defun detached-list--unmark-session (session) + "Unmark SESSION." + (when (detached-list--marked-session-p session) + (tabulated-list-put-tag " ") + (setq detached-list--marked-sessions + (seq-remove (lambda (it) + (eq (detached-session-id it) + (detached-session-id session))) + detached-list--marked-sessions)))) + +(defun detached-list--mark-session (session) + "Mark SESSION." + (unless (detached-list--marked-session-p session) + (tabulated-list-put-tag (detached-list--mark-identifier)) + (setq detached-list--marked-sessions + (push session detached-list--marked-sessions)))) + +(defun detached-list--restore-marks () + "Restore mark(s) in `detached-list-mode' buffer." + (save-excursion + (goto-char (point-min)) + (while (not (eobp)) + (let ((session (tabulated-list-get-id))) + (when (detached-list--marked-session-p session) + (tabulated-list-put-tag (detached-list--mark-identifier)))) + (forward-line)))) + +(defun detached-list--mark-identifier () + "Return identifier for marked sessions." + (let ((str "*")) + (propertize str 'face 'detached-mark-face))) + +(defun detached-list--status-str (session) + "Return a string representation of SESSION's status." + (let* ((status (detached-session-status session)) + (status-str + (cond ((detached-session-active-p session) + (alist-get 'active detached-list-state-symbols "?")) + ((and (detached-session-inactive-p session) (eq status 'failure)) + (alist-get 'failure detached-list-state-symbols "?")) + ((and (detached-session-inactive-p session) (eq status 'success)) + (alist-get 'success detached-list-state-symbols "?")) + ((not (detached-session-started-p session)) + (alist-get 'unknown detached-list-state-symbols "?")) + (t "?"))) + (status-face + (cond ((and (detached-session-uninitialized-p session) + (detached-session-active-p session)) + 'detached-identifier-face) + ((detached-session-active-p session) 'font-lock-type-face) + ((eq status 'failure) 'detached-failure-face) + ((eq status 'success) 'detached-state-face) + (t 'detached-identifier-face))) + (attach-str + (cond ((eq 'attached (detached--session-initial-mode session)) + (alist-get 'initially-attached detached-list-state-symbols "?")) + ((eq 'detached (detached--session-initial-mode session)) + (alist-get 'initially-detached detached-list-state-symbols "?")) + (t "?"))) + (initial-mode-face + (cond ((eq 'attached (detached--session-initial-mode session)) 'detached-identifier-face) + ((eq 'detached (detached--session-initial-mode session)) 'detached-identifier-face) + (t "?")))) + (string-join + `(,(propertize status-str 'face status-face) + ,(propertize attach-str 'face initial-mode-face)) + " "))) + +(defun detached-list--command-str (session) + "Return command string for SESSION." + (let ((command-str (detached-session-command session))) + (if (detached-session-uninitialized-p session) + (propertize command-str 'face 'detached-uninitialized-face) + command-str))) + +(defun detached-list--get-marked-or-current-sessions () + "Return a list of relevant sessions." + (or detached-list--marked-sessions + `(,(tabulated-list-get-id)))) + +(defun detached-list--get-narrowed-sessions () + "Return a list of narrowed sessions." + (let ((sessions (detached-get-sessions))) + (seq-do (lambda (it) + (pcase-let ((`(,_identifier . ,criteria) it)) + (setq sessions (funcall criteria sessions)))) + detached-list--narrow-criteria) + sessions)) + +(cl-defmethod detached--session-in-context ((_mode (derived-mode detached-list-mode))) + "Return session when in `detached-list-mode'." + (tabulated-list-get-id)) + +(defun detached--grep-sesssions-output (sessions regexp) + "Return a narrowed list with SESSIONS containing REGEXP." + (let* ((sessions-and-directories + (thread-last sessions + (seq-group-by #'detached-session-directory) + (seq-filter (lambda (it) + ;; Filter out only accessible directories + (or (not (file-remote-p (car it))) + (file-remote-p (car it) nil t)))))) + (session-ids + (thread-last sessions-and-directories + (seq-map + (lambda (it) + (pcase-let* ((`(,session-directory . ,sessions) it) + (default-directory session-directory) + (includes + (seq-map (lambda (session) + (format "--include=%s" + (file-name-nondirectory + (detached--session-file + session + 'log)))) + sessions)) + (grep-command + (string-join `(,detached-grep-program + "--files-with-matches" + ,@includes + "--no-messages" + "--ignore-case" + "--recursive" + ,(format "\"%s\"" regexp)) + " "))) + (split-string + (with-connection-local-variables + (with-temp-buffer + (process-file-shell-command grep-command nil t) + (buffer-string))) + "\n" t)))) + (flatten-tree) + (seq-remove #'null) + (seq-map #'file-name-sans-extension)))) + (seq-filter (lambda (it) + (member (symbol-name (detached-session-id it)) session-ids)) + sessions))) + +;;;; Major mode + +(defvar detached-list-mode-map + (let ((map (make-sparse-keymap))) + (define-key map (kbd "a") #'detached-edit-session-annotation) + (define-key map (kbd "d") #'detached-list-delete-session) + (define-key map (kbd "e") #'detached-edit-and-run-session) + (define-key map (kbd "f") #'detached-list-select-filter) + (define-key map (kbd "g") #'detached-list-revert) + (define-key map (kbd "i") #'detached-list-initialize-session-directory) + (define-key map (kbd "j") #'imenu) + (define-key map (kbd "k") #'detached-list-kill-session) + (define-key map (kbd "m") #'detached-list-mark-session) + ;; Narrow + (define-key map (kbd "n a") #'detached-list-narrow-annotation) + (define-key map (kbd "n c") #'detached-list-narrow-command) + (define-key map (kbd "n d") #'detached-list-narrow-session-directory) + ;; Host + (define-key map (kbd "n h h") #'detached-list-narrow-host) + (define-key map (kbd "n h c") #'detached-list-narrow-currenthost) + (define-key map (kbd "n h l") #'detached-list-narrow-localhost) + (define-key map (kbd "n h r") #'detached-list-narrow-remotehost) + (define-key map (kbd "n o") #'detached-list-narrow-output) + (define-key map (kbd "n O") #'detached-list-narrow-origin) + ;; State + (define-key map (kbd "n s a") #'detached-list-narrow-active) + (define-key map (kbd "n s f") #'detached-list-narrow-failure) + (define-key map (kbd "n s i") #'detached-list-narrow-inactive) + (define-key map (kbd "n s s") #'detached-list-narrow-success) + (define-key map (kbd "n u") #'detached-list-narrow-unique) + (define-key map (kbd "n w") #'detached-list-narrow-working-directory) + (define-key map (kbd "n +") #'detached-list-narrow-after-time) + (define-key map (kbd "n -") #'detached-list-narrow-before-time) + (define-key map (kbd "q") #'detached-list-quit) + (define-key map (kbd "r") #'detached-rerun-session) + (define-key map (kbd "t") #'detached-list-toggle-mark-session) + (define-key map (kbd "T") #'detached-list-toggle-sessions) + (define-key map (kbd "u") #'detached-list-unmark-session) + (define-key map (kbd "U") #'detached-list-unmark-sessions) + (define-key map (kbd "v") #'detached-list-view-session) + (define-key map (kbd "w") #'detached-copy-session-command) + (define-key map (kbd "W") #'detached-copy-session-output) + (define-key map (kbd "x") #'detached-list-detach-from-session) + (define-key map (kbd "%") #'detached-list-mark-regexp) + (define-key map (kbd "=") #'detached-list-diff-marked-sessions) + (define-key map (kbd "-") #'detached-list-widen) + (define-key map (kbd "!") #'detached-shell-command) + ;; Describe + (define-key map (kbd ". s") #'detached-describe-session) + (define-key map (kbd ". d") #'detached-describe-duration) + (define-key map (kbd "") #'detached-list-remove-narrow-criterion) + (define-key map (kbd "") #'detached-list-open-session) + map) + "Keymap used in `detached-list-mode'.") + +(defun detached-list--mode-line-indicator () + "Return the mode line indicator based on narrow criteria." + (if detached-list--narrow-criteria + (string-join + (thread-last detached-list--narrow-criteria + (seq-map #'car)) + " > ") + "")) + +(define-derived-mode detached-list-mode tabulated-list-mode "Detached List" + "Mode for `detached' list." + (setq tabulated-list-format (detached-list--get-format)) + (setq tabulated-list-padding 2) + (setq tabulated-list-sort-key nil) + (setq imenu-create-index-function #'detached-list-imenu-index) + (setq-local eldoc-echo-area-use-multiline-p t) + (setq-local eldoc-idle-delay 0) + (hl-line-mode) + (add-hook 'eldoc-documentation-functions #'detached-list-eldoc nil t) + (add-hook 'tabulated-list-revert-hook #'detached-list--revert-sessions nil t) + (add-hook 'window-selection-change-functions #'detached--revert-selection-change nil t) + (setq-local mode-line-position '((:eval (detached-list--mode-line-indicator)))) + (tabulated-list-init-header)) + +(provide 'detached-list) + +;;; detached-list.el ends here diff --git a/elpa/detached-20221129.1430/detached-org.el b/elpa/detached-20221129.1430/detached-org.el new file mode 100644 index 00000000..b38c1947 --- /dev/null +++ b/elpa/detached-20221129.1430/detached-org.el @@ -0,0 +1,73 @@ +;;; detached-org.el --- Detached integration for org -*- lexical-binding: t -*- + +;; Copyright (C) 2022 Free Software Foundation, Inc. + +;; This file is part of GNU Emacs. + +;; This program is free software; you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation, either version 3 of the License, or +;; (at your option) any later version. + +;; This program is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. + +;; You should have received a copy of the GNU General Public License +;; along with this program. If not, see . + +;;; Commentary: + +;; This package integrates `detached' with `org'. In particular it +;; integrates with `ob-shell' in order to detach babel src blocks. + +;;; Code: + +;;;; Requirements + +(require 'detached) +(require 'ob-shell) + +;;;; Variables + +(defcustom detached-org-session-action + '(:attach detached-shell-command-attach-session + :view detached-view-dwim + :run detached-shell-command) + "Actions for a session created with `detached-org'." + :group 'detached + :type 'plist) + +;;;; Functions + +;;;###autoload +(defun detached-org-babel-sh (org-babel-sh-evaluate-fun &rest args) + "Modify ARGS before calling ORG-BABEL-SH-EVALUATE-FUN. + +This function modifies the full-body in ARGS and replaces it with a +`detached' command. The functionality is enabled by setting a header +property of :detached t in the org babel src block." + (pcase-let* ((`(,session ,full-body ,params ,stdin ,cmdline) args)) + (if (alist-get :detached params) + (cl-letf* ((detached-session-origin 'org) + (detached-session-action detached-org-session-action) + (detached-session-mode 'detached) + (new-command (replace-regexp-in-string "\n" " && " full-body)) + (detached-session (detached-create-session new-command)) + (dtach-command + (if (string= "none" (alist-get :session params)) + (detached-session-start-command detached-session + :type 'string) + (format "%s\necho \"[detached]\"" (detached-session-start-command detached-session + :type 'string)))) + ((symbol-function #'org-babel-eval) + (lambda (_ command) + (start-file-process-shell-command "detached-org" nil command) + "[detached]"))) + (apply org-babel-sh-evaluate-fun `(,session ,dtach-command ,params ,stdin ,cmdline))) + (apply org-babel-sh-evaluate-fun args)))) + +(provide 'detached-org) + +;;; detached-org.el ends here diff --git a/elpa/detached-20221129.1430/detached-pkg.el b/elpa/detached-20221129.1430/detached-pkg.el new file mode 100644 index 00000000..ed5f0408 --- /dev/null +++ b/elpa/detached-20221129.1430/detached-pkg.el @@ -0,0 +1,12 @@ +(define-package "detached" "20221129.1430" "A package to launch, and manage, detached processes" + '((emacs "27.1")) + :commit "6b64d4d8064cee781e071e825857b442ea96c3d9" :authors + '(("Niklas Eklund" . "niklas.eklund@posteo.net")) + :maintainer + '("detached.el Development" . "~niklaseklund/detached.el@lists.sr.ht") + :keywords + '("convenience" "processes") + :url "https://sr.ht/~niklaseklund/detached.el/") +;; Local Variables: +;; no-byte-compile: t +;; End: diff --git a/elpa/detached-20221129.1430/detached-shell.el b/elpa/detached-20221129.1430/detached-shell.el new file mode 100644 index 00000000..f0f5b565 --- /dev/null +++ b/elpa/detached-20221129.1430/detached-shell.el @@ -0,0 +1,112 @@ +;;; detached-shell.el --- Detached integration for shell -*- lexical-binding: t -*- + +;; Copyright (C) 2020-2022 Free Software Foundation, Inc. + +;; This file is part of GNU Emacs. + +;; This program is free software; you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation, either version 3 of the License, or +;; (at your option) any later version. + +;; This program is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. + +;; You should have received a copy of the GNU General Public License +;; along with this program. If not, see . + +;;; Commentary: + +;; This is a `detached' extension which provides integration for `shell'. + +;;; Code: + +;;;; Requirements + +(require 'detached) + +;;;; Variables + +(defcustom detached-shell-session-action + '(:attach detached-shell-command-attach-session + :view detached-view-dwim + :run detached-start-shell-command-session) + "Actions for a session created with `detached-shell'." + :group 'detached + :type 'plist) + +;;;; Functions + +;;;###autoload +(defun detached-shell-override-history (orig-fun &rest args) + "Override history in ORIG-FUN with ARGS. + +This function also makes sure that the HISTFILE is disabled for local shells." + (cl-letf (((getenv "HISTFILE") "")) + (advice-add 'comint-read-input-ring :around #'detached-shell--comint-read-input-ring-advice) + (apply (if (called-interactively-p 'any) + #'funcall-interactively #'funcall) + orig-fun + args))) + +;;;###autoload +(defun detached-shell-save-history-on-kill () + "Add hook to save history when killing `shell' buffer." + (add-hook 'kill-buffer-hook #'detached-shell--save-history 0 t)) + +;;;; Commands + +(defun detached-shell-send-input (&optional detached) + "Start a `detached-session' and attach to it, unless DETACHED." + (interactive "P") + (let* ((detached-session-origin 'shell) + (detached-session-action detached-shell-session-action) + (detached-session-mode (if detached 'detached 'attached)) + (comint-input-sender #'detached-shell--create-input-sender)) + (comint-send-input))) + +;;;; Support functions + +(defun detached-shell--create-input-sender (proc string) + "Create a detached session based on STRING and send to PROC." + (with-connection-local-variables + (let* ((session + (detached-create-session (substring-no-properties string))) + (command + (detached-session-start-command session :type 'string))) + (setq detached-buffer-session session) + (comint-simple-send proc command)))) + +(defun detached-shell--comint-read-input-ring-advice (orig-fun &rest args) + "Set `comint-input-ring-file-name' before calling ORIG-FUN with ARGS." + (with-connection-local-variables + (let* ((history-file (cond ((string= shell--start-prog "bash") "~/.bash_history") + ((string= shell--start-prog "ksh") "~/.sh_history") + ((string= shell--start-prog "zsh") "~/.zsh_history") + (t nil)))) + (setq comint-input-ring-file-name + (if history-file + (concat + (file-remote-p default-directory) + history-file) + comint-input-ring-file-name)) + (apply orig-fun args) + (advice-remove 'comint-read-input-ring #'detached-shell--comint-read-input-ring-advice)))) + +(defun detached-shell--save-history () + "Save `shell' history." + (unless (string-prefix-p detached--shell-command-buffer (buffer-name)) + (let* ((inhibit-message t)) + (comint-write-input-ring)))) + +;;;; Minor mode + +(let ((map detached-shell-mode-map)) + (define-key map (kbd "") #'detached-shell-send-input) + (define-key map (kbd "") #'detached-shell-attach-session)) + +(provide 'detached-shell) + +;;; detached-shell.el ends here diff --git a/elpa/detached-20221129.1430/detached-vterm.el b/elpa/detached-20221129.1430/detached-vterm.el new file mode 100644 index 00000000..45dfb79b --- /dev/null +++ b/elpa/detached-20221129.1430/detached-vterm.el @@ -0,0 +1,113 @@ +;;; detached-vterm.el --- Detached integration with vterm -*- lexical-binding: t -*- + +;; Copyright (C) 2022 Free Software Foundation, Inc. + +;; This file is part of GNU Emacs. + +;; This program is free software; you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation, either version 3 of the License, or +;; (at your option) any later version. + +;; This program is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. + +;; You should have received a copy of the GNU General Public License +;; along with this program. If not, see . + +;;; Commentary: + +;; This package integrates `detached' with `vterm'. + +;;; Code: + +;;;; Requirements + +(require 'detached) + +(declare-function vterm-send-C-a "vterm") +(declare-function vterm-send-C-k "vterm") +(declare-function vterm-send-C-e "vterm") +(declare-function vterm-send-return "vterm") +(declare-function vterm-end-of-line "vterm") +(declare-function vterm-beginning-of-line "vterm") + +(defvar vterm--process) + +;;;; Variables + +(defcustom detached-vterm-session-action + '(:attach detached-shell-command-attach-session + :view detached-view-dwim + :run detached-shell-command) + "Actions for a session created with `detached-vterm'." + :group 'detached + :type 'plist) + +;;;; Commands + +(defun detached-vterm-send-input (&optional detached) + "Start a `detached-session' and attach to it, unless DETACHED." + (interactive) + (let* ((input (buffer-substring-no-properties (vterm-beginning-of-line) (vterm-end-of-line))) + (detached-session-origin 'vterm) + (detached-session-action detached-vterm-session-action) + (detached-session-mode + (if detached 'detached 'attached)) + (detached-current-session (detached-create-session input)) + (command (detached-session-start-command detached-current-session + :type 'string))) + (vterm-send-C-a) + (vterm-send-C-k) + (process-send-string vterm--process command) + (setq detached-buffer-session detached-current-session) + (vterm-send-C-e) + (vterm-send-return))) + +(defun detached-vterm-attach (session) + "Attach to an active `detached' SESSION." + (interactive + (list + (let* ((host-name (car (detached--host))) + (sessions + (thread-last (detached-get-sessions) + (seq-filter (lambda (it) + (string= (detached-session-host-name it) host-name))) + (seq-filter #'detached-session-active-p)))) + (detached-completing-read sessions)))) + (let ((command + (detached-session-attach-command session + :type 'string))) + (setq detached-buffer-session session) + (process-send-string vterm--process command) + (vterm-send-return))) + +(cl-defmethod detached--detach-session ((_mode (derived-mode vterm-mode))) + "Detach from session when MODE is `vterm-mode'." + (process-send-string + vterm--process + detached--dtach-detach-character)) + +;;;; Minor mode + +(defvar detached-vterm-mode-map + (let ((map (make-sparse-keymap))) + (define-key map (kbd "") #'detached-vterm-send-input) + (define-key map (kbd "") #'detached-vterm-attach) + (define-key map (kbd "C-c C-.") #'detached-describe-session) + (define-key map (kbd detached-detach-key) #'detached-detach-session) + map) + "Keymap for `detached-vterm-mode'.") + +;;;###autoload +(define-minor-mode detached-vterm-mode + "Integrate `detached' in `vterm'." + :lighter " detached-vterm" + :keymap (let ((map (make-sparse-keymap))) + map)) + +(provide 'detached-vterm) + +;;; detached-vterm.el ends here diff --git a/elpa/detached-20221129.1430/detached.el b/elpa/detached-20221129.1430/detached.el new file mode 100644 index 00000000..e139639f --- /dev/null +++ b/elpa/detached-20221129.1430/detached.el @@ -0,0 +1,2209 @@ +;;; detached.el --- A package to launch, and manage, detached processes -*- lexical-binding: t -*- + +;; Copyright (C) 2020-2022 Free Software Foundation, Inc. + +;; Author: Niklas Eklund +;; Maintainer: detached.el Development <~niklaseklund/detached.el@lists.sr.ht> +;; URL: https://sr.ht/~niklaseklund/detached.el/ +;; Version: 0.10.1 +;; Package-Requires: ((emacs "27.1")) +;; Keywords: convenience processes + +;; This file is part of GNU Emacs. + +;; This program is free software; you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation, either version 3 of the License, or +;; (at your option) any later version. + +;; This program is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. + +;; You should have received a copy of the GNU General Public License +;; along with this program. If not, see . + +;;; Commentary: + +;; The detached package allows users to run shell commands detached from +;; Emacs. These commands are launched in sessions, using the program +;; dtach[1]. These sessions can be easily created through the command +;; `detached-shell-command', or any of the commands provided by the +;; `detached-shell', `detached-eshell' and `detached-compile' extensions. + +;; When a session is created, detached makes sure that Emacs is attached +;; to it the same time, which makes it a seamless experience for the +;; users. The `detached' package internally creates a `detached-session' +;; for all commands. + +;; [1] https://github.com/crigler/dtach + +;;; Code: + +;;;; Requirements + +(require 'ansi-color) +(require 'autorevert) +(require 'comint) +(require 'notifications) +(require 'filenotify) +(eval-when-compile (require 'rx)) +(require 'simple) +(require 'subr-x) +(require 'tramp) + +;;;; Variables + +;;;;; Customizable + +(defcustom detached-session-directory + (locate-user-emacs-file "detached/sessions") + "The directory to store sessions." + :type 'string + :group 'detached) + +(defcustom detached-db-directory + (locate-user-emacs-file "detached") + "The directory to store the `detached' database." + :type 'string + :group 'detached) + +(defcustom detached-dtach-program "dtach" + "The name of the dtach program." + :type 'string + :group 'detached) + +(defcustom detached-tail-program "tail" + "The name of the tail program." + :type 'string + :group 'detached) + +(defcustom detached-grep-program "grep" + "The name of the grep program." + :type 'string + :group 'detached) + +(defcustom detached-tee-program "tee" + "The name of the tee program." + :type 'string + :group 'detached) + +(defcustom detached-script-program "script" + "The name of the script program." + :type 'string + :group 'detached) + +(defcustom detached-shell-program shell-file-name + "Path to the shell to run the dtach command in." + :type 'string + :group 'detached) + +(defcustom detached-session-context-lines 50 + "Number of context lines to display for a session." + :type 'string + :group 'detached) + +(defcustom detached-show-session-context t + "If session context should be shown when attaching." + :type 'boolean + :group 'detached) + +(defcustom detached-terminal-data-command nil + "The command for the tool script, which is used to record terminal data. +The variable needs to be set in order for terminal data to be +recorded, instead of plain-text data. + +Acceptable values are +- `gnu/linux' +- `darwin' +- a custom string +" + :type 'string + :group 'detached) + +(defcustom detached-plain-text-commands nil + "A list of regexps for commands to run in plain-text mode." + :type 'list + :group 'detached) + +(defcustom detached-annotation-format + '((:width 3 :padding 2 :function detached--status-str :face detached-failure-face) + (:width 3 :padding 4 :function detached--state-str :face detached-state-face) + (:width 10 :padding 4 :function detached--host-str :face detached-host-face) + (:width 40 :padding 4 :function detached--working-dir-str :face detached-working-dir-face) + (:width 40 :padding 4 :function detached--metadata-str :face detached-metadata-face) + (:width 10 :padding 4 :function detached--duration-str :face detached-duration-face) + (:width 8 :padding 4 :function detached--size-str :face detached-size-face) + (:width 12 :padding 4 :function detached--creation-str :face detached-creation-face)) + "The format of the annotations." + :type '(repeat symbol) + :group 'detached) + +(defcustom detached-command-format + '(:width 90 :padding 4 :function detached-command-str) + "The format for displaying the command." + :type 'integer + :group 'detached) + +(defcustom detached-shell-command-session-action + '(:attach detached-shell-command-attach-session + :view detached-view-dwim + :run detached-start-shell-command-session) + "Actions for a session created with `detached-shell-command'." + :type 'plist + :group 'detached) + +(defcustom detached-session-command + nil + "Command to run in session." + :group 'detached + :type 'string) + +(defcustom detached-session-environment + nil + "A property list with variables for session." + :group 'detached + :type 'plist) + +(defcustom detached-shell-command-initial-input t + "Variable to control initial command input for `detached-shell-command'. +If set to a non nil value the latest entry to +`detached-shell-command-history' will be used as the initial input in +`detached-shell-command' when it is used as a command." + :type 'bool + :group 'detached) + +(defcustom detached-degraded-commands nil + "A list of commands which `detached' should consider degraded." + :type '(repeat (regexp :format "%v")) + :group 'detached) + +(defcustom detached-notification-function #'detached-state-transition-notifications-message + "Variable to set which function to use to issue a notification." + :type 'function + :group 'detached) + +(defcustom detached-detach-key "C-c C-d" + "Variable to set the keybinding for detaching." + :type 'string + :group 'detached) + +(defcustom detached-filter-ansi-sequences t + "Variable to instruct `detached' to use `ansi-filter'." + :type 'bool + :group 'detached) + +(defcustom detached-log-mode-hook '() + "Hook for customizing `detached-log' mode." + :type 'hook + :group 'detached) + +(defcustom detached-shell-mode-filter-functions + '(detached--env-message-filter + detached--dtach-eof-message-filter) + "A list of filter functions that are run in `detached-shell-mode'." + :type 'list + :group 'detached) + +(defcustom detached-dtach-socket-creation-delay 1.0 + "A maximum delay, in seconds, that is reasonable for dtach socket creation." + :type 'float + :group 'detached) + +(defcustom detached-open-session-display-buffer-action + '(display-buffer-pop-up-window) + "The action used to display a detached session." + :group 'detached + :type 'sexp) + +(defcustom detached-session-info-buffer-action + '(display-buffer-in-side-window + (side . bottom) + (dedicated . t)) + "The action used to display a information about a detached session." + :group 'detached + :type 'sexp) + +(defcustom detached-debug-enabled + nil + "If t enable debug messages in `detached'." + :group 'detached + :type 'boolean) + +;;;;; Public + +(defvar detached-session-map + (let ((map (make-sparse-keymap))) + (define-key map "a" #'detached-edit-session-annotation) + (define-key map "d" #'detached-detach-session) + (define-key map "D" #'detached-describe-duration) + (define-key map "e" #'detached-edit-and-run-session) + (define-key map "k" #'detached-kill-session) + (define-key map "r" #'detached-rerun-session) + (define-key map "S" #'detached-describe-session) + (define-key map "w" #'detached-copy-session-command) + (define-key map "W" #'detached-copy-session-output) + map)) + +(defvar detached-enabled nil) + +(defvar detached-session-mode 'attached + "Mode of operation for session. +Valid values are: create, new and attach") + +(defvar detached-session-origin nil + "Variable to specify the origin of the session.") + +(defvar detached-session-action nil + "A property list of actions for a session.") + +(defvar detached-shell-command-history nil + "History of commands run with `detached-shell-command'.") + +(defvar detached-local-session nil + "If set to t enforces a local session.") + +(defvar detached-compile-session-hooks nil + "Hooks to run when compiling a session.") + +(defvar detached-update-db-hooks nil + "Hooks to run when the database is updated.") + +(defvar detached-metadata-annotators-alist nil + "An alist of annotators for metadata.") + +(defvar detached-session-annotation nil + "An annotation string.") + +(defconst detached-session-version "0.10.1.0" + "The version of `detached-session'. +This version is encoded as [package-version].[revision].") + +(defconst detached-minimum-session-version "0.9.2.2" + "The version of `detached-session' that the package is compatible with.") + +;;;;; Faces + +(defgroup detached-faces nil + "Faces used by `detached'." + :group 'detached + :group 'faces) + +(defface detached-metadata-face + '((t :inherit font-lock-builtin-face)) + "Face used to highlight metadata in `detached'.") + +(defface detached-failure-face + '((t :inherit error)) + "Face used to highlight failure in `detached'.") + +(defface detached-state-face + '((t :inherit success)) + "Face used to highlight state in `detached'.") + +(defface detached-duration-face + '((t :inherit font-lock-builtin-face)) + "Face used to highlight duration in `detached'.") + +(defface detached-size-face + '((t :inherit font-lock-function-name-face)) + "Face used to highlight size in `detached'.") + +(defface detached-creation-face + '((t :inherit font-lock-comment-face)) + "Face used to highlight date in `detached'.") + +(defface detached-working-dir-face + '((t :inherit font-lock-variable-name-face)) + "Face used to highlight working directory in `detached'.") + +(defface detached-host-face + '((t :inherit font-lock-constant-face)) + "Face used to highlight host in `detached'.") + +(defface detached-identifier-face + '((t :inherit font-lock-comment-face)) + "Face used to highlight identifier in `detached'.") + +(defface detached-mark-face + '((t :inherit detached-state-face)) + "Face used to highlight marked session in `detached-list-mode'.") + +(defface detached-annotation-face + '((t :inherit font-lock-comment-face)) + "Face used to highlight the annotation of a session in `eldoc-mode'.") + +(defface detached-uninitialized-face + '((t :inherit font-lock-comment-face)) + "Face used to highlight that a session is uninitialized.") + + +;;;;; Private + +(defvar detached--sessions-initialized nil + "Sessions are initialized.") + +(defvar detached--sessions nil + "A list of sessions.") + +(defvar detached--watched-session-directories nil + "An alist where values are a (directory . descriptor).") + +(defvar detached--hashed-sessions nil + "Hashed sessions.") + +(defvar detached--unvalidated-session-ids nil + "A list of unvalidated session ids.") + +(defvar detached--current-emacsen nil + "List of current detached Emacsen.") + +(defvar detached--db-watch nil + "A descriptor to the `detached-db-directory'.") + +(defvar-local detached-buffer-session nil + "The `detached-session' session in current buffer.") + +(defvar detached-current-session nil + "The current session.") + +(defvar detached--session-candidates nil + "An alist of session candidates.") + +(defvar detached--annotation-widths nil + "An alist of widths to use for annotation.") + +(defvar detached--update-database t + "Update the database when value is t.") + +(defconst detached--shell-command-buffer "*Detached Shell Command*" + "Name of the `detached-shell-command' buffer.") + +(defconst detached--dtach-eof-message "\\[EOF - dtach terminating\\]" + "Message printed when `dtach' terminates.") + +(defconst detached--dtach-detached-message "\\[detached\\]\^M" + "Message printed when detaching from `dtach'.") + +(defconst detached--dtach-detach-character "\C-\\" + "Character used to detach from a session.") + +;;;; Data structures + +(cl-defstruct (detached-session (:constructor detached--session-create) + (:conc-name detached--session-)) + (id nil :read-only t) + (command nil :read-only t) + (origin nil :read-only t) + (working-directory nil :read-only t) + (initial-mode nil) + (directory nil :read-only t) + (metadata nil :read-only t) + (host nil :read-only t) + (degraded nil :read-only t) + (text-mode nil :read-only t) + (env nil :read-only t) + (action nil :read-only t) + (local nil :read-only t) + (annotation) + (time nil) + (status nil) + (size nil) + (state nil) + (initialized-emacsen nil)) + +;;;; Macros + +(defmacro detached-connection-local-variables (&rest body) + "A macro that conditionally use `connection-local-variables' when executing BODY." + `(if detached-local-session + (progn + ,@body) + (with-connection-local-variables + (progn + ,@body)))) + +(defmacro detached-with-session (session &rest body) + "A macro that set up SESSION's environment before evaluating BODY." + (declare (indent 1)) + `(let ((default-directory (detached-session-working-directory ,session)) + (detached-session-origin (detached--session-origin ,session)) + (detached-local-session (detached--session-local-p ,session)) + (detached-session-mode (detached--session-initial-mode ,session)) + (detached-session-action (detached--session-action ,session)) + (detached-session-command (detached-session-command ,session)) + (detached-session-environment (detached--session-env ,session)) + (detached-current-session ,session)) + ,@body)) + +;;;; Commands + +;;;###autoload +(defun detached-shell-command (command &optional suppress-output) + "Execute COMMAND with `detached'. + +Optionally SUPPRESS-OUTPUT if prefix-argument is provided." + (interactive + (list + (read-shell-command (if shell-command-prompt-show-cwd + (format-message "Detached shell command in `%s': " + (abbreviate-file-name + default-directory)) + "Detached shell command: ") + (when detached-shell-command-initial-input + (car detached-shell-command-history)) + 'detached-shell-command-history) + current-prefix-arg)) + (let* ((detached-session-origin (or detached-session-origin 'shell-command)) + (detached-session-action (or detached-session-action + detached-shell-command-session-action)) + (detached-session-mode (or detached-session-mode + (if suppress-output 'detached 'attached))) + (session (detached-create-session command))) + (detached-start-session session))) + +;;;###autoload +(defun detached-open-session (session) + "Open a `detached' SESSION." + (interactive + (list (detached-completing-read (detached-get-sessions)))) + (when (detached-valid-session session) + (let ((initialized-session (detached--get-initialized-session session))) + (if (detached-session-active-p initialized-session) + (detached-attach-session initialized-session) + (funcall + (detached-session-view-function initialized-session) + initialized-session))))) + +;;;###autoload +(defun detached-compile-session (session) + "Compile SESSION. + +The session is compiled by opening its output and enabling +`compilation-minor-mode'." + (interactive + (list (detached-completing-read (detached-get-sessions)))) + (when (detached-valid-session session) + (let ((buffer-name "*detached-session-output*") + (file + (detached--session-file session 'log)) + (tramp-verbose 1)) + (when (file-exists-p file) + (with-current-buffer (get-buffer-create buffer-name) + (let ((inhibit-read-only t)) + (setq-local buffer-read-only nil) + (erase-buffer) + (insert (detached-session-output session)) + (setq-local default-directory + (detached-session-working-directory session)) + (run-hooks 'detached-compile-session-hooks) + (detached-log-mode) + (compilation-minor-mode) + (setq detached-buffer-session session) + (setq-local font-lock-defaults '(compilation-mode-font-lock-keywords t))) + (font-lock-mode) + (read-only-mode) + (goto-char (point-max))) + (select-window + (display-buffer buffer-name detached-open-session-display-buffer-action)))))) + +;;;###autoload +(defun detached-edit-session-annotation (session) + "Edit SESSION's annotation." + (interactive + (list (detached-session-in-context))) + (when-let* ((initial-value (or + (detached--session-annotation session) + "")) + (annotation (read-string "Annotation: " initial-value))) + (setf (detached--session-annotation session) annotation) + (detached--db-update-entry session))) + +;;;###autoload +(defun detached-edit-and-run-session (session &optional toggle-session-mode) + "Edit and re-run SESSION at point. + +Optionally TOGGLE-SESSION-MODE." + (interactive + (list (detached-session-in-context) + current-prefix-arg)) + (when session + (detached-with-session session + (when-let* ((detached-session-command + (read-string "Edit command: " + (detached-session-command session))) + (detached-session-mode + (if toggle-session-mode + (if (eq 'detached (detached--session-initial-mode session)) + 'attached + 'detached) + (detached--session-initial-mode session))) + (detached-current-session + (detached-create-session detached-session-command))) + (detached-start-session detached-current-session))))) + +;;;###autoload +(defun detached-rerun-session (session &optional toggle-session-mode) + "Re-run SESSION at point. + +Optionally TOGGLE-SESSION-MODE." + (interactive + (list (detached-session-in-context) + current-prefix-arg)) + (when session + (detached-with-session session + (let* ((detached-session-mode + (if toggle-session-mode + (if (eq 'detached (detached--session-initial-mode session)) + 'attached + 'detached) + (detached--session-initial-mode session))) + (detached-current-session (detached-create-session detached-session-command))) + (detached-start-session detached-current-session))))) + +;;;###autoload +(defun detached-describe-session () + "Describe current session." + (interactive) + (when-let* ((session (detached-session-in-context)) + (buffer (get-buffer-create "*detached-session-info*")) + (window (display-buffer buffer detached-session-info-buffer-action))) + (select-window window) + (with-current-buffer buffer + (erase-buffer) + (insert + (string-trim + (detached--session-header session))) + (goto-char (point-min))))) + +;;;###autoload +(defun detached-describe-duration (session) + "Describe the SESSION's duration statistics." + (interactive + (list (detached-session-in-context))) + (let* ((statistics (detached-session-duration-statistics session))) + (message "%s: %s %s: %s" + (propertize "μ" 'face 'detached-mark-face) + (if-let ((mean (plist-get statistics :mean))) + (detached--duration-str2 (plist-get statistics :mean)) "-") + (propertize "σ" 'face 'detached-mark-face) + (if-let ((std (plist-get statistics :std))) + (detached--duration-str2 std) "-")))) + +;;;###autoload +(defun detached-attach-session (session) + "Attach to SESSION." + (interactive + (list (detached-session-in-context))) + (when (and session + (detached--valid-dtach-executable-p session)) + (let ((initialized-session (detached--get-initialized-session session))) + (if (detached-session-inactive-p initialized-session) + (detached-open-session initialized-session) + (funcall (detached-session-attach-function initialized-session) + initialized-session))))) + +;;;###autoload +(defun detached-copy-session-output (session) + "Copy SESSION's output." + (interactive + (list (detached-session-in-context))) + (when session + (with-temp-buffer + (insert (detached-session-output session)) + (when (detached-session-terminal-data-p session) + ;; Enable `detached-log-mode' to parse ansi-escape sequences + (detached-log-mode)) + (kill-new (buffer-string))))) + +;;;###autoload +(defun detached-copy-session-command (session) + "Copy SESSION's command." + (interactive + (list (detached-session-in-context))) + (kill-new (detached-session-command session))) + +;;;###autoload +(defun detached-insert-session-command (session) + "Insert SESSION's command." + (interactive + (list (detached-completing-read (detached-get-sessions)))) + (when (detached-valid-session session) + (insert (detached-session-command session)))) + +;;;###autoload +(defun detached-delete-session (session) + "Delete SESSION." + (interactive + (list (detached-session-in-context))) + (when session + (if (detached-session-active-p session) + (message "Kill session first before removing it.") + (detached--db-remove-entry session)))) + +;;;###autoload +(defun detached-kill-session (session &optional delete) + "Send a TERM signal to SESSION. + +Optionally DELETE the session if prefix-argument is provided." + (interactive + (list (detached-session-in-context) + current-prefix-arg)) + (when (detached-valid-session session) + (when-let* ((default-directory (detached-session-directory session))) + (if (derived-mode-p 'comint-mode) + (call-interactively #'comint-interrupt-subjob) + (detached-session-kill session))) + (when delete + (detached--db-remove-entry session)))) + +;;;###autoload +(defun detached-view-session (session) + "View the SESSION." + (interactive + (list (detached-completing-read (detached-get-sessions)))) + (when (detached-valid-session session) + (let* ((buffer-name "*detached-session-output*") + (file-path + (detached--session-file session 'log)) + (tramp-verbose 1)) + (if (file-exists-p file-path) + (progn + (with-current-buffer (get-buffer-create buffer-name) + (let ((inhibit-read-only t)) + (erase-buffer) + (insert (detached-session-output session)) + (setq-local default-directory (detached-session-working-directory session)) + (detached-log-mode)) + (setq detached-buffer-session session) + (goto-char (point-max))) + (select-window + (display-buffer buffer-name detached-open-session-display-buffer-action))) + (message "Detached can't find file: %s" file-path))))) + +;;;###autoload +(defun detached-diff-session (session1 session2) + "Diff SESSION1 with SESSION2." + (interactive + (let ((sessions (detached-get-sessions))) + `(,(detached-completing-read sessions) + ,(detached-completing-read sessions)))) + (when (and (detached-valid-session session1) + (detached-valid-session session2)) + (let ((buffer1 "*detached-session-output-1*") + (buffer2 "*detached-session-output-2*")) + (with-current-buffer (get-buffer-create buffer1) + (let ((inhibit-read-only t)) + (erase-buffer) + (insert (detached--session-header session1)) + (insert (detached-session-output session1)) + (when (detached-session-terminal-data-p session1) + ;; Enable `detached-log-mode' to parse ansi-escape sequences + (detached-log-mode)))) + (with-current-buffer (get-buffer-create buffer2) + (let ((inhibit-read-only t)) + (erase-buffer) + (insert (detached--session-header session2)) + (insert (detached-session-output session2)) + (when (detached-session-terminal-data-p session2) + ;; Enable `detached-log-mode' to parse ansi-escape sequences + (detached-log-mode)))) + (ediff-buffers buffer1 buffer2)))) + +;;;###autoload +(defun detached-open-session-directory (session) + "Open SESSION's log directory." + (interactive + (list (detached-completing-read (detached-get-sessions)))) + (let* ((file-path + (detached--session-file session 'log)) + (tramp-verbose 1)) + (when (file-exists-p file-path) + (dired-jump-other-window file-path)))) + +;;;###autoload +(defun detached-detach-session () + "Detach from session in current buffer. + +This command is only activated if `detached-buffer-session' is an +active session. For sessions created with `detached-compile' or +`detached-shell-command', the command will also kill the window." + (interactive) + (if-let ((has-session (detached-session-p detached-buffer-session))) + (detached--detach-session major-mode) + (message "No detached-session found in buffer."))) + +;;;###autoload +(defun detached-delete-sessions (&optional all-hosts) + "Delete `detached' sessions which belong to the current host, unless ALL-HOSTS." + (interactive "P") + (let* ((host-name (car (detached--host))) + (sessions (if all-hosts + (detached-get-sessions) + (seq-filter (lambda (it) + (string= (detached-session-host-name it) host-name)) + (detached-get-sessions))))) + (seq-do #'detached--db-remove-entry sessions))) + +;;;; Functions + +;;;;; Session + +(defun detached-create-session (command) + "Create a `detached' session from COMMAND." + (detached-connection-local-variables + (detached--create-session-directory) + (let ((session + (detached--session-create :id (intern (detached--create-id command)) + :command command + :origin detached-session-origin + :action detached-session-action + :working-directory (detached--get-working-directory) + :degraded (detached-degraded-command-p command) + :initial-mode detached-session-mode + :time `(:start 0.0 :end 0.0 :duration 0.0 :offset 0.0) + :status '(unknown . 0) + :annotation detached-session-annotation + :local detached-local-session + :size 0 + :directory (detached--get-session-directory) + :text-mode (detached--text-mode command) + :env detached-session-environment + :host (detached--host) + :metadata (detached-metadata) + :state 'unknown + :initialized-emacsen `(,(emacs-pid))))) + (detached--db-insert-entry session) + session))) + +(defun detached--start-session-process (session start-command) + "Start SESSION with START-COMMAND." + (detached-watch-session session) + (if (detached--session-local-p session) + (apply #'start-process-shell-command `("detached" nil ,start-command)) + (apply #'start-file-process-shell-command `("detached" nil ,start-command)))) + +(defun detached-session-candidates (sessions) + "Return an alist of SESSIONS candidates." + (when sessions + (setq detached--annotation-widths + (detached--annotation-widths sessions detached-annotation-format)) + (let ((command-length + (thread-last sessions + (seq-map #'detached-session-command) + (seq-map #'length) + (seq-max) + (min (plist-get detached-command-format :width))))) + (let ((command-fun (plist-get detached-command-format :function))) + (setq detached--session-candidates + (thread-last sessions + (seq-map (lambda (it) + `(,(apply command-fun `(,it ,command-length)) + . ,it))) + (detached--session-deduplicate) + (seq-map (lambda (it) + `(,(concat (car it) + (make-string (plist-get detached-command-format :padding) ?\s)) + . ,(cdr it)))))))))) + +(defun detached-session-annotation (item) + "Associate ITEM to a session and return ts annotation." + (let ((session (cdr (assoc item detached--session-candidates)))) + (string-join + (cl-loop for annotation in detached-annotation-format + collect (let ((str (funcall (plist-get annotation :function) session)) + (width (alist-get (plist-get annotation :function) detached--annotation-widths))) + (when (> width 0) + (concat + (truncate-string-to-width + (propertize str 'face (plist-get annotation :face)) + width + 0 ?\s) + (make-string (plist-get annotation :padding) ?\s))))) + ""))) + +;;;###autoload +(defun detached-initialize-sessions () + "Initialize `detached' sessions from the database." + + ;; Initialize sessions + (unless detached--sessions-initialized + (unless (file-exists-p detached-db-directory) + (make-directory detached-db-directory t)) + (detached--db-initialize) + (detached--register-detached-emacs) + (setq detached--db-watch + (file-notify-add-watch detached-db-directory + '(change attribute-change) + #'detached--db-directory-event)) + + (setq detached--sessions-initialized t) + + ;; Hash sessions and set the status for all sessions to uninitialized + (setq detached--hashed-sessions + (let* ((sessions (detached--db-get-sessions)) + (ht (make-hash-table :test #'equal :size (length sessions)))) + (seq-do (lambda (session) + (puthash (detached-session-id session) 'uninitialized ht)) + sessions) + ht)) + + ;; Initialize accessible sessions + (let ((detached--current-emacsen (detached--active-detached-emacsen))) + (detached--update-detached-emacsen) + (let ((detached--update-database nil)) + (thread-last (detached--db-get-sessions) + (seq-filter #'detached--session-accessible-p) + (seq-do #'detached--initialize-session))) + (detached--db-update-sessions)))) + +(defun detached-valid-session (session) + "Ensure that SESSION is valid. + +If session is not valid trigger an automatic cleanup on SESSION's host." + (when (detached-session-p session) + (if (not (detached--session-missing-p session)) + t + (let ((hostname (detached-session-host-name session))) + (message "Session does not exist. Initiate session cleanup on host %s" hostname) + (detached--cleanup-host-sessions hostname) + nil)))) + +(defun detached-session-exit-code-status (session) + "Return status based on exit-code in SESSION." + (let ((detached-env-message + (with-temp-buffer + (insert-file-contents (detached--session-file session 'log)) + (goto-char (point-max)) + (thing-at-point 'line t))) + (failure-message (rx "detached-exit-code: " (group (one-or-more digit))))) + (cond ((null detached-env-message) `(success . 0)) + ((string-match failure-message detached-env-message) + `(failure . ,(string-to-number (match-string 1 detached-env-message)))) + (t `(success . 0))))) + +(defun detached-state-transitionion-echo-message (session) + "Issue a notification when SESSION transitions from active to inactive. +This function uses the echo area." + (let ((status (pcase (detached-session-status session) + ('success "Detached finished") + ('failure "Detached failed")))) + (message "%s [%s]: %s" status (detached-session-host-name session) (detached-session-command session)))) + +(defun detached-state-transition-notifications-message (session) + "Issue a notification when SESSION transitions from active to inactive. +This function uses the `notifications' library." + (let ((status (detached-session-status session)) + (host (detached-session-host-name session))) + (notifications-notify + :title (pcase status + ('success (format "Detached finished [%s]" host)) + ('failure (format "Detached failed [%s]" host))) + :body (detached-session-command session) + :urgency (pcase status + ('success 'normal) + ('failure 'critical))))) + +(defun detached-view-dwim (session) + "View SESSION in a do what I mean fashion." + (let ((status (detached-session-status session))) + (cond ((eq 'success status) + (detached-view-session session)) + ((eq 'failure status) + (detached-compile-session session)) + ((eq 'unknown status) + (detached-view-session session)) + (t (message "Detached session is in an unexpected state."))))) + +(defun detached-get-sessions () + "Return as initialized sessions as possible." + ;; Try to initialize unknown sessions + (when-let* ((detached--update-database nil) + (initialized-sessions + (thread-last (detached--uninitialized-sessions) + (seq-filter #'detached--session-accessible-p) + (seq-do #'detached--initialize-session)))) + (detached--db-update-sessions)) + (detached--db-get-sessions)) + +(defun detached-shell-attach-session (session) + "Attach to SESSION. + +`comint-add-to-input-history' is temporarily disabled to avoid +cluttering the `comint-history' with dtach commands." + (interactive + (list (detached-select-host-session))) + (when (detached-valid-session session) + (if (detached-session-active-p session) + (cl-letf ((detached-current-session session) + (comint-input-sender #'detached-shell--attach-input-sender) + ((symbol-function 'comint-add-to-input-history) (lambda (_) t))) + (setq detached-buffer-session session) + (let ((kill-ring nil)) + (comint-kill-input)) + (insert "[attached]") + (comint-send-input)) + (detached-open-session session)))) + +(defun detached-shell-command-attach-session (session) + "Attach to SESSION with `async-shell-command'." + (let* ((inhibit-message t)) + (detached-with-session session + (cl-letf* (((symbol-function #'set-process-sentinel) #'ignore) + (detached-session-mode 'attached) + (buffer (get-buffer-create detached--shell-command-buffer)) + (default-directory (detached-session-directory session)) + (command (detached-session-attach-command session :type 'string))) + (when (get-buffer-process buffer) + (setq buffer (generate-new-buffer (buffer-name buffer)))) + (funcall #'async-shell-command command buffer) + (with-current-buffer buffer + (setq-local default-directory (detached-session-working-directory session)) + (setq detached-buffer-session detached-current-session)))))) + +(defun detached-start-shell-command-session (session) + "Start SESSION as a `shell-command'." + (cl-letf* ((inhibit-message t) + (default-directory (if (detached--session-local-p session) + (detached-session-directory session) + (detached-session-working-directory session))) + ((symbol-function #'set-process-sentinel) #'ignore) + (buffer (detached--generate-buffer detached--shell-command-buffer + (lambda (buffer) + (not (get-buffer-process buffer))))) + (command (detached-session-start-command session + :type 'string))) + (detached-watch-session session) + (funcall #'async-shell-command command buffer) + (with-current-buffer buffer + (setq detached-buffer-session session)))) + +(defun detached-start-session (session) + "Start SESSION." + (when (detached--valid-dtach-executable-p session) + (if (eq 'detached (detached--session-initial-mode session)) + (detached--start-session-process session + (detached-session-start-command + session + :type 'string)) + (detached-with-session session + (funcall (detached-session-run-function session) session))))) + +(defun detached-watch-session (session) + "Start to watch SESSION. + +If there is no file-notify watch on SESSION's directory it should be +added. This watch will ensure that when a socket is created the +session is set to active and when the socket is deleted it is set to +inactive. However there could be situations where the watch creation +is delayed and takes place after the socket appears. This is most +likely to happen on remote hosts. If so we fallback to a timer for +session validation." + (detached--create-session-validator session) + (detached--watch-session-directory (detached-session-directory session))) + +;;;;; Public session functions + +(cl-defun detached-session-start-command (session &key type) + "Return command to start SESSION with specified TYPE." + (when (detached--valid-dtach-executable-p session) + (detached-watch-session session) + (detached-connection-local-variables + (let* ((socket (detached--session-file session 'socket t)) + (detached-session-mode (detached--session-initial-mode session)) + (log (detached--session-file session 'log t)) + (dtach-arg (if (eq 'detached (detached--session-initial-mode session)) + "-n" + "-c")) + (dtach-command-fun (lambda (session) + (let ((detached-session-mode (detached--session-initial-mode session))) + `(,detached-dtach-program + ,dtach-arg + ,socket + "-z" + ,detached-shell-program + "-c" + ,(if (eq type 'string) + (shell-quote-argument (detached--detached-command session)) + (detached--detached-command session)))))) + (command + (if (eq 'detached (detached--session-initial-mode session)) + (funcall dtach-command-fun session) + (if (not (detached-session-degraded-p session)) + (funcall dtach-command-fun session) + (detached--start-session-process session + (string-join + (funcall dtach-command-fun session) " ")) + `(,detached-tail-program + "-F" + "-n" + ,(number-to-string detached-session-context-lines) + ,log))))) + (pcase type + ('string (string-join command " ")) + ('list command) + (_ nil)))))) + +(cl-defun detached-session-attach-command (session &key type) + "Return command to attach SESSION with specified TYPE." + (when (detached--valid-dtach-executable-p session) + (detached-connection-local-variables + (let* ((socket (detached--session-file session 'socket t)) + (log (detached--session-file session 'log t)) + (command + (if (detached-session-degraded-p session) + `(,detached-tail-program "-F" + "-n" + ,(number-to-string detached-session-context-lines) + ,log) + (append + (when detached-show-session-context + `(,detached-tail-program "-n" + ,(number-to-string detached-session-context-lines) + ,(concat log ";"))) + `(,detached-dtach-program "-a" + ,socket + "-r" "none"))))) + (pcase type + ('string (string-join command " ")) + ('list command) + (_ nil)))))) + + +(defun detached-session-kill (session) + "Kill SESSION." + (interactive + (list (detached-session-in-context))) + (when session + (if (detached-session-active-p session) + (cl-letf* (((getenv "HISTFILE") "") + (default-directory (detached-session-directory session)) + (buffer (get-buffer-create (format "*dtach-%s*" (detached-session-id session)))) + (termination-delay 0.5) + (comint-exec-hook + `(,(lambda () + (when-let ((process (get-buffer-process (current-buffer)))) + (run-with-timer termination-delay nil + (lambda () + ;; Attach to session + (with-current-buffer buffer + (let ((detached-show-session-context nil)) + (when detached-debug-enabled + (message "Kill function attaching to session %s" (detached-session-id session))) + (detached-shell-attach-session session)) + (run-with-timer termination-delay nil + (lambda () + ;; Send termination signal to session + (when detached-debug-enabled + (message "Kill function sending termination signal to session %s" (detached-session-id session))) + (with-current-buffer buffer + (call-interactively #'comint-interrupt-subjob) + (let ((kill-buffer-query-functions nil)) + (kill-buffer))))))))))))) + (apply #'make-comint-in-buffer `("dtach" ,buffer ,detached-shell-program nil))) + (message "Session %s is already inactive." (detached-session-id session))))) + +(defun detached-session-output (session) + "Return content of SESSION's output." + (let* ((filename (detached--session-file session 'log)) + (detached-message + (rx (regexp "\\[detached-exit-code: .*\\]")))) + (with-temp-buffer + (insert-file-contents filename) + (detached--maybe-watch-session session) + (goto-char (point-max)) + (forward-line -1) + (let ((beginning (point-min)) + (end (if (string-match detached-message (thing-at-point 'line t)) + (and (forward-line -1) (point)) + (point-max)))) + (buffer-substring beginning end))))) + +(defun detached-session-state (session) + "Return SESSION's state." + (detached--session-state session)) + +(defun detached-session-status (session) + "Return status for SESSION." + (pcase-let ((`(,status . ,_exit-code) + (detached--session-status session))) + status)) + +(defun detached-session-host-name (session) + "Return SESSION's host name." + (pcase-let ((`(,name . ,_type) + (detached--session-host session))) + name)) + +(defun detached-session-start-time (session) + "Return SESSION's start time." + (plist-get (detached--session-time session) :start)) + +(defun detached-session-end-time (session) + "Return SESSION's end time." + (plist-get (detached--session-time session) :end)) + +(defun detached-session-duration (session) + "Return SESSION's duration." + (if (detached-session-inactive-p session) + (plist-get + (detached--session-time session) :duration) + (- (time-to-seconds) (detached-session-start-time session)))) + +(defun detached-session-host-type (session) + "Return the type of SESSION's host." + (pcase-let ((`(,_name . ,type) + (detached--session-host session))) + type)) + +(defun detached-session-exit-code (session) + "Return exit code for SESSION." + (pcase-let ((`(,_status . ,exit-code) + (detached--session-status session))) + exit-code)) + +(defun detached-session-id (session) + "Return SESSION's id." + (detached--session-id session)) + +(defun detached-session-identifier (session) + "Return SESSION's identifier string." + (string-join + `(,(detached-session-command session) + ,(detached--host-str session) + ,(detached-session-working-directory session)) + ", ")) + +(defun detached-session-view-function (session) + "Return SESSION's view function." + (or + (plist-get (detached--session-action session) :view) + #'detached-view-dwim)) + +(defun detached-session-attach-function (session) + "Return SESSION's attach function." + (or + (plist-get (detached--session-action session) :attach) + #'detached-shell-command-attach-session)) + +(defun detached-session-run-function (session) + "Return SESSION's run function." + (or + (plist-get (detached--session-action session) :run) + #'detached-start-shell-command-session)) + +(defun detached-session-callback-function (session) + "Return SESSION's callback function." + (or + (plist-get (detached--session-action session) :callback) + #'ignore)) + +(defun detached-session-environment-property (session property) + "Return PROPERTY from SESSION environment." + (plist-get (detached--session-env session) property)) + +(defun detached-session-status-function (session) + "Return SESSION's status function." + (or + (plist-get (detached--session-action session) :status) + #'detached-session-exit-code-status)) + +(defun detached-session-command (session) + "Return command run in SESSION." + (detached--session-command session)) + +(defun detached-session-directory (session) + "Return directory where SESSION files are located." + (detached--session-directory session)) + +(defun detached-session-working-directory (session) + "Return SESSION's working directory." + (detached--session-working-directory session)) + +(defun detached-session-in-context () + "Return session from current context." + (let ((session + (or (detached--session-in-context major-mode) + (detached-completing-read (detached-get-sessions))))) + (when (detached-session-p session) + session))) + +(defun detached-session-duration-statistics (session) + "Return duration statistics for SESSION." + (when-let* ((identifier (detached-session-identifier session)) + (sessions (thread-last (detached-get-sessions) + (seq-filter (lambda (it) + (string= identifier (detached-session-identifier it)))) + (seq-remove #'detached-session-failed-p) + (seq-filter #'detached-session-inactive-p)))) + (detached--get-duration-statistics sessions))) + +(defun detached-session-validated-p (session) + "Return t if SESSION has been validated." + (not + (member (detached-session-id session) + detached--unvalidated-session-ids))) + +(defun detached-session-failed-p (session) + "Return t if SESSION failed." + (eq 'failure (detached-session-status session))) + +(defun detached-session-remotehost-p (session) + "Return t if SESSION is running on a remote host." + (eq 'remotehost (detached-session-host-type session))) + +(defun detached-session-localhost-p (session) + "Return t if SESSION is running on the local host." + (eq 'localhost (detached-session-host-type session))) + +(defun detached-session-started-p (session) + "Return t if SESSION has been started." + (not (eq 'unknown (detached-session-state session)))) + +(defun detached-session-active-p (session) + "Return t if SESSION is active." + (eq 'active (detached-session-state session))) + +(defun detached-session-inactive-p (session) + "Return t if SESSION is inactive." + (eq 'inactive (detached-session-state session))) + +(defun detached-session-degraded-p (session) + "Return t if SESSION is degraded." + (detached--session-degraded session)) + +(defun detached-session-uninitialized-p (session) + "Return t if SESSION is uninitialized." + (eq 'uninitialized + (gethash (detached-session-id session) detached--hashed-sessions))) + +(defun detached-session-initialized-p (session) + "Return t if SESSION is initialized." + (eq 'initialized + (gethash (detached-session-id session) detached--hashed-sessions))) + +(defun detached-session-terminal-data-p (session) + "Return t if SESSION is run with text mode set to terminal data." + (eq 'terminal-data + (detached--session-text-mode session))) + +(defun detached-session-watched-p (session) + "Return t if SESSION is being watched." + (detached--watched-session-directory-p + (detached-session-directory session))) + +;;;;; Other + +(defun detached-degraded-command-p (command) + "Return t if COMMAND is degraded." + (> + (thread-last detached-degraded-commands + (seq-filter (lambda (regexp) + (string-match-p regexp command))) + (length)) + 0)) + +(defun detached-metadata () + "Return a property list with metadata." + (let ((metadata '())) + (seq-doseq (annotator detached-metadata-annotators-alist) + (push `(,(car annotator) . ,(funcall (cdr annotator))) metadata)) + metadata)) + +(defun detached-select-host-session () + "Return selected session." + (let* ((host-name (car (detached--host))) + (sessions + (thread-last (detached-get-sessions) + (seq-filter (lambda (it) + (string= (detached-session-host-name it) host-name))) + (seq-filter #'detached-session-active-p)))) + (detached-completing-read sessions))) + +(defun detached-completing-read (sessions) + "Select a session from SESSIONS through `completing-read'." + (let* ((candidates (detached-session-candidates sessions)) + (metadata `(metadata + (category . detached) + (cycle-sort-function . identity) + (display-sort-function . identity) + (annotation-function . detached-session-annotation) + (affixation-function . + ,(lambda (cands) + (seq-map (lambda (s) + `(,s nil ,(detached-session-annotation s))) + cands))))) + (collection (lambda (string predicate action) + (if (eq action 'metadata) + metadata + (complete-with-action action candidates string predicate)))) + (cand (completing-read "Select session: " collection nil t))) + (detached--decode-session cand))) + +(defun detached-command-str (session max-length) + "Return SESSION's command as a string restrict it to MAX-LENGTH." + (let ((command (detached-session-command session))) + (if (<= (length command) max-length) + (truncate-string-to-width + command + max-length + 0 ?\s) + (concat (substring (detached-session-command session) 0 (- max-length 3)) "...")))) + +;;;; Support functions + +;;;;; Session + +(defun detached--determine-session-state (session) + "Return t if SESSION is active." + (if (file-exists-p + (detached--session-file session 'socket)) + 'active + 'inactive)) + + +(defun detached--valid-dtach-executable-p (session) + "Verify that dtach executable is accessible for SESSION." + (let ((default-directory (detached-session-directory session))) + (detached-connection-local-variables + (if (executable-find detached-dtach-program t) + t + (error "The `detached-dtach-program' doesn't contain a path to a dtach executable"))))) + +(defun detached--state-transition-p (session) + "Return t if SESSION has transitioned from active to inactive." + (and + (eq 'active (detached--session-state session)) + (eq 'inactive (detached--determine-session-state session)))) + +(defun detached--session-accessible-p (session) + "Return t if SESSION is accessible." + (or (detached-session-localhost-p session) + (file-remote-p (detached-session-directory session) nil t))) + +(defun detached--watched-session-directory-p (directory) + "Return t if DIRECTORY is being watched." + (alist-get directory + detached--watched-session-directories + nil nil #'string=)) + +(defun detached--session-local-p (session) + "Return t if SESSION is forced to run locally." + (detached--session-local session)) + +(defun detached--session-missing-p (session) + "Return t if SESSION is missing." + (not + (file-exists-p + (detached--session-file session 'log)))) + +(defun detached--session-header (session) + "Return header for SESSION." + (string-join + `(,(format "Command: %s" (detached-session-command session)) + ,(format "Working directory: %s" (detached--working-dir-str session)) + ,(format "Host: %s" (detached-session-host-name session)) + ,(format "Id: %s" (symbol-name (detached-session-id session))) + ,(format "Status: %s" (detached-session-status session)) + ,(format "Annotation: %s" (if-let ((annotation (detached--session-annotation session))) annotation "")) + ,(format "Exit-code: %s" (detached-session-exit-code session)) + ,(format "Metadata: %s" (detached--metadata-str session)) + ,(format "Created at: %s" (detached--creation-str session)) + ,(format "Duration: %s\n" (detached--duration-str session)) + "") + "\n")) + +(defun detached--session-deduplicate (sessions) + "Make car of SESSIONS unique by adding an identifier to it." + (let* ((ht (make-hash-table :test #'equal :size (length sessions))) + (occurences + (thread-last sessions + (seq-group-by #'car) + (seq-map (lambda (it) (seq-length (cdr it)))) + (seq-max))) + (identifier-width (if (> occurences 1) + (+ (length (number-to-string occurences)) 3) + 0)) + (reverse-sessions (seq-reverse sessions))) + (dolist (session reverse-sessions) + (if-let (count (gethash (car session) ht)) + (setcar session (format "%s%s" (car session) + (truncate-string-to-width + (propertize (format " (%s)" (puthash (car session) (1+ count) ht)) 'face 'detached-identifier-face) + identifier-width 0 ?\s))) + (puthash (car session) 0 ht) + (setcar session (format "%s%s" (car session) (make-string identifier-width ?\s))))) + (seq-reverse reverse-sessions))) + +(defun detached--decode-session (item) + "Return the session associated with ITEM." + (cdr (assoc item detached--session-candidates))) + +(defun detached--create-session-validator (session) + "Create a function to validate SESSION. + +It can take some time for a dtach socket to be created. Therefore all +sessions are created with state unknown. This function creates a +function to verify that a session was created correctly. If the +session is missing its deleted from the database." + (let ((session-id (detached-session-id session)) + (start-time + `(:start ,(time-to-seconds (current-time)) :end 0.0 :duration 0.0 :offset 0.0))) + (push session-id detached--unvalidated-session-ids) + (run-with-timer detached-dtach-socket-creation-delay + nil + (lambda () + (when (member session-id detached--unvalidated-session-ids) + (when detached-debug-enabled + (message "Session %s is set to active by validator" session-id)) + (let ((session (detached--db-get-session session-id))) + (setq detached--unvalidated-session-ids + (delete session-id detached--unvalidated-session-ids)) + (setf (detached--session-state session) 'active) + (setf (detached--session-time session) start-time) + (detached--db-update-entry session))))))) + +(defun detached--session-file (session file &optional local) + "Return the full path to SESSION's FILE. + +Optionally make the path LOCAL to host." + (let* ((file-name + (concat + (symbol-name + (detached-session-id session)) + (pcase file + ('socket ".socket") + ('log ".log")))) + (remote-local-path (file-remote-p (expand-file-name file-name (detached-session-directory session)) 'localname)) + (full-path (expand-file-name file-name (detached-session-directory session)))) + (if (and local remote-local-path) + remote-local-path + full-path))) + +(defun detached--cleanup-host-sessions (hostname) + "Run cleanup on HOSTNAME sessions." + (thread-last (detached--db-get-sessions) + (seq-filter (lambda (it) (string= hostname (detached-session-host-name it)))) + (seq-filter #'detached--session-missing-p) + (seq-do #'detached--db-remove-entry))) + +(defun detached--maybe-watch-session (session) + "Maybe watch SESSION." + (and (detached-session-active-p session) + (not (detached-session-watched-p session)) + (detached--watch-session-directory + (detached-session-directory session)))) + +(defun detached--create-session-directory () + "Create session directory if it doesn't exist." + (let ((directory (detached--get-session-directory))) + (unless (file-exists-p directory) + (make-directory directory t)))) + +(defun detached--get-working-directory () + "Return an abbreviated working directory path." + (if-let (remote (file-remote-p default-directory)) + (replace-regexp-in-string (expand-file-name remote) + (concat remote "~/") + (expand-file-name default-directory)) + (abbreviate-file-name default-directory))) + +(defun detached--get-session-directory () + "Return the session directory." + (if detached-local-session + detached-session-directory + (concat (file-remote-p default-directory) detached-session-directory))) + +(cl-defgeneric detached--session-in-context (_mode) + "Default implementation." + detached-buffer-session) + +(cl-defgeneric detached--detach-session (_mode) + "Default implementation." + (message "`detached' doesn't know how to detach from a session in this mode")) + +(cl-defmethod detached--detach-session ((_mode (derived-mode detached-log-mode))) + "Detach from session when MODE is `detached-log-mode'." + (detached--quit-session-buffer)) + +(cl-defmethod detached--detach-session ((_mode (derived-mode shell-mode))) + "Detach from session when MODE is `shell-mode'." + (if (detached-session-degraded-p detached-buffer-session) + (comint-interrupt-subjob) + (detached--detach-from-comint-process)) + (when (string-match detached--shell-command-buffer (buffer-name)) + (detached--quit-session-buffer))) + +(cl-defmethod detached--detach-session ((_mode (derived-mode comint-mode))) + "Detach from session when MODE is `comint-mode'." + (detached--detach-from-comint-process) + (detached--quit-session-buffer)) + +(defun detached--detach-from-comint-process () + "Detach from the underlying `comint' process." + (when-let ((active-session (detached-session-active-p + (alist-get (detached-session-id detached-buffer-session) + detached--sessions))) + (dtach-process (get-buffer-process (current-buffer)))) + (comint-simple-send dtach-process detached--dtach-detach-character) + (setq detached-buffer-session nil))) + +(defun detached--quit-session-buffer () + "Quit session buffer." + (message "[detached]") + (let ((kill-buffer-query-functions nil)) + (if (= (length (window-list)) 1) + (kill-buffer) + (kill-buffer-and-window)))) + +(defun detached--get-duration-statistics (sessions) + "Return a plist of duration statistics for SESSIONS." + (let* ((durations (seq-map #'detached-session-duration sessions)) + (mean (/ (seq-reduce #'+ durations 0) (seq-length durations))) + (std (sqrt (/ (seq-reduce (lambda (sum duration) + (+ sum (* (- duration mean) + (- duration mean)))) + durations + 0.0) + (seq-length durations))))) + `(:durations ,durations :mean ,mean :std ,std))) + +(defun detached-shell--attach-input-sender (proc _string) + "Attach to `detached--session' and send the attach command to PROC." + (let* ((input + (detached-session-attach-command detached-current-session + :type 'string))) + (comint-simple-send proc input))) + +;;;;; Database + +(defun detached--db-initialize () + "Return all sessions stored in database." + (let ((db (expand-file-name "detached-sessions.db" detached-db-directory))) + (when (file-exists-p db) + (with-temp-buffer + (insert-file-contents db) + (cl-assert (bobp)) + (if (detached--verify-db-compatibility) + (setq detached--sessions + (read (current-buffer))) + (warn "Detached database has version %s while minimum version is %s" + (detached--db-session-version) detached-minimum-session-version)))))) + +(defun detached--db-session-version () + "Return `detached-session-version' from database." + (let ((header (thing-at-point 'line)) + (regexp (rx "Detached Session Version: " (group (one-or-more (or digit punct)))))) + (string-match regexp header) + (match-string 1 header))) + +(defun detached--db-insert-entry (session) + "Insert SESSION into `detached--sessions' and update database." + (push `(,(detached-session-id session) . ,session) detached--sessions) + (when detached--update-database + (detached--db-update-sessions))) + +(defun detached--db-remove-entry (session) + "Remove SESSION from `detached--sessions', delete log and update database." + (let ((log (detached--session-file session 'log))) + (when (file-exists-p log) + (delete-file log))) + (setq detached--sessions + (assq-delete-all (detached-session-id session) detached--sessions)) + (when detached--update-database + (detached--db-update-sessions))) + +(defun detached--db-update-entry (session) + "Update SESSION in `detached--sessions' and the database." + (setf (alist-get (detached--session-id session) detached--sessions) session) + (when detached--update-database + (detached--db-update-sessions))) + +(defun detached--db-get-session (id) + "Return session with ID." + (alist-get id detached--sessions)) + +(defun detached--db-get-sessions () + "Return all sessions stored in the database." + (seq-map #'cdr detached--sessions)) + +(defun detached--db-update-sessions () + "Write `detached--sessions' to database." + (run-hooks 'detached-update-db-hooks) + (let ((db (expand-file-name "detached-sessions.db" detached-db-directory))) + (with-temp-file db + (insert (format ";; Detached Session Version: %s\n\n" detached-session-version)) + (prin1 detached--sessions (current-buffer))))) + +(defun detached--read-detached-emacsen () + "Read the PIDs of detached Emacsen." + (let ((file (expand-file-name "detached-emacsen" detached-db-directory))) + (when (file-exists-p file) + (with-temp-buffer + (insert-file-contents file) + (cl-assert (bobp)) + (read (current-buffer)))))) + +(defun detached--register-detached-emacs () + "Register the Emacs process." + (let* ((file (expand-file-name "detached-emacsen" detached-db-directory)) + (emacs-process (process-attributes (emacs-pid))) + (emacsen (detached--read-detached-emacsen))) + (setf (alist-get (emacs-pid) emacsen) emacs-process) + (with-temp-file file + (insert (format ";; Detached Emacsen\n\n")) + (prin1 emacsen (current-buffer))))) + +(defun detached--primary-detached-emacs-p (session) + "Return t if current Emacs is primary Emacs for SESSION." + (let ((emacsen (detached--read-detached-emacsen)) + (initialized-emacsen (detached--session-initialized-emacsen + session))) + (thread-last initialized-emacsen + (seq-find (lambda (pid) + (when-let ((emacs-process (alist-get pid emacsen)) + (process-attrs (process-attributes pid))) + (and + ;; Make sure the args are the same + (string= (alist-get 'args emacs-process) + (alist-get 'args process-attrs)) + ;; Make sure process id of the parent is the same + (= (alist-get 'ppid emacs-process) + (alist-get 'ppid process-attrs)) + ;; Make sure current Emacs is the right Emacs + (= pid (emacs-pid))))))))) + +(defun detached--update-detached-emacsen () + "Update list of detached Emacsen." + (with-temp-file (expand-file-name "detached-emacsen" detached-db-directory) + (insert (format ";; Detached Emacsen\n\n")) + (prin1 detached--current-emacsen (current-buffer)))) + +(defun detached--active-detached-emacsen () + "Return a list of active detached Emacsen." + (thread-last (detached--read-detached-emacsen) + (seq-filter (lambda (emacs-process) + (when-let ((process-attrs (process-attributes (car emacs-process)))) + (and + ;; Make sure the args are the same + (string= (alist-get 'args (cdr emacs-process)) + (alist-get 'args process-attrs)) + ;; Make sure process id of the parent is the same + (= (alist-get 'ppid (cdr emacs-process)) + (alist-get 'ppid process-attrs)))))))) + +(defun detached--get-initialized-session (session) + "Return an initialized copy of SESSION." + (if (detached-session-uninitialized-p session) + (progn + (detached--initialize-session session) + (detached--db-update-sessions) + (detached--db-get-session (detached-session-id session))) + session)) + +(defun detached--verify-db-compatibility () + "Verify that the database version is compatible with the package." + (let ((minimum-version + (detached--decode-version-string detached-minimum-session-version)) + (db-version + (detached--decode-version-string (detached--db-session-version)))) + (if (> (plist-get db-version :major) (plist-get minimum-version :major)) + t + (when (= (plist-get db-version :major) (plist-get minimum-version :major)) + (if (> (plist-get db-version :minor) (plist-get minimum-version :minor)) + t + (when (= (plist-get db-version :minor) (plist-get minimum-version :minor)) + (if (> (plist-get db-version :patch) (plist-get minimum-version :patch)) + t + (when (= (plist-get db-version :patch) (plist-get minimum-version :patch)) + (>= (plist-get db-version :revision) (plist-get minimum-version :revision)))))))))) + +;;;;; Other + +(defun detached--decode-version-string (version) + "Return a decode property list of VERSION." + (let ((version-regexp + (rx (group (one-or-more digit)) "." + (group (one-or-more digit)) "." + (group (one-or-more digit)) "." + (group (one-or-more digit))))) + (when (string-match version-regexp version) + `(:major ,(string-to-number (match-string 1 version)) + :minor ,(string-to-number (match-string 2 version)) + :patch ,(string-to-number (match-string 3 version)) + :revision ,(string-to-number (match-string 4 version)))))) + +(defun detached--dtach-arg () + "Return dtach argument based on `detached-session-mode'." + (pcase detached-session-mode + ('detached "-n") + ('attached "-c") + ('attach "-a") + (_ (error "`detached-session-mode' has an unknown value")))) + +(defun detached--session-state-transition-update (session &optional approximate) + "Update SESSION due to state transition. + +Optionally specify if the end-time should be APPROXIMATE or not." + (let ((session-size (file-attribute-size + (file-attributes + (detached--session-file session 'log)))) + (session-time (detached--update-session-time session approximate)) + (status-fun (detached-session-status-function session))) + (setf (detached--session-size session) session-size) + (setf (detached--session-time session) session-time) + (setf (detached--session-state session) 'inactive) + (setf (detached--session-status session) (funcall status-fun session))) + + ;; Send notification + (funcall detached-notification-function session) + + ;; Update session in database + (detached--db-update-entry session) + + ;; Execute callback + (funcall (detached-session-callback-function session) + session)) + +(defun detached--detached-command (session) + "Return the detached command for SESSION. + +If SESSION is degraded fallback to a command that doesn't rely on tee." + (let* ((log (detached--session-file session 'log t)) + (begin-shell-group (if (string= "fish" (file-name-nondirectory detached-shell-program)) + "begin;" + "{")) + (end-shell-group (if (or (string= "fish" (file-name-nondirectory detached-shell-program))) + "end" + "}")) + (redirect + (if (detached--session-degraded session) + (format "&> %s" log) + (format "2>&1 | %s %s" detached-tee-program log))) + (shell (format "%s -c" detached-shell-program)) + (command + (shell-quote-argument + (format "if %s; then true; else echo \"[detached-exit-code: $?]\"; fi" + (if (and (detached-session-terminal-data-p session) + detached-terminal-data-command) + (format "TERM=eterm-color %s" + (format + (detached--get-terminal-data-command) + (detached-session-command session))) + (detached-session-command session)))))) + (format "%s %s %s; %s %s" begin-shell-group shell command end-shell-group redirect))) + +(defun detached--get-terminal-data-command () + "Return terminal data command." + (pcase detached-terminal-data-command + ('gnu/linux (concat detached-script-program " --quiet --flush --return --command \"%s\" /dev/null")) + ('darwin (concat detached-script-program " -F -q /dev/null %s")) + ((and (pred stringp) command) command) + (_ (error "Unable to determine script command, set `detached-terminal-data-command' properly")))) + +(defun detached--text-mode (command) + "Return the text mode to run in COMMAND in." + (if (seq-find (lambda (regexp) + (string-match-p regexp command)) + detached-plain-text-commands) + 'plain-text + 'terminal-data)) + +(defun detached--host () + "Return a cons with (host . type)." + (let ((remote + (and (file-remote-p default-directory) + (not detached-local-session)))) + `(,(if remote (file-remote-p default-directory 'host) (system-name)) . ,(if remote 'remotehost 'localhost)))) + +(defun detached--update-session-time (session &optional approximate) + "Update SESSION's time property. + +If APPROXIMATE, use latest modification time of SESSION's +log to deduce the end time." + (let* ((start-time (plist-get (detached--session-time session) :start)) + (end-time)) + (if approximate + (setq end-time + (time-to-seconds + (file-attribute-modification-time + (file-attributes + (detached--session-file session 'log))))) + (setq end-time (time-to-seconds))) + `(:start ,start-time :end ,end-time :duration ,(- end-time start-time)))) + +(defun detached--create-id (command) + "Return a hash identifier for COMMAND." + (let ((current-time (number-to-string (time-to-seconds)))) + (secure-hash 'md5 (concat command current-time)))) + +(defun detached--env-message-filter (str) + "Remove `detached-env' message in STR." + (replace-regexp-in-string "\n?.*detached-exit-code:.*\n?" "" str)) + +(defun detached--dtach-eof-message-filter (str) + "Remove `detached--dtach-eof-message' in STR." + (replace-regexp-in-string (format "\n?%s\^M\n" detached--dtach-eof-message) "" str)) + +(defun detached--dtach-detached-message-filter (str) + "Remove `detached--dtach-detached-message' in STR." + (replace-regexp-in-string (format "\n?%s\n" detached--dtach-detached-message) "" str)) + +(defun detached--watch-session-directory (session-directory) + "Watch for events in SESSION-DIRECTORY." + (unless (detached--watched-session-directory-p session-directory) + (push + `(,session-directory . ,(file-notify-add-watch + session-directory + '(change) + #'detached-session-directory-event)) + detached--watched-session-directories))) + +(defun detached-session-directory-event (event) + "Act on an EVENT in a directory in `detached--watched-session-directories'. + +If event is caused by the deletion of a socket, locate the related +session and trigger a state transition." + (pcase-let* ((`(,_ ,action ,file) event)) + ;; Session becomes inactive + (when (and (eq action 'deleted) + (string= "socket" (file-name-extension file))) + + (when-let* ((id (intern (file-name-base file))) + (session (detached--db-get-session id)) + (session-directory (detached-session-directory session)) + (is-primary + (detached--primary-detached-emacs-p session))) + (when detached-debug-enabled + (message "Session %s is set to inactive by notify-watch event" (detached-session-id session))) + + ;; Remove from un-validated sessions + (setq detached--unvalidated-session-ids + (delete id detached--unvalidated-session-ids)) + + ;; Update session + (detached--session-state-transition-update session) + + ;; Remove session directory from `detached--watch-session-directory' + ;; if there is no active session associated with the directory + (unless + (thread-last (detached--db-get-sessions) + (seq-filter #'detached-session-active-p) + (seq-map #'detached-session-directory) + (seq-uniq) + (seq-filter (lambda (it) (string= it session-directory)))) + (file-notify-rm-watch + (alist-get session-directory detached--watched-session-directories nil nil #'string=)) + (setq detached--watched-session-directories + (assoc-delete-all session-directory detached--watched-session-directories))))) + + ;; Session becomes active + (when (and (eq action 'created) + (string= "log" (file-name-extension file))) + (when-let* ((id (intern (file-name-base file))) + (session (detached--db-get-session id)) + (session-directory (detached-session-directory session)) + (is-primary (detached--primary-detached-emacs-p session))) + (when detached-debug-enabled + (message "Session %s is set to active by notify-watch event" (detached-session-id session))) + (setq detached--unvalidated-session-ids + (delete (detached-session-id session) detached--unvalidated-session-ids)) + (setf (detached--session-state session) 'active) + (setf (detached--session-time session) `(:start ,(time-to-seconds (current-time)) :end 0.0 :duration 0.0 :offset 0.0)) + (detached--db-update-entry session))))) + +(defun detached--initialize-session (session) + "Initialize SESSION." + (puthash (detached-session-id session) 'initialized detached--hashed-sessions) + + (let* ((emacsen + (thread-last `(,(emacs-pid) ,@(detached--session-initialized-emacsen session)) + (seq-filter (lambda (it) + (alist-get it detached--current-emacsen))) + (seq-uniq)))) + (setf (detached--session-initialized-emacsen session) emacsen)) + + (if (detached-session-active-p session) + (if (detached--state-transition-p session) + (detached--session-state-transition-update session 'approximate) + (detached--db-update-entry session) + (detached--watch-session-directory (detached-session-directory session))))) + +(defun detached--uninitialized-sessions () + "Return a list of uninitialized sessions." + (seq-filter #'detached-session-uninitialized-p + (detached--db-get-sessions))) + +(defun detached--db-directory-event (event) + "Act on EVENT in `detached-db-directory'. + +If event is cased by an update to the `detached' database, re-initialize +`detached--sessions'." + (pcase-let* ((`(,_descriptor ,action ,file) event) + (database-updated (and (string-match "detached-sessions.db$" file) + (or (eq 'attribute-changed action) + (eq 'changed action)))) + (detached--current-emacsen (detached--active-detached-emacsen))) + (when database-updated + ;; Re-initialize the sessions + (detached--db-initialize) + ;; Initialize unknown sessions + (seq-do (lambda (session) + (unless (gethash (detached-session-id session) detached--hashed-sessions) + (if (not (detached--session-accessible-p session)) + (puthash (detached-session-id session) 'uninitialized detached--hashed-sessions) + (detached--initialize-session session)))) + (detached--db-get-sessions))))) + +(defun detached--annotation-widths (sessions annotation-format) + "Return widths for ANNOTATION-FORMAT based on SESSIONS." + (seq-map (lambda (it) (detached--annotation-width sessions it)) annotation-format)) + +(defun detached--annotation-width (sessions annotation) + "Determine width for ANNOTATION based on SESSIONS." + (let ((annotation-fun (plist-get annotation :function)) + (width (plist-get annotation :width))) + `(,annotation-fun . + ,(thread-last sessions + (seq-map annotation-fun) + (seq-map #'length) + (seq-max) + (min width))))) + +(defun detached--generate-buffer (name reuse-p &optional number) + "Reuse or generate new buffer like built-in function `generate-new-buffer'. +NAME is used the same way as `generate-new-buffer' but if a buffer which +REUSE-P for buffer returns nil, return buffer instead. + +NUMBER is used internally for recursive calls, but can be used to +start searching at NUMBER offset." + (let* ((buffer-name (if number + (format "%s<%d>" name number) + name)) + (buffer (get-buffer buffer-name)) + (number (or number 1))) + (if (and buffer (not (funcall reuse-p buffer))) + (detached--generate-buffer name reuse-p (1+ number)) + (get-buffer-create buffer-name)))) + +(defun detached--metadata-git-branch () + "Return current git branch." + (let ((args '("symbolic-ref" "HEAD" "--short"))) + (with-temp-buffer + (when (= 0 (apply #'process-file `("git" nil t nil ,@args))) + (unless (bobp) + (goto-char (point-min)) + (buffer-substring-no-properties (point) (line-end-position))))))) + +;;;;; UI + +(defun detached--metadata-str (session) + "Return SESSION's metadata as a string." + (string-join + (thread-last (detached--session-metadata session) + (seq-filter (lambda (it) (cdr it))) + (seq-map + (lambda (it) + (concat (symbol-name (car it)) ": " (cdr it))))) + "")) + +(defun detached--duration-str (session) + "Return SESSION's duration time." + (if (detached-session-started-p session) + (detached--duration-str2 (detached-session-duration session)) + "")) + +(defun detached--duration-str2 (duration) + "Return propertized DURATION." + (let* ((duration (round duration)) + (hours (/ duration 3600)) + (minutes (/ (mod duration 3600) 60)) + (seconds (mod duration 60))) + (cond ((> duration (* 60 60)) (format "%sh %sm %ss" hours minutes seconds)) + ((> duration 60) (format "%sm %ss" minutes seconds)) + (t (format "%ss" seconds))))) + +(defun detached--creation-str (session) + "Return SESSION's creation time." + (if (detached-session-started-p session) + (format-time-string + "%b %d %H:%M" + (detached-session-start-time session)) + "")) + +(defun detached--size-str (session) + "Return the size of SESSION's output." + (if (detached-session-active-p session) + "" + (file-size-human-readable + (detached--session-size session)))) + +(defun detached--status-str (session) + "Return string if SESSION has failed." + (pcase (detached-session-status session) + ('failure "!") + ('success "") + ('unknown ""))) + +(defun detached--state-str (session) + "Return string based on SESSION state." + (pcase (detached-session-state session) + ('active (if (detached--session-accessible-p session) + "*" + "?")) + ('inactive "") + ('unknown "?"))) + +(defun detached--working-dir-str (session) + "Return working directory of SESSION." + (let ((working-directory + (detached-session-working-directory session))) + (if-let ((remote (file-remote-p working-directory))) + (string-remove-prefix remote working-directory) + working-directory))) + +(defun detached--host-str (session) + "Return host name of SESSION." + (detached-session-host-name session)) + +;;;; Minor modes + +(defvar detached-shell-mode-map + (let ((map (make-sparse-keymap))) + (define-key map (kbd detached-detach-key) #'detached-detach-session) + (define-key map (kbd "C-c C-.") #'detached-describe-session) + map) + "Keymap for `detached-shell-mode'.") + +;;;###autoload +(define-minor-mode detached-shell-mode + "Integrate `detached' in `shell-mode'." + :lighter " detached-shell" + :keymap (let ((map (make-sparse-keymap))) + map) + (if detached-shell-mode + (dolist (filter detached-shell-mode-filter-functions) + (add-hook 'comint-preoutput-filter-functions filter 0 t)) + (dolist (filter detached-shell-mode-filter-functions) + (remove-hook 'comint-preoutput-filter-functions filter t)))) + +;;;; Major modes + +(defvar detached-log-mode-map + (let ((map (make-sparse-keymap))) + (define-key map (kbd detached-detach-key) #'detached-detach-session) + (define-key map (kbd "C-c C-.") #'detached-describe-session) + map) + "Keymap for `detached-log-mode'.") + +;;;###autoload +(define-derived-mode detached-log-mode nil "Detached Log" + "Major mode for `detached' logs." + (when detached-filter-ansi-sequences + (comint-carriage-motion (point-min) (point-max)) + (set-buffer-modified-p nil) + (ansi-color-apply-on-region (point-min) (point-max))) + (read-only-mode t)) + +;; TODO Deprecate the following functions: + +(cl-defgeneric detached--shell-command (entity &optional concat) + "Return shell command for ENTITY optionally CONCAT.") + +(cl-defmethod detached--shell-command ((command string) &optional concat) + "Return shell command for COMMAND. + +Optionally CONCAT the command return command into a string." + (detached--shell-command (detached-create-session command) concat)) + +(cl-defmethod detached--shell-command ((session detached-session) &optional concat) + "Return shell command for SESSION. + +Optionally CONCAT the command return command into a string." + (if (detached--session-degraded session) + (detached--tail-command session concat) + (detached--dtach-command session concat))) + +(cl-defgeneric detached--tail-command (entity &optional concat) + "Return tail command for ENTITY optionally CONCAT.") + +(cl-defmethod detached--tail-command ((command string) &optional concat) + "Return tail command for COMMAND. + +Optionally CONCAT the command return command into a string." + (detached--tail-command (detached-create-session command) concat)) + +(cl-defmethod detached--tail-command ((session detached-session) &optional concat) + "Return tail command for SESSION. + +Optionally CONCAT the command return command into a string." + (detached-connection-local-variables + (let* ((log (detached--session-file session 'log t)) + (tail-command `(,detached-tail-program "-F" + "-n" + ,(number-to-string detached-session-context-lines) + ,log))) + (cond ((eq 'detached detached-session-mode) + (detached--dtach-command session)) + ((eq 'attached detached-session-mode) + (let ((detached-session-mode 'detached) + (detached-current-session session)) + (detached-start-session (detached-session-command session)) + (if concat + (string-join tail-command " ") + tail-command))) + ((eq 'attach detached-session-mode) + (if concat + (string-join tail-command " ") + tail-command)))))) + +(cl-defgeneric detached--dtach-command (entity &optional concat) + "Return dtach command for ENTITY optionally CONCAT.") + +(cl-defmethod detached--dtach-command ((command string) &optional concat) + "Return dtach command for COMMAND. + +Optionally CONCAT the command return command into a string." + (detached--dtach-command (detached-create-session command) concat)) + +(cl-defmethod detached--dtach-command ((session detached-session) &optional concat) + "Return dtach command for SESSION. + +Optionally CONCAT the command return command into a string." + (detached-connection-local-variables + (let* ((socket (detached--session-file session 'socket t)) + (log (detached--session-file session 'log t)) + (dtach-arg (detached--dtach-arg))) + (if (eq detached-session-mode 'attach) + (if concat + (string-join + `(,(when detached-show-session-context + (format "%s -n %s %s;" detached-tail-program detached-session-context-lines log)) + ,detached-dtach-program + ,dtach-arg + ,socket + "-r none") + " ") + (append + (when detached-show-session-context + `(,detached-tail-program "-n" + ,(number-to-string detached-session-context-lines) + ,(concat log ";"))) + `(,detached-dtach-program ,dtach-arg ,socket "-r" "none"))) + (if concat + (string-join + `(,detached-dtach-program + ,dtach-arg + ,socket "-z" + ,detached-shell-program "-c" + ,(shell-quote-argument (detached--detached-command session))) + " ") + `(,detached-dtach-program + ,dtach-arg ,socket "-z" + ,detached-shell-program "-c" + ,(detached--detached-command session))))))) + +(provide 'detached) + +;;; detached.el ends here diff --git a/elpa/detached-20221129.1430/detached.info b/elpa/detached-20221129.1430/detached.info new file mode 100644 index 00000000..49fa30bf --- /dev/null +++ b/elpa/detached-20221129.1430/detached.info @@ -0,0 +1,604 @@ +This is detached.info, produced by makeinfo version 7.0 from +detached.texi. + +This manual describes the design of the ‘detached’ (version 0.10.1, last +updated 1 November 2022) package, as well as its options and +customizations. + + Copyright © 2020-2022 Free Software Foundation, Inc. + + Permission is granted to copy, distribute and/or modify this + document under the terms of the GNU Free Documentation License, + Version 1.3 or any later version published by the Free Software + Foundation; with no Invariant Sections, with no Front-Cover Texts, + and with no Back-Cover Texts. A copy of the license is included in + the section entitled “GNU Free Documentation License”. + +INFO-DIR-SECTION Emacs misc features +START-INFO-DIR-ENTRY +* Detached: (detached). Launch, interact and manage detached processes. +END-INFO-DIR-ENTRY + + +File: detached.info, Node: Top, Next: Overview, Up: (dir) + +Detached processes for GNU Emacs +******************************** + +This manual describes the design of the ‘detached’ (version 0.10.1, last +updated 1 November 2022) package, as well as its options and +customizations. + + Copyright © 2020-2022 Free Software Foundation, Inc. + + Permission is granted to copy, distribute and/or modify this + document under the terms of the GNU Free Documentation License, + Version 1.3 or any later version published by the Free Software + Foundation; with no Invariant Sections, with no Front-Cover Texts, + and with no Back-Cover Texts. A copy of the license is included in + the section entitled “GNU Free Documentation License”. + +* Menu: + +* Overview:: +* Dependencies:: +* Installation:: +* Configuration:: +* Workflows:: +* Customization:: +* Design:: +* Known limitations:: +* Contributions:: +* Acknowledgments:: + +— The Detailed Node Listing — + +Installation + +* Install from source:: +* Install from package archives:: +* Install from Guix:: + +Workflows + +* Integration with shells:: +* Integration with compile and shell-command:: +* Session management:: + +Session management + +* Detached list sessions:: + +Customization + +* Notifications:: +* Detached list:: + +Design + +* Sessions:: + +Sessions + +* Session creation:: +* Session initialization:: +* Session transition:: + +Known limitations + +* Quotation marks:: +* Reproducible environments:: + +Acknowledgments + +* Code contributors:: +* Idea contributors:: + + + +File: detached.info, Node: Overview, Next: Dependencies, Prev: Top, Up: Top + +1 Overview +********** + +The ‘detached’ package allows Emacs to run processes which are +independent from Emacs itself. This is accomplished by offloading the +process creation to the program ‘dtach’. + + The package introduces a concept of a detached session. A session +constitutes of three parts: + + • a dtach process + • a log with all the process output + • a datastructure in Emacs-Lisp + + After a session has been created, Emacs can optionally attach to the +process. This is something that can be done seamlessly and therefore +provides no disruption in the ordinary workflows. Another feature +provided by the package is its monitoring of sessions. It will notify +the user whenever a session transitions from active to inactive. Since +all the output of the session is captured, and stored persistently, it +can be thought of as a text representation of the process. + + The package provides integration with the following built-in Emacs +packages: + + • ‘compile’ + • ‘dired’ + • ‘eshell’ + • ‘org’ + + It also offers support for some 3rd-party packages. For a full list +of all supported packages, see the variable +‘detached-init-package-integration’. + + +File: detached.info, Node: Dependencies, Next: Installation, Prev: Overview, Up: Top + +2 Dependencies +************** + +The package has no dependencies to other Emacs packages. The +requirements are that it needs to run on either ‘Linux’ or ‘macOS,’ +since those are the operating systems supported by the program ‘dtach’. +Apart from dtach, the complete list of external tools to get all the +features from the package are: + + • ‘tail’ + • ‘script’ + • ‘grep’ + • ‘tee’ + + +File: detached.info, Node: Installation, Next: Configuration, Prev: Dependencies, Up: Top + +3 Installation +************** + +The ‘detached’ package can be installed from ‘GNU ELPA’ which is enabled +by default. It is also available for installation through the ‘Guix’ +package manager. + +* Menu: + +* Install from source:: +* Install from package archives:: +* Install from Guix:: + + +File: detached.info, Node: Install from source, Next: Install from package archives, Up: Installation + +3.1 Install from source +======================= + +Download the source code from ‘sourcehut’. + + git clone https://git.sr.ht/~niklaseklund/detached.el ~/.config/emacs/packages/detached.el + + Add the following code to ‘init.el’. + + (add-to-list 'load-path "~/.config/emacs/packages/detached.el") + + +File: detached.info, Node: Install from package archives, Next: Install from Guix, Prev: Install from source, Up: Installation + +3.2 Install from package archives +================================= + +Run ‘M-x package-refresh-contents’ to make sure the list of packages are +up to date. List all the packages with ‘M-x package-list-packages’ and +and find ‘detached’. Press return to get information about the package, +and press install. + + +File: detached.info, Node: Install from Guix, Prev: Install from package archives, Up: Installation + +3.3 Install from Guix +===================== + +To install ‘detached’ with Guix run the following command in a terminal. + + guix install emacs-detached + + +File: detached.info, Node: Configuration, Next: Workflows, Prev: Installation, Up: Top + +4 Configuration +*************** + +This is a a minimal ‘use-package’ configuration. + + (use-package detached + :init + (detached-init) + :bind (([remap async-shell-command] . detached-shell-command) + ([remap compile] . detached-compile) + ([remap recompile] . detached-compile-recompile)) + :custom ((detached-terminal-data-command system-type) + (detached-terminal-data-command system-type))) + + +File: detached.info, Node: Workflows, Next: Customization, Prev: Configuration, Up: Top + +5 Workflows +*********** + +* Menu: + +* Integration with shells:: +* Integration with compile and shell-command:: +* Session management:: + + +File: detached.info, Node: Integration with shells, Next: Integration with compile and shell-command, Up: Workflows + +5.1 Integration with shells +=========================== + +The package supports integration with ‘shell’, ‘eshell’ and ‘vterm’. +All of the these extensions work similarly, each implementing a detached +minor mode, ‘detached-.*mode’. Each of the minor modes provides +keybindings for the following operations: + + • Creating a session + • Detaching from a session + • Attach to a session + + When creating a session Emacs will automatically attach to the +underlying ‘dtach’ process and the result will look the same as if +‘detached’ wasn’t used. The difference is that the user can detach from +the session with the keybinding determined by the ‘detached-detach-key’. +If the prefix argument is provided when creating the session detached +will not attach to the session. To later attach to a running session +the user can do this with the attach command, by default bound to +‘C-’. + + +File: detached.info, Node: Integration with compile and shell-command, Next: Session management, Prev: Integration with shells, Up: Workflows + +5.2 Integration with compile and shell-command +============================================== + +When creating a session with ‘detached-compile’ or +‘detached-shell-command’ Emacs will attach to the process and show the +result in a buffer. It is possible to detach from the process with the +‘detached-detach-key’ and the buffer will then be closed. Later on to +attach to a session use any of the commands provided mentioned in +Session management. + + +File: detached.info, Node: Session management, Prev: Integration with compile and shell-command, Up: Workflows + +5.3 Session management +====================== + +The package offers two different interfaces for session management, +‘detached-open-session’ and ‘detached-list-sessions’. The former uses +the completing-read interface, whilst the latter uses the tabulated list +interface. Each of the interfaces can be used to interact with a +session. The ‘detached-list-sessions’ interface is however more feature +rich, and is what is recommended from the package author. + +* Menu: + +* Detached list sessions:: + + +File: detached.info, Node: Detached list sessions, Up: Session management + +5.3.1 Detached list sessions +---------------------------- + +After opening ‘detached-list-sessions’ the user can press ‘?’ to view a +list of all commands accessible in the buffer. Generally speaking there +are commands to either operate on the session at point, such as +rerunning it and deleting it. Or there are commands that operate on +multiple sessions at once, such as marking sessions or narrow the list +of sessions based on a criteria. + + Narrow criteria are composable so that they each take effect after +each other. For example narrowing based on local sessions, narrows the +list of candidates, which further can be narrowed to sessions where the +command match a regular expression. The mode also features a command +‘detached-list-select-filter’ which allows the user to select a +preconfigured set of criteria defined in ‘detached-list-filters’. + + +File: detached.info, Node: Customization, Next: Design, Prev: Workflows, Up: Top + +6 Customization +*************** + +* Menu: + +* Notifications:: +* Detached list:: + + +File: detached.info, Node: Notifications, Next: Detached list, Up: Customization + +6.1 Notifications +================= + +By default ‘detached’ uses the built in ‘notifications’ library to issue +notifications to the user. This solution is dependent on ‘dbus’, and if +that doesn’t work the user has the posbility to customize the +‘detached-notification-function’. + + The variable can be changed to: + • a custom function, created by the user + • the ‘detached-state-transition-echo-message’, which uses the echo + area + • the ‘detached-extra-alert-notification’, which relies on the + 3rd-party package ‘alert’ + + +File: detached.info, Node: Detached list, Prev: Notifications, Up: Customization + +6.2 Detached list +================= + +The buffer created by ‘detached-list-sessions’ can be customized in its +appearance. This is done through the +‘detached-list-display-buffer-action’. By default it will use: + + '(display-buffer-same-window + (inhibit-same-window . nil)) + + Which makes the buffer replace the buffer in the current window. For +those that prefer a popup window, created at the bottom of the frame, +the following value can be used: + + '(display-buffer-at-bottom + (window-height . 0.33)) + + +File: detached.info, Node: Design, Next: Known limitations, Prev: Customization, Up: Top + +7 Design +******** + +* Menu: + +* Sessions:: + + +File: detached.info, Node: Sessions, Up: Design + +7.1 Sessions +============ + +* Menu: + +* Session creation:: +* Session initialization:: +* Session transition:: + + +File: detached.info, Node: Session creation, Next: Session initialization, Up: Sessions + +7.1.1 Session creation +---------------------- + +When creating a session ‘three actions’ are taken: + • The session’s directory is getting watched, unless its already + watched + • A timer-based validator function is being created + • The session object is added to the list of unvalidated sessions + + The steps taken above is done in order to handle a couple of +different scenarios: + • The underlying ‘dtach’ session is never created, the validator will + recognize this and never insert the session into the database + • The session is correctly created and is running when the validator + is called, it will update the state from ‘unknown’ to ‘active’ and + insert the object into the database + • The session is finished quickly before the validator is being run, + it will trigger a session directory event, this will update the + session and insert it into the database, at the same time it is + removed from the list of ‘unvalidated’ sessions which will cause + the ‘validator’ to not do anything + + The logic described mean that the Emacs that creates the session is +responsible for validating the session. Only when a session has been +validated, or become inactive, it will be added to the database. When +it is added to the database it will cause other ‘Emacsen’ to read from +the database and then become aware of existence the session. + + +File: detached.info, Node: Session initialization, Next: Session transition, Prev: Session creation, Up: Sessions + +7.1.2 Session initialization +---------------------------- + +The initialization of ‘sessions’ when ‘detached’ is loaded is performed +in the following steps: + • All known sessions are read from the database + • The directory where the database resides are being watched in order + to detect updates made to the database + • A hash-table is created where all keys are session ids, and the + values are the status, which is initialized to ‘uninitialized’ + • For all sessions that are accessible, which is either local + sessions or remote sessions with an active connection + • The hash-table is updated to ‘initialized’ + • The sessions ‘initialized-emacsen’ property is updated with + the process id of the current Emacs + • If the session is registered as ‘active’ the session is + updated if it has transitioned to ‘inactive’, otherwise its + session directory is being watched + • If the session is missing, ‘e.g.’ it has been deleted from the + computer, the session is removed from the database + + +File: detached.info, Node: Session transition, Prev: Session initialization, Up: Sessions + +7.1.3 Session transition +------------------------ + +The ‘detached’ sessions are read from the persistent ‘detached.db’ file +when the package is loaded. All ‘Emacsen’ that loads ‘detached’ are +reading and writing to the same database file, that allows the sessions +to be shared among Emacsen. + + To avoid all Emacsen from sending a notification when a session +becomes inactive the responsibility is determined dynamically. All +Emacsen, where detached is loaded, registers their process in a file +named ‘detached-emacsen’. Initialization of sessions are done on a +session basis + + +File: detached.info, Node: Known limitations, Next: Contributions, Prev: Design, Up: Top + +8 Known limitations +******************* + +The ‘detached’ package have some known limitations. It is designed +around the idea that a single command is executed. Therefore it does +not support a session that can be re-used over time, which is a common +use-case with for example ‘tmux’. + +* Menu: + +* Quotation marks:: +* Reproducible environments:: + + +File: detached.info, Node: Quotation marks, Next: Reproducible environments, Up: Known limitations + +8.1 Quotation marks +=================== + +The way the package transforms, the command the user wants to run, into +a ‘detached’ command behaves incorrectly when it contain double +quotation marks. For example the following command: + + git shortlog --summary --numbered --all --no-merges --since="01 Jan 2022" + + Will unfortunately not be interpreted correctly. Instead the user +needs to use single quotation marks in order for it to work: + + git shortlog --summary --numbered --all --no-merges --since='01 Jan 2022' + + +File: detached.info, Node: Reproducible environments, Prev: Quotation marks, Up: Known limitations + +8.2 Reproducible environments +============================= + +The package works best when sessions can be re-run. For this to work, +the command needs to be robust against changes in the environment. +Since only a single command is run there can be no dependency to +previously run commands in the same shell, when the session is created. +If the command the user wants to run have such requirements the best +thing to do is to create a shell script that can set up that +environment, and execute that script instead. + + The tool ‘direnv’ could ideally be used with ‘detached’, however +there seems to be no way to make it work for remote processes. So the +recommendation for that use-case is the same as above, create a shell +script. For example a script named ‘direnv_command’ with the following +content: + + #!/usr/bin/env bash + direnv exec . "$@" + + Then the following command would be reproducible. + + direnv_command python foo.py + + +File: detached.info, Node: Contributions, Next: Acknowledgments, Prev: Known limitations, Up: Top + +9 Contributions +*************** + +The package is part of ‘ELPA’, which means that if you want to +contribute, you must have a copyright assignement +(https://www.gnu.org/software/emacs/manual/html_node/emacs/Copyright-Assignment.html). + + +File: detached.info, Node: Acknowledgments, Prev: Contributions, Up: Top + +10 Acknowledgments +****************** + +This package wouldn’t have been were it is today without these +contributors. + +* Menu: + +* Code contributors:: +* Idea contributors:: + + +File: detached.info, Node: Code contributors, Next: Idea contributors, Up: Acknowledgments + +10.1 Code contributors +====================== + + • rosetail (https://gitlab.com/rosetail) + • protesilaos (https://lists.sr.ht/~protesilaos) + • Stefan Monnier (https://www.iro.umontreal.ca/~monnier) + • dpettersson + • Greg Pfeil (http://technomadic.org) + + +File: detached.info, Node: Idea contributors, Prev: Code contributors, Up: Acknowledgments + +10.2 Idea contributors +====================== + + • rosetail (https://gitlab.com/rosetail) for all the great ideas and + improvements to the package. Without those contributions + ‘detached.el‘ would be a less sophisticated package. + • Troy de Freitas (https://gitlab.com/ntdef) for solving the problem + of getting ‘detached.el‘ to work with ‘filenotify‘ on macOS. + • Daniel Mendler (https://gitlab.com/minad) for helping out in + improving ‘detached.el‘, among other things integration with other + packages such as ‘embark‘ and ‘consult‘. + • Ambrevar (https://gitlab.com/ambrevar) who indirectly contributed + by inspiring me with his yes eshell is my main shell + (https://www.reddit.com/r/emacs/comments/6y3q4k/yes_eshell_is_my_main_shell/). + It was through that I discovered his package-eshell-detach + (https://github.com/Ambrevar/dotfiles/blob/master/.emacs.d/lisp/package-eshell-detach.el) + which got me into the idea of using ‘dtach‘ as a base for detached + shell commands. + + + +Tag Table: +Node: Top854 +Node: Overview2367 +Node: Dependencies3658 +Node: Installation4170 +Node: Install from source4561 +Node: Install from package archives4978 +Node: Install from Guix5431 +Node: Configuration5694 +Node: Workflows6247 +Node: Integration with shells6475 +Node: Integration with compile and shell-command7524 +Node: Session management8132 +Node: Detached list sessions8755 +Node: Customization9707 +Node: Notifications9874 +Node: Detached list10535 +Node: Design11154 +Node: Sessions11292 +Node: Session creation11453 +Node: Session initialization12969 +Node: Session transition14207 +Node: Known limitations14903 +Node: Quotation marks15352 +Node: Reproducible environments15984 +Node: Contributions17044 +Node: Acknowledgments17387 +Node: Code contributors17639 +Node: Idea contributors18008 + +End Tag Table + + +Local Variables: +coding: utf-8 +End: diff --git a/elpa/detached-20221129.1430/dir b/elpa/detached-20221129.1430/dir new file mode 100644 index 00000000..5ffdbfc1 --- /dev/null +++ b/elpa/detached-20221129.1430/dir @@ -0,0 +1,18 @@ +This is the file .../info/dir, which contains the +topmost node of the Info hierarchy, called (dir)Top. +The first time you invoke Info you start off looking at this node. + +File: dir, Node: Top This is the top of the INFO tree + + This (the Directory node) gives a menu of major topics. + Typing "q" exits, "H" lists all Info commands, "d" returns here, + "h" gives a primer for first-timers, + "mEmacs" visits the Emacs manual, etc. + + In Emacs, you can click mouse button 2 on a menu item or cross reference + to select it. + +* Menu: + +Emacs misc features +* Detached: (detached). Launch, interact and manage detached processes.