aboutsummaryrefslogtreecommitdiff
path: root/modules/dirvish-config.el
diff options
context:
space:
mode:
Diffstat (limited to 'modules/dirvish-config.el')
-rw-r--r--modules/dirvish-config.el223
1 files changed, 173 insertions, 50 deletions
diff --git a/modules/dirvish-config.el b/modules/dirvish-config.el
index 29ba2ebac..04f9ce20e 100644
--- a/modules/dirvish-config.el
+++ b/modules/dirvish-config.el
@@ -27,7 +27,7 @@
;; - p: Copy absolute file path
;; - P: Print the file at point via CUPS
;; - S: Study — start an org-drill session on the .org file at point
-;; - M-S-d (Meta-Shift-d): DWIM shell commands menu
+;; - M-D (Meta-Shift-d): DWIM shell commands menu
;; - TAB: Toggle subtree expansion
;; - F11: Toggle sidebar view
@@ -119,6 +119,35 @@ through a `../' or absolute path. Pure helper."
(and (not (string-empty-p name))
(not (string-match-p "/" name))))
+(defun cj/--playlist-resolve-target ()
+ "Prompt for a playlist name and return the .m3u path to write under `music-dir'.
+Re-prompt until the name is a safe bare filename (no `/'). When the target
+already exists, ask whether to overwrite, cancel, or rename: overwrite returns
+the path, cancel signals a `user-error', rename re-prompts. Interactive
+prompting only -- the caller does the file write."
+ (let ((base-name nil)
+ (playlist-path nil)
+ (done nil))
+ (while (not done)
+ (setq base-name (cj/--playlist-sanitize-name
+ (read-string "Playlist name (without .m3u): ")))
+ (cond
+ ((not (cj/--playlist-name-safe-p base-name))
+ (message "Playlist name must be a bare filename, without '/'."))
+ (t
+ (setq playlist-path (expand-file-name (concat base-name ".m3u") music-dir))
+ (if (not (file-exists-p playlist-path))
+ (setq done t)
+ (let ((choice (read-char-choice
+ (format "Playlist '%s' exists. [o]verwrite, [c]ancel, [r]ename? "
+ (file-name-nondirectory playlist-path))
+ '(?o ?c ?r))))
+ (cl-case choice
+ (?o (setq done t))
+ (?c (user-error "Cancelled playlist creation"))
+ (?r (setq done nil))))))))
+ playlist-path))
+
(defun cj/dired-create-playlist-from-marked ()
"Create an .m3u playlist file from marked files in Dired (or Dirvish).
Filters for audio files, prompts for the playlist name, and saves the resulting
@@ -131,27 +160,7 @@ Filters for audio files, prompts for the playlist name, and saves the resulting
(if (zerop count)
(user-error "No audio files marked (extensions: %s)"
(string-join cj/audio-file-extensions ", "))
- (let ((base-name nil)
- (playlist-path nil)
- (done nil))
- (while (not done)
- (setq base-name (cj/--playlist-sanitize-name
- (read-string "Playlist name (without .m3u): ")))
- (cond
- ((not (cj/--playlist-name-safe-p base-name))
- (message "Playlist name must be a bare filename, without '/'."))
- (t
- (setq playlist-path (expand-file-name (concat base-name ".m3u") music-dir))
- (if (not (file-exists-p playlist-path))
- (setq done t)
- (let ((choice (read-char-choice
- (format "Playlist '%s' exists. [o]verwrite, [c]ancel, [r]ename? "
- (file-name-nondirectory playlist-path))
- '(?o ?c ?r))))
- (cl-case choice
- (?o (setq done t))
- (?c (user-error "Cancelled playlist creation"))
- (?r (setq done nil))))))))
+ (let ((playlist-path (cj/--playlist-resolve-target)))
(with-temp-file playlist-path
(dolist (af audio-files)
(insert af "\n")))
@@ -204,28 +213,20 @@ used by `cj/dirvish-open-html-in-eww'."
;;; ------------------------ Dired Mark All Visible Files -----------------------
-(defun cj/--dired-line-is-directory-p (line)
- "Return non-nil when LINE is a Dired listing of a directory.
-
-Dired prefixes each file line with a one-character mark column followed
-by `ls -l' output, so a directory line reads as `<mark> drwx...' (mark,
-space, `d'). Header lines (` /path/to:'), `total N' lines, and empty
-lines all fail this match.
-
-Pure helper used by `cj/dired-mark-all-visible-files'."
- (and line (string-match-p "\\`. d" line)))
-
(defun cj/dired-mark-all-visible-files ()
"Mark all visible files in Dired mode."
(interactive)
(save-excursion
(goto-char (point-min))
(while (not (eobp))
- (let ((line (buffer-substring-no-properties
- (line-beginning-position) (line-end-position))))
- (unless (cj/--dired-line-is-directory-p line)
- (dired-mark 1)))
- (forward-line 1))))
+ ;; dired-mark advances point itself, so only advance manually on the
+ ;; lines it isn't called for (directories, headers, totals). Use
+ ;; dired-get-filename to identify real file lines; it returns nil on
+ ;; non-file lines (no error with the second arg).
+ (let ((fn (dired-get-filename nil t)))
+ (if (and fn (not (file-directory-p fn)))
+ (dired-mark 1)
+ (forward-line 1))))))
;;; ------------------------ Dirvish Duplicate File Copy ------------------------
@@ -267,6 +268,37 @@ Examples:
(message "Duplicated: %s → %s"
(file-name-nondirectory file) new-name))))
+;;; ----------------------------- Dirvish Hard Delete ---------------------------
+
+(defun cj/--dirvish-hard-delete-command (files)
+ "Return the `sudo rm -rf' shell command that force-deletes FILES.
+Each path is shell-quoted and the list is preceded by `--' so a
+leading-dash filename can't be misread as an option. Pure helper used by
+`cj/dirvish-hard-delete'."
+ (concat "sudo rm -rf -- "
+ (mapconcat #'shell-quote-argument files " ")))
+
+(defun cj/dirvish-hard-delete ()
+ "Force-delete the marked files (or the file at point) via `sudo rm -rf'.
+This bypasses the trash and is IRREVERSIBLE. Prompts with the exact
+targets named before running."
+ (interactive)
+ (let ((files (dired-get-marked-files)))
+ (unless files
+ (user-error "No file at point"))
+ (let ((targets (mapconcat #'file-name-nondirectory files ", ")))
+ (when (yes-or-no-p
+ (format "Force-delete (sudo rm -rf, NO undo): %s? " targets))
+ (let ((status (shell-command (cj/--dirvish-hard-delete-command files))))
+ ;; Revert either way so the listing reflects whatever was removed,
+ ;; but only claim success when `rm' actually exited 0 -- a failed or
+ ;; cancelled `sudo' must not report files gone that are still there.
+ (revert-buffer)
+ (if (zerop status)
+ (message "Force-deleted: %s" targets)
+ (message "Hard delete failed (exit %d) -- see *Shell Command Output*"
+ status)))))))
+
;;; ------------------------------ Dirvish Print File ---------------------------
(defvar cj/dirvish-print-extensions
@@ -379,6 +411,101 @@ Uses feh on X11, swww on Wayland."
(message "Wallpaper set: %s (%s)"
(file-name-nondirectory file) (car cmd))))))
+;;; ------------------------- Dirvish Hyprland Popup ----------------------------
+
+;; The Hyprland Super+F popup opens an emacsclient frame named "dirvish" (window
+;; rules float/size/center it by that name) and runs `cj/dirvish-popup', rooted
+;; at home. `q' in that frame runs `cj/dirvish-popup-quit', which quits Dirvish
+;; and deletes the popup frame so a stray launch never orphans it; `q' in any
+;; other frame quits Dirvish normally. The launcher script calls this command
+;; instead of plain `dirvish'. This mirrors the Super+Shift+N quick-capture
+;; popup (see `cj/quick-capture' in org-capture-config.el).
+
+(defun cj/--dirvish-popup-frame ()
+ "Return a live frame named \"dirvish\" (the Hyprland popup), or nil."
+ (seq-find (lambda (f)
+ (and (frame-live-p f)
+ (equal (frame-parameter f 'name) "dirvish")))
+ (frame-list)))
+
+(defun cj/dirvish-popup ()
+ "Open Dirvish in the Hyprland popup frame (frame \"dirvish\"), rooted at home.
+The launcher script calls this through =emacsclient -c -e=. `q'
+(`cj/dirvish-popup-quit') closes the frame.
+
+Selects the \"dirvish\" frame by name before opening rather than trusting the
+ambient selected frame: the launching =emacsclient -c -e= runs before Hyprland
+settles focus on the new float, so =(selected-frame)= is still the daemon's main
+frame and Dirvish would otherwise open there."
+ (interactive)
+ (let ((frame (cj/--dirvish-popup-frame)))
+ (when frame (select-frame-set-input-focus frame))
+ (dirvish (expand-file-name "~/"))))
+
+(defun cj/dirvish-popup-focus-existing ()
+ "Raise and focus the live dirvish popup frame, returning t; nil if none.
+The launcher script calls this before creating a frame, so a second Super+F
+re-uses the open popup instead of spawning a second one (the popup is a
+single-instance, transient launcher -- use =C-x d= for several independent
+Dirvish sessions)."
+ (let ((popup (cj/--dirvish-popup-frame)))
+ (when popup
+ (select-frame-set-input-focus popup)
+ t)))
+
+(defun cj/dirvish-popup-quit ()
+ "Quit Dirvish. In the Hyprland popup frame (\"dirvish\"), delete the frame too.
+Bound to `q' in `dirvish-mode-map'. A normal Dirvish session (any other frame)
+quits as usual; only the popup frame is torn down, so the Super+F launch never
+leaves an empty frame behind."
+ (interactive)
+ (let ((popup (cj/--dirvish-popup-frame)))
+ (if (and popup (eq popup (selected-frame)))
+ (progn
+ (ignore-errors (dirvish-quit))
+ (when (frame-live-p popup) (delete-frame popup)))
+ (dirvish-quit))))
+
+(defun cj/--dirvish-popup-selected-p ()
+ "Return non-nil when the selected frame is the dirvish popup frame."
+ (let ((popup (cj/--dirvish-popup-frame)))
+ (and popup (eq popup (selected-frame)))))
+
+(defun cj/dirvish-popup-find-file ()
+ "Open the file at point.
+In the Hyprland popup frame the popup is a context-free launcher: files open
+through the OS handler (`cj/xdg-open' -> xdg-open), so nothing lands inside the
+throwaway frame and the launch is independent of the running Emacs session (a
+text/code file opens its own new emacsclient frame, not your working session --
+use =C-x d= when you want a file in the session you're in). Directories are
+entered normally so you can keep browsing. The popup then dismisses itself on
+focus loss. Outside the popup this is exactly `dired-find-file'."
+ (interactive)
+ (if (cj/--dirvish-popup-selected-p)
+ (let ((file (dired-get-file-for-visit)))
+ (if (file-directory-p file)
+ (dired-find-file)
+ (cj/xdg-open file)))
+ (dired-find-file)))
+
+(defun cj/--dirvish-popup-focus-watch (&rest _)
+ "Dismiss the dirvish popup frame once it loses focus.
+Armed only after the popup has actually held focus (a per-frame flag), so the
+frame is never torn down during its own creation, before Hyprland settles focus
+on the new float. Installed on `after-focus-change-function'; a no-op whenever
+no popup frame is live."
+ (let ((popup (cj/--dirvish-popup-frame)))
+ (when popup
+ (if (frame-focus-state popup)
+ (set-frame-parameter popup 'cj-dirvish-popup-had-focus t)
+ (when (frame-parameter popup 'cj-dirvish-popup-had-focus)
+ (delete-frame popup))))))
+
+;; Install idempotently: remove any prior copy before adding, so re-loading the
+;; module updates the watch rather than stacking duplicate copies.
+(remove-function after-focus-change-function #'cj/--dirvish-popup-focus-watch)
+(add-function :after after-focus-change-function #'cj/--dirvish-popup-focus-watch)
+
;;; ---------------------------------- Dirvish ----------------------------------
(use-package dirvish
@@ -483,7 +610,8 @@ Uses feh on X11, swww on Wayland."
("bg" . cj/set-wallpaper)
("/" . dirvish-narrow)
("<left>" . dired-up-directory)
- ("<right>" . dired-find-file)
+ ("RET" . cj/dirvish-popup-find-file) ; popup: launch file externally; else normal
+ ("<right>" . cj/dirvish-popup-find-file)
("C-," . dirvish-history-go-backward)
("C-." . dirvish-history-go-forward)
("F" . dirvish-file-info-menu)
@@ -497,14 +625,15 @@ Uses feh on X11, swww on Wayland."
("M-p" . dirvish-peek-toggle)
("M-s" . dirvish-setup-menu)
("TAB" . dirvish-subtree-toggle)
- ("d" . dired-do-delete)
- ("D" . cj/dirvish-duplicate-file)
+ ("d" . cj/dirvish-duplicate-file)
+ ("D" . cj/dirvish-hard-delete)
("f" . cj/dirvish-open-file-manager-here)
("g" . dirvish-quick-access)
("o" . cj/xdg-open)
("O" . cj/open-file-with-command) ; Prompts for command to run
("p" . (lambda () (interactive) (cj/dired-copy-path-as-kill nil t)))
("P" . cj/dirvish-print-file)
+ ("q" . cj/dirvish-popup-quit) ; quit; in the Hyprland popup frame, close it
("r" . dirvish-rsync)
("S" . cj/dirvish-drill-file) ; Study: org-drill the .org file at point
("s" . dirvish-quicksort)
@@ -513,15 +642,9 @@ Uses feh on X11, swww on Wayland."
;;; ----------------------------- Dired Text Greying ----------------------------
-;; The right-column file-size attribute uses `shadow' (#969385). Match the
-;; visible text faces to it so the column reads as one tone, with icon color
-;; supplying the only accent. `default' is remapped buffer-locally inside
-;; dired/dirvish so plain files match too — no global side effects.
-
-(with-eval-after-load 'dired
- (set-face-attribute 'dired-directory nil :foreground 'unspecified :inherit 'shadow)
- (set-face-attribute 'dired-symlink nil :foreground 'unspecified :inherit 'shadow)
- (set-face-attribute 'dired-header nil :foreground 'unspecified :inherit 'shadow))
+;; `default' is remapped buffer-locally to `shadow' inside dired/dirvish (see
+;; `cj/--dired-text-greyout' below) so plain files read grey, with icon color
+;; the only accent. The dired text faces themselves are left to the theme.
(defun cj/--dired-text-greyout ()
"Buffer-local: render `default' in `shadow' so plain files read grey."