summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--modules/video-audio-recording.el154
-rw-r--r--tests/test-video-audio-recording--get-available-mics.el35
-rw-r--r--tests/test-video-audio-recording--get-available-sinks.el34
-rw-r--r--tests/test-video-audio-recording-quick-setup.el166
4 files changed, 256 insertions, 133 deletions
diff --git a/modules/video-audio-recording.el b/modules/video-audio-recording.el
index f3cfbca0..2f0a2298 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 — [active - running] means audio is flowing
+;; 3. Pick an audio output — [in use] shows which apps are playing
;; 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)
@@ -44,9 +44,10 @@
;; Manual device selection:
;;
;; C-; r s (cj/recording-quick-setup) - RECOMMENDED
-;; Two-step setup: pick a mic, then pick an audio output (sink).
-;; Both steps show device state: [active - running], [active - idle],
-;; or [inactive - suspended]. Sorted running → idle → suspended.
+;; Two-step setup: pick a mic, then pick an audio output to capture.
+;; Both steps show status: [in use], [ready], [available], [muted].
+;; Audio outputs also show which apps are playing through them.
+;; Sorted: in use → ready → available → muted.
;;
;; C-; r S (cj/recording-select-devices) - ADVANCED
;; Manual selection: choose mic and monitor separately.
@@ -299,11 +300,12 @@ OUTPUT should be the full output of `pactl list sources'."
(nreverse sources)))
(defun cj/recording--get-available-mics ()
- "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. State is the PulseAudio state string
-\(RUNNING, IDLE, or SUSPENDED)."
+ "Return available microphone sources as list of (name description state mute).
+Filters out monitor sources but includes muted devices (shown with
+a [muted] label in the UI). Uses the friendly description from
+PulseAudio (e.g. \"Jabra SPEAK 510 Mono\") rather than the raw
+device name. State is the PulseAudio state string (RUNNING, IDLE,
+or SUSPENDED). Mute is \"yes\" or \"no\"."
(let* ((output (shell-command-to-string "pactl list sources 2>/dev/null"))
(sources (cj/recording--parse-pactl-sources-verbose output))
(mics nil))
@@ -312,10 +314,8 @@ than the raw device name. State is the PulseAudio state string
(desc (nth 1 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 (list name (or desc name) state) mics))))
+ (when (not (string-match-p "\\.monitor$" name))
+ (push (list name (or desc name) state mute) mics))))
(nreverse mics)))
(defun cj/recording--parse-pactl-sinks-verbose (output)
@@ -351,10 +351,11 @@ OUTPUT should be the full output of `pactl list sinks'."
(nreverse sinks)))
(defun cj/recording--get-available-sinks ()
- "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\"). State is
-the PulseAudio state string (RUNNING, IDLE, or SUSPENDED)."
+ "Return available audio sinks as list of (name description state mute).
+Includes muted sinks (shown with a [muted] label in the UI). Uses
+the friendly description from PulseAudio (e.g. \"JDS Labs Element IV
+Analog Stereo\"). State is the PulseAudio state string (RUNNING,
+IDLE, or SUSPENDED). Mute is \"yes\" or \"no\"."
(let* ((output (shell-command-to-string "pactl list sinks 2>/dev/null"))
(sinks (cj/recording--parse-pactl-sinks-verbose output))
(result nil))
@@ -363,8 +364,7 @@ the PulseAudio state string (RUNNING, IDLE, or SUSPENDED)."
(desc (nth 1 sink))
(mute (nth 2 sink))
(state (nth 3 sink)))
- (when (not (equal mute "yes"))
- (push (list name (or desc name) state) result))))
+ (push (list name (or desc name) state mute) result)))
(nreverse result)))
;;; ============================================================
@@ -509,48 +509,104 @@ 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--device-sort-key (state muted)
+ "Return a numeric sort key for a device with STATE and MUTED flag.
+Lower values sort first: RUNNING (0) → IDLE (1) → SUSPENDED (2) → muted (3)."
+ (if (equal muted "yes")
+ 3
+ (pcase (upcase (or state ""))
+ ("RUNNING" 0)
+ ("IDLE" 1)
+ (_ 2))))
+
+(defun cj/recording--device-status-label (state muted)
+ "Return a human-readable status label for a device.
+MUTED is \"yes\" or \"no\". STATE is the PulseAudio state string."
+ (if (equal muted "yes")
+ "[muted]"
+ (pcase (upcase (or state ""))
+ ("RUNNING" "[in use]")
+ ("IDLE" "[ready]")
+ (_ "[available]"))))
(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
+DEVICES is a list of (name description state mute) 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."
+Labels are formatted as \"Description [in use]\" etc.
+Sorted: in use → ready → available → muted."
(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))))
+ (muted (nth 3 dev))
+ (label (concat desc " "
+ (cj/recording--device-status-label state muted))))
+ (list label name (cj/recording--device-sort-key state muted))))
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--get-sink-apps ()
+ "Return alist mapping sink index to list of application names.
+Parses `pactl list sink-inputs' to find which apps are playing
+audio through each sink."
+ (let ((output (shell-command-to-string "pactl list sink-inputs 2>/dev/null"))
+ (apps (make-hash-table :test 'equal))
+ (current-sink nil))
+ (dolist (line (split-string output "\n"))
+ (cond
+ ((string-match "^Sink Input #" line)
+ (setq current-sink nil))
+ ((string-match "^[ \t]+Sink:[ \t]+\\([0-9]+\\)" line)
+ (setq current-sink (match-string 1 line)))
+ ((and current-sink
+ (string-match "application\\.name = \"\\([^\"]+\\)\"" line))
+ (let ((existing (gethash current-sink apps)))
+ (unless (member (match-string 1 line) existing)
+ (puthash current-sink
+ (append existing (list (match-string 1 line)))
+ apps))))))
+ ;; Convert hash to alist
+ (let ((result nil))
+ (maphash (lambda (k v) (push (cons k v) result)) apps)
+ result)))
+
+(defun cj/recording--label-sinks (sinks)
+ "Build labeled (label . name) alist from SINKS for `completing-read'.
+Like `cj/recording--label-devices' but also appends application names
+for sinks with active audio streams. E.g. \"JDS Labs [in use] (Firefox)\"."
+ (let* ((sink-apps (cj/recording--get-sink-apps))
+ (sinks-short (shell-command-to-string "pactl list sinks short 2>/dev/null"))
+ (labeled
+ (mapcar
+ (lambda (dev)
+ (let* ((name (nth 0 dev))
+ (desc (nth 1 dev))
+ (state (nth 2 dev))
+ (muted (nth 3 dev))
+ (index (cj/recording--get-sink-index name sinks-short))
+ (apps (and index (cdr (assoc index sink-apps))))
+ (status (cj/recording--device-status-label state muted))
+ (app-str (if apps (concat " (" (string-join apps ", ") ")") ""))
+ (label (concat desc " " status app-str)))
+ (list label name (cj/recording--device-sort-key state muted))))
+ sinks))
+ (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. 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.
+Step 1: Pick a microphone. Each mic shows its status:
+ [in use] = an app is actively using this mic
+ [ready] = recently used, still open
+ [available] = no app has this mic open
+ [muted] = device is muted in PulseAudio
+Step 2: Pick an audio output to capture. Same status labels, plus
+application names for outputs with active streams (e.g. \"Firefox\").
+Devices are sorted: in use → ready → available → muted.
+The chosen output'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
@@ -561,7 +617,7 @@ needed."
(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?")
+ (user-error "No microphones found. Is a mic connected?")
(let* ((mic-choice (completing-read "Select microphone: "
(lambda (string pred action)
(if (eq action 'metadata)
@@ -573,9 +629,9 @@ needed."
(user-error "Device setup cancelled")
;; Step 2: Sink selection
(let* ((sinks (cj/recording--get-available-sinks))
- (sink-entries (cj/recording--label-devices sinks))
+ (sink-entries (cj/recording--label-sinks sinks))
(sink-alist-with-cancel (append sink-entries '(("Cancel" . nil))))
- (sink-choice (completing-read "Select audio output to monitor: "
+ (sink-choice (completing-read "Select audio output to capture: "
(lambda (string pred action)
(if (eq action 'metadata)
'(metadata (display-sort-function . identity))
diff --git a/tests/test-video-audio-recording--get-available-mics.el b/tests/test-video-audio-recording--get-available-mics.el
index de9335af..86a9d05c 100644
--- a/tests/test-video-audio-recording--get-available-mics.el
+++ b/tests/test-video-audio-recording--get-available-mics.el
@@ -4,9 +4,9 @@
;; Unit tests for cj/recording--get-available-mics.
;; Verifies that available microphones are discovered correctly:
;; - Monitor sources are excluded (they capture output, not input)
-;; - Muted sources are excluded
+;; - Muted sources are included (shown with [muted] label in UI)
;; - Friendly descriptions from PulseAudio are used
-;; - PulseAudio state is included
+;; - PulseAudio state and mute status are included
;;; Code:
@@ -41,16 +41,16 @@ Each source is (name description mute state)."
(should (= 1 (length mics)))
(should (equal "alsa_input.usb-Jabra.mono" (nth 0 (car mics)))))))
-(ert-deftest test-video-audio-recording--get-available-mics-normal-filters-muted ()
- "Test that muted sources are excluded from mic list."
+(ert-deftest test-video-audio-recording--get-available-mics-normal-includes-muted ()
+ "Test that muted sources are included in mic list."
(cl-letf (((symbol-function 'shell-command-to-string)
(lambda (_cmd)
(test-mics--make-pactl-output
'(("active-mic" "Active Mic" "no" "SUSPENDED")
("muted-mic" "Muted Mic" "yes" "SUSPENDED"))))))
(let ((mics (cj/recording--get-available-mics)))
- (should (= 1 (length mics)))
- (should (equal "active-mic" (nth 0 (car mics)))))))
+ (should (= 2 (length mics)))
+ (should (equal "yes" (nth 3 (nth 1 mics)))))))
(ert-deftest test-video-audio-recording--get-available-mics-normal-uses-descriptions ()
"Test that friendly descriptions are returned as second element."
@@ -70,8 +70,17 @@ Each source is (name description mute state)."
(let ((mics (cj/recording--get-available-mics)))
(should (equal "RUNNING" (nth 2 (car mics)))))))
+(ert-deftest test-video-audio-recording--get-available-mics-normal-includes-mute-status ()
+ "Test that mute status is returned as fourth element."
+ (cl-letf (((symbol-function 'shell-command-to-string)
+ (lambda (_cmd)
+ (test-mics--make-pactl-output
+ '(("mic-a" "Mic A" "no" "RUNNING"))))))
+ (let ((mics (cj/recording--get-available-mics)))
+ (should (equal "no" (nth 3 (car mics)))))))
+
(ert-deftest test-video-audio-recording--get-available-mics-normal-multiple-mics ()
- "Test that multiple non-muted, non-monitor mics are returned."
+ "Test that multiple mics are returned including muted ones."
(cl-letf (((symbol-function 'shell-command-to-string)
(lambda (_cmd)
(test-mics--make-pactl-output
@@ -80,7 +89,8 @@ Each source is (name description mute state)."
("alsa_output.jabra.monitor" "Monitor of Jabra" "no" "SUSPENDED")
("muted-mic" "Muted Mic" "yes" "SUSPENDED"))))))
(let ((mics (cj/recording--get-available-mics)))
- (should (= 2 (length mics))))))
+ ;; 3 non-monitor sources (including the muted one)
+ (should (= 3 (length mics))))))
;;; Boundary Cases
@@ -99,14 +109,5 @@ Each source is (name description mute state)."
("sink-b.monitor" "Monitor B" "no" "SUSPENDED"))))))
(should (null (cj/recording--get-available-mics)))))
-(ert-deftest test-video-audio-recording--get-available-mics-boundary-all-muted ()
- "Test that if all non-monitor sources are muted, returns empty list."
- (cl-letf (((symbol-function 'shell-command-to-string)
- (lambda (_cmd)
- (test-mics--make-pactl-output
- '(("muted-a" "Mic A" "yes" "SUSPENDED")
- ("muted-b" "Mic B" "yes" "SUSPENDED"))))))
- (should (null (cj/recording--get-available-mics)))))
-
(provide 'test-video-audio-recording--get-available-mics)
;;; test-video-audio-recording--get-available-mics.el ends here
diff --git a/tests/test-video-audio-recording--get-available-sinks.el b/tests/test-video-audio-recording--get-available-sinks.el
index a8e7ad6a..2f0d965c 100644
--- a/tests/test-video-audio-recording--get-available-sinks.el
+++ b/tests/test-video-audio-recording--get-available-sinks.el
@@ -3,9 +3,9 @@
;;; Commentary:
;; Unit tests for cj/recording--get-available-sinks.
;; Verifies that available sinks are discovered correctly:
-;; - Muted sinks are excluded
+;; - Muted sinks are included (shown with [muted] label in UI)
;; - Friendly descriptions from PulseAudio are used
-;; - PulseAudio state is included
+;; - PulseAudio state and mute status are included
;;; Code:
@@ -29,16 +29,16 @@ Each sink is (name description mute state)."
;;; Normal Cases
-(ert-deftest test-get-available-sinks-normal-filters-muted ()
- "Test that muted sinks are excluded from sink list."
+(ert-deftest test-get-available-sinks-normal-includes-muted ()
+ "Test that muted sinks are included in sink list."
(cl-letf (((symbol-function 'shell-command-to-string)
(lambda (_cmd)
(test-sinks--make-pactl-output
'(("active-sink" "Active Sink" "no" "RUNNING")
("muted-sink" "Muted Sink" "yes" "SUSPENDED"))))))
(let ((sinks (cj/recording--get-available-sinks)))
- (should (= 1 (length sinks)))
- (should (equal "active-sink" (nth 0 (car sinks)))))))
+ (should (= 2 (length sinks)))
+ (should (equal "yes" (nth 3 (nth 1 sinks)))))))
(ert-deftest test-get-available-sinks-normal-uses-descriptions ()
"Test that friendly descriptions are returned as second element."
@@ -58,8 +58,17 @@ Each sink is (name description mute state)."
(let ((sinks (cj/recording--get-available-sinks)))
(should (equal "RUNNING" (nth 2 (car sinks)))))))
+(ert-deftest test-get-available-sinks-normal-includes-mute-status ()
+ "Test that mute status is returned as fourth element."
+ (cl-letf (((symbol-function 'shell-command-to-string)
+ (lambda (_cmd)
+ (test-sinks--make-pactl-output
+ '(("sink-a" "Sink A" "yes" "SUSPENDED"))))))
+ (let ((sinks (cj/recording--get-available-sinks)))
+ (should (equal "yes" (nth 3 (car sinks)))))))
+
(ert-deftest test-get-available-sinks-normal-multiple-sinks ()
- "Test that multiple non-muted sinks are returned."
+ "Test that multiple sinks are returned including muted."
(cl-letf (((symbol-function 'shell-command-to-string)
(lambda (_cmd)
(test-sinks--make-pactl-output
@@ -67,7 +76,7 @@ Each sink is (name description mute state)."
("sink-b" "Shure MV7+" "no" "SUSPENDED")
("muted-sink" "Muted" "yes" "SUSPENDED"))))))
(let ((sinks (cj/recording--get-available-sinks)))
- (should (= 2 (length sinks))))))
+ (should (= 3 (length sinks))))))
;;; Boundary Cases
@@ -77,14 +86,5 @@ Each sink is (name description mute state)."
(lambda (_cmd) "")))
(should (null (cj/recording--get-available-sinks)))))
-(ert-deftest test-get-available-sinks-boundary-all-muted ()
- "Test that if all sinks are muted, returns empty list."
- (cl-letf (((symbol-function 'shell-command-to-string)
- (lambda (_cmd)
- (test-sinks--make-pactl-output
- '(("muted-a" "Sink A" "yes" "SUSPENDED")
- ("muted-b" "Sink B" "yes" "SUSPENDED"))))))
- (should (null (cj/recording--get-available-sinks)))))
-
(provide 'test-video-audio-recording--get-available-sinks)
;;; test-video-audio-recording--get-available-sinks.el ends here
diff --git a/tests/test-video-audio-recording-quick-setup.el b/tests/test-video-audio-recording-quick-setup.el
index 23314bd5..154ad26e 100644
--- a/tests/test-video-audio-recording-quick-setup.el
+++ b/tests/test-video-audio-recording-quick-setup.el
@@ -3,10 +3,10 @@
;;; Commentary:
;; Unit tests for cj/recording-quick-setup function.
;; The quick setup is a two-step flow:
-;; Step 1: Pick a microphone (with state labels)
-;; Step 2: Pick an audio output (sink) with state labels
-;; Both steps show [active - running], [active - idle], or
-;; [inactive - suspended] after the device description.
+;; Step 1: Pick a microphone (with status labels)
+;; Step 2: Pick an audio output (with status labels + app names)
+;; Status labels: [in use], [ready], [available], [muted]
+;; Sorted: in use → ready → available → muted.
;; The chosen sink's .monitor source is set as the system audio device.
;;; Code:
@@ -39,10 +39,14 @@
(test-quick-setup-setup)
(unwind-protect
(cl-letf (((symbol-function 'cj/recording--get-available-mics)
- (lambda () '(("jabra-input" "Jabra SPEAK 510 Mono" "SUSPENDED")
- ("builtin-input" "Built-in Analog" "SUSPENDED"))))
+ (lambda () '(("jabra-input" "Jabra SPEAK 510 Mono" "SUSPENDED" "no")
+ ("builtin-input" "Built-in Analog" "SUSPENDED" "no"))))
((symbol-function 'cj/recording--get-available-sinks)
- (lambda () '(("jds-labs" "JDS Labs Element IV" "RUNNING"))))
+ (lambda () '(("jds-labs" "JDS Labs Element IV" "RUNNING" "no"))))
+ ((symbol-function 'cj/recording--get-sink-apps)
+ (lambda () nil))
+ ((symbol-function 'shell-command-to-string)
+ (lambda (_cmd) ""))
((symbol-function 'completing-read)
(lambda (_prompt table &rest _args)
(car (all-completions "" table)))))
@@ -55,9 +59,13 @@
(test-quick-setup-setup)
(unwind-protect
(cl-letf (((symbol-function 'cj/recording--get-available-mics)
- (lambda () '(("jabra-input" "Jabra SPEAK 510 Mono" "SUSPENDED"))))
+ (lambda () '(("jabra-input" "Jabra SPEAK 510 Mono" "SUSPENDED" "no"))))
((symbol-function 'cj/recording--get-available-sinks)
- (lambda () '(("alsa_output.usb-JDS_Labs-00.analog-stereo" "JDS Labs Element IV" "RUNNING"))))
+ (lambda () '(("alsa_output.usb-JDS_Labs-00.analog-stereo" "JDS Labs Element IV" "RUNNING" "no"))))
+ ((symbol-function 'cj/recording--get-sink-apps)
+ (lambda () nil))
+ ((symbol-function 'shell-command-to-string)
+ (lambda (_cmd) ""))
((symbol-function 'completing-read)
(lambda (_prompt table &rest _args)
(car (all-completions "" table)))))
@@ -72,9 +80,13 @@
(unwind-protect
(let ((call-count 0))
(cl-letf (((symbol-function 'cj/recording--get-available-mics)
- (lambda () '(("mic-1" "Mic One" "SUSPENDED"))))
+ (lambda () '(("mic-1" "Mic One" "SUSPENDED" "no"))))
((symbol-function 'cj/recording--get-available-sinks)
- (lambda () '(("sink-1" "Sink One" "SUSPENDED"))))
+ (lambda () '(("sink-1" "Sink One" "SUSPENDED" "no"))))
+ ((symbol-function 'cj/recording--get-sink-apps)
+ (lambda () nil))
+ ((symbol-function 'shell-command-to-string)
+ (lambda (_cmd) ""))
((symbol-function 'completing-read)
(lambda (_prompt table &rest _args)
(setq call-count (1+ call-count))
@@ -83,16 +95,20 @@
(should (= 2 call-count))))
(test-quick-setup-teardown)))
-(ert-deftest test-video-audio-recording-quick-setup-normal-running-label ()
- "Test that RUNNING devices show [active - running] label."
+(ert-deftest test-video-audio-recording-quick-setup-normal-in-use-label ()
+ "Test that RUNNING devices show [in use] label."
(test-quick-setup-setup)
(unwind-protect
(let ((mic-candidates nil)
(call-count 0))
(cl-letf (((symbol-function 'cj/recording--get-available-mics)
- (lambda () '(("mic-1" "Running Mic" "RUNNING"))))
+ (lambda () '(("mic-1" "Running Mic" "RUNNING" "no"))))
((symbol-function 'cj/recording--get-available-sinks)
- (lambda () '(("sink-1" "Sink One" "SUSPENDED"))))
+ (lambda () '(("sink-1" "Sink One" "SUSPENDED" "no"))))
+ ((symbol-function 'cj/recording--get-sink-apps)
+ (lambda () nil))
+ ((symbol-function 'shell-command-to-string)
+ (lambda (_cmd) ""))
((symbol-function 'completing-read)
(lambda (_prompt table &rest _args)
(setq call-count (1+ call-count))
@@ -101,42 +117,50 @@
(setq mic-candidates candidates))
(car candidates)))))
(cj/recording-quick-setup)
- (should (cl-some (lambda (c) (string-match-p "\\[active - running\\]" c))
+ (should (cl-some (lambda (c) (string-match-p "\\[in use\\]" c))
mic-candidates))))
(test-quick-setup-teardown)))
-(ert-deftest test-video-audio-recording-quick-setup-normal-idle-label ()
- "Test that IDLE devices show [active - idle] label."
+(ert-deftest test-video-audio-recording-quick-setup-normal-ready-label ()
+ "Test that IDLE devices show [ready] label."
(test-quick-setup-setup)
(unwind-protect
- (let ((sink-candidates nil)
+ (let ((mic-candidates nil)
(call-count 0))
(cl-letf (((symbol-function 'cj/recording--get-available-mics)
- (lambda () '(("mic-1" "Mic One" "SUSPENDED"))))
+ (lambda () '(("mic-1" "Idle Mic" "IDLE" "no"))))
((symbol-function 'cj/recording--get-available-sinks)
- (lambda () '(("sink-1" "Idle Sink" "IDLE"))))
+ (lambda () '(("sink-1" "Sink One" "SUSPENDED" "no"))))
+ ((symbol-function 'cj/recording--get-sink-apps)
+ (lambda () nil))
+ ((symbol-function 'shell-command-to-string)
+ (lambda (_cmd) ""))
((symbol-function 'completing-read)
(lambda (_prompt table &rest _args)
(setq call-count (1+ call-count))
(let ((candidates (all-completions "" table)))
- (when (= call-count 2)
- (setq sink-candidates candidates))
+ (when (= call-count 1)
+ (setq mic-candidates candidates))
(car candidates)))))
(cj/recording-quick-setup)
- (should (cl-some (lambda (c) (string-match-p "\\[active - idle\\]" c))
- sink-candidates))))
+ (should (cl-some (lambda (c) (string-match-p "\\[ready\\]" c))
+ mic-candidates))))
(test-quick-setup-teardown)))
-(ert-deftest test-video-audio-recording-quick-setup-normal-suspended-label ()
- "Test that SUSPENDED devices show [inactive - suspended] label."
+(ert-deftest test-video-audio-recording-quick-setup-normal-muted-label ()
+ "Test that muted devices show [muted] label."
(test-quick-setup-setup)
(unwind-protect
(let ((mic-candidates nil)
(call-count 0))
(cl-letf (((symbol-function 'cj/recording--get-available-mics)
- (lambda () '(("mic-1" "Suspended Mic" "SUSPENDED"))))
+ (lambda () '(("mic-1" "Muted Mic" "SUSPENDED" "yes"))))
((symbol-function 'cj/recording--get-available-sinks)
- (lambda () '(("sink-1" "Sink One" "SUSPENDED"))))
+ (lambda () '(("sink-1" "Sink One" "SUSPENDED" "no"))))
+ ((symbol-function 'cj/recording--get-sink-apps)
+ (lambda () nil))
+ ((symbol-function 'shell-command-to-string)
+ (lambda (_cmd) ""))
((symbol-function 'completing-read)
(lambda (_prompt table &rest _args)
(setq call-count (1+ call-count))
@@ -145,22 +169,27 @@
(setq mic-candidates candidates))
(car candidates)))))
(cj/recording-quick-setup)
- (should (cl-some (lambda (c) (string-match-p "\\[inactive - suspended\\]" c))
+ (should (cl-some (lambda (c) (string-match-p "\\[muted\\]" c))
mic-candidates))))
(test-quick-setup-teardown)))
-(ert-deftest test-video-audio-recording-quick-setup-normal-sorted-by-state ()
- "Test that devices are sorted running → idle → suspended."
+(ert-deftest test-video-audio-recording-quick-setup-normal-sorted-by-status ()
+ "Test that devices are sorted: in use → ready → available → muted."
(test-quick-setup-setup)
(unwind-protect
(let ((mic-candidates nil)
(call-count 0))
(cl-letf (((symbol-function 'cj/recording--get-available-mics)
- (lambda () '(("suspended-mic" "Suspended Mic" "SUSPENDED")
- ("running-mic" "Running Mic" "RUNNING")
- ("idle-mic" "Idle Mic" "IDLE"))))
+ (lambda () '(("muted-mic" "Muted Mic" "SUSPENDED" "yes")
+ ("running-mic" "Running Mic" "RUNNING" "no")
+ ("suspended-mic" "Suspended Mic" "SUSPENDED" "no")
+ ("idle-mic" "Idle Mic" "IDLE" "no"))))
((symbol-function 'cj/recording--get-available-sinks)
- (lambda () '(("sink-1" "Sink One" "SUSPENDED"))))
+ (lambda () '(("sink-1" "Sink One" "SUSPENDED" "no"))))
+ ((symbol-function 'cj/recording--get-sink-apps)
+ (lambda () nil))
+ ((symbol-function 'shell-command-to-string)
+ (lambda (_cmd) ""))
((symbol-function 'completing-read)
(lambda (_prompt table &rest _args)
(setq call-count (1+ call-count))
@@ -169,10 +198,39 @@
(setq mic-candidates candidates))
(car candidates)))))
(cj/recording-quick-setup)
- ;; First should be running, then idle, then suspended
+ ;; in use → ready → available → muted
(should (string-match-p "Running Mic" (nth 0 mic-candidates)))
(should (string-match-p "Idle Mic" (nth 1 mic-candidates)))
- (should (string-match-p "Suspended Mic" (nth 2 mic-candidates)))))
+ (should (string-match-p "Suspended Mic" (nth 2 mic-candidates)))
+ (should (string-match-p "Muted Mic" (nth 3 mic-candidates)))))
+ (test-quick-setup-teardown)))
+
+(ert-deftest test-video-audio-recording-quick-setup-normal-sink-shows-apps ()
+ "Test that sink labels include application names."
+ (test-quick-setup-setup)
+ (unwind-protect
+ (let ((sink-candidates nil)
+ (call-count 0))
+ (cl-letf (((symbol-function 'cj/recording--get-available-mics)
+ (lambda () '(("mic-1" "Mic One" "SUSPENDED" "no"))))
+ ((symbol-function 'cj/recording--get-available-sinks)
+ (lambda () '(("sink-with-apps" "JDS Labs" "RUNNING" "no"))))
+ ((symbol-function 'cj/recording--get-sink-apps)
+ (lambda () '(("65" "Firefox" "Spotify"))))
+ ((symbol-function 'shell-command-to-string)
+ (lambda (_cmd) "65\tsink-with-apps\tPipeWire\n"))
+ ((symbol-function 'completing-read)
+ (lambda (_prompt table &rest _args)
+ (setq call-count (1+ call-count))
+ (let ((candidates (all-completions "" table)))
+ (when (= call-count 2)
+ (setq sink-candidates candidates))
+ (car candidates)))))
+ (cj/recording-quick-setup)
+ (should (cl-some (lambda (c) (string-match-p "Firefox" c))
+ sink-candidates))
+ (should (cl-some (lambda (c) (string-match-p "Spotify" c))
+ sink-candidates))))
(test-quick-setup-teardown)))
(ert-deftest test-video-audio-recording-quick-setup-normal-confirmation-message ()
@@ -181,9 +239,13 @@
(unwind-protect
(let ((message-text nil))
(cl-letf (((symbol-function 'cj/recording--get-available-mics)
- (lambda () '(("jabra-input" "Jabra SPEAK 510 Mono" "RUNNING"))))
+ (lambda () '(("jabra-input" "Jabra SPEAK 510 Mono" "RUNNING" "no"))))
((symbol-function 'cj/recording--get-available-sinks)
- (lambda () '(("jds-labs.analog-stereo" "JDS Labs Element IV" "RUNNING"))))
+ (lambda () '(("jds-labs.analog-stereo" "JDS Labs Element IV" "RUNNING" "no"))))
+ ((symbol-function 'cj/recording--get-sink-apps)
+ (lambda () nil))
+ ((symbol-function 'shell-command-to-string)
+ (lambda (_cmd) ""))
((symbol-function 'completing-read)
(lambda (_prompt table &rest _args)
(car (all-completions "" table))))
@@ -203,9 +265,13 @@
(unwind-protect
(let ((read-called 0))
(cl-letf (((symbol-function 'cj/recording--get-available-mics)
- (lambda () '(("sole-mic" "Only Mic Available" "SUSPENDED"))))
+ (lambda () '(("sole-mic" "Only Mic Available" "SUSPENDED" "no"))))
((symbol-function 'cj/recording--get-available-sinks)
- (lambda () '(("sole-sink" "Only Sink" "SUSPENDED"))))
+ (lambda () '(("sole-sink" "Only Sink" "SUSPENDED" "no"))))
+ ((symbol-function 'cj/recording--get-sink-apps)
+ (lambda () nil))
+ ((symbol-function 'shell-command-to-string)
+ (lambda (_cmd) ""))
((symbol-function 'completing-read)
(lambda (_prompt table &rest _args)
(setq read-called (1+ read-called))
@@ -222,9 +288,7 @@
(test-quick-setup-setup)
(unwind-protect
(cl-letf (((symbol-function 'cj/recording--get-available-mics)
- (lambda () '(("jabra-input" "Jabra SPEAK 510 Mono" "SUSPENDED"))))
- ((symbol-function 'cj/recording--get-available-sinks)
- (lambda () '(("sink-1" "Sink One" "SUSPENDED"))))
+ (lambda () '(("jabra-input" "Jabra SPEAK 510 Mono" "SUSPENDED" "no"))))
((symbol-function 'completing-read)
(lambda (_prompt _choices &rest _args)
"Cancel")))
@@ -239,16 +303,18 @@
(unwind-protect
(let ((call-count 0))
(cl-letf (((symbol-function 'cj/recording--get-available-mics)
- (lambda () '(("jabra-input" "Jabra SPEAK 510 Mono" "SUSPENDED"))))
+ (lambda () '(("jabra-input" "Jabra SPEAK 510 Mono" "SUSPENDED" "no"))))
((symbol-function 'cj/recording--get-available-sinks)
- (lambda () '(("sink-1" "Sink One" "SUSPENDED"))))
+ (lambda () '(("sink-1" "Sink One" "SUSPENDED" "no"))))
+ ((symbol-function 'cj/recording--get-sink-apps)
+ (lambda () nil))
+ ((symbol-function 'shell-command-to-string)
+ (lambda (_cmd) ""))
((symbol-function 'completing-read)
(lambda (_prompt table &rest _args)
(setq call-count (1+ call-count))
(if (= call-count 1)
- ;; First call: select mic
(car (all-completions "" table))
- ;; Second call: cancel sink
"Cancel"))))
(should-error (cj/recording-quick-setup) :type 'user-error)
(should (null cj/recording-system-device))))
@@ -264,7 +330,7 @@
(test-quick-setup-teardown)))
(ert-deftest test-video-audio-recording-quick-setup-error-no-mics-message ()
- "Test that error message mentions mic and unmuted."
+ "Test that error message mentions mic."
(test-quick-setup-setup)
(unwind-protect
(cl-letf (((symbol-function 'cj/recording--get-available-mics)