diff options
| author | Craig Jennings <c@cjennings.net> | 2026-02-26 18:12:59 -0600 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-02-26 18:12:59 -0600 |
| commit | 5eb90dbbd18cf9b22d83fe31303fbb09099088ed (patch) | |
| tree | e064dc5609bb9db75c7b3d7b1a24726321a0fc60 /modules | |
| parent | ed5e3f61d00a9462801e15179fcceca65315050a (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')
| -rw-r--r-- | modules/video-audio-recording.el | 135 |
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) |
