emacs-elpa-diffs
[Top][All Lists]
Advanced

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

[elpa] externals/dtache db230154e4 016/158: Merge develop branch into ma


From: ELPA Syncer
Subject: [elpa] externals/dtache db230154e4 016/158: Merge develop branch into master
Date: Wed, 19 Jan 2022 18:57:40 -0500 (EST)

branch: externals/dtache
commit db230154e40d180671f2c6f947f49e99e2902b85
Author: Niklas Eklund <niklas.eklund@posteo.net>
Commit: Niklas Eklund <niklas.eklund@posteo.net>

    Merge develop branch into master
    
    This commit contains a lot of recent improvements. To mention a few of
    them:
    
    - Dtache now has proper support for running on remote hosts
    - Neither dtache.el nor dtache-shell.el have any external dependencies
    - A command to list the sessions using a tabulated list interface now
    if one doesn't want to use the completing-read interface
    - Dtache now requires at least Emacs 27.1. This is to allow proper
    customization for remote sessions
    - Sessions are now launched asynchronously using start-file-process,
    which takes the current default-directory into account
    - Notification functionality has been implemented to notify users when
    a session finishes
    - dtache.el now exposes two different functions for usage of
    others. The first one is dtache-shell-command, which is similar to
    async-shell-command and is supposed to be used by the user
    directly. The other one is dtache-start-process which targets usage
    from other packages
    - Sessions have been improved so that they now can store closures for
    custom open/callback functionality
    - The usage of the sql database has been dropped and instead reworked
    to save the lisp objects to file directly for persistent storage
    - Annotators have been reworked
---
 .dir-locals.el                 |   2 +-
 README.org                     | 110 ++++-
 dtache-shell.el                | 150 ++++---
 dtache.el                      | 930 +++++++++++++++++++++++++----------------
 embark-dtache.el               |  20 +-
 guix.scm                       |   3 +-
 marginalia-dtache.el           |  63 +--
 test/dtache-test.el            | 155 +++----
 test/marginalia-dtache-test.el |  61 ---
 9 files changed, 844 insertions(+), 650 deletions(-)

diff --git a/.dir-locals.el b/.dir-locals.el
index 846a89e9bf..a31c9294c7 100644
--- a/.dir-locals.el
+++ b/.dir-locals.el
@@ -1,3 +1,3 @@
 ((nil . ((compile-command . "guix build --file=guix.scm")))
- (prog-mode (eval flycheck-mode))
+ (prog-mode (eval flymake-mode))
  (magit-status-mode (magit-todos-exclude-globs)))
