2 * mixer_display.c - handles displaying of mixer widget and controls
3 * Copyright (c) 1874 Lewis Carroll
4 * Copyright (c) 2009 Clemens Ladisch <clemens@ladisch.de>
6 * This program is free software: you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation, either version 2 of the License, or
9 * (at your option) any later version.
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
16 * You should have received a copy of the GNU General Public License
17 * along with this program. If not, see <http://www.gnu.org/licenses/>.
20 #define _C99_SOURCE /* lrint() */
27 #include <alsa/asoundlib.h>
28 #include "gettext_curses.h"
33 #include "volume_mapping.h"
34 #include "mixer_widget.h"
35 #include "mixer_controls.h"
36 #include "mixer_display.h"
44 static bool screen_too_small;
45 static bool has_info_items;
47 static int info_items_left;
48 static int info_items_width;
50 static int visible_controls;
51 static int first_visible_control_index;
52 static int first_control_x;
53 static int control_width;
54 static int control_name_width;
57 static int volume_height;
61 static int channel_name_y;
63 static void display_string_in_field(int y, int x, const char *s, int width, enum align align)
70 wmove(mixer_widget.window, y, x);
72 s_end = mbs_at_width(s, &string_width, -1);
73 if (string_width >= width) {
74 waddnstr(mixer_widget.window, s, s_end - s);
76 if (align != ALIGN_LEFT) {
77 spaces = width - string_width;
78 if (align == ALIGN_CENTER)
81 wprintw(mixer_widget.window, "%*s", spaces, "");
83 waddstr(mixer_widget.window, s);
84 if (align != ALIGN_RIGHT) {
85 getyx(mixer_widget.window, cur_y, cur_x);
87 spaces = x + width - cur_x;
89 wprintw(mixer_widget.window, "%*s", spaces, "");
95 void init_mixer_layout(void)
97 const char *labels_left[4] = {
103 const char *labels_right[4] = {
105 _("F2: System information"),
106 _("F6: Select sound card"),
109 unsigned int label_width_left, label_width_right;
110 unsigned int right_x, i;
112 screen_too_small = screen_lines < 14 || screen_cols < 12;
113 has_info_items = screen_lines >= 6;
117 label_width_left = get_max_mbs_width(labels_left, 4);
118 label_width_right = get_max_mbs_width(labels_right, 4);
119 if (2 + label_width_left + 1 + 28 + label_width_right + 2 > screen_cols)
120 label_width_right = 0;
121 if (2 + label_width_left + 1 + 28 + label_width_right + 2 > screen_cols)
122 label_width_left = 0;
124 info_items_left = label_width_left ? 3 + label_width_left : 2;
125 right_x = screen_cols - label_width_right - 2;
126 info_items_width = right_x - info_items_left;
127 if (info_items_width < 1) {
128 has_info_items = FALSE;
132 wattrset(mixer_widget.window, attr_mixer_text);
133 if (label_width_left)
134 for (i = 0; i < 4; ++i)
135 display_string_in_field(1 + i, 2, labels_left[i],
136 label_width_left, ALIGN_RIGHT);
137 if (label_width_right)
138 for (i = 0; i < 4; ++i)
139 display_string_in_field(1 + i, right_x, labels_right[i],
140 label_width_right, ALIGN_LEFT);
143 void display_card_info(void)
147 snd_ctl_card_info_t *card_info;
148 const char *card_name = NULL;
149 const char *mixer_name = NULL;
155 snd_ctl_card_info_alloca(&card_info);
156 if (mixer_device_name)
157 err = snd_mixer_get_hctl(mixer, mixer_device_name, &hctl);
161 ctl = snd_hctl_ctl(hctl);
162 err = snd_ctl_card_info(ctl, card_info);
164 card_name = snd_ctl_card_info_get_name(card_info);
165 mixer_name = snd_ctl_card_info_get_mixername(card_info);
170 wattrset(mixer_widget.window, attr_mixer_active);
172 wattrset(mixer_widget.window, attr_mixer_text);
174 card_name = _("(unplugged)");
178 display_string_in_field(1, info_items_left, card_name, info_items_width, ALIGN_LEFT);
181 wattrset(mixer_widget.window, attr_mixer_active);
183 wattrset(mixer_widget.window, attr_mixer_text);
186 display_string_in_field(2, info_items_left, mixer_name, info_items_width, ALIGN_LEFT);
189 void display_view_mode(void)
191 const char *modes[3] = {
196 unsigned int widths[3];
203 has_view_mode = controls_count > 0 || are_there_any_controls();
204 for (i = 0; i < 3; ++i)
205 widths[i] = get_mbs_width(modes[i]);
206 if (4 + widths[0] + 6 + widths[1] + 6 + widths[2] + 1 <= info_items_width) {
207 wmove(mixer_widget.window, 3, info_items_left);
208 wattrset(mixer_widget.window, attr_mixer_text);
209 for (i = 0; i < 3; ++i) {
210 wprintw(mixer_widget.window, "F%c:", '3' + i);
211 if (has_view_mode && (int)view_mode == i) {
212 wattrset(mixer_widget.window, attr_mixer_active);
213 wprintw(mixer_widget.window, "[%s]", modes[i]);
214 wattrset(mixer_widget.window, attr_mixer_text);
216 wprintw(mixer_widget.window, " %s ", modes[i]);
219 waddch(mixer_widget.window, ' ');
222 wattrset(mixer_widget.window, attr_mixer_active);
223 display_string_in_field(3, info_items_left,
224 has_view_mode ? modes[view_mode] : "",
225 info_items_width, ALIGN_LEFT);
229 static char *format_gain(long db)
231 if (db != SND_CTL_TLV_DB_GAIN_MUTE)
232 return casprintf("%.2f", db / 100.0);
234 return cstrdup(_("mute"));
237 static void display_focus_item_info(void)
239 struct control *control;
251 wattrset(mixer_widget.window, attr_mixer_active);
252 if (!controls_count || screen_too_small) {
253 display_string_in_field(4, info_items_left, "", info_items_width, ALIGN_LEFT);
256 control = &controls[focus_control_index];
258 if (control->flags & TYPE_ENUM) {
259 err = snd_mixer_selem_get_enum_item(control->elem, ffs(control->enum_channel_bits) - 1, &index);
261 err = snd_mixer_selem_get_enum_item_name(control->elem, index, sizeof buf - 1, buf);
263 value_info = casprintf(" [%s]", buf);
264 } else if (control->flags & (TYPE_PVOLUME | TYPE_CVOLUME)) {
265 int (*get_vol_func)(snd_mixer_elem_t *, snd_mixer_selem_channel_id_t, long *);
267 if (control->flags & TYPE_PVOLUME)
268 get_vol_func = snd_mixer_selem_get_playback_dB;
270 get_vol_func = snd_mixer_selem_get_capture_dB;
271 if (!(control->flags & HAS_VOLUME_1)) {
272 err = get_vol_func(control->elem, control->volume_channels[0], &db);
274 dbs = format_gain(db);
275 value_info = casprintf(" [%s %s]", _("dB gain:"), dbs);
279 err = get_vol_func(control->elem, control->volume_channels[0], &db);
281 err = get_vol_func(control->elem, control->volume_channels[1], &db2);
283 dbs = format_gain(db);
284 dbs2 = format_gain(db2);
285 value_info = casprintf(_(" [%s %s, %s]"), _("dB gain:"), dbs, dbs2);
290 } else if (control->flags & TYPE_PSWITCH) {
291 if (!(control->flags & HAS_PSWITCH_1)) {
292 err = snd_mixer_selem_get_playback_switch(control->elem, control->pswitch_channels[0], &sw);
294 value_info = casprintf(" [%s]", _("Off"));
296 err = snd_mixer_selem_get_playback_switch(control->elem, control->pswitch_channels[0], &sw);
298 err = snd_mixer_selem_get_playback_switch(control->elem, control->pswitch_channels[1], &sw2);
299 if (err >= 0 && (!sw || !sw2))
300 value_info = casprintf(" [%s, %s]", sw ? _("On") : _("Off"), sw2 ? _("On") : _("Off"));
302 } else if (control->flags & TYPE_CSWITCH) {
303 if (!(control->flags & HAS_CSWITCH_1)) {
304 err = snd_mixer_selem_get_capture_switch(control->elem, control->cswitch_channels[0], &sw);
306 value_info = casprintf(" [%s]", _("Off"));
308 err = snd_mixer_selem_get_capture_switch(control->elem, control->cswitch_channels[0], &sw);
310 err = snd_mixer_selem_get_capture_switch(control->elem, control->cswitch_channels[1], &sw2);
311 if (err >= 0 && (!sw || !sw2))
312 value_info = casprintf(" [%s, %s]", sw ? _("On") : _("Off"), sw2 ? _("On") : _("Off"));
315 item_info = casprintf("%s%s", control->name, value_info ? value_info : "");
317 display_string_in_field(4, info_items_left, item_info, info_items_width, ALIGN_LEFT);
321 static void clear_controls_display(void)
325 wattrset(mixer_widget.window, attr_mixer_frame);
326 for (i = 5; i < screen_lines - 1; ++i)
327 mvwprintw(mixer_widget.window, i, 1, "%*s", screen_cols - 2, "");
330 static void center_string(int line, const char *s)
332 int width = get_mbs_width(s);
333 if (width <= screen_cols - 2)
334 mvwaddstr(mixer_widget.window, line, (screen_cols - width) / 2, s);
337 static void display_unplugged(void)
339 int lines, top, left;
342 lines = screen_lines - 6;
346 boojum = lines >= 10 && screen_cols >= 48;
347 top -= boojum ? 5 : 1;
351 left = (screen_cols - 46) / 2;
352 wattrset(mixer_widget.window, attr_mixer_text);
353 mvwaddstr(mixer_widget.window, top + 0, left, "In the midst of the word he was trying to say,");
354 mvwaddstr(mixer_widget.window, top + 1, left + 2, "In the midst of his laughter and glee,");
355 mvwaddstr(mixer_widget.window, top + 2, left, "He had softly and suddenly vanished away---");
356 mvwaddstr(mixer_widget.window, top + 3, left + 2, "For the Snark was a Boojum, you see.");
357 mvwchgat(mixer_widget.window, top + 3, left + 16, 3, /* ^^^ */
358 attr_mixer_text | A_BOLD, PAIR_NUMBER(attr_mixer_text), NULL);
359 mvwaddstr(mixer_widget.window, top + 5, left, "(Lewis Carroll, \"The Hunting of the Snark\")");
362 wattrset(mixer_widget.window, attr_errormsg);
363 center_string(top, _("The sound device was unplugged."));
364 center_string(top + 1, _("Press F6 to select another sound card."));
367 static void display_no_controls(void)
372 y = (screen_lines - 6) / 2 - 1;
375 if (y >= screen_lines - 1)
377 wattrset(mixer_widget.window, attr_infomsg);
378 if (view_mode == VIEW_MODE_PLAYBACK && are_there_any_controls())
379 msg = _("This sound device does not have any playback controls.");
380 else if (view_mode == VIEW_MODE_CAPTURE && are_there_any_controls())
381 msg = _("This sound device does not have any capture controls.");
383 msg = _("This sound device does not have any controls.");
384 center_string(y, msg);
387 static void display_string_centered_in_control(int y, int col, const char *s, int width)
391 left = first_control_x + col * (control_width + 1);
392 x = left + (control_width - width) / 2;
393 display_string_in_field(y, x, s, width, ALIGN_CENTER);
396 static void display_control(unsigned int control_index)
398 struct control *control;
401 int left, frame_left;
410 control = &controls[control_index];
411 col = control_index - first_visible_control_index;
412 left = first_control_x + col * (control_width + 1);
413 frame_left = left + (control_width - 4) / 2;
414 if (control->flags & IS_ACTIVE)
415 wattrset(mixer_widget.window, attr_ctl_frame);
417 wattrset(mixer_widget.window, attr_ctl_inactive);
418 if (control->flags & (TYPE_PVOLUME | TYPE_CVOLUME)) {
419 mvwaddch(mixer_widget.window, base_y - volume_height - 1, frame_left, ACS_ULCORNER);
420 waddch(mixer_widget.window, ACS_HLINE);
421 waddch(mixer_widget.window, ACS_HLINE);
422 waddch(mixer_widget.window, ACS_URCORNER);
423 for (i = 0; i < volume_height; ++i) {
424 mvwaddch(mixer_widget.window, base_y - i - 1, frame_left, ACS_VLINE);
425 mvwaddch(mixer_widget.window, base_y - i - 1, frame_left + 3, ACS_VLINE);
427 mvwaddch(mixer_widget.window, base_y, frame_left,
428 control->flags & TYPE_PSWITCH ? ACS_LTEE : ACS_LLCORNER);
429 waddch(mixer_widget.window, ACS_HLINE);
430 waddch(mixer_widget.window, ACS_HLINE);
431 waddch(mixer_widget.window,
432 control->flags & TYPE_PSWITCH ? ACS_RTEE : ACS_LRCORNER);
433 } else if (control->flags & TYPE_PSWITCH) {
434 mvwaddch(mixer_widget.window, base_y, frame_left, ACS_ULCORNER);
435 waddch(mixer_widget.window, ACS_HLINE);
436 waddch(mixer_widget.window, ACS_HLINE);
437 waddch(mixer_widget.window, ACS_URCORNER);
439 if (control->flags & TYPE_PSWITCH) {
440 mvwaddch(mixer_widget.window, base_y + 1, frame_left, ACS_VLINE);
441 mvwaddch(mixer_widget.window, base_y + 1, frame_left + 3, ACS_VLINE);
442 mvwaddch(mixer_widget.window, base_y + 2, frame_left, ACS_LLCORNER);
443 waddch(mixer_widget.window, ACS_HLINE);
444 waddch(mixer_widget.window, ACS_HLINE);
445 waddch(mixer_widget.window, ACS_LRCORNER);
447 if (control->flags & (TYPE_PVOLUME | TYPE_CVOLUME)) {
448 double (*get_vol_func)(snd_mixer_elem_t *, snd_mixer_selem_channel_id_t);
450 if (control->flags & TYPE_PVOLUME)
451 get_vol_func = get_normalized_playback_volume;
453 get_vol_func = get_normalized_capture_volume;
454 volumes[0] = get_vol_func(control->elem, control->volume_channels[0]);
455 if (control->flags & HAS_VOLUME_1)
456 volumes[1] = get_vol_func(control->elem, control->volume_channels[1]);
458 volumes[1] = volumes[0];
460 if (control->flags & IS_ACTIVE)
461 wattrset(mixer_widget.window, 0);
462 for (c = 0; c < 2; c++) {
463 bar_height = lrint(volumes[c] * volume_height);
464 for (i = 0; i < volume_height; ++i) {
466 if (i + 1 > bar_height)
467 ch = ' ' | (control->flags & IS_ACTIVE ?
471 if (!(control->flags & IS_ACTIVE))
473 #ifdef TRICOLOR_VOLUME_BAR
474 else if (i > volume_height * 8 / 10)
475 ch |= attr_ctl_bar_hi;
476 else if (i > volume_height * 4 / 10)
477 ch |= attr_ctl_bar_mi;
480 ch |= attr_ctl_bar_lo;
482 mvwaddch(mixer_widget.window, base_y - i - 1,
483 frame_left + c + 1, ch);
486 if (control->flags & IS_ACTIVE)
487 wattrset(mixer_widget.window, attr_mixer_active);
488 if (!(control->flags & HAS_VOLUME_1)) {
489 sprintf(buf, "%d", (int)lrint(volumes[0] * 100));
490 display_string_in_field(values_y, frame_left - 2, buf, 8, ALIGN_CENTER);
492 mvwprintw(mixer_widget.window, values_y, frame_left - 2,
493 "%3d", (int)lrint(volumes[0] * 100));
494 if (control->flags & IS_ACTIVE)
495 wattrset(mixer_widget.window, attr_ctl_frame);
496 waddstr(mixer_widget.window, "<>");
497 if (control->flags & IS_ACTIVE)
498 wattrset(mixer_widget.window, attr_mixer_active);
499 wprintw(mixer_widget.window, "%-3d", (int)lrint(volumes[1] * 100));
503 if (control->flags & TYPE_PSWITCH) {
504 err = snd_mixer_selem_get_playback_switch(control->elem, control->pswitch_channels[0], &switches[0]);
505 if (err >= 0 && (control->flags & HAS_PSWITCH_1))
506 err = snd_mixer_selem_get_playback_switch(control->elem, control->pswitch_channels[1], &switches[1]);
508 switches[1] = switches[0];
511 if (control->flags & IS_ACTIVE)
512 wattrset(mixer_widget.window, 0);
513 mvwaddch(mixer_widget.window, base_y + 1, frame_left + 1,
515 /* TRANSLATORS: playback on; one character */
516 ? _("O")[0] | (control->flags & IS_ACTIVE ? attr_ctl_nomute : 0)
517 /* TRANSLATORS: playback muted; one character */
518 : _("M")[0] | (control->flags & IS_ACTIVE ? attr_ctl_mute : 0));
519 waddch(mixer_widget.window,
521 ? _("O")[0] | (control->flags & IS_ACTIVE ? attr_ctl_nomute : 0)
522 : _("M")[0] | (control->flags & IS_ACTIVE ? attr_ctl_mute : 0));
525 if (control->flags & TYPE_CSWITCH) {
526 err = snd_mixer_selem_get_capture_switch(control->elem, control->cswitch_channels[0], &switches[0]);
527 if (err >= 0 && (control->flags & HAS_CSWITCH_1))
528 err = snd_mixer_selem_get_capture_switch(control->elem, control->cswitch_channels[1], &switches[1]);
530 switches[1] = switches[0];
533 if (control->flags & IS_ACTIVE)
534 wattrset(mixer_widget.window, switches[0] ? attr_ctl_capture : attr_ctl_nocapture);
535 /* TRANSLATORS: "left"; no more than two characters */
536 display_string_in_field(cswitch_y - 1, frame_left - 2, switches[0] ? _("L") : "", 2, ALIGN_RIGHT);
537 if (control->flags & IS_ACTIVE)
538 wattrset(mixer_widget.window, switches[1] ? attr_ctl_capture : attr_ctl_nocapture);
539 /* TRANSLATORS: "right"; no more than two characters */
540 display_string_in_field(cswitch_y - 1, frame_left + 4, switches[1] ? _("R") : "", 2, ALIGN_LEFT);
541 /* TRANSLATORS: no more than eight characters */
543 if (switches[0] || switches[1]) {
544 if (control->flags & IS_ACTIVE)
545 wattrset(mixer_widget.window, attr_ctl_capture);
546 display_string_in_field(cswitch_y, frame_left - 2, s, 8, ALIGN_CENTER);
548 i = get_mbs_width(s);
553 if (control->flags & IS_ACTIVE)
554 wattrset(mixer_widget.window, attr_ctl_nocapture);
555 display_string_in_field(cswitch_y, frame_left - 2, buf, 8, ALIGN_CENTER);
559 if (control->flags & TYPE_ENUM) {
560 err = snd_mixer_selem_get_enum_item(control->elem, ffs(control->enum_channel_bits) - 1, &index);
563 err = snd_mixer_selem_get_enum_item_name(control->elem, index, sizeof buf - 1, buf);
566 if (control->flags & IS_ACTIVE)
567 wattrset(mixer_widget.window, attr_mixer_active);
568 display_string_centered_in_control(base_y, col, buf, control_width);
571 if (control_index == focus_control_index) {
572 i = first_control_x + col * (control_width + 1) + (control_width - control_name_width) / 2;
573 wattrset(mixer_widget.window, attr_ctl_mark_focus);
574 mvwaddch(mixer_widget.window, name_y, i - 1, '<');
575 mvwaddch(mixer_widget.window, name_y, i + control_name_width, '>');
576 if (control->flags & IS_ACTIVE)
577 wattrset(mixer_widget.window, attr_ctl_label_focus);
579 wattrset(mixer_widget.window, attr_ctl_label_inactive);
581 if (control->flags & IS_ACTIVE)
582 wattrset(mixer_widget.window, attr_ctl_label);
584 wattrset(mixer_widget.window, attr_ctl_label_inactive);
586 display_string_centered_in_control(name_y, col, control->name, control_name_width);
587 if (channel_name_y > name_y) {
588 if (control->flags & IS_MULTICH) {
589 switch (control->flags & MULTICH_MASK) {
609 wattrset(mixer_widget.window, attr_mixer_frame);
611 display_string_centered_in_control(channel_name_y, col, s,
616 static void display_scroll_indicators(void)
621 if (screen_too_small)
623 y0 = screen_lines * 3 / 8;
624 y1 = screen_lines * 5 / 8;
625 left = first_visible_control_index > 0 ? ACS_LARROW : ACS_VLINE;
626 right = first_visible_control_index + visible_controls < controls_count
627 ? ACS_RARROW : ACS_VLINE;
628 wattrset(mixer_widget.window, attr_mixer_frame);
629 for (y = y0; y <= y1; ++y) {
630 mvwaddch(mixer_widget.window, y, 0, left);
631 mvwaddch(mixer_widget.window, y, screen_cols - 1, right);
635 void display_controls(void)
639 if (first_visible_control_index > controls_count - visible_controls)
640 first_visible_control_index = controls_count - visible_controls;
641 if (first_visible_control_index > focus_control_index)
642 first_visible_control_index = focus_control_index;
643 else if (first_visible_control_index < focus_control_index - visible_controls + 1 && visible_controls)
644 first_visible_control_index = focus_control_index - visible_controls + 1;
646 clear_controls_display();
648 display_focus_item_info();
650 if (controls_count > 0) {
651 if (!screen_too_small)
652 for (i = 0; i < visible_controls; ++i)
653 display_control(first_visible_control_index + i);
654 } else if (unplugged) {
656 } else if (mixer_device_name) {
657 display_no_controls();
659 display_scroll_indicators();
662 void compute_controls_layout(void)
664 bool any_volume, any_pswitch, any_cswitch, any_multich;
665 int max_width, name_len;
669 if (controls_count == 0 || screen_too_small) {
670 visible_controls = 0;
678 for (i = 0; i < controls_count; ++i) {
679 if (controls[i].flags & (TYPE_PVOLUME | TYPE_CVOLUME))
681 if (controls[i].flags & TYPE_PSWITCH)
683 if (controls[i].flags & TYPE_CSWITCH)
685 if (controls[i].flags & IS_MULTICH)
690 for (i = 0; i < controls_count; ++i) {
691 name_len = strlen(controls[i].name);
692 if (name_len > max_width)
693 max_width = name_len;
695 max_width = (max_width + 1) & ~1;
697 control_width = (screen_cols - 3 - (int)controls_count) / controls_count;
698 if (control_width < 8)
700 if (control_width > max_width)
701 control_width = max_width;
702 if (control_width > screen_cols - 4)
703 control_width = screen_cols - 4;
705 visible_controls = (screen_cols - 3) / (control_width + 1);
706 if (visible_controls > controls_count)
707 visible_controls = controls_count;
709 first_control_x = 2 + (screen_cols - 3 - visible_controls * (control_width + 1)) / 2;
711 if (control_width < max_width)
712 control_name_width = control_width;
714 control_name_width = max_width;
726 space = screen_lines - 6 - height;
729 else if (space <= 10)
730 volume_height = space;
732 volume_height = 10 + (space - 10) / 2;
733 height += volume_height;
736 space = screen_lines - 6 - height;
737 channel_name_y = screen_lines - 2 - space / 2;
738 name_y = channel_name_y - any_multich;
739 values_y = name_y - any_volume;
740 cswitch_y = values_y - any_cswitch;
741 base_y = cswitch_y - 1 - 2 * any_pswitch;