summaryrefslogtreecommitdiff
path: root/modules/video-audio-recording.el
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-02-26 18:12:59 -0600
committerCraig Jennings <c@cjennings.net>2026-02-26 18:12:59 -0600
commit5eb90dbbd18cf9b22d83fe31303fbb09099088ed (patch)
treee064dc5609bb9db75c7b3d7b1a24726321a0fc60 /modules/video-audio-recording.el
parented5e3f61d00a9462801e15179fcceca65315050a (diff)
feat(recording): replace icons with text state labels in quick-setup
Replace hard-to-distinguish nerd font icons with clear text labels: Jabra SPEAK 510 Mono [active - running] Shure MV7+ Analog Stereo [active - idle] Ryzen HD Audio Controller [inactive - suspended] Both mic and sink steps use the same labeling. Devices sorted running → idle → suspended. Removed mic-active-p and sink-active-p helpers — state now comes directly from the verbose pactl parsers via get-available-mics/sinks which return (name description state).
Diffstat (limited to 'modules/video-audio-recording.el')
-rw-r--r--modules/video-audio-recording.el135
1 files changed, 60 insertions, 75 deletions
diff --git a/modules/video-audio-recording.el b/modules/video-audio-recording.el
index d5c59cb7..f3cfbca0 100644
--- a/modules/video-audio-recording.el
+++ b/modules/video-audio-recording.el
@@ -31,7 +31,7 @@
;; ===========
;; 1. Press C-; r s to run quick setup
;; 2. Pick a microphone from the list
-;; 3. Pick an audio output — 󰕾 marks sinks with active audio
+;; 3. Pick an audio output — [active - running] means audio is flowing
;; 4. Press C-; r a to start/stop audio recording
;; 5. Recording starts - you'll see 󰍬 in your modeline
;; 6. Press C-; r a again to stop (🔴 disappears)
@@ -45,8 +45,8 @@
;;
;; C-; r s (cj/recording-quick-setup) - RECOMMENDED
;; Two-step setup: pick a mic, then pick an audio output (sink).
-;; Sinks show 󰕾/󰖁 icons indicating active/inactive audio streams.
-;; Active sinks are sorted to the top for easy selection.
+;; Both steps show device state: [active - running], [active - idle],
+;; or [inactive - suspended]. Sorted running → idle → suspended.
;;
;; C-; r S (cj/recording-select-devices) - ADVANCED
;; Manual selection: choose mic and monitor separately.
@@ -299,32 +299,25 @@ OUTPUT should be the full output of `pactl list sources'."
(nreverse sources)))
(defun cj/recording--get-available-mics ()
- "Return available microphone sources as (name . description) alist.
+ "Return available microphone sources as list of (name description state).
Filters out monitor sources and muted devices. Uses the friendly
description from PulseAudio (e.g. \"Jabra SPEAK 510 Mono\") rather
-than the raw device name."
+than the raw device name. State is the PulseAudio state string
+\(RUNNING, IDLE, or SUSPENDED)."
(let* ((output (shell-command-to-string "pactl list sources 2>/dev/null"))
(sources (cj/recording--parse-pactl-sources-verbose output))
(mics nil))
(dolist (source sources)
(let ((name (nth 0 source))
(desc (nth 1 source))
- (mute (nth 2 source)))
+ (mute (nth 2 source))
+ (state (nth 3 source)))
;; Include non-monitor, non-muted sources
(when (and (not (string-match-p "\\.monitor$" name))
(not (equal mute "yes")))
- (push (cons name (or desc name)) mics))))
+ (push (list name (or desc name) state) mics))))
(nreverse mics)))
-(defun cj/recording--mic-active-p (source-name)
- "Return non-nil if SOURCE-NAME is actively in use (RUNNING state).
-Checks `pactl list sources short' for the source's current state."
- (let ((output (shell-command-to-string "pactl list sources short 2>/dev/null")))
- (cl-some (lambda (line)
- (and (string-match-p (regexp-quote source-name) line)
- (string-match-p "RUNNING" line)))
- (split-string output "\n" t))))
-
(defun cj/recording--parse-pactl-sinks-verbose (output)
"Parse verbose `pactl list sinks' OUTPUT into structured list.
Returns list of (name description mute state) tuples.
@@ -358,30 +351,22 @@ OUTPUT should be the full output of `pactl list sinks'."
(nreverse sinks)))
(defun cj/recording--get-available-sinks ()
- "Return available audio sinks as (name . description) alist.
+ "Return available audio sinks as list of (name description state).
Filters out muted sinks. Uses the friendly description from
-PulseAudio (e.g. \"JDS Labs Element IV Analog Stereo\")."
+PulseAudio (e.g. \"JDS Labs Element IV Analog Stereo\"). State is
+the PulseAudio state string (RUNNING, IDLE, or SUSPENDED)."
(let* ((output (shell-command-to-string "pactl list sinks 2>/dev/null"))
(sinks (cj/recording--parse-pactl-sinks-verbose output))
(result nil))
(dolist (sink sinks)
(let ((name (nth 0 sink))
(desc (nth 1 sink))
- (mute (nth 2 sink)))
+ (mute (nth 2 sink))
+ (state (nth 3 sink)))
(when (not (equal mute "yes"))
- (push (cons name (or desc name)) result))))
+ (push (list name (or desc name) state) result))))
(nreverse result)))
-(defun cj/recording--sink-active-p (sink-name)
- "Return non-nil if SINK-NAME has active audio streams.
-Resolves the sink name to its index via `pactl list sinks short',
-then checks `pactl list sink-inputs' for connected streams."
- (let* ((sinks-output (shell-command-to-string "pactl list sinks short 2>/dev/null"))
- (sink-index (cj/recording--get-sink-index sink-name sinks-output))
- (sink-inputs (shell-command-to-string "pactl list sink-inputs 2>/dev/null")))
- (and sink-index
- (cj/recording--sink-has-active-audio-p sink-index sink-inputs))))
-
;;; ============================================================
;;; Device Selection UI
;;; ============================================================
@@ -524,14 +509,48 @@ since recording needs both to capture your voice and system audio."
devices)
(nreverse result)))
+(defun cj/recording--state-sort-key (state)
+ "Return a numeric sort key for PulseAudio STATE.
+Lower values sort first: RUNNING (0) → IDLE (1) → SUSPENDED (2)."
+ (pcase (upcase (or state ""))
+ ("RUNNING" 0)
+ ("IDLE" 1)
+ (_ 2)))
+
+(defun cj/recording--state-label (state)
+ "Return a human-readable label for PulseAudio STATE.
+RUNNING and IDLE are active states; SUSPENDED is inactive."
+ (pcase (upcase (or state ""))
+ ("RUNNING" "[active - running]")
+ ("IDLE" "[active - idle]")
+ (_ "[inactive - suspended]")))
+
+(defun cj/recording--label-devices (devices)
+ "Build labeled (label . name) alist from DEVICES for `completing-read'.
+DEVICES is a list of (name description state) as returned by
+`cj/recording--get-available-mics' or `cj/recording--get-available-sinks'.
+Labels are formatted as \"Description [active - running]\" etc.
+Sorted: running → idle → suspended."
+ (let* ((labeled (mapcar
+ (lambda (dev)
+ (let* ((name (nth 0 dev))
+ (desc (nth 1 dev))
+ (state (nth 2 dev))
+ (label (concat desc " " (cj/recording--state-label state))))
+ (list label name (cj/recording--state-sort-key state))))
+ devices))
+ (sorted (sort labeled (lambda (a b) (< (nth 2 a) (nth 2 b))))))
+ (mapcar (lambda (entry) (cons (nth 0 entry) (nth 1 entry))) sorted)))
+
(defun cj/recording-quick-setup ()
"Quick device setup for recording — two-step mic + sink selection.
-Step 1: Pick a microphone. Mics in use by an app (RUNNING) show
-a green 󰍬 icon and are sorted to the top; idle mics show dim 󰍬.
-Step 2: Pick an audio output (sink) to monitor. Sinks with active
-audio streams show green 󰕾 and are sorted to the top; idle sinks
-show dim 󰖀. The chosen sink's .monitor source is set as the
-system audio device.
+Step 1: Pick a microphone. Each mic shows its PulseAudio state:
+ [active - running] = an app is using this mic right now
+ [active - idle] = recently used, still open
+ [inactive - suspended] = no app has this mic open
+Step 2: Pick an audio output (sink) to monitor, with the same
+state labels. Devices are sorted running → idle → suspended.
+The chosen sink's .monitor source is set as the system audio device.
This approach is portable across systems — plug in a new mic, run this
command, and it appears in the list. No hardware-specific configuration
@@ -539,26 +558,9 @@ needed."
(interactive)
;; Step 1: Mic selection
(let* ((mics (cj/recording--get-available-mics))
- (mic-labels
- (mapcar
- (lambda (mic)
- (let* ((name (car mic))
- (desc (cdr mic))
- (active (cj/recording--mic-active-p name))
- (icon "󰍬")
- (face (if active '(:foreground "#50fa7b") '(:foreground "#6272a4")))
- (label (concat (propertize icon 'face face) " " desc)))
- (list label name active)))
- mics))
- ;; Sort active mics to top
- (sorted-mics (sort mic-labels
- (lambda (a b)
- (and (nth 2 a) (not (nth 2 b))))))
- (mic-alist (mapcar (lambda (entry)
- (cons (nth 0 entry) (nth 1 entry)))
- sorted-mics))
- (mic-alist-with-cancel (append mic-alist '(("Cancel" . nil)))))
- (if (null mic-alist)
+ (mic-entries (cj/recording--label-devices mics))
+ (mic-alist-with-cancel (append mic-entries '(("Cancel" . nil)))))
+ (if (null mic-entries)
(user-error "No microphones found. Is a mic plugged in and unmuted?")
(let* ((mic-choice (completing-read "Select microphone: "
(lambda (string pred action)
@@ -571,25 +573,8 @@ needed."
(user-error "Device setup cancelled")
;; Step 2: Sink selection
(let* ((sinks (cj/recording--get-available-sinks))
- (sink-labels
- (mapcar
- (lambda (sink)
- (let* ((name (car sink))
- (desc (cdr sink))
- (active (cj/recording--sink-active-p name))
- (icon (if active "󰕾" "󰖀"))
- (face (if active '(:foreground "#50fa7b") '(:foreground "#6272a4")))
- (label (concat (propertize icon 'face face) " " desc)))
- (list label name active)))
- sinks))
- ;; Sort active sinks to top
- (sorted-labels (sort sink-labels
- (lambda (a b)
- (and (nth 2 a) (not (nth 2 b))))))
- (sink-alist (mapcar (lambda (entry)
- (cons (nth 0 entry) (nth 1 entry)))
- sorted-labels))
- (sink-alist-with-cancel (append sink-alist '(("Cancel" . nil))))
+ (sink-entries (cj/recording--label-devices sinks))
+ (sink-alist-with-cancel (append sink-entries '(("Cancel" . nil))))
(sink-choice (completing-read "Select audio output to monitor: "
(lambda (string pred action)
(if (eq action 'metadata)