diff options
| -rw-r--r-- | modules/video-audio-recording.el | 154 | ||||
| -rw-r--r-- | tests/test-video-audio-recording--get-available-mics.el | 35 | ||||
| -rw-r--r-- | tests/test-video-audio-recording--get-available-sinks.el | 34 | ||||
| -rw-r--r-- | tests/test-video-audio-recording-quick-setup.el | 166 |
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) |
