summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--modules/video-audio-recording.el57
-rw-r--r--tests/test-video-audio-recording--parse-pactl-sinks-verbose.el16
-rw-r--r--tests/test-video-audio-recording--parse-pactl-sources-verbose.el16
3 files changed, 28 insertions, 61 deletions
diff --git a/modules/video-audio-recording.el b/modules/video-audio-recording.el
index 2f0a2298..6664ded1 100644
--- a/modules/video-audio-recording.el
+++ b/modules/video-audio-recording.el
@@ -267,22 +267,22 @@ for capturing \"what I hear\" regardless of which output hardware is active."
(user-error "No default audio output found. Is PulseAudio/PipeWire running?")
(concat default-sink ".monitor"))))
-(defun cj/recording--parse-pactl-sources-verbose (output)
- "Parse verbose `pactl list sources' OUTPUT into structured list.
-Returns list of (name description mute state) tuples.
-OUTPUT should be the full output of `pactl list sources'."
- (let ((sources nil)
+(defun cj/recording--parse-pactl-verbose (output record-type)
+ "Parse verbose pactl OUTPUT into structured list.
+RECORD-TYPE is \"Source\" or \"Sink\" — the record header in pactl output.
+Returns list of (name description mute state) tuples."
+ (let ((entries nil)
+ (header-re (concat "^" record-type " #"))
(current-name nil)
(current-desc nil)
(current-mute nil)
(current-state nil))
(dolist (line (split-string output "\n"))
(cond
- ((string-match "^Source #" line)
- ;; Save previous source if complete
+ ((string-match-p header-re line)
(when current-name
(push (list current-name current-desc current-mute current-state)
- sources))
+ entries))
(setq current-name nil current-desc nil
current-mute nil current-state nil))
((string-match "^\\s-+Name:\\s-+\\(.+\\)" line)
@@ -293,11 +293,10 @@ OUTPUT should be the full output of `pactl list sources'."
(setq current-mute (match-string 1 line)))
((string-match "^\\s-+State:\\s-+\\(.+\\)" line)
(setq current-state (match-string 1 line)))))
- ;; Don't forget the last source
(when current-name
(push (list current-name current-desc current-mute current-state)
- sources))
- (nreverse sources)))
+ entries))
+ (nreverse entries)))
(defun cj/recording--get-available-mics ()
"Return available microphone sources as list of (name description state mute).
@@ -307,7 +306,7 @@ 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))
+ (sources (cj/recording--parse-pactl-verbose output "Source"))
(mics nil))
(dolist (source sources)
(let ((name (nth 0 source))
@@ -318,38 +317,6 @@ or SUSPENDED). Mute is \"yes\" or \"no\"."
(push (list name (or desc name) state mute) mics))))
(nreverse mics)))
-(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.
-OUTPUT should be the full output of `pactl list sinks'."
- (let ((sinks nil)
- (current-name nil)
- (current-desc nil)
- (current-mute nil)
- (current-state nil))
- (dolist (line (split-string output "\n"))
- (cond
- ((string-match "^Sink #" line)
- ;; Save previous sink if complete
- (when current-name
- (push (list current-name current-desc current-mute current-state)
- sinks))
- (setq current-name nil current-desc nil
- current-mute nil current-state nil))
- ((string-match "^\\s-+Name:\\s-+\\(.+\\)" line)
- (setq current-name (match-string 1 line)))
- ((string-match "^\\s-+Description:\\s-+\\(.+\\)" line)
- (setq current-desc (match-string 1 line)))
- ((string-match "^\\s-+Mute:\\s-+\\(.+\\)" line)
- (setq current-mute (match-string 1 line)))
- ((string-match "^\\s-+State:\\s-+\\(.+\\)" line)
- (setq current-state (match-string 1 line)))))
- ;; Don't forget the last sink
- (when current-name
- (push (list current-name current-desc current-mute current-state)
- sinks))
- (nreverse sinks)))
-
(defun cj/recording--get-available-sinks ()
"Return available audio sinks as list of (name description state mute).
Includes muted sinks (shown with a [muted] label in the UI). Uses
@@ -357,7 +324,7 @@ 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))
+ (sinks (cj/recording--parse-pactl-verbose output "Sink"))
(result nil))
(dolist (sink sinks)
(let ((name (nth 0 sink))
diff --git a/tests/test-video-audio-recording--parse-pactl-sinks-verbose.el b/tests/test-video-audio-recording--parse-pactl-sinks-verbose.el
index 8a2cba2d..59159039 100644
--- a/tests/test-video-audio-recording--parse-pactl-sinks-verbose.el
+++ b/tests/test-video-audio-recording--parse-pactl-sinks-verbose.el
@@ -1,7 +1,7 @@
;;; test-video-audio-recording--parse-pactl-sinks-verbose.el --- Tests for verbose pactl sinks parser -*- lexical-binding: t; -*-
;;; Commentary:
-;; Unit tests for cj/recording--parse-pactl-sinks-verbose.
+;; Unit tests for cj/recording--parse-pactl-verbose.
;; Parses the verbose output of `pactl list sinks' into structured tuples
;; of (name description mute state).
@@ -34,7 +34,7 @@
(ert-deftest test-parse-pactl-sinks-verbose-normal-multiple-sinks ()
"Test parsing multiple sink entries from fixture."
(let* ((output (test-sinks--fixture "pactl-sinks-verbose-normal.txt"))
- (result (cj/recording--parse-pactl-sinks-verbose output)))
+ (result (cj/recording--parse-pactl-verbose output "Sink")))
(should (= 3 (length result)))
(should (equal "alsa_output.usb-JDS_Labs-00.analog-stereo" (nth 0 (nth 0 result))))
(should (equal "JDS Labs Element IV Analog Stereo" (nth 1 (nth 0 result))))
@@ -44,7 +44,7 @@
(ert-deftest test-parse-pactl-sinks-verbose-normal-single-sink ()
"Test parsing a single sink entry."
(let* ((output "Sink #65\n\tState: SUSPENDED\n\tName: alsa_output.usb-JDS-00.analog-stereo\n\tDescription: JDS Labs Element IV\n\tMute: no\n")
- (result (cj/recording--parse-pactl-sinks-verbose output)))
+ (result (cj/recording--parse-pactl-verbose output "Sink")))
(should (= 1 (length result)))
(should (equal "alsa_output.usb-JDS-00.analog-stereo" (nth 0 (car result))))
(should (equal "JDS Labs Element IV" (nth 1 (car result))))
@@ -54,7 +54,7 @@
(ert-deftest test-parse-pactl-sinks-verbose-normal-muted-sink ()
"Test that muted sinks are parsed (filtering is done by caller)."
(let* ((output (test-sinks--fixture "pactl-sinks-verbose-muted.txt"))
- (result (cj/recording--parse-pactl-sinks-verbose output)))
+ (result (cj/recording--parse-pactl-verbose output "Sink")))
(should (= 3 (length result)))
;; Second sink is muted
(should (equal "yes" (nth 2 (nth 1 result))))))
@@ -63,14 +63,14 @@
(ert-deftest test-parse-pactl-sinks-verbose-boundary-empty-input ()
"Test that empty input returns empty list."
- (should (null (cj/recording--parse-pactl-sinks-verbose ""))))
+ (should (null (cj/recording--parse-pactl-verbose "" "Sink"))))
(ert-deftest test-parse-pactl-sinks-verbose-boundary-extra-fields ()
"Test that extra fields between sinks are ignored."
(let* ((output (concat "Sink #65\n\tState: IDLE\n\tName: sink-a\n\tDescription: Sink A\n\tMute: no\n"
"\tDriver: PipeWire\n\tSample Specification: s16le 2ch 48000Hz\n"
"Sink #66\n\tState: SUSPENDED\n\tName: sink-b\n\tDescription: Sink B\n\tMute: no\n"))
- (result (cj/recording--parse-pactl-sinks-verbose output)))
+ (result (cj/recording--parse-pactl-verbose output "Sink")))
(should (= 2 (length result)))
(should (equal "sink-a" (nth 0 (car result))))
(should (equal "sink-b" (nth 0 (cadr result))))))
@@ -78,7 +78,7 @@
(ert-deftest test-parse-pactl-sinks-verbose-boundary-description-with-parens ()
"Test descriptions containing parentheses are captured fully."
(let* ((output "Sink #91\n\tState: SUSPENDED\n\tName: hdmi-output\n\tDescription: Radeon (HDMI 2) Output\n\tMute: no\n")
- (result (cj/recording--parse-pactl-sinks-verbose output)))
+ (result (cj/recording--parse-pactl-verbose output "Sink")))
(should (equal "Radeon (HDMI 2) Output" (nth 1 (car result))))))
;;; Error Cases
@@ -86,7 +86,7 @@
(ert-deftest test-parse-pactl-sinks-verbose-error-malformed-no-name ()
"Test that sink entries without Name field are skipped."
(let* ((output "Sink #65\n\tState: SUSPENDED\n\tDescription: Orphan\n\tMute: no\n")
- (result (cj/recording--parse-pactl-sinks-verbose output)))
+ (result (cj/recording--parse-pactl-verbose output "Sink")))
(should (null result))))
(provide 'test-video-audio-recording--parse-pactl-sinks-verbose)
diff --git a/tests/test-video-audio-recording--parse-pactl-sources-verbose.el b/tests/test-video-audio-recording--parse-pactl-sources-verbose.el
index e856f29a..67945846 100644
--- a/tests/test-video-audio-recording--parse-pactl-sources-verbose.el
+++ b/tests/test-video-audio-recording--parse-pactl-sources-verbose.el
@@ -1,7 +1,7 @@
;;; test-video-audio-recording--parse-pactl-sources-verbose.el --- Tests for verbose pactl parser -*- lexical-binding: t; -*-
;;; Commentary:
-;; Unit tests for cj/recording--parse-pactl-sources-verbose.
+;; Unit tests for cj/recording--parse-pactl-verbose.
;; Parses the verbose output of `pactl list sources' into structured tuples
;; of (name description mute state).
@@ -20,7 +20,7 @@
(ert-deftest test-video-audio-recording--parse-pactl-sources-verbose-normal-single-source ()
"Test parsing a single source entry."
(let* ((output "Source #65\n\tState: SUSPENDED\n\tName: alsa_input.usb-Jabra-00.mono\n\tDescription: Jabra SPEAK 510 Mono\n\tMute: no\n")
- (result (cj/recording--parse-pactl-sources-verbose output)))
+ (result (cj/recording--parse-pactl-verbose output "Source")))
(should (= 1 (length result)))
(should (equal "alsa_input.usb-Jabra-00.mono" (nth 0 (car result))))
(should (equal "Jabra SPEAK 510 Mono" (nth 1 (car result))))
@@ -31,7 +31,7 @@
"Test parsing multiple source entries."
(let* ((output (concat "Source #65\n\tState: SUSPENDED\n\tName: device-a\n\tDescription: Device A\n\tMute: no\n"
"Source #66\n\tState: RUNNING\n\tName: device-b\n\tDescription: Device B\n\tMute: yes\n"))
- (result (cj/recording--parse-pactl-sources-verbose output)))
+ (result (cj/recording--parse-pactl-verbose output "Source")))
(should (= 2 (length result)))
(should (equal "device-a" (nth 0 (car result))))
(should (equal "Device B" (nth 1 (cadr result))))
@@ -40,7 +40,7 @@
(ert-deftest test-video-audio-recording--parse-pactl-sources-verbose-normal-monitors-included ()
"Test that monitor sources are parsed (filtering is done by caller)."
(let* ((output "Source #67\n\tState: SUSPENDED\n\tName: alsa_output.jds.monitor\n\tDescription: Monitor of JDS Labs\n\tMute: no\n")
- (result (cj/recording--parse-pactl-sources-verbose output)))
+ (result (cj/recording--parse-pactl-verbose output "Source")))
(should (= 1 (length result)))
(should (string-match-p "\\.monitor$" (nth 0 (car result))))))
@@ -48,7 +48,7 @@
(ert-deftest test-video-audio-recording--parse-pactl-sources-verbose-boundary-empty-input ()
"Test that empty input returns empty list."
- (should (null (cj/recording--parse-pactl-sources-verbose ""))))
+ (should (null (cj/recording--parse-pactl-verbose "" "Source"))))
(ert-deftest test-video-audio-recording--parse-pactl-sources-verbose-boundary-extra-fields ()
"Test that extra fields between sources are ignored."
@@ -56,7 +56,7 @@
"\tDriver: PipeWire\n\tSample Specification: s16le 2ch 48000Hz\n"
"\tChannel Map: front-left,front-right\n"
"Source #66\n\tState: SUSPENDED\n\tName: dev-b\n\tDescription: Dev B\n\tMute: no\n"))
- (result (cj/recording--parse-pactl-sources-verbose output)))
+ (result (cj/recording--parse-pactl-verbose output "Source")))
(should (= 2 (length result)))
(should (equal "dev-a" (nth 0 (car result))))
(should (equal "dev-b" (nth 0 (cadr result))))))
@@ -64,7 +64,7 @@
(ert-deftest test-video-audio-recording--parse-pactl-sources-verbose-boundary-description-with-parens ()
"Test descriptions containing parentheses are captured fully."
(let* ((output "Source #91\n\tState: SUSPENDED\n\tName: hdmi.monitor\n\tDescription: Monitor of Radeon (HDMI 2)\n\tMute: no\n")
- (result (cj/recording--parse-pactl-sources-verbose output)))
+ (result (cj/recording--parse-pactl-verbose output "Source")))
(should (equal "Monitor of Radeon (HDMI 2)" (nth 1 (car result))))))
;;; Error Cases
@@ -72,7 +72,7 @@
(ert-deftest test-video-audio-recording--parse-pactl-sources-verbose-error-malformed-no-name ()
"Test that source entries without Name field are skipped."
(let* ((output "Source #65\n\tState: SUSPENDED\n\tDescription: Orphan\n\tMute: no\n")
- (result (cj/recording--parse-pactl-sources-verbose output)))
+ (result (cj/recording--parse-pactl-verbose output "Source")))
(should (null result))))
(provide 'test-video-audio-recording--parse-pactl-sources-verbose)