1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
|
;;; dirvish-config.el --- Dired/Dirvish Configuration -*- lexical-binding: t; coding: utf-8; -*-
;; author: Craig Jennings <c@cjennings.net>
;;; Commentary:
;; Enhanced file management via Dirvish (modern dired replacement) with icons,
;; previews, and quick access directories (press 'g'). Includes utilities for
;; ediff, playlist creation, path copying, and external file manager integration.
;;
;; Key Bindings:
;; - g: Quick access menu (jump to predefined directories)
;; - f: Open system file manager in current directory
;; - o/O: Open file with xdg-open/custom command
;; - l: Copy file path (project-relative or home-relative)
;; - L: Copy absolute file path
;; - P: Create M3U playlist from marked audio files
;; - M-D: DWIM menu (context actions for files)
;; - TAB: Toggle subtree expansion
;; - F11: Toggle sidebar view
;;; Code:
(eval-when-compile (require 'user-constants))
(eval-when-compile (require 'system-utils))
;;; ----------------------------- Dired Ediff Files -----------------------------
(defun cj/dired-ediff-files ()
"Ediff two selected files within Dired."
(interactive)
(let ((files (dired-get-marked-files))
(wnd (current-window-configuration)))
(if (<= (length files) 2)
(let ((file1 (car files))
(file2 (if (cdr files)
(cadr files)
(read-file-name
"file: "
(dired-dwim-target-directory)))))
(if (file-newer-than-file-p file1 file2)
(ediff-files file2 file1)
(ediff-files file1 file2))
(add-hook 'ediff-after-quit-hook-internal
(lambda ()
(setq ediff-after-quit-hook-internal nil)
(set-window-configuration wnd))))
(error "No more than 2 files should be marked"))))
;; ------------------------ Create Playlist From Marked ------------------------
(defvar cj/audio-file-extensions
'("mp3" "flac" "m4a" "wav" "ogg" "aac" "opus" "aiff" "alac" "wma")
"List of audio file extensions (lowercase, no dot).
Used to filter files for M3U playlists.")
(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
.m3u in the directory specified by =music-dir=. Interactive use only."
(interactive)
(let* ((marked-files (dired-get-marked-files))
(audio-files
(cl-remove-if-not
(lambda (f)
(let ((ext (file-name-extension f)))
(and ext
(member (downcase ext) cj/audio-file-extensions))))
marked-files))
(count (length audio-files)))
(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 (read-string
(format "Playlist name (without .m3u): ")))
;; Sanitize: strip any trailing .m3u
(setq base-name (replace-regexp-in-string "\\.m3u\\'" "" base-name))
(setq playlist-path (expand-file-name (concat base-name ".m3u") music-dir))
(cond
((not (file-exists-p playlist-path))
;; Safe to write
(setq done t))
(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)))))))
;; Actually write the file
(with-temp-file playlist-path
(dolist (af audio-files)
(insert af "\n")))
(message "Wrote playlist %s with %d tracks" (file-name-nondirectory playlist-path) count)))))
;;; ----------------------------------- Dired -----------------------------------
(use-package dired
:ensure nil ;; built-in
:defer t
:bind
(:map dired-mode-map
([remap dired-summary] . which-key-show-major-mode)
("E" . wdired-change-to-wdired-mode) ;; edit names and properties in buffer
("e" . cj/dired-ediff-files)) ;; ediff files
:custom
(dired-use-ls-dired nil) ;; non GNU FreeBSD doesn't support a "--dired" switch
:config
(setq dired-listing-switches "-l --almost-all --human-readable --group-directories-first")
(setq dired-dwim-target t)
(setq dired-clean-up-buffers-too t) ;; offer to kill buffers associated deleted files and dirs
(setq dired-clean-confirm-killing-deleted-buffers t) ;; don't ask; just kill buffers associated with deleted files
(setq dired-recursive-copies (quote always)) ;; “always” means no asking
(setq dired-recursive-deletes (quote top))) ;; “top” means ask once
;; note: disabled as it prevents marking and moving files to another directory
;; (setq dired-kill-when-opening-new-dired-buffer t) ;; don't litter by leaving buffers when navigating directories
(add-hook 'dired-mode-hook 'auto-revert-mode) ;; auto revert dired when files change
;;; --------------------------- Dired Open HTML In EWW --------------------------
(defun cj/dirvish-open-html-in-eww ()
"Open HTML file at point in dired/dirvish using eww."
(interactive)
(let ((file (dired-get-file-for-visit)))
(if (string-match-p "\\.html?\\'" file)
(eww-open-file file)
(message "Not an HTML file: %s" file))))
;;; ------------------------ Dired Mark All Visible Files -----------------------
(defun cj/dired-mark-all-visible-files ()
"Mark all visible files in Dired mode."
(interactive)
(save-excursion
(goto-char (point-min))
(while (not (eobp))
(if (not (looking-at "^. d"))
(dired-mark 1))
(forward-line 1))))
;;; ----------------------- Dirvish Open File Manager Here ----------------------
(defun cj/dirvish-open-file-manager-here ()
"Open system's default file manager in the current dired/dirvish directory.
Always opens the file manager in the directory currently being displayed,
regardless of what file or subdirectory the point is on."
(interactive)
(let ((current-dir (dired-current-directory)))
(if (and current-dir (file-exists-p current-dir))
(progn
(message "Opening file manager in %s..." current-dir)
;; Use shell-command with & to run asynchronously and detached
(let ((process-connection-type nil)) ; Use pipe instead of pty
(cond
;; Linux/Unix with xdg-open
((executable-find "xdg-open")
(call-process "xdg-open" nil 0 nil current-dir))
;; macOS
((eq system-type 'darwin)
(call-process "open" nil 0 nil current-dir))
;; Windows
((eq system-type 'windows-nt)
(call-process "explorer" nil 0 nil current-dir))
;; Fallback to shell-command
(t
(shell-command (format "xdg-open %s &"
(shell-quote-argument current-dir)))))))
(message "Could not determine current directory."))))
;;; ---------------------------------- Dirvish ----------------------------------
(use-package dirvish
:defer 0.5
:init
(dirvish-override-dired-mode)
:custom
;; This MUST be in :custom section, not :config
(dirvish-quick-access-entries
`(("h" "~/" "home")
("cx" ,code-dir "code directory")
("ex" ,user-emacs-directory "emacs home")
("es" ,sounds-dir "notification sounds")
("ra" ,video-recordings-dir "video recordings")
("rv" ,audio-recordings-dir "audio recordings")
("dl" ,dl-dir "downloads")
("dr" ,(concat org-dir "/drill/") "drill files")
("dt" ,(concat dl-dir "/torrents/complete/") "torrents")
("dx" "~/documents/" "documents")
("gd" "~/documents/google-drive/" "google-drive")
("lx" "~/lectures/" "lectures")
("mb" "/media/backup/" "backup directory")
("mx" "~/music/" "music")
("pD" "~/projects/documents/" "project documents")
("pd" "~/projects/danneel/" "project danneel")
("pl" "~/projects/elibrary/" "project elibrary")
("pf" "~/projects/finances/" "project finances")
("pjr" "~/projects/jr-estate/" "project jr-estate")
("ps" ,(concat pix-dir "/screenshots/") "pictures screenshots")
("pw" ,(concat pix-dir "/wallpaper/") "pictures wallpaper")
("px" ,pix-dir "pictures directory")
("rcj" "/sshx:cjennings@cjennings.net:~" "remote c@cjennings.net")
("rcg" "/sshx:git@cjennings.net:~" "remote git@cjennings.net")
("rsb" "/sshx:cjennings@wolf.usbx.me:/home/cjennings/" "remote seedbox")
("sx" ,sync-dir "sync directory")
("so" ,(concat sync-dir "/org/") "sync/org directory")
("sr" ,(concat sync-dir "/recordings/") "sync/recordings directory")
("sv" ,(concat sync-dir "/videos/") "sync/videos directory")
("tg" ,(concat sync-dir "/text.games/") "text games")
("vr" ,video-recordings-dir "video recordings directory")
("vx" ,videos-dir "videos")))
:config
;; Add the extensions directory to load-path
(let ((extensions-dir (expand-file-name "extensions"
(file-name-directory (locate-library "dirvish")))))
(when (file-directory-p extensions-dir)
(add-to-list 'load-path extensions-dir)))
;; Load dirvish modules with error checking
(let ((dirvish-modules '(dirvish-emerge
dirvish-subtree
dirvish-narrow
dirvish-history
dirvish-ls
dirvish-yank
dirvish-quick-access
dirvish-collapse
dirvish-rsync
dirvish-vc
dirvish-icons
dirvish-side
dirvish-peek)))
(dolist (module dirvish-modules)
(condition-case err
(require module)
(error
(message "Failed to load %s: %s" module (error-message-string err))))))
;; Enable peek mode with error checking
(condition-case err
(dirvish-peek-mode 1)
(error (message "Failed to enable dirvish-peek-mode: %s" (error-message-string err))))
;; Enable side-follow mode with error checking
(condition-case err
(dirvish-side-follow-mode 1)
(error (message "Failed to enable dirvish-side-follow-mode: %s"
(error-message-string err))))
;; Your other configuration settings
(setq dirvish-attributes '(nerd-icons file-size))
(setq dirvish-side-attributes '(nerd-icons file-size)) ;; Explicitly set for sidebar
(setq dirvish-preview-dispatchers '(image gif video audio epub pdf archive))
(setq dirvish-use-mode-line nil)
(setq dirvish-use-header-line nil)
:bind
(("C-x d" . dirvish)
("C-x C-d" . dirvish)
("C-x D" . dirvish)
("<f11>" . dirvish-side)
:map dirvish-mode-map
("bg" . (lambda () (interactive)
(shell-command
(concat "nitrogen --save --set-zoom-fill "
(dired-file-name-at-point) " >>/dev/null 2>&1"))))
("/" . dirvish-narrow)
("<left>" . dired-up-directory)
("<right>" . dired-find-file)
("C-," . dirvish-history-go-backward)
("C-." . dirvish-history-go-forward)
("F" . dirvish-file-info-menu)
("G" . revert-buffer)
("l" . (lambda () (interactive) (cj/dired-copy-path-as-kill))) ;; overwrites dired-do-redisplay
("L" . (lambda () (interactive) (cj/dired-copy-path-as-kill nil t))) ;; copy absolute path
("h" . cj/dirvish-open-html-in-eww) ;; it does what it says it does
("M" . cj/dired-mark-all-visible-files)
("M-e" . dirvish-emerge-menu)
("M-l" . dirvish-ls-switches-menu)
("M-m" . dirvish-mark-menu)
("M-p" . dirvish-peek-toggle)
("M-s" . dirvish-setup-menu)
("TAB" . dirvish-subtree-toggle)
("d" . dired-flag-file-deletion)
("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
("r" . dirvish-rsync)
("P" . cj/dired-create-playlist-from-marked)
("s" . dirvish-quicksort)
("v" . dirvish-vc-menu)
("y" . dirvish-yank-menu)))
;;; -------------------------------- Nerd Icons -------------------------------
(use-package nerd-icons
:defer .5)
(use-package nerd-icons-dired
:commands (nerd-icons-dired-mode))
;;; ---------------------------- Dired Hide Dotfiles ----------------------------
(use-package dired-hide-dotfiles
:after dired
:hook
;; Auto-hide dotfiles when entering dired/dirvish
((dired-mode . dired-hide-dotfiles-mode)
(dirvish-mode . dired-hide-dotfiles-mode))
:bind
(:map dired-mode-map
("." . dired-hide-dotfiles-mode)))
;;; ------------------------------- Dired Sidebar -------------------------------
(use-package dired-sidebar
:after (dired projectile)
:defer t
:commands (dired-sidebar-toggle-sidebar)
:init
(add-hook 'dired-sidebar-mode-hook
(lambda ()
(unless (file-remote-p default-directory)
(auto-revert-mode))))
:config
(push 'toggle-window-split dired-sidebar-toggle-hidden-commands) ;; disallow splitting dired window when it's showing
(push 'rotate-windows dired-sidebar-toggle-hidden-commands) ;; disallow rotating windows when sidebar is showing
(setq dired-sidebar-subtree-line-prefix " ") ;; two spaces give simple and aesthetic indentation
(setq dired-sidebar-no-delete-other-windows t) ;; don't close when calling 'delete other windows'
(setq dired-sidebar-theme 'nerd-icons) ;; gimme fancy icons, please
(setq dired-sidebar-use-custom-font 'nil) ;; keep the same font as the rest of Emacs
(setq dired-sidebar-delay-auto-revert-updates 'nil) ;; don't delay auto-reverting
(setq dired-sidebar-pop-to-sidebar-on-toggle-open 'nil)) ;; don't jump to sidebar when it's toggled on
;; --------------------------------- Copy Path ---------------------------------
(defun cj/dired-copy-path-as-kill (&optional as-org-link force-absolute)
"Copy path of file at point in Dired/Dirvish.
Copies relative path from project root if in a project, otherwise from home
directory (with ~ prefix) if applicable, otherwise the absolute path.
With prefix arg or when AS-ORG-LINK is non-nil, format as \='org-mode\=' link.
When FORCE-ABSOLUTE is non-nil, always copy the absolute path."
(interactive "P")
(unless (derived-mode-p 'dired-mode)
(user-error "Not in a Dired buffer"))
(let* ((file (dired-get-filename nil t))
(file-name (file-name-nondirectory file))
(project-root (cj/get-project-root))
(home-dir (expand-file-name "~"))
path path-type)
(unless file
(user-error "No file at point"))
(cond
;; Force absolute path
(force-absolute
(setq path file
path-type "absolute"))
;; Project-relative path
(project-root
(setq path (file-relative-name file project-root)
path-type "project-relative"))
;; Home-relative path
((string-prefix-p home-dir file)
(let ((relative-from-home (file-relative-name file home-dir)))
(setq path (if (string= relative-from-home ".")
"~"
(concat "~/" relative-from-home))
path-type "home-relative")))
;; Absolute path
(t
(setq path file
path-type "absolute")))
;; Format as org-link if requested
(when as-org-link
(setq path (format "[[file:%s][%s]]" path file-name)))
;; Copy to kill-ring and clipboard
(kill-new path)
;; Provide feedback
(message "Copied %s path%s: %s"
path-type
(if as-org-link " as org-link" "")
(if (> (length path) 60)
(concat (substring path 0 57) "...")
path))))
(defun cj/get-project-root ()
"Get project root using projectile or project.el.
Returns nil if not in a project."
(cond
;; Try projectile first if available
((and (fboundp 'projectile-project-root)
(ignore-errors (projectile-project-root))))
;; Fallback to project.el
((and (fboundp 'project-current)
(project-current))
(let ((proj (project-current)))
(if (fboundp 'project-root)
(project-root proj)
;; Compatibility with older versions
(car (project-roots proj)))))
;; No project found
(t nil)))
(provide 'dirvish-config)
;;; dirvish-config.el ends here.
|