aboutsummaryrefslogtreecommitdiff
path: root/duet.el
diff options
context:
space:
mode:
Diffstat (limited to 'duet.el')
-rw-r--r--duet.el133
1 files changed, 130 insertions, 3 deletions
diff --git a/duet.el b/duet.el
index b546264..4c49eca 100644
--- a/duet.el
+++ b/duet.el
@@ -721,11 +721,138 @@ ungated."
(list :op 'delete :source s :gate 'copy-success)))))
sources)))
+;;; Commander mode, pane layout, and entry
+
+(defcustom duet-default-left-directory nil
+ "Directory shown in DUET's left pane at launch.
+nil means the current directory when `duet' is invoked."
+ :type '(choice (const :tag "Current directory" nil) directory)
+ :group 'duet)
+
+(defcustom duet-default-right-directory nil
+ "Directory shown in DUET's right pane at launch.
+nil means the current directory when `duet' is invoked."
+ :type '(choice (const :tag "Current directory" nil) directory)
+ :group 'duet)
+
+(defvar duet--saved-window-configuration nil
+ "Window configuration saved when DUET launched, restored by `duet-quit'.")
+
+;; The transfer/file actions land with their owning phases; until then they
+;; announce themselves so the key is bound and its precedence is testable.
+
+(defun duet-view ()
+ "View the file under point. Arrives with the viewer phase."
+ (interactive)
+ (user-error "DUET: view is not implemented yet"))
+
+(defun duet-edit ()
+ "Edit the file under point. Arrives with the viewer phase."
+ (interactive)
+ (user-error "DUET: edit is not implemented yet"))
+
+(defun duet-copy ()
+ "Copy the marked files to the other pane. Arrives with transfer execution."
+ (interactive)
+ (user-error "DUET: copy is not implemented yet"))
+
+(defun duet-move ()
+ "Move the marked files to the other pane. Arrives with transfer execution."
+ (interactive)
+ (user-error "DUET: move is not implemented yet"))
+
+(defun duet-mkdir ()
+ "Make a directory in the active pane. Arrives with transfer execution."
+ (interactive)
+ (user-error "DUET: mkdir is not implemented yet"))
+
+(defun duet-delete ()
+ "Delete the marked files (to trash). Arrives with transfer execution."
+ (interactive)
+ (user-error "DUET: delete is not implemented yet"))
+
+(defun duet-quit ()
+ "Close the DUET commander, restoring the window layout from before launch."
+ (interactive)
+ (if duet--saved-window-configuration
+ (progn
+ (set-window-configuration duet--saved-window-configuration)
+ (setq duet--saved-window-configuration nil))
+ (message "DUET: no saved layout to restore")))
+
+(defvar duet-mode-map
+ (let ((map (make-sparse-keymap)))
+ (define-key map (kbd "<f3>") #'duet-view)
+ (define-key map (kbd "<f4>") #'duet-edit)
+ (define-key map (kbd "<f5>") #'duet-copy)
+ (define-key map (kbd "<f6>") #'duet-move)
+ (define-key map (kbd "<f7>") #'duet-mkdir)
+ (define-key map (kbd "<f8>") #'duet-delete)
+ (define-key map (kbd "<f10>") #'duet-quit)
+ map)
+ "Keymap active in DUET commander panes.
+mc/Norton F-keys, taking precedence inside a pane only. dired's own chords
+\(C, R, D, v, +) keep working as free aliases.")
+
+(define-minor-mode duet-mode
+ "Buffer-local minor mode marking a buffer as a DUET commander pane.
+Carries `duet-mode-map', so the single-key F-key actions fire only inside a
+commander pane and leave the global F-keys untouched elsewhere."
+ :lighter " Duet"
+ :keymap duet-mode-map)
+
+(defun duet--sibling-pane (window panes)
+ "Return the commander pane in PANES that is not WINDOW.
+PANES is the list of commander windows. Signal a `user-error' unless there
+are exactly two panes and WINDOW is one of them. This is the explicit
+other-pane resolution that replaces `dired-dwim-target' (the dirvish#36 fix)."
+ (cond
+ ((/= (length panes) 2)
+ (user-error "DUET needs exactly two commander panes (found %d)"
+ (length panes)))
+ ((not (memq window panes))
+ (user-error "Point is not in a DUET commander pane"))
+ (t (car (delq window (copy-sequence panes))))))
+
+(defun duet--commander-windows ()
+ "Return the live windows whose buffer has `duet-mode' enabled."
+ (cl-remove-if-not (lambda (w) (buffer-local-value 'duet-mode (window-buffer w)))
+ (window-list)))
+
+(defun duet--other-pane (&optional window)
+ "Return the sibling commander pane window to WINDOW (default selected)."
+ (duet--sibling-pane (or window (selected-window)) (duet--commander-windows)))
+
+(defun duet--pane-directory (window)
+ "Return the directory shown in commander pane WINDOW."
+ (with-current-buffer (window-buffer window) default-directory))
+
+(defun duet--open-pane (directory)
+ "Open DIRECTORY in the selected window as a DUET commander pane.
+The pane is a dired buffer with `duet-mode' enabled; when the user has dirvish
+rendering dired buffers, it renders as dirvish with no extra work, which is why
+dirvish is recommended but never required."
+ (let ((buffer (dired-noselect (expand-file-name directory))))
+ (set-window-buffer (selected-window) buffer)
+ (with-current-buffer buffer (duet-mode 1))
+ buffer))
+
;;;###autoload
-(defun duet ()
- "Launch the DUET dual-pane file commander."
+(defun duet (&optional left right)
+ "Launch the DUET dual-pane file commander.
+Lay out two side-by-side commander panes: LEFT (or
+`duet-default-left-directory', else the current directory) on the left and
+RIGHT (or `duet-default-right-directory', else the current directory) on the
+right. Each pane is a dired buffer with `duet-mode' enabled. `duet-quit'
+\(F10) restores the window layout from before launch."
(interactive)
- (user-error "DUET is not yet implemented; see the design document"))
+ (setq duet--saved-window-configuration (current-window-configuration))
+ (let ((left-dir (or left duet-default-left-directory default-directory))
+ (right-dir (or right duet-default-right-directory default-directory)))
+ (delete-other-windows)
+ (duet--open-pane left-dir)
+ (with-selected-window (split-window-right)
+ (duet--open-pane right-dir))))
(provide 'duet)
;;; duet.el ends here