diff --git a/README.org b/README.org
index 925a967d30..e106a27c14 100644
--- a/README.org
+++ b/README.org
@@ -23,17 +23,13 @@ Configuration for the =dtache= package. This package 
provides the backend for =d
 
 #+begin_src elisp
   (use-package dtache
-    :hook (after-init . dtache-setup)
+    :hook (after-init . dtache-initialize)
     :config
+    ;; Configure `dtache'
     (setq dtache-db-directory (no-littering-expand-var-file-name "dtache"))
     (setq dtache-session-directory (expand-file-name "dtache" 
(temporary-file-directory)))
-    (setq dtache-shell-history-file "~/.bash_history")
-
-    (general-def '(motion normal) dtache-log-mode-map
-      "q" #'kill-buffer-and-window)
-    (general-def '(motion normal) dtache-tail-mode-map
-      "q" #'dtache-quit-tail-log)
 
+    ;; Exclude dtache log files from `recentf'
     (add-to-list 'recentf-exclude (rx (regexp "dtache.*\.log"))))
 #+end_src
 
@@ -43,12 +39,34 @@ Configuration for the =dtache-shell= package. This package 
provides the integrat
 
 #+begin_src elisp
   (use-package dtache-shell
-    :hook (shell-mode . dtache-shell-mode)
+    :hook (after-init . dtache-shell-setup)
+    :general
+    (:keymaps 'dtache-shell-mode-map
+              "<S-return>" #'dtache-shell-create-session
+              "<C-return>" #'dtache-shell-attach)
     :config
-    (general-def dtache-shell-mode-map
-      "<S-return>" #'dtache-shell-create
-      "<C-return>" #'dtache-tail-log
-      "C-c C-q" #'dtache-shell-detach))
+    (setq dtache-shell-history-file "~/.bash_history"))
+#+end_src
+
+*** Metadata annotators
+
+Create a custom function that captures the branch name if the session is 
started in a git repository.
+
+#+begin_src elisp
+  (defun dtache--session-git-branch ()
+    "Return current git branch."
+    (let ((git-directory (locate-dominating-file "." ".git")))
+      (when git-directory
+        (let ((args '("name-rev" "--name-only" "HEAD")))
+          (with-temp-buffer
+            (apply #'process-file `("git" nil t nil ,@args))
+            (string-trim (buffer-string)))))))
+#+end_src
+
+Configure the metadata annotators list so that it runs your annotator.
+
+#+begin_src elisp
+  (setq dtache-metadata-annotators-alist '((branch . 
dtache--session-git-branch))
 #+end_src
 
 ** Integration with other packages
@@ -82,7 +100,7 @@ The =dtache= package supports 
[[https://www.gnu.org/software/emacs/manual/html_n
    '((dtache-shell . "/bin/bash")
      (dtache-shell-history-file . "~/.bash_history")
      (dtache-session-directory . "~/tmp")
-     (dtache-program . "/home/user/.local/bin/dtach")))
+     (dtache-dtach-program . "/home/user/.local/bin/dtach")))
 
   (connection-local-set-profiles
    '(:application tramp :protocol "ssh") 'remote-dtache)
@@ -103,7 +121,44 @@ In degraded mode =dtache= will skip the usage of =tee= and 
instead redirect all
 Make =dtache= send a notification once a session is finished. This would only 
make sense to add for sessions on the localhost. Add the following advice to 
the config.
 
 #+begin_src elisp
-  (advice-add 'dtache-session-command :override 
#'dtache-session-notify-command)
+  (defun dtache-session-finish-notification-a (session)
+    "Send a notification when SESSION finish."
+    (let* ((min-duration 5.0)
+           (send-alert (> (dtache--session-duration session) min-duration)))
+      (if send-alert
+          (alert (format "Command: %s" (dtache--session-command session))
+                 :title (format "Dtache session finished!")
+                 :severity 'moderate
+                 :category 'compile
+                 :id 'compile-ok)
+        (message "Dtache finished session: %s"
+                 (dtache--session-command session)))))
+#+end_src
+
+#+begin_src elisp
+  (advice-add 'dtache-session-finish-notification :override 
#'dtache-session-finish-notification-a)
+#+end_src
+
+** Evil bindings
+
+#+begin_src elisp
+  (general-def '(normal motion) dtache-sessions-mode-map
+    "<return>" #'dtache-open-session
+    "e" #'dtache-open-stderr
+    "c" #'dtache-compile-session
+    "d" #'dtache-remove-session
+    "gr" #'dtache-list-sessions
+    "K" #'dtache-kill-session
+    "L" #'dtache-open-log
+    "o" #'dtache-open-stdout
+    "r" #'dtache-rerun-session
+    "t" #'dtache-tail-log
+    "w" #'dtache-copy-session-command
+    "W" #'dtache-copy-session-log)
+  (general-def '(motion normal) dtache-log-mode-map
+    "q" #'kill-buffer-and-window)
+  (general-def '(motion normal) dtache-tail-mode-map
+    "q" #'dtache-quit-tail-log)
 #+end_src
 
 * Commands
@@ -116,7 +171,6 @@ Commands to be used in shell buffers.
 
 | Command                 | Description                 |
 |-------------------------+-----------------------------|
-| dtache-shell-send-input | Optionally create a session |
 | dtache-shell-create     | Create a session            |
 | dtache-shell-attach     | Attach to a session         |
 | dtache-shell-detach     | Detach from a session       |
@@ -136,6 +190,32 @@ General commands that can be used anywhere.
 | dtache-remove-session       | Remove a session                            |
 | dtache-compile-session      | Open the session output in compilation mode |
 
+* Tips & Tricks
+** Advice functions
+
+The following two functions are examples on how to create functions that can 
be used to advice other functions in order to replace =compile= and 
=async-shell-command= with =dtache-start-session=
+
+#+begin_src elisp
+  (defun dtache-compile-advice (orig-fun &rest args)
+    "Function to replace usage of `compile' before calling ORIG-FUN with ARGS."
+    (cl-letf (((symbol-function 'compile)
+               (lambda (args)
+                 (dtache-start-session (car args)))))
+      (apply orig-fun args)))
+
+  (defun dtache-start-session-advice (orig-fun &rest args)
+    "Function to replace usage of `async-shell-command' before calling 
ORIG-FUN with ARGS."
+    (cl-letf (((symbol-function 'async-shell-command)
+               (lambda (args)
+                 (dtache-start-session (car args)))))
+      (apply orig-fun args)))
+#+end_src
+
 * Credits
 
 The inspiration for the package comes from ~ambrevar's~ 
[[https://github.com/Ambrevar/dotfiles/blob/master/.emacs.d/lisp/package-eshell-detach.el][package-eshell-detach]].
+
+* TODO Things to do before next release
+- [X] Update header documentation in files
+- [ ] Squash the development from the branch and merge to master
+- [ ] Update the README.org file
diff --git a/dtache-shell.el b/dtache-shell.el
index 11f2717ab6..74fa248c8c 100755
--- a/dtache-shell.el
+++ b/dtache-shell.el
@@ -35,58 +35,77 @@
 
 ;;;; Variables
 
+(defvar dtache-shell-history-file nil
+  "File to store history.")
 (defvar dtache-shell-block-list '("^$")
   "A list of regexps to block non-supported input.")
 (defvar dtache-shell-new-block-list '("^sudo.*")
   "A list of regexps to block from creating a session without attaching.")
 (defvar dtache-shell-silence-dtach-messages t
   "Filter out messages from the `dtach' program.")
-(defvar dtache-shell-create-primary-function #'dtache-shell-new-session
-  "Primary function for creating a session.")
-(defvar dtache-shell-create-secondary-function #'dtache-shell-create-session
-  "Secondary function for creating a session.")
+
+(defconst dtache-shell-detach-character "\C-\\"
+  "Character used to detach from a session.")
+(defconst dtache-shell-eof-message "\\[EOF - dtach terminating\\]\^M"
+  "Message printed when `dtach' finishes.")
+(defconst dtache-shell-detached-message "\\[detached\\]\^M"
+  "Message printed when `dtach' finishes.")
+
+;;;;; Private
+
+(defvar dtache-shell--current-session nil "The current session.")
 
 ;;;; Functions
 
+(defun dtache-shell-override-history (orig-fun &rest args)
+  "Override history to read `dtache-shell-history-file' 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 
#'dtache-shell--comint-read-input-ring-advice)
+    (apply orig-fun args)))
+
+(defun dtache-shell-save-history ()
+  "Add hook to save history when killing `shell' buffer."
+  (add-hook 'kill-buffer-hook #'dtache-shell-save-history 0 t))
+
 (defun dtache-shell-filter-dtach-eof (string)
   "Remove eof message from dtach in STRING."
-  (if (string-match dtache-eof-message string)
-      (replace-regexp-in-string (format "%s\n" dtache-eof-message) "" string)
+  (if (string-match dtache-shell-eof-message string)
+      (replace-regexp-in-string (format "%s\n" dtache-shell-eof-message) "" 
string)
     string))
 
 (defun dtache-shell-filter-dtach-detached (string)
   "Remove detached message from dtach in STRING."
-  (if (string-match dtache-detached-message string)
-      (replace-regexp-in-string (format "%s\n" dtache-detached-message) "" 
string)
+  (if (string-match dtache-shell-detached-message string)
+      (replace-regexp-in-string (format "%s\n" dtache-shell-detached-message) 
"" string)
     string))
 
-;;;; Commands
-
-;;;###autoload
-(defun dtache-shell-send-input (&optional create-session)
-  "Send input to `shell'.
+(defun dtache-shell-setup ()
+  "Setup `dtache-shell'."
+  (add-hook 'shell-mode-hook #'dtache-shell-save-history)
+  (add-hook 'shell-mode-hook #'dtache-shell-mode)
+  (advice-add 'shell :around #'dtache-shell-override-history))
+
+(defun dtache-shell-select-session ()
+  "Return selected session."
+  (dtache-update-sessions)
+  (let* ((current-host (dtache--host))
+         (sessions
+          (thread-last dtache--sessions
+            (seq-filter (lambda (it)
+                          (string= (dtache--session-host it) current-host)))
+            (seq-filter #'dtache--session-active-p))))
+    (dtache-completing-read sessions)))
 
-Optionally CREATE-SESSION with prefix argument."
-  (interactive "P")
-  (if create-session
-      (funcall dtache-shell-create-primary-function)
-    (comint-send-input)))
+;;;; Commands
 
 ;;;###autoload
-(defun dtache-shell-create (&optional secondary)
-  "Create a new session with `dtache-shell-create-primary-function'.
-
-If prefix argument SECONDARY call `dtache-shell-create-secondary-function'."
+(defun dtache-shell-create-session (&optional detach)
+  "Create a session and attach to it unless DETACH."
   (interactive "P")
-  (if secondary
-      (funcall dtache-shell-create-secondary-function)
-    (funcall dtache-shell-create-primary-function)))
-
-;;;###autoload
-(defun dtache-shell-create-session ()
-  "Create a session and attach to it."
-  (interactive)
-  (let* ((dtache--dtach-mode "-c")
+  (let* ((dtache-session-type 'shell)
+         (dtache--dtach-mode (if detach 'new 'create))
          (comint-input-sender #'dtache-shell--create-input-sender))
     (comint-send-input)))
 
@@ -94,7 +113,8 @@ If prefix argument SECONDARY call 
`dtache-shell-create-secondary-function'."
 (defun dtache-shell-new-session ()
   "Create a new session."
   (interactive)
-  (let ((dtache--dtach-mode "-n")
+  (let ((dtache-session-type 'shell)
+        (dtache--dtach-mode 'new)
         (comint-input-sender #'dtache-shell--create-input-sender))
     (comint-send-input)))
 
@@ -103,7 +123,7 @@ If prefix argument SECONDARY call 
`dtache-shell-create-secondary-function'."
   "Detach from session."
   (interactive)
   (let ((proc (get-buffer-process (current-buffer)))
-        (input dtache-detach-character))
+        (input dtache-shell-detach-character))
     (comint-simple-send proc input)))
 
 ;;;###autoload
@@ -113,29 +133,27 @@ If prefix argument SECONDARY call 
`dtache-shell-create-secondary-function'."
 `comint-add-to-input-history' is temporarily disabled to avoid
 cluttering the comint-history with dtach commands."
   (interactive
-   (list (dtache-select-session)))
-  (cl-letf ((dtache--current-session session)
-            (comint-input-sender #'dtache-shell--attach-input-sender)
-            ((symbol-function 'comint-add-to-input-history) (lambda (_) t)))
-    (comint-kill-input)
-    (comint-send-input)))
+   (list (dtache-shell-select-session)))
+  (if (dtache--session-active-p session)
+      (cl-letf ((dtache-shell--current-session session)
+                (comint-input-sender #'dtache-shell--attach-input-sender)
+                ((symbol-function 'comint-add-to-input-history) (lambda (_) 
t)))
+        (comint-kill-input)
+        (comint-send-input))
+    (dtache-open-session session)))
 
 ;;;; Support functions
 
-(cl-defmethod dtache--attach-to-session (session &context (major-mode 
shell-mode))
-  "Attach to a dtache SESSION when MAJOR-MODE is `shell-mode'."
-  (dtache-shell-attach session))
-
 (defun dtache-shell--attach-input-sender (proc _string)
   "Attach to `dtache--session' and send the attach command to PROC."
-  (let* ((dtache--dtach-mode "-a")
+  (let* ((dtache--dtach-mode 'attach)
          (socket
           (concat
-           (dtache--session-session-directory dtache--current-session)
-           (dtache--session-id dtache--current-session)
-           dtache-socket-ext))
+           (dtache--session-session-directory dtache-shell--current-session)
+           (dtache--session-id dtache-shell--current-session)
+           dtache--socket-ext))
          (input
-          (concat dtache-program " " dtache--dtach-mode " " socket)))
+          (concat dtache-dtach-program " " (dtache--dtach-arg) " " socket)))
     (comint-simple-send proc input)))
 
 (defun dtache-shell--create-input-sender (proc string)
@@ -151,14 +169,40 @@ cluttering the comint-history with dtach commands."
                    (lambda (blocked)
                      (string-match-p blocked string))
                    dtache-shell-new-block-list)
-                  "-c"
+                  'create
                 dtache--dtach-mode))
-             (command (dtache-dtach-command
-                       (dtache--create-session
-                        (substring-no-properties string)))))
-       (comint-simple-send proc command)
+             (session (dtache--create-session
+                       (substring-no-properties string)))
+             (command (dtache-dtach-command session))
+             (shell-command
+              (mapconcat 'identity `(,dtache-dtach-program
+                                     ,@(butlast command)
+                                     ,(shell-quote-argument (car (last 
command))))
+                         " ")))
+       (progn
+         (dtache-setup-notification session)
+         (comint-simple-send proc shell-command))
      (comint-simple-send proc string))))
 
+(defun dtache-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 ((comint-input-ring-file-name
+          (concat
+           (file-remote-p default-directory)
+           dtache-shell-history-file)))
+     (apply orig-fun args)
+     (advice-remove 'comint-read-input-ring 
#'dtache-shell--comint-read-input-ring-advice))))
+
+(defun dtache-shell--save-history ()
+  "Save `shell' history."
+  (with-connection-local-variables
+   (let ((comint-input-ring-file-name
+          (concat
+           (file-remote-p default-directory)
+           dtache-shell-history-file)))
+     (comint-write-input-ring))))
+
 ;;;; Minor mode
 
 (define-minor-mode dtache-shell-mode
diff --git a/dtache.el b/dtache.el
index a252a52017..a4d201cc23 100644
--- a/dtache.el
+++ b/dtache.el
@@ -1,4 +1,4 @@
-;;; dtache.el --- Dtache core -*- lexical-binding: t -*-
+;;; dtache.el --- Run and manage detached commands -*- lexical-binding: t -*-
 
 ;; Copyright (C) 2020-2021 Niklas Eklund
 
@@ -26,9 +26,16 @@
 ;;; Commentary:
 
 ;; Dtache allows a program to be seamlessly executed in an environment
-;; that is isolated from Emacs.  This package provides the core
-;; implementation.  Dtache sessions is supposed to be created and
-;; interacted with through a front end package such as `dtache-shell'.
+;; that is isolated from Emacs.  This package provides functionality
+;; for the user to launch detached commands with
+;; `dtache-shell-command', which is inspired by `async-shell-command'.
+;; Another function `dtache-start-session' is supposed to be used by
+;; other functions or packages.  This is also useful if the user wants
+;; to advice packages to use it in favor of for example `compile'.
+
+;; To manage the sessions the user can either use
+;; `dtache-list-sessions' for a tabulated list interface, or
+;; `dtache-open-session' for a `completing-read' equivalent.
 
 ;; The package requires the program dtach[1] to be installed.
 ;;
@@ -38,9 +45,9 @@
 
 ;;;; Requirements
 
-(require 'emacsql-sqlite)
-(require 'tramp-sh)
 (require 'autorevert)
+(require 'filenotify)
+(require 'tramp)
 
 ;;;; Variables
 
@@ -48,45 +55,49 @@
   "The directory to store `dtache' sessions.")
 (defvar dtache-db-directory user-emacs-directory
   "The directory to store `dtache' database.")
-(defvar dtache-db nil
-  "The connection to the `dtache' database.")
-(defvar dtache-program "dtach"
-  "The `dtach' program.")
-(defvar dtache-shell "bash"
+(defvar dtache-dtach-program "dtach"
+  "The name of the `dtach' program.")
+(defvar dtache-shell-program "bash"
   "Shell to run the dtach command in.")
-(defvar dtache-metadata-annotators '((:git-branch . 
dtache--session-git-branch))
-  "An alist of annotators for metadata.")
 (defvar dtache-max-command-length 95
   "Maximum length of displayed command.")
-(defvar dtache-attach-alternate-function #'dtache-open-log
-  "Alternate function to use when attaching to inactive sessions.")
-(defvar dtache-shell-history-file nil
-  "File to store history.")
-
-(defconst dtache-socket-ext ".socket"
-  "The file name extension for the socket for `dtache-program'.")
-(defconst dtache-log-ext ".log"
-  "The file name extension for combined stdout and stderr.")
-(defconst dtache-stdout-ext ".stdout"
-  "The file name extension for stdout.")
-(defconst dtache-stderr-ext ".stderr"
-  "The file name extension for stderr.")
-(defconst dtache-eof-message "\\[EOF - dtach terminating\\]\^M"
-  "Message printed when `dtach' finishes.")
-(defconst dtache-detached-message "\\[detached\\]\^M"
-  "Message printed when `dtach' finishes.")
-(defconst dtache-detach-character "\C-\\"
-  "Character used to detach from a session.")
 (defvar dtache-degraded-list '()
-  "Regexps that should run in dedgraded mode.")
+  "Regexps for commands that should be run in dedgraded mode.")
 (defvar dtache-tail-interval 2
-  "Interval in seconds for dtache to tail.")
+  "Interval in seconds for the update rate when tailing a session.")
+(defvar dtache-session-type nil
+  "Variable to specify the origin of the session.")
+(defvar dtache-open-session-function nil
+  "Custom function to use to open a session.")
+(defvar dtache-session-callback-function nil
+  "Custom function to callback when a session finish.")
+(defvar dtache-compile-hooks nil
+  "Hooks to run when compiling a session.")
+(defvar dtache-dispatch-alist `((standard . dtache-tail-log)
+                                (shell . dtache-tail-log))
+  "Specify function to open session with based on type.")
+(defvar dtache-metadata-annotators-alist nil
+  "An alist of annotators for metadata.")
 
 ;;;;; Private
 
-(defvar dtache--dtach-mode nil "Mode of operation.")
-(defvar dtache--session-candidates nil "An alist of session candidates.")
-(defvar dtache--current-session nil "The current session.")
+(defvar dtache--sessions-initialized nil
+  "Sessions are initialized.")
+(defvar dtache--dtach-mode nil
+  "Mode of operation.")
+(defvar dtache--sessions nil
+  "A list of sessions.")
+(defvar dtache--remote-session-timer nil
+  "Timer object for remote polling.")
+
+(defconst dtache--socket-ext ".socket"
+  "The file name extension for the socket.")
+(defconst dtache--log-ext ".log"
+  "The file name extension for combined stdout and stderr.")
+(defconst dtache--stdout-ext ".stdout"
+  "The file name extension for stdout.")
+(defconst dtache--stderr-ext ".stderr"
+  "The file name extension for stderr.")
 
 ;;;; Data structures
 
@@ -94,6 +105,9 @@
                               (:conc-name dtache--session-))
   (id nil :read-only t)
   (command nil :read-only t)
+  (type nil :read-only t)
+  (open-function nil :read-only t)
+  (callback-function nil :read-only t)
   (working-directory nil :read-only t)
   (creation-time nil :read-only t)
   (session-directory nil :read-only t)
@@ -104,194 +118,67 @@
   (log-size nil)
   (active nil))
 
-;;;; Functions
-
-;;;;; Session
+;;;; Commands
 
-(defun dtache-select-session ()
-  "Return selected session."
-  (let* ((candidates (dtache-session-candidates))
-         (candidate
-          (completing-read "Select session: "
-                           (lambda (str pred action)
-                             (pcase action
-                               ('metadata '(metadata (category . dtache)
-                                                     (cycle-sort-function . 
identity)
-                                                     (display-sort-function . 
identity)))
-                               (`(boundaries . ,_) nil)
-                               ('nil (try-completion str candidates pred))
-                               ('t (all-completions str candidates pred))
-                               (_ (test-completion str candidates pred))))
-                           nil t nil 'dtache-session-history)))
-    (dtache-decode-session candidate)))
+;;;###autoload
+(defun dtache-shell-command (command &optional suppress-output)
+  "Execute COMMAND asynchronously with `dtache'.
 
-(defun dtache-session-file (session file)
-  "Return the path to SESSION's FILE."
-  (let ((file-name
-         (concat
-          (dtache--session-id session)
-          (pcase file
-            ('socket dtache-socket-ext)
-            ('log dtache-log-ext)
-            ('stdout dtache-stdout-ext)
-            ('stderr dtache-stderr-ext))))
-        (directory (concat
-                    (file-remote-p default-directory)
-                    (dtache--session-session-directory session))))
-    (expand-file-name file-name directory)))
+The input parameters are kept in sync with `async-shell-command'.  If
+the optional parameters SUPPRESS-OUTPUT has a value the output buffer
+is not opened and the command will run in the background."
+  (interactive
+   (list
+    (read-shell-command (if shell-command-prompt-show-cwd
+                            (format-message "Dtache shell command in `%s': "
+                                            (abbreviate-file-name
+                                             default-directory))
+                          "Dtache shell command: ")
+                        nil nil)
+    current-prefix-arg))
+  (let* ((inhibit-message t)
+         (dtache-session-type 'standard))
+    (dtache-start-session command (not suppress-output))))
 
-(defun dtache-update-sessions ()
-  "Update sessions in the database."
-  (thread-last (dtache--db-select-active-sessions (dtache--host))
-    (seq-remove (lambda (session)
-                  (when (dtache--session-dead-p session)
-                    (dtache--db-remove-session session)
-                    t)))
-    (seq-map #'dtache--session-update)
-    (seq-map #'dtache--db-update-session)))
-
-(defun dtache-cleanup-sessions ()
-  "Remove dead sessions from the database."
-  (thread-last (dtache--db-select-host-sessions (dtache--host))
-    (seq-filter #'dtache--session-dead-p)
-    (seq-map #'dtache--db-remove-session)))
-
-(defun dtache-session-command (session)
-  "Return SESSION's command."
-  (dtache--session-command session))
-
-(defun dtache-session-candidates ()
-  "Return an alist of session candidates."
-  (dtache-initialize)
+;;;###autoload
+(defun dtache-list-sessions ()
+  "List `dtache' sessions."
+  (interactive)
+  (pop-to-buffer-same-window
+   (get-buffer-create "*dtache-sessions*"))
+  (dtache-sessions-mode)
   (dtache-update-sessions)
-  (let* ((sessions (nreverse
-                    (dtache--db-select-host-sessions (dtache--host)))))
-    (setq dtache--session-candidates
-          (seq-map (lambda (session)
-                     `(,(dtache-encode-session session) . ,session))
-                   sessions))))
-
-(defun dtache-initialize ()
-  "Initialize `dtache'."
-  (unless dtache-db
-    (dtache-db-initialize)
-    (dtache-cleanup-sessions))
-  (dtache-create-session-directory))
-
-;;;;; Database
+  (let* ((tabulated-list-entries
+          (seq-map #'dtache-get-sesssion-entry dtache--sessions)))
+    (tabulated-list-print t)))
 
-(defun dtache-db-initialize ()
-  "Initialize the `dtache' database."
-  (unless (file-exists-p dtache-db-directory)
-    (make-directory dtache-db-directory t))
-  (unless dtache-db
-    (setq dtache-db
-          (emacsql-sqlite
-           (expand-file-name "dtache.db" dtache-db-directory)))
-    (emacsql dtache-db
-             [:create-table
-              :if :not :exists dtache-sessions
-              ([(id text :primary-key) host active dtache-session])])))
-
-;;;;; Shell
-
-(defun dtache-override-shell-history (orig-fun &rest args)
-  "Override history to read `dtache-shell-history-file' 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 
#'dtache--shell-comint-read-input-ring-a)
-    (apply orig-fun args)))
-
-(defun dtache-save-shell-history ()
-  "Add hook to save history when killing `shell' buffer."
-  (add-hook 'kill-buffer-hook #'dtache--shell-save-history 0 t))
-
-;;;;; Other
-
-(defun dtache-setup ()
-  "Setup `dtache'."
-  (advice-add 'shell :around #'dtache-override-shell-history)
-  (add-hook 'shell-mode-hook #'dtache-save-shell-history))
-
-(defun dtache-dtach-command (session)
-  "Return a dtach command for SESSION."
-  (let* ((directory (dtache--session-session-directory session))
-         (file-name (dtache--session-id session))
-         (socket (concat directory file-name dtache-socket-ext))
-         ;; Construct the command line
-         (commandline (dtache--output-command session))
-         (dtach-mode (if (dtache--session-degraded session)
-                         "-n"
-                       dtache--dtach-mode)))
-    (format "%s %s %s -z %s -c %s" dtache-program dtach-mode socket 
dtache-shell (shell-quote-argument commandline))))
-
-(defun dtache-degraded-p (command)
-  "Return t if COMMAND should run in degreaded mode."
-  (if (thread-last dtache-degraded-list
-        (seq-filter (lambda (regexp)
-                      (string-match-p regexp command)))
-        (length)
-        (= 0))
-      nil
-    t))
-
-(defun dtache-session-notify-command (session)
-  "Append notify-send to SESSION's command."
-  (let* ((command (dtache--session-command session))
-         (emacs-icon
-          (concat data-directory
-                  "images/icons/hicolor/scalable/apps/emacs.svg")))
-    (if (file-remote-p default-directory)
-        command
-      (concat
-       command
-       (format " && notify-send \"Dtache finished: %s\"" command)
-       (format " --icon %s" emacs-icon)))))
-
-(defun dtache-metadata ()
-  "Return a property list with metadata."
-  (let ((metadata '()))
-    (seq-doseq (annotator dtache-metadata-annotators)
-      (setq metadata (plist-put metadata (car annotator) (funcall (cdr 
annotator)))))
-    metadata))
-
-(defun dtache-encode-session (session)
-  "Encode SESSION as a string."
-  (let ((command
-         (dtache--session-truncate-command session))
-        (id
-         (dtache--session-short-id session)))
-    (concat
-     command
-     "  "
-     (propertize id 'face 'font-lock-comment-face))))
-
-(defun dtache-decode-session (candidate)
-  "Return the session that match CANDIDATE."
-  (cdr (assoc candidate dtache--session-candidates)))
-
-(defun dtache-create-session-directory ()
-  "Create session directory if it doesn't exist."
-  (let ((directory
-         (concat
-          (file-remote-p default-directory)
-          dtache-session-directory)))
-    (unless (file-exists-p directory)
-      (make-directory directory t))))
-
-;;;; Commands
+;;;###autoload
+(defun dtache-open-session (session)
+  "Open a `dtache' SESSION."
+  (interactive
+   (list (if (eq major-mode 'dtache-sessions-mode)
+             (tabulated-list-get-id)
+           (dtache-select-session))))
+  (let* ((dispatch-function
+          (or (dtache--session-open-function session)
+              (alist-get (dtache--session-type session)
+                         dtache-dispatch-alist)
+              #'dtache-open-log)))
+    (funcall dispatch-function session)))
 
 ;;;###autoload
 (defun dtache-compile-session (session)
   "Open log of SESSION in `compilation-mode'."
   (interactive
-   (list (dtache-select-session)))
+   (list (if (eq major-mode 'dtache-sessions-mode)
+             (tabulated-list-get-id)
+           (dtache-select-session))))
   (let ((buffer-name
          (format "*dtache-compile-%s*"
                  (dtache--session-short-id session)))
         (file
-         (dtache-session-file session 'log)))
+         (dtache-session-file session 'log))
+        (tramp-verbose 1))
     (when (file-exists-p file)
       (with-current-buffer (get-buffer-create buffer-name)
         (setq-local buffer-read-only nil)
@@ -299,14 +186,35 @@ This function also makes sure that the HISTFILE is 
disabled for local shells."
         (insert-file-contents file)
         (setq-local default-directory
                     (dtache--session-working-directory session))
-        (compilation-mode))
+        (run-hooks 'dtache-compile-hooks)
+        (compilation-minor-mode)
+        (setq-local font-lock-defaults '(compilation-mode-font-lock-keywords 
t))
+        (font-lock-mode)
+        (read-only-mode))
       (pop-to-buffer buffer-name))))
 
+;;;###autoload
+(defun dtache-rerun-session (session)
+  "Rerun SESSION."
+  (interactive
+   (list (if (eq major-mode 'dtache-sessions-mode)
+             (tabulated-list-get-id)
+           (dtache-select-session))))
+  (let* ((default-directory
+           (dtache--session-working-directory session))
+         (dtache-open-session-function
+          (dtache--session-open-function session))
+         (dtache-session-callback-function
+          (dtache--session-callback-function session)))
+    (dtache-start-session (dtache--session-command session))))
+
 ;;;###autoload
 (defun dtache-copy-session-log (session)
   "Copy SESSION's log."
   (interactive
-   (list (dtache-select-session)))
+   (list (if (eq major-mode 'dtache-sessions-mode)
+             (tabulated-list-get-id)
+           (dtache-select-session))))
   (dtache--file-content
    (dtache-session-file session 'log)))
 
@@ -314,21 +222,27 @@ This function also makes sure that the HISTFILE is 
disabled for local shells."
 (defun dtache-copy-session-command (session)
   "Copy SESSION command."
   (interactive
-   (list (dtache-select-session)))
+   (list (if (eq major-mode 'dtache-sessions-mode)
+             (tabulated-list-get-id)
+           (dtache-select-session))))
   (kill-new (dtache--session-command session)))
 
 ;;;###autoload
 (defun dtache-insert-session-command (session)
   "Insert SESSION."
   (interactive
-   (list (dtache-select-session)))
+   (list (if (eq major-mode 'dtache-sessions-mode)
+             (tabulated-list-get-id)
+           (dtache-select-session))))
   (insert (dtache--session-command session)))
 
 ;;;###autoload
 (defun dtache-remove-session (session)
   "Remove SESSION."
   (interactive
-   (list (dtache-select-session)))
+   (list (if (eq major-mode 'dtache-sessions-mode)
+             (tabulated-list-get-id)
+           (dtache-select-session))))
   (if (dtache--session-active-p session)
       (message "Kill session first before removing it.")
     (dtache--db-remove-session session)))
@@ -337,32 +251,29 @@ This function also makes sure that the HISTFILE is 
disabled for local shells."
 (defun dtache-kill-session (session)
   "Send a TERM signal to SESSION."
   (interactive
-   (list (dtache-select-session)))
-  (let ((pid (dtache--session-pid session)))
+   (list (if (eq major-mode 'dtache-sessions-mode)
+             (tabulated-list-get-id)
+           (dtache-select-session))))
+  (let* ((pid (dtache--session-pid session)))
     (when pid
       (dtache--kill-processes pid))))
 
-(defun dtache--kill-processes (pid)
-  "Kill PID and all of its children."
-  (let ((child-processes
-         (split-string
-          (shell-command-to-string (format "pgrep -P %s" pid))
-          "\n" t)))
-    (seq-do (lambda (pid) (dtache--kill-processes pid)) child-processes)
-    (apply #'process-file `("kill" nil nil nil ,pid))))
-
 ;;;###autoload
 (defun dtache-open-log (session)
   "Open SESSION's log."
   (interactive
-   (list (dtache-select-session)))
+   (list (if (eq major-mode 'dtache-sessions-mode)
+             (tabulated-list-get-id)
+           (dtache-select-session))))
   (dtache--open-file session 'log))
 
 ;;;###autoload
 (defun dtache-tail-log (session)
   "Tail SESSION's log."
   (interactive
-   (list (dtache-select-session)))
+   (list (if (eq major-mode 'dtache-sessions-mode)
+             (tabulated-list-get-id)
+           (dtache-select-session))))
   (if (dtache--session-active-p session)
       (dtache--tail-file session 'log)
     (dtache--open-file session 'log)))
@@ -371,25 +282,20 @@ This function also makes sure that the HISTFILE is 
disabled for local shells."
 (defun dtache-open-stdout (session)
   "Open SESSION's stdout."
   (interactive
-   (list (dtache-select-session)))
+   (list (if (eq major-mode 'dtache-sessions-mode)
+             (tabulated-list-get-id)
+           (dtache-select-session))))
   (dtache--open-file session 'stdout))
 
 ;;;###autoload
 (defun dtache-open-stderr (session)
   "Open SESSION's stderr."
   (interactive
-   (list (dtache-select-session)))
+   (list (if (eq major-mode 'dtache-sessions-mode)
+             (tabulated-list-get-id)
+           (dtache-select-session))))
   (dtache--open-file session 'stderr))
 
-;;;###autoload
-(defun dtache-attach-to-session (session)
-  "Attach to SESSION."
-  (interactive
-   (list (dtache-select-session)))
-  (if (dtache--session-active-p session)
-      (dtache--attach-to-session session)
-    (funcall dtache-attach-alternate-function session)))
-
 ;;;###autoload
 (defun dtache-quit-tail-log ()
   "Quit `dtache' tail log.
@@ -401,26 +307,235 @@ nil before closing."
   (set-buffer-modified-p nil)
   (kill-buffer-and-window))
 
-;;;; Support functions
+;;;; Functions
 
 ;;;;; Session
 
-(cl-defgeneric dtache--attach-to-session (session)
-  "Attach to SESSION.")
+(defun dtache-start-session (command &optional show-output)
+  "Start a `dtache' session running COMMAND optionally SHOW-OUTPUT."
+  (let* ((dtache--dtach-mode 'new)
+         (session (dtache--create-session command))
+         (dtache-command (dtache-dtach-command session)))
+    (dtache-setup-notification session)
+    (when show-output
+      (if (file-remote-p default-directory)
+          (run-with-timer 0.1 nil (lambda () (dtache-tail-log session)))
+        (file-notify-add-watch
+         (dtache-session-file session 'log)
+         '(change)
+         (lambda (event)
+           (pcase-let ((`(,_ ,action ,_) event))
+             (when (eq action 'created)
+               (dtache-tail-log session)))))))
+
+    (apply #'start-file-process
+           `("dtache" nil ,dtache-dtach-program ,@dtache-command))))
+
+(defun dtache-select-session ()
+  "Return selected session."
+  (dtache-update-sessions)
+  (dtache-completing-read dtache--sessions))
+
+(defun dtache-update-sessions ()
+  "Update `dtache' sessions.
+
+Sessions running on  current host or localhost are updated."
+  (let ((current-host (dtache--host))
+        (updated-sessions))
+    (setq updated-sessions
+          (seq-map (lambda (it)
+                     (if (and (or (string= current-host (dtache--session-host 
it))
+                                  (string= "localhost" (dtache--session-host 
it)))
+                              (or (dtache--session-active it)
+                                  (dtache--session-deactivated-p it)))
+                         (dtache-update-session it)
+                       it))
+                   dtache--sessions))
+    (dtache--db-update-sessions updated-sessions)))
+
+(defun dtache-session-file (session file)
+  "Return the path to SESSION's FILE."
+  (let ((file-name
+         (concat
+          (dtache--session-id session)
+          (pcase file
+            ('socket dtache--socket-ext)
+            ('log dtache--log-ext)
+            ('stdout dtache--stdout-ext)
+            ('stderr dtache--stderr-ext))))
+        (directory (concat
+                    (file-remote-p (dtache--session-working-directory session))
+                    (dtache--session-session-directory session))))
+    (expand-file-name file-name directory)))
+
+(defun dtache-session-candidates (sessions)
+  "Return an alist of SESSIONS candidates."
+  (seq-map (lambda (it)
+             (let ((s (format #("%s\0%s" 2 5 (invisible t))
+                              (dtache--session-truncate-command  it)
+                              (dtache--session-short-id it))))
+               (prog1 s (put-text-property 0 1 'dtache--data it s))))
+           sessions))
+
+(defun dtache-update-session (session)
+  "Update SESSION."
+  (when (dtache--session-deactivated-p session)
+    (progn
+      (setf (dtache--session-active session) nil)
+      (setf (dtache--session-duration session)
+            (dtache--duration session))
+      (dtache-session-finish-notification session)
+      (when-let ((callback (dtache--session-callback-function session)))
+        (funcall callback))))
+  (setf (dtache--session-log-size session)
+        (file-attribute-size (file-attributes
+                              (dtache-session-file session 'log))))
+  session)
+
+(defun dtache-initialize ()
+  "Initialize `dtache'."
+
+  ;; Initialize sessions
+  (unless dtache--sessions-initialized
+    (unless (file-exists-p dtache-db-directory)
+      (make-directory dtache-db-directory t))
+
+    (setq dtache--sessions
+          (thread-last (dtache--db-select-sessions)
+                       ;; Remove missing local sessions
+                       (seq-remove (lambda (it)
+                                     (and (string= "localhost" 
(dtache--session-host it))
+                                          (dtache--session-dead-p it))))
+                       ;; Update local active sessions
+                       (seq-map (lambda (it)
+                                  (if (and (string= "localhost" 
(dtache--session-host it))
+                                           (dtache--session-active it))
+                                      (dtache-update-session it)
+                                    it)))))
+
+    ;; Setup notifications
+    (thread-last dtache--sessions
+      (seq-filter #'dtache--session-active)
+      (seq-do #'dtache-setup-notification))))
+
+(defun dtache-update-remote-sessions ()
+  "Update active remote sessions."
+  (let ((predicate
+         (lambda (s) (and (not (string= "localhost" (dtache--session-host s)))
+                     (dtache--session-active s)))))
+
+    ;; Update sessions
+    (thread-last dtache--sessions
+      (seq-map (lambda (it)
+                 (if (funcall predicate it)
+                     (dtache-update-session it)
+                   it)))
+      (dtache--db-update-sessions))
+
+    ;; Cancel timer if no active remote sessions
+    (unless (> (seq-count predicate dtache--sessions) 0)
+      (cancel-timer dtache--remote-session-timer)
+      (setq dtache--remote-session-timer nil))))
+
+(defun dtache-cleanup-host-sessions (host)
+  "Run cleanuup on HOST sessions."
+  (dtache--db-update-sessions
+   (seq-remove
+    (lambda (it)
+      (and (string= host (dtache--session-host it))
+           (dtache--session-dead-p it)))
+    dtache--sessions)))
+
+;;;;; Other
+
+(defun dtache-completing-read (sessions)
+  "Select a session from SESSIONS through `completing-read'."
+  (let* ((candidates (dtache-session-candidates sessions))
+         (metadata '(metadata
+                     (category . dtache)
+                     (cycle-sort-function . identity)
+                     (display-sort-function . identity)))
+         (coll (lambda (string predicate action)
+                 (if (eq action 'metadata)
+                     metadata
+                   (complete-with-action action candidates string predicate))))
+         (cand (minibuffer-with-setup-hook
+                   (lambda ()
+                     (add-hook 'after-change-functions 'dtache--eat-cookie nil 
t))
+                 (completing-read "Select session: " coll nil t nil
+                                  'dtache-session-history))))
+    (get-text-property 0 'dtache--data (car (member cand candidates)))))
+
+(defun dtache-setup-notification (session)
+  "Setup notification for SESSION."
+  (if (file-remote-p default-directory)
+      (dtache--create-remote-session-timer)
+    (dtache--add-end-of-session-notification session)))
+
+(defun dtache-dtach-command (session)
+  "Return a dtach command for SESSION."
+  (with-connection-local-variables
+   (let* ((directory (dtache--session-session-directory session))
+          (file-name (dtache--session-id session))
+          (socket (concat directory file-name dtache--socket-ext))
+          ;; Construct the command line
+          (commandline (dtache--output-command session))
+          (dtache--dtach-mode (if (dtache--session-degraded session)
+                                  'new
+                                dtache--dtach-mode)))
+     `(,(dtache--dtach-arg) ,socket "-z" ,dtache-shell-program "-c" 
,commandline))))
+
+(defun dtache-degraded-p (command)
+  "Return t if COMMAND should run in degreaded mode."
+  (if (thread-last dtache-degraded-list
+        (seq-filter (lambda (regexp)
+                      (string-match-p regexp command)))
+        (length)
+        (= 0))
+      nil
+    t))
+
+(defun dtache-metadata ()
+  "Return a property list with metadata."
+  (let ((metadata '()))
+    (seq-doseq (annotator dtache-metadata-annotators-alist)
+      (push `(,(car annotator) . ,(funcall (cdr annotator))) metadata))
+    metadata))
+
+(defun dtache-create-session-directory ()
+  "Create session directory if it doesn't exist."
+  (let ((directory
+         (concat
+          (file-remote-p default-directory)
+          dtache-session-directory)))
+    (unless (file-exists-p directory)
+      (make-directory directory t))))
+
+;;;; Support functions
+
+;;;;; Session
 
 (defun dtache--create-session (command)
   "Create a `dtache' session from COMMAND."
+  (dtache-create-session-directory)
   (let ((session
          (dtache--session-create :id (dtache--create-id command)
                                  :command command
+                                 :type dtache-session-type
+                                 :open-function dtache-open-session-function
+                                 :callback-function 
dtache-session-callback-function
                                  :working-directory default-directory
                                  :degraded (dtache-degraded-p command)
                                  :creation-time (time-to-seconds 
(current-time))
+                                 :log-size 0
                                  :session-directory (file-name-as-directory 
dtache-session-directory)
                                  :host (dtache--host)
                                  :metadata (dtache-metadata)
                                  :active t)))
-    (dtache--db-insert-session session)
+    ;; Update list of sessions
+    (push session dtache--sessions)
+    ;; Update database
+    (dtache--db-update-sessions dtache--sessions)
     session))
 
 (defun dtache--session-pid (session)
@@ -429,7 +544,7 @@ nil before closing."
           (concat
            (dtache--session-session-directory session)
            (dtache--session-id session)
-           dtache-socket-ext))
+           dtache--socket-ext))
          (regexp (rx-to-string `(and "dtach " (or "-n " "-c ") ,socket)))
          (ps-args '("aux" "-w")))
     (with-temp-buffer
@@ -465,21 +580,11 @@ nil before closing."
 (defun dtache--session-update (session)
   "Update the `dtache' SESSION."
   (setf (dtache--session-active session) (dtache--session-active-p session))
-  (setf (dtache--session-duration session) (dtache--duration session))
   (setf (dtache--session-log-size session) (file-attribute-size
                                             (file-attributes
                                              (dtache-session-file session 
'log))))
   session)
 
-(defun dtache--session-git-branch ()
-  "Return current git branch."
-  (let ((git-directory (locate-dominating-file "." ".git")))
-    (when git-directory
-      (let ((args '("name-rev" "--name-only" "HEAD")))
-        (with-temp-buffer
-          (apply #'process-file `("git" nil t nil ,@args))
-          (string-trim (buffer-string)))))))
-
 (defun dtache--session-short-id (session)
   "Return the short representation of the SESSION's id."
   (let ((id (dtache--session-id session)))
@@ -490,95 +595,122 @@ nil before closing."
   (file-exists-p
    (dtache-session-file session 'socket)))
 
+(defun dtache--session-deactivated-p (session)
+  "Return t if SESSION has been deactivated."
+  (and
+   (dtache--session-active session)
+   (not (file-exists-p (dtache-session-file session 'socket)))))
+
 (defun dtache--session-dead-p (session)
   "Return t if SESSION is dead."
   (not
    (file-exists-p
     (dtache-session-file session 'log))))
 
+(defun dtache--create-remote-session-timer ()
+  "Create a new remote session and trigger timer."
+  (unless dtache--remote-session-timer
+    (setq dtache--remote-session-timer
+          (run-with-timer 10 60 #'dtache-update-remote-sessions))))
+
 ;;;;; Database
 
-(defun dtache--db-insert-session (session)
-  "Insert SESSION into the database."
-  (dtache-initialize)
-  (let ((id (dtache--session-id session))
-        (host (dtache--session-host session))
-        (active (dtache--session-active session)))
-    (emacsql dtache-db `[:insert
-                         :into dtache-sessions
-                         :values ([,id ,host ,active ,session])])))
-
-(defun dtache--db-update-session (session)
-  "Update the database with SESSION."
-  (let ((id (dtache--session-id session)))
-    (emacsql dtache-db [:update dtache-sessions
-                        :set (= dtache-session $s2)
-                        :where (= id $s1)]
-             id session)
-    (emacsql dtache-db [:update dtache-sessions
-                        :set (= active $s2)
-                        :where (= id $s1)]
-             id (dtache--session-active session))))
+(defun dtache--db-select-sessions ()
+  "Return all sessions stored in database."
+  (let ((db (expand-file-name "dtache.db" dtache-db-directory)))
+    (when (file-exists-p db)
+      (with-temp-buffer
+        (insert-file-contents db)
+        (cl-assert (eq (point) (point-min)))
+        (read (current-buffer))))))
 
 (defun dtache--db-remove-session (session)
-  "Remove SESSION from the database."
+  "Remove SESSION from database."
   (let ((id (dtache--session-id session)))
-    (emacsql dtache-db [:delete
-                        :from dtache-sessions
-                        :where (= id $s1)]
-             id)))
-
-(defun dtache--db-select-session (id)
-  "Return the session with ID from the database."
-  (caar
-   (emacsql dtache-db [:select dtache-session
-                       :from dtache-sessions
-                       :where (= id $s1)]
-            id)))
-
-(defun dtache--db-select-host-sessions (host)
-  "Return all HOST sessions from the database."
-  (let ((sessions
-         (emacsql dtache-db
-                  [:select dtache-session
-                   :from dtache-sessions
-                   :where (=  host $s1)]
-                  host)))
-    (seq-map #'car sessions)))
-
-(defun dtache--db-select-active-sessions (host)
-  "Return all active HOST sessions from the database."
-  (let ((sessions
-         (emacsql dtache-db
-                  [:select dtache-session
-                   :from dtache-sessions
-                   :where (= host $s1) :and (=  active $s2)]
-                  host t)))
-    (seq-map #'car sessions)))
-
-;;;;; Shell
-
-(defun dtache--shell-comint-read-input-ring-a (orig-fun &rest args)
-  "Set `comint-input-ring-file-name' before calling ORIG-FUN with ARGS."
-  (with-connection-local-variables
-   (let ((comint-input-ring-file-name
-          (concat
-           (file-remote-p default-directory)
-           dtache-shell-history-file)))
-     (apply orig-fun args)
-     (advice-remove 'comint-read-input-ring 
#'dtache--shell-comint-read-input-ring-a))))
+    (setq dtache--sessions
+          (seq-remove (lambda (it)
+                        (string= id (dtache--session-id it)))
+                      dtache--sessions))
+    (dtache--db-update-sessions dtache--sessions)))
 
-(defun dtache--shell-save-history ()
-  "Save `shell' history."
-  (with-connection-local-variables
-   (let ((comint-input-ring-file-name
-          (concat
-           (file-remote-p default-directory)
-           dtache-shell-history-file)))
-     (comint-write-input-ring))))
+(defun dtache--db-update-session (session)
+  "Update SESSION in database."
+  (let ((id (dtache--session-id session)))
+    (setq dtache--sessions
+          (seq-map (lambda (it)
+                     (if (string= (dtache--session-id it) id)
+                         session
+                       it))
+                   dtache--sessions))
+    (dtache--db-update-sessions dtache--sessions)))
+
+(defun dtache--db-update-sessions (sessions)
+  "Write SESSIONS to database."
+  (setq dtache--sessions sessions)
+  (let ((db (expand-file-name "dtache.db" dtache-db-directory)))
+    (with-temp-file db
+      (prin1 dtache--sessions (current-buffer)))))
 
 ;;;;; Other
 
+(defun dtache--dtach-arg ()
+  "Return dtach argument based on mode."
+  (pcase dtache--dtach-mode
+    ('new "-n")
+    ('create "-c")
+    ('attach "-a")
+    (_ "-n")))
+
+(defun dtache-session-finish-notification (session)
+  "Send a notification when SESSION finish."
+  (message "Dtache finished session: %s"
+           (dtache--session-command session)))
+
+(defun dtache--add-end-of-session-notification (session)
+  "Trigger an event when SESSION is stopped."
+  (file-notify-add-watch
+   (dtache-session-file session 'socket)
+   '(change)
+   (lambda (event)
+     (pcase-let ((`(,_ ,action ,_) event))
+       (when (eq action 'deleted)
+         ;; Update session
+         (setf (dtache--session-log-size session) (file-attribute-size
+                                                   (file-attributes
+                                                    (dtache-session-file 
session 'log))))
+         (setf (dtache--session-active session) nil)
+         (setf (dtache--session-duration session)
+               (- (time-to-seconds) (dtache--session-creation-time session)))
+
+         ;; Update session in database
+         (dtache--db-update-session session)
+
+         ;; Send notification
+         (dtache-session-finish-notification session)
+
+         ;; Execute callback
+         (when-let ((callback (dtache--session-callback-function session)))
+           (funcall callback)))))))
+
+(defun dtache--eat-cookie (&rest _)
+  "Eat the disambiguation cookie in the minibuffer."
+  (let* ((pos (minibuffer-prompt-end))
+         (max (point-max)))
+    (while (and (< pos max) (/= 0 (char-after pos)))
+      (setq pos (1+ pos)))
+    (when (< pos max)
+      (add-text-properties pos (next-property-change pos nil max)
+                           '(invisible t rear-nonsticky t)))))
+
+(defun dtache--kill-processes (pid)
+  "Kill PID and all of its children."
+  (let ((child-processes
+         (split-string
+          (shell-command-to-string (format "pgrep -P %s" pid))
+          "\n" t)))
+    (seq-do (lambda (pid) (dtache--kill-processes pid)) child-processes)
+    (apply #'process-file `("kill" nil nil nil ,pid))))
+
 (defun dtache--output-command (session)
   "Return output command for SESSION."
   (if (dtache--session-degraded session)
@@ -587,22 +719,22 @@ nil before closing."
 
 (defun dtache--output-to-file-command (session)
   "Return a command to send SESSION's output directly to log."
-  (let* ((command (dtache-session-command session))
+  (let* ((command (dtache--session-command session))
          (directory (dtache--session-session-directory session))
          (file-name (dtache--session-id session))
-         (log (concat directory file-name dtache-log-ext)))
+         (log (concat directory file-name dtache--log-ext)))
     ;; Construct the command line
     ;;   echo &> log
     (format "{ %s; } &> %s" command log)))
 
 (defun dtache--output-to-both-command (session)
   "Return a command to send SESSION's output to both shell and log."
-  (let* ((command (dtache-session-command session))
+  (let* ((command (dtache--session-command session))
          (directory (dtache--session-session-directory session))
          (file-name (dtache--session-id session))
-         (stdout (concat directory file-name dtache-stdout-ext))
-         (stderr (concat directory file-name dtache-stderr-ext))
-         (log (concat directory file-name dtache-log-ext)))
+         (stdout (concat directory file-name dtache--stdout-ext))
+         (stderr (concat directory file-name dtache--stderr-ext))
+         (log (concat directory file-name dtache--log-ext)))
     ;; Construct the command line
     ;;   { { echo stdout; echo stderr >&2; } >>(tee stdout ); } 2>>(tee 
stderr) | tee log
     (format "{ { %s; }%s }%s %s"
@@ -613,12 +745,9 @@ nil before closing."
 
 (defun dtache--host ()
   "Return name of host."
-  (if-let ((remote-host (file-remote-p default-directory))
-           (regexp (rx "/" (one-or-more alpha) ":" (group (regexp ".*")) ":")))
-      (progn
-        (string-match regexp remote-host)
-        (match-string 1 remote-host))
-    "localhost"))
+  (or
+   (file-remote-p default-directory 'host)
+   "localhost"))
 
 (defun dtache--file-content (file)
   "Copy FILE's content."
@@ -631,20 +760,17 @@ nil before closing."
 
 Modification time is not reliable whilst a session is active.  Instead
 the current time is used."
-  ;; TODO: Consider calculating a time offset between host and remote
-  ;; computer
-  (if (dtache--session-active session)
-      (- (time-to-seconds) (dtache--session-creation-time session))
-    (- (time-to-seconds
-        (file-attribute-modification-time
-         (file-attributes
-          (dtache-session-file session 'log))))
-       (dtache--session-creation-time session))))
+  (- (time-to-seconds
+      (file-attribute-modification-time
+       (file-attributes
+        (dtache-session-file session 'log))))
+     (dtache--session-creation-time session)))
 
 (defun dtache--open-file (session file)
   "Oen SESSION's FILE."
   (let* ((file-path
-          (dtache-session-file session file)))
+          (dtache-session-file session file))
+         (tramp-verbose 1))
     (if (file-exists-p file-path)
         (progn
           (find-file-other-window file-path)
@@ -656,7 +782,8 @@ the current time is used."
 (defun dtache--tail-file (session file)
   "Tail SESSION's FILE."
   (let* ((file-path
-          (dtache-session-file session file)))
+          (dtache-session-file session file))
+         (tramp-verbose 1))
     (when (file-exists-p file-path)
       (find-file-other-window file-path)
       (dtache-tail-mode)
@@ -667,7 +794,55 @@ the current time is used."
   (let ((current-time (current-time-string)))
     (secure-hash 'md5 (concat command current-time))))
 
-;;;; Major mode
+;;;;; UI
+
+(defun dtache--metadata-str (session)
+  "Return SESSION's metadata as a string."
+  (string-join
+   (thread-last (dtache--session-metadata session)
+     (seq-filter (lambda (it) (cdr it)))
+     (seq-map
+      (lambda (it)
+        (concat (symbol-name (car it)) ": " (cdr it)))))
+   " "))
+
+(defun dtache--duration-str (session)
+  "Return SESSION's duration time."
+  (let* ((time
+          (round (if (dtache--session-active session)
+                     (- (time-to-seconds) (dtache--session-creation-time 
session))
+                   (dtache--session-duration session))))
+         (hours (/ time 3600))
+         (minutes (/ (mod time 3600) 60))
+         (seconds (mod time 60)))
+    (cond ((> time (* 60 60)) (format "%sh %sm %ss" hours minutes seconds))
+          ((> time 60) (format "%sm %ss" minutes seconds))
+          (t (format "%ss" seconds)))))
+
+(defun dtache--creation-str (session)
+  "Return SESSION's creation time."
+  (format-time-string
+   "%b %d %H:%M"
+   (dtache--session-creation-time session)))
+
+(defun dtache--size-str (session)
+  "Return the size of SESSION's log."
+  (file-size-human-readable
+   (dtache--session-log-size session)))
+
+(defun dtache--degraded-str (session)
+  "Return string if SESSION is degraded."
+  (if (dtache--session-degraded session)
+      "!"
+    ""))
+
+(defun dtache--active-str (session)
+  "Return string if SESSION is active."
+  (if (dtache--session-active session)
+      "*"
+    ""))
+
+;;;; Major modes
 
 (defvar dtache-log-mode-map
   (let ((map (make-sparse-keymap)))
@@ -688,19 +863,54 @@ the current time is used."
 (define-derived-mode dtache-tail-mode auto-revert-tail-mode "Dtache Tail"
   "Major mode for tailing dtache logs."
   (setq-local auto-revert-interval dtache-tail-interval)
+  (setq-local tramp-verbose 1)
+  (setq-local auto-revert-remote-files t)
+  (defvar revert-buffer-preserve-modes)
   (setq-local revert-buffer-preserve-modes nil)
   (auto-revert-set-timer)
   (setq-local auto-revert-verbose nil)
   (auto-revert-tail-mode)
   (read-only-mode t))
 
-(defun dtache-setup-evil-bindings ()
-  "Function that use `general' to setup `evil' bindings."
-  (when (fboundp 'general-def)
-    (general-def '(motion normal) dtache-log-mode-map
-      "q" #'kill-buffer-and-window)
-    (general-def '(motion normal) dtache-tail-mode-map
-      "q" #'dtache-quit-tail-log)))
+;;;; Tabulated list interface
+
+(define-derived-mode dtache-sessions-mode tabulated-list-mode "Dtache Sessions"
+  "Dtache sessions."
+  (setq tabulated-list-format
+        `[("Command" ,dtache-max-command-length nil)
+          ("Active" 10 nil)
+          ("Directory" 30 nil)
+          ("Host" 20 nil)
+          ("Duration" 10 nil)
+          ("Created" 20 nil)
+          ("ID" 8 nil)])
+  (setq tabulated-list-padding 2)
+  (setq tabulated-list-sort-key nil)
+  (tabulated-list-init-header))
+
+(defun dtache-get-sesssion-entry (session)
+  "Return expected format of SESSION."
+  `(,session
+    [,(dtache--session-command session)
+     ,(dtache--active-str session)
+     ,(dtache--session-working-directory session)
+     ,(dtache--session-host session)
+     ,(dtache--duration-str session)
+     ,(dtache--creation-str session)
+     ,(dtache--session-short-id session)]))
+
+(let ((map dtache-sessions-mode-map))
+  (define-key map (kbd "<return>") #'dtache-open-session)
+  (define-key map (kbd "c") #'dtache-compile-session)
+  (define-key map (kbd "d") #'dtache-remove-session)
+  (define-key map (kbd "e") #'dtache-open-stderr)
+  (define-key map (kbd "k") #'dtache-kill-session)
+  (define-key map (kbd "l") #'dtache-open-log)
+  (define-key map (kbd "o") #'dtache-open-stdout)
+  (define-key map (kbd "r") #'dtache-rerun-session)
+  (define-key map (kbd "t") #'dtache-tail-log)
+  (define-key map (kbd "w") #'dtache-copy-session-command)
+  (define-key map (kbd "W") #'dtache-copy-session-log))
 
 (provide 'dtache)
 
diff --git a/embark-dtache.el b/embark-dtache.el
index 7e308d62c6..1f214c7d84 100644
--- a/embark-dtache.el
+++ b/embark-dtache.el
@@ -5,7 +5,7 @@
 ;; Author: Niklas Eklund <niklas.eklund@posteo.net>
 ;; URL: https://www.gitlab.com/niklaseklund/dtache.git
 ;; Version: 0.1
-;; Package-Requires: ((emacs "26.1"))
+;; Package-Requires: ((emacs "27.1"))
 ;; Keywords: convenience processes
 
 ;; This file is not part of GNU Emacs.
@@ -25,7 +25,7 @@
 
 ;;; Commentary:
 
-;; This package provides `embark' actions to operate on `dtache' sessions.
+;; This package provides `embark' actions to operate on a `dtache' session.
 
 ;;; Code:
 
@@ -38,17 +38,17 @@
 
 (embark-define-keymap embark-dtache-map
   "Keymap for Embark dtache actions."
-  ("a" dtache-attach-to-session)
-  ("l" dtache-open-log)
-  ("t" dtache-tail-log)
+  ("c" dtache-compile-session)
+  ("d" dtache-remove-session)
   ("e" dtache-open-stderr)
-  ("o" dtache-open-stdout)
   ("i" dtache-insert-session-command)
+  ("k" dtache-kill-session)
+  ("l" dtache-open-log)
+  ("o" dtache-open-stdout)
+  ("r" dtache-rerun-session)
+  ("t" dtache-tail-log)
   ("w" dtache-copy-session-command)
-  ("W" dtache-copy-session-log)
-  ("c" dtache-compile-session)
-  ("d" dtache-remove-session)
-  ("k" dtache-kill-session))
+  ("W" dtache-copy-session-log))
 
 (add-to-list 'embark-keymap-alist '(dtache . embark-dtache-map))
 
diff --git a/guix.scm b/guix.scm
index 322a8f2b72..d37d25601b 100644
--- a/guix.scm
+++ b/guix.scm
@@ -36,8 +36,7 @@
        (file-name (git-file-name name version))))
      (build-system emacs-build-system)
      (propagated-inputs
-      `(("emacs-emacsql-sqlite3" ,emacs-emacsql-sqlite3)
-        ("emacs-embark" ,emacs-embark)
+      `(("emacs-embark" ,emacs-embark)
         ("emacs-marginalia" ,emacs-marginalia)))
      (native-inputs
       `(("emacs-ert-runner" ,emacs-ert-runner)))
diff --git a/marginalia-dtache.el b/marginalia-dtache.el
index cac5694fac..c1889be8f4 100644
--- a/marginalia-dtache.el
+++ b/marginalia-dtache.el
@@ -5,7 +5,7 @@
 ;; Author: Niklas Eklund <niklas.eklund@posteo.net>
 ;; URL: https://www.gitlab.com/niklaseklund/dtache.git
 ;; Version: 0.1
-;; Package-Requires: ((emacs "26.1"))
+;; Package-Requires: ((emacs "27.1"))
 ;; Keywords: convenience processes
 
 ;; This file is not part of GNU Emacs.
@@ -25,7 +25,8 @@
 
 ;;; Commentary:
 
-;; This package provides annotated `dtache' sessions with `marginalia'.
+;; This package provides annotated `dtache' sessions through
+;; `marginalia' which enhances the `dtache-open-session'.
 
 ;;; Code:
 
@@ -36,7 +37,7 @@
 
 ;;;; Variables
 
-(defvar marginalia-dtache-git-branch-length 30)
+(defvar marginalia-dtache-metadata-length 30)
 (defvar marginalia-dtache-duration-length 10)
 (defvar marginalia-dtache-size-length 8)
 (defvar marginalia-dtache-date-length 12)
@@ -48,7 +49,7 @@
   :group 'marginalia
   :group 'faces)
 
-(defface marginalia-dtache-git
+(defface marginalia-dtache-metadata
   '((t :inherit marginalia-char))
   "Face used to highlight git information in `marginalia-mode'.")
 
@@ -76,53 +77,15 @@
 
 (defun marginalia-dtache-annotate (candidate)
   "Annotate dtache CANDIDATE."
-  (let* ((session (dtache-decode-session candidate)))
+  (let* ((session
+          (get-text-property 0 'dtache--data candidate)))
     (marginalia--fields
-     ((marginalia-dtache--active session) :width 3 :face 
'marginalia-dtache-active)
-     ((marginalia-dtache--degraded session) :width 3 :face 
'marginalia-dtache-error)
-     ((marginalia-dtache--git-branch session) :truncate 
marginalia-dtache-git-branch-length :face 'marginalia-dtache-git)
-     ((marginalia-dtache--duration session) :truncate 
marginalia-dtache-duration-length :face 'marginalia-dtache-duration)
-     ((marginalia-dtache--size session) :truncate 
marginalia-dtache-size-length :face 'marginalia-dtache-size)
-     ((marginalia-dtache--creation session) :truncate 
marginalia-dtache-date-length :face 'marginalia-dtache-date))))
-
-;;;; Support functions
-
-(defun marginalia-dtache--duration (session)
-  "Return SESSION's duration time."
-  (let* ((time (round (dtache--session-duration session)))
-         (hours (/ time 3600))
-         (minutes (/ (mod time 3600) 60))
-         (seconds (mod time 60)))
-    (cond ((> time (* 60 60)) (format "%sh %sm %ss" hours minutes seconds))
-          ((> time 60) (format "%sm %ss" minutes seconds))
-          (t (format "%ss" seconds)))))
-
-(defun marginalia-dtache--creation (session)
-  "Return SESSION's creation time."
-  (format-time-string
-   "%b %d %H:%M"
-   (dtache--session-creation-time session)))
-
-(defun marginalia-dtache--size (session)
-  "Return the size of SESSION's log."
-  (file-size-human-readable
-   (dtache--session-log-size session)))
-
-(defun marginalia-dtache--git-branch (session)
-  "Return the git branch for SESSION."
-  (plist-get (dtache--session-metadata session) :git-branch))
-
-(defun marginalia-dtache--active (session)
-  "Return string if SESSION is active."
-  (if (dtache--session-active session)
-      "*"
-    ""))
-
-(defun marginalia-dtache--degraded (session)
-  "Return string if SESSION is degraded."
-  (if (dtache--session-degraded session)
-      "!"
-    ""))
+     ((dtache--active-str session) :width 3 :face 'marginalia-dtache-active)
+     ((dtache--degraded-str session) :width 3 :face 'marginalia-dtache-error)
+     ((dtache--metadata-str session) :truncate 
marginalia-dtache-metadata-length :face 'marginalia-dtache-metadata)
+     ((dtache--duration-str session) :truncate 
marginalia-dtache-duration-length :face 'marginalia-dtache-duration)
+     ((dtache--size-str session) :truncate marginalia-dtache-size-length :face 
'marginalia-dtache-size)
+     ((dtache--creation-str session) :truncate marginalia-dtache-date-length 
:face 'marginalia-dtache-creation))))
 
 (provide 'marginalia-dtache)
 
diff --git a/test/dtache-test.el b/test/dtache-test.el
index 232de41bc3..e85a815215 100644
--- a/test/dtache-test.el
+++ b/test/dtache-test.el
@@ -36,13 +36,11 @@
 (defmacro dtache-test--with-temp-database (&rest body)
   "Initialize a dtache database and evaluate BODY."
   `(let* ((temp-directory (make-temp-file "dtache" t))
-          (dtache-db-directory (expand-file-name "db" temp-directory))
-          (dtache-session-directory (expand-file-name "sessions" 
temp-directory))
-          (dtache-db))
+          (dtache-db-directory (expand-file-name "dtache.db" temp-directory))
+          (dtache-session-directory (expand-file-name "sessions" 
temp-directory)))
      (unwind-protect
          (progn
-           (dtache-db-initialize)
-           (dtache-create-session-directory)
+           (dtache-initialize)
            ,@body)
        (delete-directory temp-directory t))))
 
@@ -71,30 +69,32 @@
 
 (ert-deftest dtache-test-dtach-command ()
   (cl-letf* (((symbol-function #'dtache--output-command) (lambda (_) 
"command"))
-             (dtache-shell "zsh")
-             (dtache-program "/usr/bin/dtach")
-             (dtache--dtach-mode "-c")
+             (dtache-shell-program "zsh")
+             (dtache-dtach-program "/usr/bin/dtach")
+             (dtache--dtach-mode 'create)
              (actual
               (dtache-dtach-command
                (dtache--session-create :id "12345" :session-directory 
"/tmp/dtache/")))
-             (expected "/usr/bin/dtach -c /tmp/dtache/12345.socket -z zsh -c 
command"))
-    (should (string= expected actual))))
+             (expected `(, "-c" "/tmp/dtache/12345.socket" "-z" "zsh" "-c" 
"command")))
+    (should (equal expected actual))))
 
 (ert-deftest dtache-test-metadata ()
   ;; No annotators
-  (let ((dtache-metadata-annotators '()))
+  (let ((dtache-metadata-annotators-alist '()))
     (should (not (dtache-metadata))))
 
-  ;; Two annotatos
-  (let ((dtache-metadata-annotators
-         '((:git-branch . (lambda () "foo"))
-           (:username . (lambda () "bar"))))
-        (expected '(:git-branch "foo" :username "bar")))
+  ;; Two annotators
+  (let ((dtache-metadata-annotators-alist
+         '((git-branch . (lambda () "foo"))
+           (username . (lambda () "bar"))))
+        (expected '((username . "bar")
+                    (git-branch . "foo"))))
     (should (equal (dtache-metadata) expected))))
 
 (ert-deftest dtache-test-session-file ()
   ;; Local files
   (cl-letf* (((symbol-function #'expand-file-name) (lambda (file directory) 
(concat directory file)))
+             ((symbol-function #'file-remote-p) (lambda (_directory) nil))
              (session (dtache--session-create :id "12345" :session-directory 
"/home/user/tmp/")))
     (should (string= "/home/user/tmp/12345.log" (dtache-session-file session 
'log)))
     (should (string= "/home/user/tmp/12345.stderr" (dtache-session-file 
session 'stderr)))
@@ -124,13 +124,6 @@
                      (dtache--session-truncate-command
                       (dtache--session-create :command "12345678"))))))
 
-(ert-deftest dtache-test-session-encode ()
-  (let ((session
-         (dtache--session-create :command "abcdefghijk"
-                                 :id "-------12345678"))
-        (dtache-max-command-length 8))
-    (should (string= "ab...jk  12345678" (dtache-encode-session session)))))
-
 (ert-deftest dtache-test-host ()
   (should (string= "localhost" (dtache--host)))
   (let ((default-directory "/ssh:remotehost:/home/user/git"))
@@ -152,117 +145,58 @@
      (dtache-test--change-session-state session 'kill)
      (should (dtache--session-dead-p session)))))
 
-(ert-deftest dtache-test-session-decode ()
-  (dtache-test--with-temp-database
-   (dtache-test--create-session :command "foo" :host "localhost")
-   (dtache-session-candidates)
-   (should
-    (equal (elt (dtache--db-select-host-sessions "localhost") 0)
-           (dtache-decode-session
-            (car (elt dtache--session-candidates 0)))))))
-
-(ert-deftest dtache-test-session-candidates ()
-  (dtache-test--with-temp-database
-   (dtache-test--create-session :command "foo" :host "localhost")
-   (dtache-test--create-session :command "bar" :host "localhost")
-   (should
-    (seq-set-equal-p
-     (thread-last (dtache-session-candidates)
-       (seq-map #'cdr))
-     (seq-reverse
-      (dtache--db-select-host-sessions "localhost"))))))
-
-(ert-deftest dtache-test-update-sessions ()
-  (dtache-test--with-temp-database
-   (cl-letf* ((session1 (dtache-test--create-session :command "foo" :host 
"localhost"))
-              (session2 (dtache-test--create-session :command "bar" :host 
"localhost"))
-              (session3 (dtache-test--create-session :command "baz" :host 
"remotehost"))
-              (host "localhost")
-              ((symbol-function #'dtache--host) (lambda () host)))
-     ;; Add three sessions two matching host which will be
-     ;; updated. One of them is dead and should be removed
-     (dtache-test--change-session-state session2 'kill)
-     (dtache-test--change-session-state session3 'deactivate)
-     (dtache-update-sessions)
-     (let ((db-sessions (dtache--db-select-host-sessions host)))
-       (should (= (length db-sessions) 1))
-       (should (string= (dtache--session-id (elt db-sessions 0)) 
(dtache--session-id session1)))
-       (should (not (equal (elt db-sessions 0) session1)))))))
-
-(ert-deftest dtache-test-cleanup-sessions ()
+(ert-deftest dtache-test-cleanup-host-sessions ()
   (dtache-test--with-temp-database
    (cl-letf* ((session1 (dtache-test--create-session :command "foo" :host 
"remotehost"))
               (session2 (dtache-test--create-session :command "bar" :host 
"localhost"))
               (session3 (dtache-test--create-session :command "baz" :host 
"localhost"))
               (host "localhost")
               ((symbol-function #'dtache--host) (lambda () host)))
-     ;; One active, one dead, one active
+     ;; One inactive, one missing, one active
      (dtache-test--change-session-state session1 'deactivate)
      (dtache-test--change-session-state session2 'kill)
-     (dtache-cleanup-sessions)
+     (dtache-cleanup-host-sessions host)
      (should (seq-set-equal-p
-              (dtache--db-select-host-sessions host)
-              `(,session3))))))
+              (dtache--db-select-sessions)
+              `(,session1 ,session3))))))
 
 ;;;;; Database
 
-(ert-deftest dtache-test-db-initialize ()
-  (dtache-test--with-temp-database
-   (should (emacsql-live-p dtache-db))))
-
 (ert-deftest dtache-test-db-insert-session ()
   (dtache-test--with-temp-database
-   (let* ((session (dtache-test--create-session :command "foo" :host 
"localhost"))
-          (id (dtache--session-id session)))
-     (should (equal (dtache--db-select-session id) session)))))
+   (let* ((session (dtache-test--create-session :command "foo" :host 
"localhost")))
+     (should (equal (dtache--db-select-sessions) `(,session))))))
 
 (ert-deftest dtache-test-db-remove-session ()
   (dtache-test--with-temp-database
    (let* ((host "localhost")
           (session1 (dtache-test--create-session :command "foo" :host host))
           (session2 (dtache-test--create-session :command "bar" :host host)))
-     (should (seq-set-equal-p `(,session1 ,session2) 
(dtache--db-select-host-sessions host)))
+     (should (seq-set-equal-p `(,session1 ,session2) 
(dtache--db-select-sessions)))
      (dtache--db-remove-session session1)
-     (should (seq-set-equal-p `(,session2) (dtache--db-select-host-sessions 
host))))))
+     (should (seq-set-equal-p `(,session2) (dtache--db-select-sessions))))))
 
 (ert-deftest dtache-test-db-update-session ()
   (dtache-test--with-temp-database
    (let* ((session (dtache-test--create-session :command "foo" :host 
"localhost"))
           (id (dtache--session-id session)))
      (setf (dtache--session-active session) nil)
-     (should (not (equal session (dtache--db-select-session id))))
+     (should (not (equal session (car (dtache--db-select-sessions)))))
      (dtache--db-update-session session)
-     (should (equal session (dtache--db-select-session id))))))
-
-(ert-deftest dtache-test-db-select-host-sessions ()
-  (dtache-test--with-temp-database
-   (let* ((session1 (dtache-test--create-session :command "foo" :host 
"localhost"))
-          (session2 (dtache-test--create-session :command "bar" :host 
"remotehost"))
-          (session3 (dtache-test--create-session :command "baz" :host 
"localhost")))
-     (should (seq-set-equal-p `(,session2) (dtache--db-select-host-sessions 
"remotehost")))
-     (should (seq-set-equal-p `(,session1 ,session3) 
(dtache--db-select-host-sessions "localhost"))))))
-
-(ert-deftest dtache-test-db-select-active-sessions ()
-  (dtache-test--with-temp-database
-   (let* ((session1 (dtache-test--create-session :command "foo" :host 
"localhost"))
-          (session2 (dtache-test--create-session :command "bar" :host 
"remotehost"))
-          (session3 (dtache-test--create-session :command "baz" :host 
"localhost")))
-     (dtache-test--change-session-state session1 'deactivate)
-     (dtache-update-sessions)
-     (let ((sessions (dtache--db-select-active-sessions "localhost")))
-       (should (= (length sessions) 1))
-       (should (string= (dtache--session-id (elt sessions 0)) 
(dtache--session-id session3)))))))
+     (should (equal session (car (dtache--db-select-sessions)))))))
 
 (ert-deftest dtache-test-output-command ()
   ;; Degraded
-  (let* ((actual
+  (let* ((dtache-notify-send nil)
+         (actual
           (dtache--output-command
            (dtache--session-create :id "12345" :session-directory 
"/tmp/dtache/" :command "ls" :degraded t)))
          (expected "{ ls; } &> /tmp/dtache/12345.log"))
     (should (string= actual expected)))
 
   ;; Normal
-  (let* ((actual
+  (let* ((dtache-notify-send nil)
+         (actual
           (dtache--output-command
            (dtache--session-create :id "12345" :session-directory 
"/tmp/dtache/" :command "ls")))
          (expected "{ { ls; } > >(tee /tmp/dtache/12345.stdout ); } 2> >(tee 
/tmp/dtache/12345.stderr )  | tee /tmp/dtache/12345.log"))
@@ -280,11 +214,36 @@
              (session1 (dtache--session-create :id "foo" :session-directory 
"/tmp/"))
              (session2 (dtache--session-create :id "bar" :session-directory 
"/tmp/"))
              (session3 (dtache--session-create :id "baz" :session-directory 
"/tmp/"))
-             (dtache-socket-ext ".socket"))
+             (dtache--socket-ext ".socket"))
     (should (string= "6699" (dtache--session-pid session1)))
     (should (string= "6698" (dtache--session-pid session2)))
     (should (not (dtache--session-pid session3)))))
 
+;;;;; String representations
+
+(ert-deftest dtache-test-duration-str ()
+  (should (string= "1s" (dtache--duration-str (dtache--session-create 
:duration 1))))
+  (should (string= "1m 1s" (dtache--duration-str (dtache--session-create 
:duration 61))))
+  (should (string= "1h 1m 1s" (dtache--duration-str (dtache--session-create 
:duration 3661)))))
+
+(ert-deftest dtache-test-creation-str ()
+  ;; Make sure to set the TIMEZONE before executing the test to avoid
+  ;; differences between machines
+  (cl-letf (((getenv "TZ") "UTC0"))
+    (should (string= "May 08 08:49" (dtache--creation-str 
(dtache--session-create :creation-time 1620463748.7636228))))))
+
+(ert-deftest dtache-test-size-str ()
+  (should (string= "100" (dtache--size-str (dtache--session-create :log-size 
100))))
+  (should (string= "1k" (dtache--size-str (dtache--session-create :log-size 
1024)))))
+
+(ert-deftest dtache-test-degraded-str ()
+  (should (string= "!" (dtache--degraded-str (dtache--session-create :degraded 
t))))
+  (should (string= "" (dtache--degraded-str (dtache--session-create :degraded 
nil)))))
+
+(ert-deftest dtache-test-active-str ()
+  (should (string= "*" (dtache--active-str (dtache--session-create :active 
t))))
+  (should (string= "" (dtache--active-str (dtache--session-create :active 
nil)))))
+
 (provide 'dtache-test)
 
 ;;; dtache-test.el ends here
diff --git a/test/marginalia-dtache-test.el b/test/marginalia-dtache-test.el
deleted file mode 100644
index 6c8c9abb14..0000000000
--- a/test/marginalia-dtache-test.el
+++ /dev/null
@@ -1,61 +0,0 @@
-;;; marginalia-dtache-test.el --- Tests for marginalia-dtache.el -*- 
lexical-binding: t; -*-
-
-;; Copyright (C) 2020-2021  Niklas Eklund
-
-;; Author: Niklas Eklund <niklas.eklund@posteo.net>
-;; Url: https://gitlab.com/niklaseklund/dtache
-;; Package-Requires: ((emacs "27.1"))
-;; Version: 0.1
-
-;; 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 <https://www.gnu.org/licenses/>.
-
-;;; Commentary:
-
-;; Tests for `marginalia-dtache'.
-
-;;; Code:
-
-(require 'ert)
-(require 'marginalia-dtache)
-
-(ert-deftest marginalia-dtache-test-duration ()
-  (should (string= "1s" (marginalia-dtache--duration (dtache--session-create 
:duration 1))))
-  (should (string= "1m 1s" (marginalia-dtache--duration 
(dtache--session-create :duration 61))))
-  (should (string= "1h 1m 1s" (marginalia-dtache--duration 
(dtache--session-create :duration 3661)))))
-
-(ert-deftest marginalia-dtache-test-creation ()
-  ;; Make sure to set the TIMEZONE before executing the test to avoid
-  ;; differences between machines
-  (cl-letf (((getenv "TZ") "UTC0"))
-    (should (string= "May 08 08:49" (marginalia-dtache--creation 
(dtache--session-create :creation-time 1620463748.7636228))))))
-
-(ert-deftest marginalia-dtache-test-size ()
-  (should (string= "100" (marginalia-dtache--size (dtache--session-create 
:log-size 100))))
-  (should (string= "1k" (marginalia-dtache--size (dtache--session-create 
:log-size 1024)))))
-
-(ert-deftest marginalia-dtache-git ()
-  (should (string= "foo" (marginalia-dtache--git-branch 
(dtache--session-create :metadata '(:git-branch "foo")))))
-  (should (not (marginalia-dtache--git-branch (dtache--session-create)))))
-
-(ert-deftest marginalia-dtache-active ()
-  (should (string= "*" (marginalia-dtache--active (dtache--session-create 
:active t))))
-  (should (string= "" (marginalia-dtache--active (dtache--session-create 
:active nil)))))
-
-(ert-deftest marginalia-dtache-degraded ()
-  (should (string= "!" (marginalia-dtache--degraded (dtache--session-create 
:degraded t))))
-  (should (string= "" (marginalia-dtache--degraded (dtache--session-create 
:degraded nil)))))
-
-(provide 'marginalia-dtache-test)
-
-;;; marginalia-dtache-test.el ends here



reply via email to

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