Skip to content

Sources/sinks name display (v), and (s)hell to pactl from ui#80

Open
jaggzh wants to merge 5 commits intoGeorgeFilipkin:masterfrom
jaggzh:feature/hotkeys-and-info
Open

Sources/sinks name display (v), and (s)hell to pactl from ui#80
jaggzh wants to merge 5 commits intoGeorgeFilipkin:masterfrom
jaggzh:feature/hotkeys-and-info

Conversation

@jaggzh
Copy link

@jaggzh jaggzh commented Aug 5, 2025

Okay, so I'm not sure how to handle the UI for this.
(fwiw, I don't know if people appreciate how much thought people (you/you all) put into making an interface comfortable and intuitive and.. just proper).
In any case, I had google jules add 'v' for verbose mode, which merely enables the display of the name: field of the source/sink.
And 's' to shell and display the pactl output for the specific src/sink/stream.
(I have it pipe into PAGER/less for user convenience).

image

With stereo, at present, it displays the 'name' between the left and right, as you can see. I don't like it, but moving the name lower is worse on my eyes.

What do you think?

For faster review:

diff --git a/pulsemixer b/pulsemixer
index 6b0c8c6..39b8568 100755
--- a/pulsemixer
+++ b/pulsemixer
@@ -1097,6 +1097,7 @@ class Screen():
         self.action = None
         self.server_info = None
         self.ev = threading.Event()
+        self.verbose = False
 
     def getch(self):
         # blocking getch, can be 'interrupted' by ev.set
@@ -1184,9 +1185,13 @@ class Screen():
             _, x, y, _, c = curses.getmouse()
             if c & curses.BUTTON1_CLICKED:
                 if y > 0:
-                    top, bottom = self.top_line_num, len(self.data[self.top_line_num:self.top_line_num + self.lines]) - 1
+                    top = self.top_line_num
+                    bottom = len(self.data[top:top + self.lines]) - 1
                     if y - 1 <= bottom:
-                        self.focus_line_num = max(top, min(bottom, y - 1))
+                        self.focus_line_num = y - 1
+                        bar = self.data[top + self.focus_line_num][0]
+                        if not isinstance(bar, Bar):
+                            self.scroll(self.UP)
                 else:
                     f1 = len(self.menu_titles[0]) + 1  # 1 is 'spacing' after the title
                     f2 = f1 + len(self.menu_titles[1]) + 2
@@ -1289,6 +1294,11 @@ class Screen():
                 self.change_mode(2)
             elif c == ord('?'):
                 self.helpwin_show = True
+            elif c == ord('v'):
+                self.verbose = not self.verbose
+                self.ev.set()
+            elif c == ord('s'):
+                self.shell_out()
             elif c == ord('\n'):
                 if not self.submenu_show and self.change_mode_allowed and side != Bar.NONE:
                     self.selected = self.data[focus]
@@ -1433,6 +1443,17 @@ class Screen():
                     self.scroll(self.UP)
         return target
 
+    def add_verbose_info(self, data):
+        new_data = []
+        for item in data:
+            new_data.append(item)
+            bar, bartype, _, _ = item
+            if not bar or isinstance(bar, str) or bartype != Bar.LEFT:
+                continue
+            if type(bar.pa) in (PulseSinkInfo, PulseSourceInfo, PulseCard):
+                new_data.append(("verbose", f"  ({bar.pa.name.decode()})", None, None))
+        return new_data
+
     def add_spacers(self, f):
         tmp = []
         l = len(f)
@@ -1457,6 +1478,10 @@ class Screen():
             self.data = self.build(self.modes_data[3][0], PULSE.sink_list(), [])
         elif type(self.selected[0].pa) is PulseSourceOutputInfo:
             self.data = self.build(self.modes_data[4][0], PULSE.source_list(), [])
+
+        if self.verbose:
+            self.data = self.add_verbose_info(self.data)
+
         self.server_info = PULSE.get_server_info()
         self.n_lines = len(self.data)
         if not self.n_lines:
@@ -1471,7 +1496,12 @@ class Screen():
         bottom = self.top_line_num + self.lines
         self.display_line(0, self.menu)
         for index, line in enumerate(self.data[top:bottom]):
-            bar, bartype = line[0], line[1]
+            bar, bartype, _, _ = line
+
+            if bar == "verbose":
+                self.display_line(index + 1, f"{bartype}|{curses.A_DIM}")
+                continue
+
             if not bar:
                 self.screen.addstr(index + 1, 0, '', curses.A_DIM)
                 continue
@@ -1590,6 +1620,8 @@ class Screen():
                ('H L   Shift←  Shift→',             'Change volume by 10'),
                ('1 2 3 .. 8 9 0',                   'Set volume to 10%-100%'),
                ('m',                                'Mute/Unmute'),
+               ('v',                                'Toggle verbose mode'),
+               ('s',                                'Show pactl info'),
                ('Space',                            'Lock/Unlock channels'),
                ('Enter',                            'Context menu'),
                ('{} {} {}'.format(*self.mode_keys), 'Change modes'),
@@ -1608,6 +1640,40 @@ class Screen():
         if self.getch() in CFG.keys.quit:
             self.helpwin_show = False
 
+    def shell_out(self):
+        focus = self.top_line_num + self.focus_line_num
+        try:
+            bar, side = self.data[focus][0], self.data[focus][1]
+        except IndexError:
+            return
+
+        if side is Bar.NONE:
+            return
+
+        pa_obj = bar.pa
+        obj_type = type(pa_obj)
+        obj_index = pa_obj.index
+
+        type_map = {
+            PulseSinkInfo: ("sinks", "Sink"),
+            PulseSourceInfo: ("sources", "Source"),
+            PulseSinkInputInfo: ("sink-inputs", "Sink Input"),
+            PulseSourceOutputInfo: ("source-outputs", "Source Output"),
+            PulseCard: ("cards", "Card"),
+        }
+
+        if obj_type not in type_map:
+            return
+
+        pactl_cmd, grep_pattern = type_map[obj_type]
+        pager = os.environ.get("PAGER", "less")
+        command = f"pactl list {pactl_cmd} | sed -n '/^{grep_pattern} #{obj_index}/,/^$/p' | {pager}"
+
+        curses.endwin()
+        os.system(command)
+        self.screen.refresh()
+        self.ev.set()
+
     def resize_submenu(self):
         key = lambda x: len(x.split('|')[0])
         self.submenu_width = min(self.cols + 1, max(30, len(max(self.submenu_data, key=key).split('|')[0]) + 3))
@@ -1705,22 +1771,24 @@ class Screen():
 
         if n == self.UP and self.focus_line_num == 0 and self.top_line_num != 0:
             self.top_line_num += self.UP
-            return
         elif n == self.DOWN and next_line_num == self.lines and (self.top_line_num + self.lines) != self.n_lines:
             self.top_line_num += self.DOWN
-            return
-
-        if n == self.UP:
-            if self.top_line_num != 0 or self.focus_line_num != 0:
+        elif n == self.UP:
+            if self.top_line_num > 0 or self.focus_line_num > 0:
                 self.focus_line_num = next_line_num
             elif cycle:
                 self.scroll_last()
-        elif n == self.DOWN and self.focus_line_num != self.lines:
-            if self.top_line_num + self.focus_line_num + 1 != self.n_lines:
+        elif n == self.DOWN:
+            if self.top_line_num + self.focus_line_num + 1 < self.n_lines:
                 self.focus_line_num = next_line_num
             elif cycle:
                 self.scroll_first()
 
+        # a 'bar' can be a 'Bar' object, 'None' or a string
+        bar = self.data[self.top_line_num + self.focus_line_num][0]
+        if not isinstance(bar, Bar):
+            self.scroll(n, cycle)
+
     def scroll_first(self):
         for _ in range(self.n_lines): self.scroll(self.UP)

google-labs-jules bot and others added 4 commits August 3, 2025 14:24
This commit introduces two new hotkeys to the interactive mode:

- 'v': Toggles a verbose mode which displays the internal PulseAudio/ALSA device name below the description.
- 's': Displays detailed information about the selected audio device or stream.

The help screen has been updated to include these new hotkeys.
This commit fixes a crash that occurred when navigating up or down onto the newly added verbose info lines.

The `scroll` method has been updated to check if the destination line is an interactive item. If not, it skips over it, ensuring that the cursor only lands on valid, selectable items.
This commit fixes a crash that occurred when clicking on the newly added verbose info lines.

The `run_mouse` method has been updated to check if the clicked line is an interactive item. If not, it moves the focus to the nearest valid item, preventing the crash.
@jaggzh jaggzh marked this pull request as ready for review August 5, 2025 09:44
@jaggzh
Copy link
Author

jaggzh commented Aug 5, 2025

Oh, because it would crash when moving through, I have it skip the 'name' entries. For mouse-clicks I have it select the src/sink/i/o when the name is selected.
[Edit]: Oh, 'v' verbose mode is off by default, so everything looks as it always has.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant