Files
cmus/ui_curses.c
2026-03-29 14:01:52 +03:00

2645 lines
59 KiB
C
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* Copyright 2008-2013 Various Authors
* Copyright 2004-2005 Timo Hirvonen
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation; either version 2 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, see <http://www.gnu.org/licenses/>.
*/
#include "job.h"
#include "convert.h"
#include "ui_curses.h"
#include "cmdline.h"
#include "search_mode.h"
#include "command_mode.h"
#include "options.h"
#include "play_queue.h"
#include "browser.h"
#include "filters.h"
#include "cmus.h"
#include "player.h"
#include "output.h"
#include "utils.h"
#include "lib.h"
#include "pl.h"
#include "xmalloc.h"
#include "xstrjoin.h"
#include "window.h"
#include "comment.h"
#include "misc.h"
#include "prog.h"
#include "uchar.h"
#include "spawn.h"
#include "server.h"
#include "keys.h"
#include "debug.h"
#include "help.h"
#include "worker.h"
#include "input.h"
#include "file.h"
#include "path.h"
#include "mixer.h"
#include "mpris.h"
#include "locking.h"
#include "pl_env.h"
#ifdef HAVE_CONFIG
#include "config/curses.h"
#include "config/iconv.h"
#endif
#include <unistd.h>
#include <fcntl.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <sys/ioctl.h>
#include <sys/select.h>
#include <ctype.h>
#include <dirent.h>
#include <locale.h>
#include <langinfo.h>
#ifdef HAVE_ICONV
#include <iconv.h>
#endif
#include <signal.h>
#include <stdarg.h>
#include <math.h>
#include <sys/time.h>
#if defined(__sun__) || defined(__CYGWIN__)
/* TIOCGWINSZ */
#include <termios.h>
#include <ncurses.h>
#else
#include <curses.h>
#endif
/* defined in <term.h> but without const */
char *tgetstr(const char *id, char **area);
char *tgoto(const char *cap, int col, int row);
/* globals. documented in ui_curses.h */
volatile sig_atomic_t cmus_running = 1;
int ui_initialized = 0;
enum ui_input_mode input_mode = NORMAL_MODE;
int cur_view = TREE_VIEW;
int prev_view = -1;
struct searchable *searchable;
char *lib_filename = NULL;
char *lib_ext_filename = NULL;
char *play_queue_filename = NULL;
char *play_queue_ext_filename = NULL;
char *charset = NULL;
int using_utf8 = 0;
/* ------------------------------------------------------------------------- */
static char *lib_autosave_filename;
static char *play_queue_autosave_filename;
static GBUF(print_buffer);
/* destination buffer for utf8_encode_to_buf and utf8_decode */
static char conv_buffer[4096];
/* shown error message and time stamp
* error is cleared if it is older than 3s and key was pressed
*/
static GBUF(error_buf);
static time_t error_time = 0;
/* info messages are displayed in different color */
static int msg_is_error;
static int error_count = 0;
static char *server_address = NULL;
/* used for messages to the client */
static int client_fd = -1;
static char tcap_buffer[64];
static const char *t_ts;
static const char *t_fs;
static int tree_win_x = 0;
static int tree_win_w = 0;
static int track_win_x = 0;
static int track_win_w = 0;
static int win_x = 0;
static int win_w = 0;
static int win_active = 1;
static int show_cursor;
static int cursor_x;
static int cursor_y;
static int cmdline_cursor_x;
static const int default_esc_delay = 25;
static char *title_buf = NULL;
static int in_bracketed_paste = 0;
enum {
CURSED_WIN,
CURSED_WIN_CUR,
CURSED_WIN_SEL,
CURSED_WIN_SEL_CUR,
CURSED_WIN_ACTIVE,
CURSED_WIN_ACTIVE_CUR,
CURSED_WIN_ACTIVE_SEL,
CURSED_WIN_ACTIVE_SEL_CUR,
CURSED_SEPARATOR,
CURSED_WIN_TITLE,
CURSED_COMMANDLINE,
CURSED_STATUSLINE,
CURSED_STATUSLINE_PROGRESS,
CURSED_TITLELINE,
CURSED_DIR,
CURSED_ERROR,
CURSED_INFO,
CURSED_TRACKWIN_ALBUM,
NR_CURSED
};
static unsigned char cursed_to_bg_idx[NR_CURSED] = {
COLOR_WIN_BG,
COLOR_WIN_BG,
COLOR_WIN_INACTIVE_SEL_BG,
COLOR_WIN_INACTIVE_CUR_SEL_BG,
COLOR_WIN_BG,
COLOR_WIN_BG,
COLOR_WIN_SEL_BG,
COLOR_WIN_CUR_SEL_BG,
COLOR_WIN_BG,
COLOR_WIN_TITLE_BG,
COLOR_CMDLINE_BG,
COLOR_STATUSLINE_BG,
COLOR_STATUSLINE_PROGRESS_BG,
COLOR_TITLELINE_BG,
COLOR_WIN_BG,
COLOR_CMDLINE_BG,
COLOR_CMDLINE_BG,
COLOR_TRACKWIN_ALBUM_BG,
};
static unsigned char cursed_to_fg_idx[NR_CURSED] = {
COLOR_WIN_FG,
COLOR_WIN_CUR,
COLOR_WIN_INACTIVE_SEL_FG,
COLOR_WIN_INACTIVE_CUR_SEL_FG,
COLOR_WIN_FG,
COLOR_WIN_CUR,
COLOR_WIN_SEL_FG,
COLOR_WIN_CUR_SEL_FG,
COLOR_SEPARATOR,
COLOR_WIN_TITLE_FG,
COLOR_CMDLINE_FG,
COLOR_STATUSLINE_FG,
COLOR_STATUSLINE_PROGRESS_FG,
COLOR_TITLELINE_FG,
COLOR_WIN_DIR,
COLOR_ERROR,
COLOR_INFO,
COLOR_TRACKWIN_ALBUM_FG,
};
static unsigned char cursed_to_attr_idx[NR_CURSED] = {
COLOR_WIN_ATTR,
COLOR_WIN_CUR_ATTR,
COLOR_WIN_INACTIVE_SEL_ATTR,
COLOR_WIN_INACTIVE_CUR_SEL_ATTR,
COLOR_WIN_ATTR,
COLOR_WIN_CUR_ATTR,
COLOR_WIN_SEL_ATTR,
COLOR_WIN_CUR_SEL_ATTR,
COLOR_WIN_ATTR,
COLOR_WIN_TITLE_ATTR,
COLOR_CMDLINE_ATTR,
COLOR_STATUSLINE_ATTR,
COLOR_STATUSLINE_PROGRESS_ATTR,
COLOR_TITLELINE_ATTR,
COLOR_WIN_ATTR,
COLOR_CMDLINE_ATTR,
COLOR_CMDLINE_ATTR,
COLOR_TRACKWIN_ALBUM_ATTR,
};
/* index is CURSED_*, value is fucking color pair */
static int pairs[NR_CURSED];
enum {
TF_ALBUMARTIST,
TF_ARTIST,
TF_ALBUM,
TF_DISC,
TF_TOTAL_DISCS,
TF_TRACK,
TF_TITLE,
TF_PLAY_COUNT,
TF_YEAR,
TF_MAX_YEAR,
TF_ORIGINALYEAR,
TF_GENRE,
TF_COMMENT,
TF_DURATION,
TF_DURATION_SEC,
TF_ALBUMDURATION,
TF_BITRATE,
TF_CODEC,
TF_CODEC_PROFILE,
TF_PATHFILE,
TF_FILE,
TF_RG_TRACK_GAIN,
TF_RG_TRACK_PEAK,
TF_RG_ALBUM_GAIN,
TF_RG_ALBUM_PEAK,
TF_ARRANGER,
TF_COMPOSER,
TF_CONDUCTOR,
TF_LYRICIST,
TF_PERFORMER,
TF_REMIXER,
TF_LABEL,
TF_PUBLISHER,
TF_WORK,
TF_OPUS,
TF_PARTNUMBER,
TF_PART,
TF_SUBTITLE,
TF_MEDIA,
TF_VA,
TF_STATUS,
TF_POSITION,
TF_POSITION_SEC,
TF_TOTAL,
TF_VOLUME,
TF_LVOLUME,
TF_RVOLUME,
TF_BUFFER,
TF_REPEAT,
TF_CONTINUE,
TF_FOLLOW,
TF_SHUFFLE,
TF_PLAYLISTMODE,
TF_BPM,
TF_PANEL,
NR_TFS
};
static struct format_option track_fopts[NR_TFS + 1] = {
DEF_FO_STR('A', "albumartist", 0),
DEF_FO_STR('a', "artist", 0),
DEF_FO_STR('l', "album", 0),
DEF_FO_INT('D', "discnumber", 1),
DEF_FO_INT('T', "totaldiscs", 1),
DEF_FO_INT('n', "tracknumber", 1),
DEF_FO_STR('t', "title", 0),
DEF_FO_INT('X', "play_count", 0),
DEF_FO_INT('y', "date", 1),
DEF_FO_INT('\0', "maxdate", 1),
DEF_FO_INT('\0', "originaldate", 1),
DEF_FO_STR('g', "genre", 0),
DEF_FO_STR('c', "comment", 0),
DEF_FO_TIME('d', "duration", 0),
DEF_FO_INT('\0', "duration_sec", 1),
DEF_FO_TIME('\0', "albumduration", 0),
DEF_FO_INT('\0', "bitrate", 0),
DEF_FO_STR('\0', "codec", 0),
DEF_FO_STR('\0', "codec_profile", 0),
DEF_FO_STR('f', "path", 0),
DEF_FO_STR('F', "filename", 0),
DEF_FO_DOUBLE('\0', "rg_track_gain", 0),
DEF_FO_DOUBLE('\0', "rg_track_peak", 0),
DEF_FO_DOUBLE('\0', "rg_album_gain", 0),
DEF_FO_DOUBLE('\0', "rg_album_peak", 0),
DEF_FO_STR('\0', "arranger", 0),
DEF_FO_STR('\0', "composer", 0),
DEF_FO_STR('\0', "conductor", 0),
DEF_FO_STR('\0', "lyricist", 0),
DEF_FO_STR('\0', "performer", 0),
DEF_FO_STR('\0', "remixer", 0),
DEF_FO_STR('\0', "label", 0),
DEF_FO_STR('\0', "publisher", 0),
DEF_FO_STR('\0', "work", 0),
DEF_FO_STR('\0', "opus", 0),
DEF_FO_STR('\0', "partnumber", 0),
DEF_FO_STR('\0', "part", 0),
DEF_FO_STR('\0', "subtitle", 0),
DEF_FO_STR('\0', "media", 0),
DEF_FO_INT('\0', "va", 0),
DEF_FO_STR('\0', "status", 0),
DEF_FO_TIME('\0', "position", 0),
DEF_FO_INT('\0', "position_sec", 1),
DEF_FO_TIME('\0', "total", 0),
DEF_FO_INT('\0', "volume", 1),
DEF_FO_INT('\0', "lvolume", 1),
DEF_FO_INT('\0', "rvolume", 1),
DEF_FO_INT('\0', "buffer", 1),
DEF_FO_STR('\0', "repeat", 0),
DEF_FO_STR('\0', "continue", 0),
DEF_FO_STR('\0', "follow", 0),
DEF_FO_STR('\0', "shuffle", 0),
DEF_FO_STR('\0', "playlist_mode", 0),
DEF_FO_INT('\0', "bpm", 0),
DEF_FO_INT('\0', "panel", 0),
DEF_FO_END
};
int get_track_win_x(void)
{
return track_win_x;
}
int track_format_valid(const char *format)
{
return format_valid(format, track_fopts);
}
static void utf8_encode_to_buf(const char *buffer)
{
int n;
#ifdef HAVE_ICONV
static iconv_t cd = (iconv_t)-1;
size_t is, os;
const char *i;
char *o;
int rc;
if (cd == (iconv_t)-1) {
d_print("iconv_open(UTF-8, %s)\n", charset);
cd = iconv_open("UTF-8", charset);
if (cd == (iconv_t)-1) {
d_print("iconv_open failed: %s\n", strerror(errno));
goto fallback;
}
}
i = buffer;
o = conv_buffer;
is = strlen(i);
os = sizeof(conv_buffer) - 1;
rc = iconv(cd, (void *)&i, &is, &o, &os);
*o = 0;
if (rc == -1) {
d_print("iconv failed: %s\n", strerror(errno));
goto fallback;
}
return;
fallback:
#endif
n = min_i(sizeof(conv_buffer) - 1, strlen(buffer));
memmove(conv_buffer, buffer, n);
conv_buffer[n] = '\0';
}
static void utf8_decode(const char *buffer)
{
int n;
#ifdef HAVE_ICONV
static iconv_t cd = (iconv_t)-1;
size_t is, os;
const char *i;
char *o;
int rc;
if (cd == (iconv_t)-1) {
d_print("iconv_open(%s, UTF-8)\n", charset);
cd = iconv_open(charset, "UTF-8");
if (cd == (iconv_t)-1) {
d_print("iconv_open failed: %s\n", strerror(errno));
goto fallback;
}
}
i = buffer;
o = conv_buffer;
is = strlen(i);
os = sizeof(conv_buffer) - 1;
rc = iconv(cd, (void *)&i, &is, &o, &os);
*o = 0;
if (rc == -1) {
d_print("iconv failed: %s\n", strerror(errno));
goto fallback;
}
return;
fallback:
#endif
n = u_to_ascii(conv_buffer, buffer, sizeof(conv_buffer) - 1);
conv_buffer[n] = '\0';
}
/* screen updates {{{ */
static void dump_print_buffer_no_clear(int row, int col, size_t offset)
{
if (using_utf8) {
(void) mvaddstr(row, col, print_buffer.buffer + offset);
} else {
utf8_decode(print_buffer.buffer + offset);
(void) mvaddstr(row, col, conv_buffer);
}
}
static void dump_print_buffer(int row, int col)
{
dump_print_buffer_no_clear(row, col, 0);
gbuf_clear(&print_buffer);
}
/* print @str into @buf
*
* if @str is shorter than @width pad with spaces
* if @str is wider than @width truncate and add "..."
*/
static void format_str(struct gbuf *buf, const char *str, int width)
{
gbuf_add_ustr(buf, str, &width);
gbuf_set(buf, ' ', width);
}
static void sprint(int row, int col, const char *str, int width)
{
gbuf_add_ch(&print_buffer, ' ');
format_str(&print_buffer, str, width - 2);
gbuf_add_ch(&print_buffer, ' ');
dump_print_buffer(row, col);
}
static inline void fopt_set_str(struct format_option *fopt, const char *str)
{
BUG_ON(fopt->type != FO_STR);
if (str) {
fopt->fo_str = str;
fopt->empty = 0;
} else {
fopt->empty = 1;
}
}
static inline void fopt_set_int(struct format_option *fopt, int value, int empty)
{
BUG_ON(fopt->type != FO_INT);
fopt->fo_int = value;
fopt->empty = empty;
}
static inline void fopt_set_double(struct format_option *fopt, double value, int empty)
{
BUG_ON(fopt->type != FO_DOUBLE);
fopt->fo_double = value;
fopt->empty = empty;
}
static inline void fopt_set_time(struct format_option *fopt, int value, int empty)
{
BUG_ON(fopt->type != FO_TIME);
fopt->fo_time = value;
fopt->empty = empty;
}
static void fill_track_fopts_track_info(struct track_info *info)
{
char *filename;
if (using_utf8) {
filename = info->filename;
} else {
utf8_encode_to_buf(info->filename);
filename = conv_buffer;
}
fopt_set_str(&track_fopts[TF_ALBUMARTIST], info->albumartist);
fopt_set_str(&track_fopts[TF_ARTIST], info->artist);
fopt_set_str(&track_fopts[TF_ALBUM], info->album);
fopt_set_int(&track_fopts[TF_PLAY_COUNT], info->play_count, 0);
fopt_set_int(&track_fopts[TF_DISC], info->discnumber, info->discnumber == -1);
fopt_set_int(&track_fopts[TF_TOTAL_DISCS], info->totaldiscs, info->totaldiscs == -1);
fopt_set_int(&track_fopts[TF_TRACK], info->tracknumber, info->tracknumber == -1);
fopt_set_str(&track_fopts[TF_TITLE], info->title);
fopt_set_int(&track_fopts[TF_YEAR], info->date / 10000, info->date <= 0);
fopt_set_str(&track_fopts[TF_GENRE], info->genre);
fopt_set_str(&track_fopts[TF_COMMENT], info->comment);
fopt_set_time(&track_fopts[TF_DURATION], info->duration, info->duration == -1);
fopt_set_int(&track_fopts[TF_DURATION_SEC], info->duration, info->duration == -1);
fopt_set_double(&track_fopts[TF_RG_TRACK_GAIN], info->rg_track_gain, isnan(info->rg_track_gain));
fopt_set_double(&track_fopts[TF_RG_TRACK_PEAK], info->rg_track_peak, isnan(info->rg_track_peak));
fopt_set_double(&track_fopts[TF_RG_ALBUM_GAIN], info->rg_album_gain, isnan(info->rg_album_gain));
fopt_set_double(&track_fopts[TF_RG_ALBUM_PEAK], info->rg_album_peak, isnan(info->rg_album_peak));
fopt_set_int(&track_fopts[TF_ORIGINALYEAR], info->originaldate / 10000, info->originaldate <= 0);
fopt_set_int(&track_fopts[TF_BITRATE], (int) (info->bitrate / 1000. + 0.5), info->bitrate == -1);
fopt_set_str(&track_fopts[TF_CODEC], info->codec);
fopt_set_str(&track_fopts[TF_CODEC_PROFILE], info->codec_profile);
fopt_set_str(&track_fopts[TF_PATHFILE], filename);
fopt_set_str(&track_fopts[TF_ARRANGER], keyvals_get_val(info->comments, "arranger"));
fopt_set_str(&track_fopts[TF_COMPOSER], keyvals_get_val(info->comments, "composer"));
fopt_set_str(&track_fopts[TF_CONDUCTOR], keyvals_get_val(info->comments, "conductor"));
fopt_set_str(&track_fopts[TF_LYRICIST], keyvals_get_val(info->comments, "lyricist"));
fopt_set_str(&track_fopts[TF_PERFORMER], keyvals_get_val(info->comments, "performer"));
fopt_set_str(&track_fopts[TF_REMIXER], keyvals_get_val(info->comments, "remixer"));
fopt_set_str(&track_fopts[TF_LABEL], keyvals_get_val(info->comments, "label"));
fopt_set_str(&track_fopts[TF_PUBLISHER], keyvals_get_val(info->comments, "publisher"));
fopt_set_str(&track_fopts[TF_WORK], keyvals_get_val(info->comments, "work"));
fopt_set_str(&track_fopts[TF_OPUS], keyvals_get_val(info->comments, "opus"));
fopt_set_str(&track_fopts[TF_PARTNUMBER], keyvals_get_val(info->comments, "discnumber"));
fopt_set_str(&track_fopts[TF_PART], keyvals_get_val(info->comments, "discnumber"));
fopt_set_str(&track_fopts[TF_SUBTITLE], keyvals_get_val(info->comments, "subtitle"));
fopt_set_str(&track_fopts[TF_MEDIA], info->media);
fopt_set_int(&track_fopts[TF_VA], 0, !track_is_compilation(info->comments));
if (is_http_url(info->filename)) {
fopt_set_str(&track_fopts[TF_FILE], filename);
} else {
fopt_set_str(&track_fopts[TF_FILE], path_basename(filename));
}
fopt_set_int(&track_fopts[TF_BPM], info->bpm, info->bpm == -1);
}
static int get_album_length(struct album *album)
{
struct tree_track *track;
struct rb_node *tmp;
int duration = 0;
rb_for_each_entry(track, tmp, &album->track_root, tree_node) {
duration += max_i(0, tree_track_info(track)->duration);
}
return duration;
}
static int get_artist_length(struct artist *artist)
{
struct album *album;
struct rb_node *tmp;
int duration = 0;
rb_for_each_entry(album, tmp, &artist->album_root, tree_node) {
duration += get_album_length(album);
}
return duration;
}
static void fill_track_fopts_album(struct album *album)
{
fopt_set_int(&track_fopts[TF_YEAR], album->min_date / 10000, album->min_date <= 0);
fopt_set_int(&track_fopts[TF_MAX_YEAR], album->date / 10000, album->date <= 0);
fopt_set_str(&track_fopts[TF_ALBUMARTIST], album->artist->name);
fopt_set_str(&track_fopts[TF_ARTIST], album->artist->name);
fopt_set_str(&track_fopts[TF_ALBUM], album->name);
int duration = get_album_length(album);
fopt_set_time(&track_fopts[TF_DURATION], duration, 0);
fopt_set_time(&track_fopts[TF_ALBUMDURATION], duration, 0);
}
static void fill_track_fopts_artist(struct artist *artist)
{
const char *name = display_artist_sort_name ? artist_sort_name(artist) : artist->name;
fopt_set_str(&track_fopts[TF_ARTIST], name);
fopt_set_str(&track_fopts[TF_ALBUMARTIST], name);
fopt_set_time(&track_fopts[TF_DURATION], get_artist_length(artist), 0);
}
const struct format_option *get_global_fopts(void)
{
if (player_info.ti)
fill_track_fopts_track_info(player_info.ti);
static const char *status_strs[] = { ".", ">", "|" };
static const char *cont_strs[] = { " ", "C" };
static const char *follow_strs[] = { " ", "F" };
static const char *repeat_strs[] = { " ", "R" };
static const char *shuffle_strs[] = { " ", "S", "&" };
int buffer_fill, vol, vol_left, vol_right;
int duration = -1;
unsigned int total_time = pl_playing_total_time();
if (cmus_queue_active())
total_time = play_queue_total_time();
else if (play_library)
total_time = lib_editable.total_time;
fopt_set_time(&track_fopts[TF_TOTAL], total_time, 0);
fopt_set_str(&track_fopts[TF_FOLLOW], follow_strs[follow]);
fopt_set_str(&track_fopts[TF_REPEAT], repeat_strs[repeat]);
fopt_set_str(&track_fopts[TF_SHUFFLE], shuffle_strs[shuffle]);
fopt_set_str(&track_fopts[TF_PLAYLISTMODE], aaa_mode_names[aaa_mode]);
if (player_info.ti)
duration = player_info.ti->duration;
vol_left = vol_right = vol = -1;
if (soft_vol) {
vol_left = soft_vol_l;
vol_right = soft_vol_r;
vol = (vol_left + vol_right + 1) / 2;
} else if (volume_max && volume_l >= 0 && volume_r >= 0) {
vol_left = scale_to_percentage(volume_l, volume_max);
vol_right = scale_to_percentage(volume_r, volume_max);
vol = (vol_left + vol_right + 1) / 2;
}
buffer_fill = scale_to_percentage(player_info.buffer_fill, player_info.buffer_size);
fopt_set_str(&track_fopts[TF_STATUS], status_strs[player_info.status]);
if (show_remaining_time && duration != -1) {
fopt_set_time(&track_fopts[TF_POSITION], player_info.pos - duration, 0);
} else {
fopt_set_time(&track_fopts[TF_POSITION], player_info.pos, 0);
}
fopt_set_int(&track_fopts[TF_POSITION_SEC], player_info.pos, player_info.pos < 0);
fopt_set_time(&track_fopts[TF_DURATION], duration, duration < 0);
fopt_set_int(&track_fopts[TF_VOLUME], vol, vol < 0);
fopt_set_int(&track_fopts[TF_LVOLUME], vol_left, vol_left < 0);
fopt_set_int(&track_fopts[TF_RVOLUME], vol_right, vol_right < 0);
fopt_set_int(&track_fopts[TF_BUFFER], buffer_fill, 0);
fopt_set_str(&track_fopts[TF_CONTINUE], cont_strs[player_cont]);
fopt_set_int(&track_fopts[TF_BITRATE], player_info.current_bitrate / 1000. + 0.5, 0);
return track_fopts;
}
static void print_tree(struct window *win, int row, struct iter *iter)
{
struct artist *artist;
struct album *album;
struct iter sel;
int current, selected, active;
artist = iter_to_artist(iter);
album = iter_to_album(iter);
current = 0;
if (lib_cur_track) {
if (album) {
current = CUR_ALBUM == album;
} else {
current = CUR_ARTIST == artist;
}
}
window_get_sel(win, &sel);
selected = iters_equal(iter, &sel);
active = lib_cur_win == lib_tree_win;
bkgdset(pairs[(active << 2) | (selected << 1) | current]);
if (active && selected) {
cursor_x = 0;
cursor_y = 1 + row;
}
gbuf_add_ch(&print_buffer, ' ');
if (album) {
fill_track_fopts_album(album);
format_print(&print_buffer, tree_win_w - 1, tree_win_format, track_fopts);
} else {
fill_track_fopts_artist(artist);
format_print(&print_buffer, tree_win_w - 1, tree_win_artist_format, track_fopts);
}
dump_print_buffer(row + 1, tree_win_x);
}
static void print_track(struct window *win, int row, struct iter *iter)
{
struct tree_track *track;
struct album *album;
struct track_info *ti;
struct iter sel;
int current, selected, active;
const char *format;
track = iter_to_tree_track(iter);
album = iter_to_album(iter);
if (track == (struct tree_track*)album) {
int pos;
struct fp_len len;
bkgdset(pairs[CURSED_TRACKWIN_ALBUM]);
fill_track_fopts_album(album);
len = format_print(&print_buffer, track_win_w, track_win_album_format, track_fopts);
dump_print_buffer(row + 1, track_win_x);
bkgdset(pairs[CURSED_SEPARATOR]);
for(pos = track_win_x + len.llen + len.mlen; pos < win_w - len.rlen; ++pos)
(void) mvaddch(row + 1, pos, ACS_HLINE);
return;
}
current = lib_cur_track == track;
window_get_sel(win, &sel);
selected = iters_equal(iter, &sel);
active = lib_cur_win == lib_track_win;
bkgdset(pairs[(active << 2) | (selected << 1) | current]);
if (active && selected) {
cursor_x = track_win_x;
cursor_y = 1 + row;
}
ti = tree_track_info(track);
fill_track_fopts_track_info(ti);
format = track_win_format;
if (track_info_has_tag(ti)) {
if (*track_win_format_va && track_is_compilation(ti->comments))
format = track_win_format_va;
} else if (*track_win_alt_format) {
format = track_win_alt_format;
}
format_print(&print_buffer, track_win_w, format, track_fopts);
dump_print_buffer(row + 1, track_win_x);
}
/* used by print_editable only */
static struct simple_track *current_track;
static void print_editable(struct window *win, int row, struct iter *iter)
{
struct simple_track *track;
struct iter sel;
int current, selected, active;
const char *format;
track = iter_to_simple_track(iter);
current = current_track == track;
window_get_sel(win, &sel);
selected = iters_equal(iter, &sel);
if (selected) {
cursor_x = win_x;
cursor_y = 1 + row;
}
active = win_active;
if (!selected && !!track->marked) {
selected = 1;
active = 0;
}
bkgdset(pairs[(active << 2) | (selected << 1) | current]);
fill_track_fopts_track_info(track->info);
format = list_win_format;
if (track_info_has_tag(track->info)) {
if (*list_win_format_va && track_is_compilation(track->info->comments))
format = list_win_format_va;
} else if (*list_win_alt_format) {
format = list_win_alt_format;
}
format_print(&print_buffer, win_w, format, track_fopts);
dump_print_buffer(row + 1, win_x);
}
static void print_browser(struct window *win, int row, struct iter *iter)
{
struct browser_entry *e;
struct iter sel;
int selected;
e = iter_to_browser_entry(iter);
window_get_sel(win, &sel);
selected = iters_equal(iter, &sel);
if (selected) {
int active = 1;
int current = 0;
bkgdset(pairs[(active << 2) | (selected << 1) | current]);
} else {
if (e->type == BROWSER_ENTRY_DIR) {
bkgdset(pairs[CURSED_DIR]);
} else {
bkgdset(pairs[CURSED_WIN]);
}
}
if (selected) {
cursor_x = 0;
cursor_y = 1 + row;
}
sprint(row + 1, 0, e->name, win_w);
}
static void print_filter(struct window *win, int row, struct iter *iter)
{
char buf[256];
struct filter_entry *e = iter_to_filter_entry(iter);
struct iter sel;
/* window active? */
int active = 1;
/* row selected? */
int selected;
/* is the filter currently active? */
int current = !!e->act_stat;
const char stat_chars[3] CMUS_NONSTRING = " *!";
int ch1, ch2, ch3;
const char *e_filter;
window_get_sel(win, &sel);
selected = iters_equal(iter, &sel);
bkgdset(pairs[(active << 2) | (selected << 1) | current]);
if (selected) {
cursor_x = 0;
cursor_y = 1 + row;
}
ch1 = ' ';
ch3 = ' ';
if (e->sel_stat != e->act_stat) {
ch1 = '[';
ch3 = ']';
}
ch2 = stat_chars[e->sel_stat];
e_filter = e->filter;
if (!using_utf8) {
utf8_encode_to_buf(e_filter);
e_filter = conv_buffer;
}
snprintf(buf, sizeof(buf), "%c%c%c%-15s %.235s", ch1, ch2, ch3, e->name, e_filter);
format_str(&print_buffer, buf, win_w - 1);
gbuf_add_ch(&print_buffer, ' ');
dump_print_buffer(row + 1, 0);
}
static void print_help(struct window *win, int row, struct iter *iter)
{
struct iter sel;
int selected;
int active = 1;
char buf[OPTION_MAX_SIZE];
const struct help_entry *e = iter_to_help_entry(iter);
const struct cmus_opt *opt;
window_get_sel(win, &sel);
selected = iters_equal(iter, &sel);
bkgdset(pairs[(active << 2) | (selected << 1)]);
if (selected) {
cursor_x = 0;
cursor_y = 1 + row;
}
switch (e->type) {
case HE_TEXT:
snprintf(buf, sizeof(buf), " %s", e->text);
break;
case HE_BOUND:
snprintf(buf, sizeof(buf), " %-8s %-23s %s",
key_context_names[e->binding->ctx],
e->binding->key->name,
e->binding->cmd);
break;
case HE_UNBOUND:
snprintf(buf, sizeof(buf), " %s", e->command->name);
break;
case HE_OPTION:
opt = e->option;
snprintf(buf, sizeof(buf), " %-29s ", opt->name);
size_t len = strlen(buf);
opt->get(opt->data, buf + len, sizeof(buf) - len);
break;
}
format_str(&print_buffer, buf, win_w - 1);
gbuf_add_ch(&print_buffer, ' ');
dump_print_buffer(row + 1, 0);
}
static void update_window(struct window *win, int x, int y, int w, const char *title,
void (*print)(struct window *, int, struct iter *))
{
struct iter iter;
int nr_rows;
int i;
win->changed = 0;
bkgdset(pairs[CURSED_WIN_TITLE]);
sprint(y, x, title, w);
nr_rows = window_get_nr_rows(win);
i = 0;
if (window_get_top(win, &iter)) {
while (i < nr_rows) {
print(win, i, &iter);
i++;
if (!window_get_next(win, &iter))
break;
}
}
bkgdset(pairs[0]);
gbuf_set(&print_buffer, ' ', w);
while (i < nr_rows) {
dump_print_buffer_no_clear(y + i + 1, x, 0);
i++;
}
gbuf_clear(&print_buffer);
}
static void update_tree_window(void)
{
static GBUF(buf);
gbuf_clear(&buf);
gbuf_add_str(&buf, "Library");
if (worker_has_job())
gbuf_addf(&buf, " - %d tracks", lib_editable.nr_tracks);
update_window(lib_tree_win, tree_win_x, 0, tree_win_w + 1, buf.buffer, print_tree);
}
static void update_track_window(void)
{
static GBUF(title);
gbuf_clear(&title);
struct iter iter;
struct album *album;
struct artist *artist;
const char *format_str = "Empty (use :add)";
if (window_get_sel(lib_tree_win, &iter)) {
if ((album = iter_to_album(&iter))) {
fill_track_fopts_album(album);
format_str = heading_album_format;
} else if ((artist = iter_to_artist(&iter))) {
fill_track_fopts_artist(artist);
format_str = heading_artist_format;
}
}
format_print(&title, track_win_w - 2, format_str, track_fopts);
update_window(lib_track_win, track_win_x, 0, track_win_w, title.buffer,
print_track);
}
static void print_pl_list(struct window *win, int row, struct iter *iter)
{
struct pl_list_info info;
pl_list_iter_to_info(iter, &info);
bkgdset(pairs[(info.active<<2) | (info.selected<<1) | info.current]);
const char *prefix = " ";
if (info.marked)
prefix = " * ";
size_t prefix_w = strlen(prefix);
format_str(&print_buffer, prefix, prefix_w);
if (tree_win_w > prefix_w)
format_str(&print_buffer, info.name,
tree_win_w - prefix_w);
dump_print_buffer(row + 1, 0);
}
static void draw_separator(void)
{
int row;
bkgdset(pairs[CURSED_WIN_TITLE]);
(void) mvaddch(0, tree_win_w, ' ');
bkgdset(pairs[CURSED_SEPARATOR]);
for (row = 1; row < LINES - 3; row++)
(void) mvaddch(row, tree_win_w, ACS_VLINE);
}
static void update_pl_list(struct window *win)
{
if (pl_show_panel()) {
update_window(win, tree_win_x, 0, tree_win_w + 1, "Playlist", print_pl_list);
draw_separator();
}
}
static void update_pl_tracks(struct window *win)
{
static GBUF(title);
gbuf_clear(&title);
int win_w_tmp = win_w;
if (pl_show_panel()) {
win_x = track_win_x;
win_w = track_win_w;
} else {
win_x = 0;
win_w = tree_win_w + 1 + track_win_w;
}
win_active = pl_get_cursor_in_track_window();
get_global_fopts();
fopt_set_int(&track_fopts[TF_PANEL], 1, !pl_show_panel());
fopt_set_str(&track_fopts[TF_TITLE], pl_visible_get_name());
fopt_set_time(&track_fopts[TF_DURATION], pl_visible_total_time(), 0);
format_print(&title, win_w - 2, heading_playlist_format, track_fopts);
update_window(win, win_x, 0, win_w, title.buffer, print_editable);
win_active = 1;
win_x = 0;
win_w = win_w_tmp;
}
static const char *pretty_path(const char *path)
{
static int home_len = -1;
static GBUF(buf);
if (home_len == -1)
home_len = strlen(home_dir);
if (strncmp(path, home_dir, home_len) || path[home_len] != '/')
return path;
gbuf_clear(&buf);
gbuf_add_ch(&buf, '~');
gbuf_add_str(&buf, path + home_len);
return buf.buffer;
}
static const char * const sorted_names[2] = { "", "sorted by " };
static void update_editable_window(struct editable *e, const char *title, const char *filename)
{
static GBUF(buf);
gbuf_clear(&buf);
if (filename) {
if (using_utf8) {
/* already UTF-8 */
} else {
utf8_encode_to_buf(filename);
filename = conv_buffer;
}
gbuf_addf(&buf, "%s %.256s - %d tracks", title, pretty_path(filename), e->nr_tracks);
} else {
gbuf_addf(&buf, "%s - %d tracks", title, e->nr_tracks);
}
fopt_set_time(&track_fopts[TF_TOTAL], e->total_time, 0);
format_print(&buf, 0, " (%{total})", track_fopts);
if (e->nr_marked) {
gbuf_addf(&buf, " (%d marked)", e->nr_marked);
}
gbuf_addf(&buf, " %s%s",
sorted_names[e->shared->sort_str[0] != 0],
e->shared->sort_str);
update_window(e->shared->win, 0, 0, win_w, buf.buffer, &print_editable);
}
static void update_sorted_window(void)
{
current_track = (struct simple_track *)lib_cur_track;
update_editable_window(&lib_editable, "Library", NULL);
}
static void update_play_queue_window(void)
{
current_track = NULL;
update_editable_window(&pq_editable, "Play Queue", NULL);
}
static void update_browser_window(void)
{
static GBUF(title);
gbuf_clear(&title);
char *dirname;
if (using_utf8) {
/* already UTF-8 */
dirname = browser_dir;
} else {
utf8_encode_to_buf(browser_dir);
dirname = conv_buffer;
}
gbuf_add_str(&title, "Browser - ");
gbuf_add_str(&title, dirname);
update_window(browser_win, 0, 0, win_w, title.buffer, print_browser);
}
static void update_filters_window(void)
{
update_window(filters_win, 0, 0, win_w, "Library Filters", print_filter);
}
static void update_help_window(void)
{
update_window(help_win, 0, 0, win_w, "Settings", print_help);
}
static void update_pl_view(int full)
{
current_track = pl_get_playing_track();
pl_draw(update_pl_list, update_pl_tracks, full);
}
static void do_update_view(int full)
{
if (!ui_initialized)
return;
cursor_x = -1;
cursor_y = -1;
switch (cur_view) {
case TREE_VIEW:
if (full || lib_tree_win->changed)
update_tree_window();
if (full || lib_track_win->changed)
update_track_window();
draw_separator();
update_filterline();
break;
case SORTED_VIEW:
update_sorted_window();
update_filterline();
break;
case PLAYLIST_VIEW:
update_pl_view(full);
break;
case QUEUE_VIEW:
update_play_queue_window();
break;
case BROWSER_VIEW:
update_browser_window();
break;
case FILTERS_VIEW:
update_filters_window();
break;
case HELP_VIEW:
update_help_window();
break;
}
}
static void do_update_statusline(void)
{
struct fp_len len;
len = format_print(&print_buffer, win_w, statusline_format, get_global_fopts());
bkgdset(pairs[CURSED_STATUSLINE]);
dump_print_buffer_no_clear(LINES - 2, 0, 0);
if (progress_bar && player_info.ti) {
int duration = player_info.ti->duration;
if (duration && duration >= player_info.pos) {
if (progress_bar == PROGRESS_BAR_LINE || progress_bar == PROGRESS_BAR_SHUTTLE) {
/* Draw a bar or short position marker within the blank space */
int shuttle_len = (progress_bar == PROGRESS_BAR_SHUTTLE) ? 2 : 0;
int bar_start = len.llen + len.mlen;
int bar_space = win_w - len.rlen - bar_start - shuttle_len;
if (bar_space >= 5) {
int bar_len = bar_space * player_info.pos / duration;
if (progress_bar == PROGRESS_BAR_SHUTTLE) {
bar_start += bar_len;
bar_len = shuttle_len;
}
for (int x = bar_start; bar_len; --bar_len)
(void) mvaddstr(LINES - 2, x++, using_utf8 ? "" : "-");
}
} else if (progress_bar == PROGRESS_BAR_COLOR) {
/* Draw over the played portion of bar in alt color */
int w = win_w * player_info.pos / duration;
int skip = w;
int buf_index = u_skip_chars(print_buffer.buffer, &skip, false);
print_buffer.buffer[buf_index] = '\0';
bkgdset(pairs[CURSED_STATUSLINE_PROGRESS]);
dump_print_buffer_no_clear(LINES - 2, 0, 0);
} else { // PROGRESS_BAR_COLOR_SHUTTLE
/* Redraw a few cols in alt color to mark the current position */
int shuttle_len = min_u(6, win_w);
int x = (win_w - shuttle_len) * player_info.pos / duration;
int skip = x;
int buf_index = u_skip_chars(print_buffer.buffer, &skip, false);
int end_offset = u_skip_chars(print_buffer.buffer + buf_index, &shuttle_len, true);
print_buffer.buffer[buf_index+end_offset] = '\0';
bkgdset(pairs[CURSED_STATUSLINE_PROGRESS]);
dump_print_buffer_no_clear(LINES - 2, x, buf_index);
}
}
}
gbuf_clear(&print_buffer);
if (player_info.error_msg)
error_msg("%s", player_info.error_msg);
}
static void dump_buffer(const char *buffer)
{
if (using_utf8) {
addstr(buffer);
} else {
utf8_decode(buffer);
addstr(conv_buffer);
}
}
static void do_update_commandline(void)
{
char *str;
size_t idx = 0;
char ch;
move(LINES - 1, 0);
if (error_buf.len != 0) {
if (msg_is_error) {
bkgdset(pairs[CURSED_ERROR]);
} else {
bkgdset(pairs[CURSED_INFO]);
}
addstr(error_buf.buffer);
clrtoeol();
return;
}
bkgdset(pairs[CURSED_COMMANDLINE]);
if (input_mode == NORMAL_MODE) {
clrtoeol();
return;
}
str = cmdline.line;
if (!using_utf8) {
/* cmdline.line actually pretends to be UTF-8 but all non-ASCII
* characters are invalid UTF-8 so it really is in locale's
* encoding.
*
* This code should be safe because cmdline.bpos ==
* cmdline.cpos as every non-ASCII character is counted as one
* invalid UTF-8 byte.
*
* NOTE: This has nothing to do with widths of printed
* characters. I.e. even if there were control characters
* (displayed as <xx>) there would be no problem because bpos
* still equals to cpos, I think.
*/
utf8_encode_to_buf(cmdline.line);
str = conv_buffer;
}
/* COMMAND_MODE or SEARCH_MODE */
ch = ':';
if (input_mode == SEARCH_MODE)
ch = search_direction == SEARCH_FORWARD ? '/' : '?';
int width = win_w - 2; // ':' at start and ' ' at end
/* width of the text in the buffer before and after cursor */
int cw = u_str_nwidth(str, cmdline.cpos);
int extra_w = u_str_width(str + cmdline.bpos);
/* shift by third of bar width to provide visual context when editing */
int context_w = min_u(extra_w, win_w / 3);
int skip = cw + context_w - width;
if (skip <= 0) {
addch(ch);
cmdline_cursor_x = 1 + cw;
} else {
/* ':' will not be printed */
skip--;
width++;
idx = u_skip_chars(str, &skip, true);
gbuf_set(&print_buffer, ' ', -skip);
width += skip;
cmdline_cursor_x = win_w - 1 - context_w;
}
/* allow printing in ' ' space we kept at end, cursor isn't always there */
width++;
gbuf_add_ustr(&print_buffer, str + idx, &width);
dump_buffer(print_buffer.buffer);
gbuf_clear(&print_buffer);
clrtoeol();
}
static void set_title(const char *title)
{
if (!set_term_title)
return;
if (t_ts) {
printf("%s%s%s", tgoto(t_ts, 0, 0), title, t_fs);
fflush(stdout);
}
}
static void do_update_titleline(void)
{
if (!ui_initialized)
return;
bkgdset(pairs[CURSED_TITLELINE]);
if (player_info.ti) {
int use_alt_format = 0;
char *wtitle;
fill_track_fopts_track_info(player_info.ti);
use_alt_format = !track_info_has_tag(player_info.ti);
if (is_http_url(player_info.ti->filename)) {
const char *title = get_stream_title();
if (title != NULL) {
free(title_buf);
title_buf = to_utf8(title, icecast_default_charset);
/*
* StreamTitle overrides radio station name
*/
use_alt_format = 0;
fopt_set_str(&track_fopts[TF_TITLE], title_buf);
}
}
if (use_alt_format && *current_alt_format) {
format_print(&print_buffer, win_w, current_alt_format, track_fopts);
} else {
format_print(&print_buffer, win_w, current_format, track_fopts);
}
dump_print_buffer(LINES - 3, 0);
/* set window title */
if (use_alt_format && *window_title_alt_format) {
format_print(&print_buffer, 0,
window_title_alt_format, track_fopts);
} else {
format_print(&print_buffer, 0,
window_title_format, track_fopts);
}
if (using_utf8) {
wtitle = print_buffer.buffer;
} else {
utf8_decode(print_buffer.buffer);
wtitle = conv_buffer;
}
set_title(wtitle);
gbuf_clear(&print_buffer);
} else {
move(LINES - 3, 0);
clrtoeol();
set_title("cmus " VERSION);
}
}
static void post_update(void)
{
/* refresh makes cursor visible at least for urxvt */
if (input_mode == COMMAND_MODE || input_mode == SEARCH_MODE) {
move(LINES - 1, cmdline_cursor_x);
refresh();
curs_set(1);
} else {
if (cursor_x >= 0) {
move(cursor_y, cursor_x);
} else {
move(LINES - 1, 0);
}
refresh();
/* visible cursor is useful for screen readers */
if (show_cursor) {
curs_set(1);
} else {
curs_set(0);
}
}
}
static const char *get_stream_title_locked(void)
{
static char stream_title[255 * 16 + 1];
char *ptr, *title;
ptr = strstr(player_metadata, "StreamTitle='");
if (ptr == NULL)
return NULL;
ptr += 13;
title = ptr;
while (*ptr) {
if (*ptr == '\'' && *(ptr + 1) == ';') {
memcpy(stream_title, title, ptr - title);
stream_title[ptr - title] = 0;
return stream_title;
}
ptr++;
}
return NULL;
}
const char *get_stream_title(void)
{
player_metadata_lock();
const char *rv = get_stream_title_locked();
player_metadata_unlock();
return rv;
}
void update_titleline(void)
{
curs_set(0);
do_update_titleline();
post_update();
}
void update_full(void)
{
if (!ui_initialized)
return;
curs_set(0);
do_update_view(1);
do_update_titleline();
do_update_statusline();
do_update_commandline();
post_update();
}
static void update_commandline(void)
{
curs_set(0);
do_update_commandline();
post_update();
}
void update_statusline(void)
{
if (!ui_initialized)
return;
curs_set(0);
do_update_statusline();
post_update();
}
void update_filterline(void)
{
if (cur_view != TREE_VIEW && cur_view != SORTED_VIEW)
return;
if (lib_live_filter) {
static GBUF(buf);
gbuf_clear(&buf);
int w;
bkgdset(pairs[CURSED_STATUSLINE]);
gbuf_addf(&buf, "filtered: %s", lib_live_filter);
w = clamp(u_str_width(buf.buffer) + 2, win_w/4, win_w/2);
sprint(LINES-4, win_w-w, buf.buffer, w);
}
}
void info_msg(const char *format, ...)
{
va_list ap;
gbuf_clear(&error_buf);
va_start(ap, format);
gbuf_vaddf(&error_buf, format, ap);
va_end(ap);
if (client_fd != -1) {
write_all(client_fd, error_buf.buffer, error_buf.len);
write_all(client_fd, "\n", 1);
}
msg_is_error = 0;
update_commandline();
}
void error_msg(const char *format, ...)
{
va_list ap;
gbuf_clear(&error_buf);
gbuf_add_str(&error_buf, "Error: ");
va_start(ap, format);
gbuf_vaddf(&error_buf, format, ap);
va_end(ap);
d_print("%s\n", error_buf.buffer);
if (client_fd != -1) {
write_all(client_fd, error_buf.buffer, error_buf.len);
write_all(client_fd, "\n", 1);
}
msg_is_error = 1;
error_count++;
if (ui_initialized) {
error_time = time(NULL);
update_commandline();
} else {
warn("%s\n", error_buf.buffer);
gbuf_clear(&error_buf);
}
}
enum ui_query_answer yes_no_query(const char *format, ...)
{
static GBUF(buffer);
gbuf_clear(&buffer);
va_list ap;
int ret = 0;
va_start(ap, format);
gbuf_vaddf(&buffer, format, ap);
va_end(ap);
move(LINES - 1, 0);
bkgdset(pairs[CURSED_INFO]);
/* no need to convert buffer.
* it is always encoded in the right charset (assuming filenames are
* encoded in same charset as LC_CTYPE).
*/
addstr(buffer.buffer);
clrtoeol();
refresh();
while (1) {
int ch = getch();
if (ch == ERR || ch == 0) {
if (!cmus_running) {
ret = UI_QUERY_ANSWER_ERROR;
break;
}
continue;
}
if (ch == 'y') {
ret = UI_QUERY_ANSWER_YES;
break;
} else {
ret = UI_QUERY_ANSWER_NO;
break;
}
}
update_commandline();
return ret;
}
void search_not_found(void)
{
const char *what = "Track";
if (search_restricted) {
switch (cur_view) {
case TREE_VIEW:
what = "Artist/album";
break;
case SORTED_VIEW:
case PLAYLIST_VIEW:
case QUEUE_VIEW:
what = "Title";
break;
case BROWSER_VIEW:
what = "File/Directory";
break;
case FILTERS_VIEW:
what = "Filter";
break;
case HELP_VIEW:
what = "Binding/command/option";
break;
}
} else {
switch (cur_view) {
case TREE_VIEW:
case SORTED_VIEW:
case PLAYLIST_VIEW:
case QUEUE_VIEW:
what = "Track";
break;
case BROWSER_VIEW:
what = "File/Directory";
break;
case FILTERS_VIEW:
what = "Filter";
break;
case HELP_VIEW:
what = "Binding/command/option";
break;
}
}
info_msg("%s not found: %s", what, search_str ? search_str : "");
}
void set_client_fd(int fd)
{
client_fd = fd;
}
int get_client_fd(void)
{
return client_fd;
}
void set_view(int view)
{
if (view == cur_view)
return;
prev_view = cur_view;
cur_view = view;
switch (cur_view) {
case TREE_VIEW:
searchable = tree_searchable;
break;
case SORTED_VIEW:
searchable = lib_editable.shared->searchable;
break;
case PLAYLIST_VIEW:
searchable = pl_get_searchable();
break;
case QUEUE_VIEW:
searchable = pq_editable.shared->searchable;
break;
case BROWSER_VIEW:
searchable = browser_searchable;
break;
case FILTERS_VIEW:
searchable = filters_searchable;
break;
case HELP_VIEW:
searchable = help_searchable;
update_help_window();
break;
}
curs_set(0);
do_update_view(1);
post_update();
}
void enter_command_mode(void)
{
gbuf_clear(&error_buf);
error_time = 0;
input_mode = COMMAND_MODE;
update_commandline();
}
void enter_search_mode(void)
{
gbuf_clear(&error_buf);
error_time = 0;
input_mode = SEARCH_MODE;
search_direction = SEARCH_FORWARD;
update_commandline();
}
void enter_search_backward_mode(void)
{
gbuf_clear(&error_buf);
error_time = 0;
input_mode = SEARCH_MODE;
search_direction = SEARCH_BACKWARD;
update_commandline();
}
void update_colors(void)
{
int i;
if (!ui_initialized)
return;
for (i = 0; i < NR_CURSED; i++) {
int bg = colors[cursed_to_bg_idx[i]];
int fg = colors[cursed_to_fg_idx[i]];
int attr = attrs[cursed_to_attr_idx[i]];
int pair = i + 1;
if (fg >= 8 && fg <= 15) {
/* fg colors 8..15 are special (0..7 + bold) */
init_pair(pair, fg & 7, bg);
pairs[i] = COLOR_PAIR(pair) | (fg & BRIGHT ? A_BOLD : 0) | attr;
} else {
init_pair(pair, fg, bg);
pairs[i] = COLOR_PAIR(pair) | attr;
}
}
}
static void clear_error(void)
{
time_t t = time(NULL);
/* prevent accidental clearing of error messages */
if (t - error_time < 2)
return;
if (error_buf.len != 0) {
error_time = 0;
gbuf_clear(&error_buf);
update_commandline();
}
}
/* screen updates }}} */
static int fill_status_program_track_info_args(char **argv, int i, struct track_info *ti)
{
/* returns first free argument index */
const char *stream_title = NULL;
if (player_info.status == PLAYER_STATUS_PLAYING && is_http_url(ti->filename))
stream_title = get_stream_title();
static const char *keys[] = {
"artist", "albumartist", "album", "discnumber", "tracknumber", "title",
"date", "musicbrainz_trackid", NULL
};
int j;
if (is_http_url(ti->filename)) {
argv[i++] = xstrdup("url");
} else {
argv[i++] = xstrdup("file");
}
argv[i++] = xstrdup(ti->filename);
if (track_info_has_tag(ti)) {
for (j = 0; keys[j]; j++) {
const char *key = keys[j];
const char *val;
if (strcmp(key, "title") == 0 && stream_title)
/*
* StreamTitle overrides radio station name
*/
val = stream_title;
else
val = keyvals_get_val(ti->comments, key);
if (val) {
argv[i++] = xstrdup(key);
argv[i++] = xstrdup(val);
}
}
if (ti->duration > 0) {
char buf[32];
snprintf(buf, sizeof(buf), "%d", ti->duration);
argv[i++] = xstrdup("duration");
argv[i++] = xstrdup(buf);
}
} else if (stream_title) {
argv[i++] = xstrdup("title");
argv[i++] = xstrdup(stream_title);
}
return i;
}
static void spawn_status_program_inner(const char *status_text, struct track_info *ti)
{
if (status_display_program == NULL || status_display_program[0] == 0)
return;
char *argv[32];
int i = 0;
argv[i++] = xstrdup(status_display_program);
argv[i++] = xstrdup("status");
argv[i++] = xstrdup(status_text);
if (ti) {
i = fill_status_program_track_info_args(argv, i, ti);
}
argv[i++] = NULL;
if (spawn(argv, NULL, 0) == -1)
error_msg("couldn't run `%s': %s", status_display_program, strerror(errno));
for (i = 0; argv[i]; i++)
free(argv[i]);
}
static void spawn_status_program(void)
{
spawn_status_program_inner(player_status_names[player_info.status], player_info.ti);
}
static volatile sig_atomic_t ctrl_c_pressed = 0;
static void sig_int(int sig)
{
ctrl_c_pressed = 1;
}
static void sig_shutdown(int sig)
{
d_print("sig_shutdown %d\n", sig);
cmus_running = 0;
}
static volatile sig_atomic_t needs_to_resize = 0;
static void sig_winch(int sig)
{
needs_to_resize = 1;
}
void update_size(void) {
needs_to_resize = 1;
}
static int get_window_size(int *lines, int *columns)
{
struct winsize ws;
if (ioctl(0, TIOCGWINSZ, &ws) == -1)
return -1;
*columns = ws.ws_col;
*lines = ws.ws_row;
return 0;
}
static void resize_tree_view(int w, int h)
{
tree_win_w = w * ((float)tree_width_percent / 100.0f);
if (tree_width_max && tree_win_w > tree_width_max)
tree_win_w = tree_width_max;
/* at least one character of formatted text and one space either side */
if (tree_win_w < 3)
tree_win_w = 3;
track_win_w = w - tree_win_w - 1;
if (track_win_w < 3)
track_win_w = 3;
tree_win_x = 0;
track_win_x = tree_win_w + 1;
h--;
window_set_nr_rows(lib_tree_win, h);
window_set_nr_rows(lib_track_win, h);
}
static void update_window_size(void)
{
int w, h;
int columns, lines;
if (get_window_size(&lines, &columns) == 0) {
needs_to_resize = 0;
#if HAVE_RESIZETERM
resizeterm(lines, columns);
#endif
w = COLS;
h = LINES - 3;
if (w < 4)
w = 4;
if (h < 2)
h = 2;
win_w = w;
resize_tree_view(w, h);
window_set_nr_rows(lib_editable.shared->win, h - 1);
pl_set_nr_rows(h - 1);
window_set_nr_rows(pq_editable.shared->win, h - 1);
window_set_nr_rows(filters_win, h - 1);
window_set_nr_rows(help_win, h - 1);
window_set_nr_rows(browser_win, h - 1);
}
clearok(curscr, TRUE);
refresh();
}
static void update(void)
{
static bool first_update = true;
int needs_view_update = 0;
int needs_title_update = 0;
int needs_status_update = 0;
int needs_command_update = 0;
int needs_spawn = 0;
if (first_update) {
needs_title_update = 1;
needs_command_update = 1;
first_update = false;
}
if (needs_to_resize) {
update_window_size();
needs_title_update = 1;
needs_status_update = 1;
needs_command_update = 1;
}
if (player_info.status_changed)
mpris_playback_status_changed();
if (player_info.file_changed || player_info.metadata_changed)
mpris_metadata_changed();
needs_spawn = player_info.status_changed || player_info.file_changed ||
player_info.metadata_changed;
if (player_info.file_changed) {
needs_title_update = 1;
needs_status_update = 1;
}
if (player_info.metadata_changed)
needs_title_update = 1;
if (player_info.position_changed || player_info.status_changed)
needs_status_update = 1;
switch (cur_view) {
case TREE_VIEW:
needs_view_update += lib_tree_win->changed || lib_track_win->changed;
break;
case SORTED_VIEW:
needs_view_update += lib_editable.shared->win->changed;
break;
case PLAYLIST_VIEW:
needs_view_update += pl_needs_redraw();
break;
case QUEUE_VIEW:
needs_view_update += pq_editable.shared->win->changed;
break;
case BROWSER_VIEW:
needs_view_update += browser_win->changed;
break;
case FILTERS_VIEW:
needs_view_update += filters_win->changed;
break;
case HELP_VIEW:
needs_view_update += help_win->changed;
break;
}
/* total time changed? */
if (cmus_queue_active()) {
needs_status_update += queue_needs_redraw();
} else if (play_library) {
needs_status_update += lib_editable.shared->win->changed;
lib_editable.shared->win->changed = 0;
} else {
needs_status_update += pl_needs_redraw();
}
if (needs_spawn)
spawn_status_program();
if (needs_view_update || needs_title_update || needs_status_update || needs_command_update) {
curs_set(0);
if (needs_view_update)
do_update_view(0);
if (needs_title_update)
do_update_titleline();
if (needs_status_update)
do_update_statusline();
if (needs_command_update)
do_update_commandline();
post_update();
}
/* Reset changed flags */
queue_post_update();
}
static void handle_ch(uchar ch)
{
clear_error();
if (input_mode == NORMAL_MODE) {
if (!block_key_paste || !in_bracketed_paste) {
normal_mode_ch(ch);
}
} else if (input_mode == COMMAND_MODE) {
command_mode_ch(ch);
update_commandline();
} else if (input_mode == SEARCH_MODE) {
search_mode_ch(ch);
update_commandline();
}
}
static void handle_csi(void) {
// after ESC[ until 0x40-0x7E (@AZ[\]^_`az{|}~)
// https://en.wikipedia.org/wiki/ANSI_escape_code#CSI_(Control_Sequence_Introducer)_sequences
// https://www.ecma-international.org/wp-content/uploads/ECMA-48_5th_edition_june_1991.pdf
int c;
int buf[16]; // buffer a reasonable length
size_t buf_n = 0;
int overflow = 0;
while (1) {
c = getch();
if (c == ERR || c == 0) {
return;
}
if (buf_n < sizeof(buf)/sizeof(*buf)) {
buf[buf_n++] = c;
} else {
overflow = 1;
}
if (c >= 0x40 && c <= 0x7E) {
break;
}
}
if (overflow) {
return;
}
if (buf_n == 4) {
// bracketed paste
// https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-Bracketed-Paste-Mode
if (buf[0] == '2' && buf[1] == '0' && (buf[2] == '0' || buf[2] == '1') && buf[3] == '~') {
in_bracketed_paste = buf[2] == '0';
return;
}
}
}
static void handle_escape(int c)
{
clear_error();
if (input_mode == NORMAL_MODE) {
normal_mode_ch(c + 128);
} else if (input_mode == COMMAND_MODE) {
command_mode_escape(c);
update_commandline();
} else if (input_mode == SEARCH_MODE) {
search_mode_escape(c);
update_commandline();
}
}
static void handle_key(int key)
{
clear_error();
if (input_mode == NORMAL_MODE) {
if (!block_key_paste || !in_bracketed_paste) {
normal_mode_key(key);
}
} else if (input_mode == COMMAND_MODE) {
command_mode_key(key);
update_commandline();
} else if (input_mode == SEARCH_MODE) {
search_mode_key(key);
update_commandline();
}
}
static void handle_mouse(MEVENT *event)
{
#if NCURSES_MOUSE_VERSION <= 1
static int last_mevent;
if ((last_mevent & BUTTON1_PRESSED) && (event->bstate & REPORT_MOUSE_POSITION))
event->bstate = BUTTON1_RELEASED;
last_mevent = event->bstate;
#endif
clear_error();
if (input_mode == NORMAL_MODE) {
normal_mode_mouse(event);
} else if (input_mode == COMMAND_MODE) {
command_mode_mouse(event);
update_commandline();
} else if (input_mode == SEARCH_MODE) {
search_mode_mouse(event);
update_commandline();
}
}
static void u_getch(void)
{
int key;
int bit = 7;
int mask = (1 << 7);
uchar u, ch;
key = getch();
if (key == ERR || key == 0)
return;
if (key == KEY_MOUSE) {
MEVENT event;
if (getmouse(&event) == OK)
handle_mouse(&event);
return;
}
if (key > 255) {
handle_key(key);
return;
}
/* escape sequence */
if (key == 0x1B) {
cbreak();
int e_key = getch();
halfdelay(5);
if (e_key != ERR) {
if (e_key == '[')
handle_csi();
else if (e_key != 0)
handle_escape(e_key);
return;
}
}
ch = (unsigned char)key;
while (bit > 0 && ch & mask) {
mask >>= 1;
bit--;
}
if (bit == 7) {
/* ascii */
u = ch;
} else if (using_utf8) {
int count;
u = ch & ((1 << bit) - 1);
count = 6 - bit;
while (count) {
key = getch();
if (key == ERR || key == 0)
return;
ch = (unsigned char)key;
u = (u << 6) | (ch & 63);
count--;
}
} else
u = ch | U_INVALID_MASK;
handle_ch(u);
}
static void main_loop(void)
{
int rc, fd_high;
#define SELECT_ADD_FD(fd) do {\
FD_SET((fd), &set); \
if ((fd) > fd_high) \
fd_high = (fd); \
} while(0)
fd_high = server_socket;
while (cmus_running) {
fd_set set;
struct timeval tv;
int poll_mixer = 0;
int i;
int nr_fds_vol = 0, fds_vol[NR_MIXER_FDS];
int nr_fds_out = 0, fds_out[NR_MIXER_FDS];
struct list_head *item;
struct client *client;
player_info_snapshot();
update();
/* Timeout must be so small that screen updates seem instant.
* Only affects changes done in other threads (player).
*
* Too small timeout makes window updates too fast (wastes CPU).
*
* Too large timeout makes status line (position) updates too slow.
* The timeout is accuracy of player position.
*/
tv.tv_sec = 0;
tv.tv_usec = 0;
if (player_info.status == PLAYER_STATUS_PLAYING) {
// player position updates need to be fast
tv.tv_usec = 100e3;
}
FD_ZERO(&set);
SELECT_ADD_FD(0);
SELECT_ADD_FD(job_fd);
SELECT_ADD_FD(cmus_next_track_request_fd);
SELECT_ADD_FD(server_socket);
if (mpris_fd != -1)
SELECT_ADD_FD(mpris_fd);
list_for_each_entry(client, &client_head, node) {
SELECT_ADD_FD(client->fd);
}
if (!soft_vol) {
nr_fds_vol = mixer_get_fds(MIXER_FDS_VOLUME, fds_vol);
if (nr_fds_vol <= 0) {
poll_mixer = 1;
if (!tv.tv_usec)
tv.tv_usec = 500e3;
}
for (i = 0; i < nr_fds_vol; i++) {
BUG_ON(fds_vol[i] <= 0);
SELECT_ADD_FD(fds_vol[i]);
}
}
nr_fds_out = mixer_get_fds(MIXER_FDS_OUTPUT, fds_out);
for (i = 0; i < nr_fds_out; i++) {
BUG_ON(fds_out[i] <= 0);
SELECT_ADD_FD(fds_out[i]);
}
rc = select(fd_high + 1, &set, NULL, NULL, tv.tv_usec ? &tv : NULL);
if (poll_mixer) {
int ol = volume_l;
int or = volume_r;
mixer_read_volume();
if (ol != volume_l || or != volume_r) {
mpris_volume_changed();
update_statusline();
}
}
if (rc <= 0) {
if (ctrl_c_pressed) {
handle_ch(0x03);
ctrl_c_pressed = 0;
}
continue;
}
for (i = 0; i < nr_fds_vol; i++) {
if (FD_ISSET(fds_vol[i], &set)) {
d_print("vol changed\n");
mixer_read_volume();
mpris_volume_changed();
update_statusline();
}
}
for (i = 0; i < nr_fds_out; i++) {
if (FD_ISSET(fds_out[i], &set)) {
d_print("out changed\n");
if (pause_on_output_change) {
player_pause_playback();
update_statusline();
}
clear_pipe(fds_out[i], -1);
}
}
if (FD_ISSET(server_socket, &set))
server_accept();
// server_serve() can remove client from the list
item = client_head.next;
while (item != &client_head) {
struct list_head *next = item->next;
client = container_of(item, struct client, node);
if (FD_ISSET(client->fd, &set))
server_serve(client);
item = next;
}
if (FD_ISSET(0, &set))
u_getch();
if (mpris_fd != -1 && FD_ISSET(mpris_fd, &set))
mpris_process();
if (FD_ISSET(job_fd, &set))
job_handle();
if (FD_ISSET(cmus_next_track_request_fd, &set))
cmus_provide_next_track();
}
}
static void init_curses(void)
{
struct sigaction act;
char *ptr, *term;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
act.sa_handler = sig_int;
sigaction(SIGINT, &act, NULL);
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
act.sa_handler = sig_shutdown;
sigaction(SIGHUP, &act, NULL);
sigaction(SIGTERM, &act, NULL);
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
act.sa_handler = SIG_IGN;
sigaction(SIGPIPE, &act, NULL);
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
act.sa_handler = sig_winch;
sigaction(SIGWINCH, &act, NULL);
initscr();
nodelay(stdscr, TRUE);
keypad(stdscr, TRUE);
halfdelay(5);
noecho();
if (has_colors()) {
#if HAVE_USE_DEFAULT_COLORS
start_color();
use_default_colors();
#endif
}
d_print("Number of supported colors: %d\n", COLORS);
ui_initialized = 1;
/* this was disabled while initializing because it needs to be
* called only once after all colors have been set
*/
update_colors();
ptr = tcap_buffer;
t_ts = tgetstr("ts", &ptr);
t_fs = tgetstr("fs", &ptr);
d_print("ts: %d fs: %d\n", !!t_ts, !!t_fs);
if (!t_fs)
t_ts = NULL;
term = getenv("TERM");
if (!t_ts && term) {
/*
* Eterm: Eterm
* aterm: rxvt
* mlterm: xterm
* terminal (xfce): xterm
* urxvt: rxvt-unicode
* xterm: xterm, xterm-{,16,88,256}color
*/
if (!strcmp(term, "screen")) {
t_ts = "\033_";
t_fs = "\033\\";
} else if (!strncmp(term, "xterm", 5) ||
!strncmp(term, "rxvt", 4) ||
!strcmp(term, "Eterm")) {
/* \033]1; change icon
* \033]2; change title
* \033]0; change both
*/
t_ts = "\033]0;";
t_fs = "\007";
}
}
update_mouse();
if (!getenv("ESCDELAY")) {
set_escdelay(default_esc_delay);
}
update_window_size();
}
static void init_all(void)
{
main_thread = pthread_self();
cmus_track_request_init();
server_init(server_address);
/* does not select output plugin */
player_init();
/* plugins have been loaded so we know what plugin options are available */
options_add();
/* cache the normalized env vars for pl_env */
pl_env_init();
lib_init();
searchable = tree_searchable;
cmus_init();
pl_init();
browser_init();
filters_init();
help_init();
cmdline_init();
commands_init();
search_mode_init();
/* almost everything must be initialized now */
options_load();
pl_init_options();
if (mpris)
mpris_init();
/* finally we can set the output plugin */
player_set_op(output_plugin);
if (!soft_vol || pause_on_output_change)
mixer_open();
lib_autosave_filename = xstrjoin(cmus_config_dir, "/lib.pl");
play_queue_autosave_filename = xstrjoin(cmus_config_dir, "/queue.pl");
lib_filename = xstrdup(lib_autosave_filename);
if (error_count) {
char buf[16];
char *ret;
warn("Press <enter> to continue.");
ret = fgets(buf, sizeof(buf), stdin);
BUG_ON(ret == NULL);
}
help_add_all_unbound();
init_curses();
// enable bracketed paste (will be ignored if not supported)
printf("\033[?2004h");
fflush(stdout);
if (resume_cmus) {
resume_load();
cmus_add(play_queue_append, play_queue_autosave_filename,
FILE_TYPE_PL, JOB_TYPE_QUEUE, 0, NULL);
} else {
set_view(start_view);
}
cmus_add(lib_add_track, lib_autosave_filename, FILE_TYPE_PL,
JOB_TYPE_LIB, 0, NULL);
worker_start();
}
static void exit_all(void)
{
endwin();
// disable bracketed paste
printf("\033[?2004l");
fflush(stdout);
if (resume_cmus)
resume_exit();
options_exit();
server_exit();
cmus_exit();
if (resume_cmus)
cmus_save(play_queue_for_each, play_queue_autosave_filename,
NULL);
cmus_save(lib_for_each, lib_autosave_filename, NULL);
pl_exit();
player_exit();
op_exit_plugins();
commands_exit();
search_mode_exit();
filters_exit();
help_exit();
browser_exit();
mpris_free();
}
enum {
FLAG_LISTEN,
FLAG_PLUGINS,
FLAG_SHOW_CURSOR,
FLAG_HELP,
FLAG_VERSION,
NR_FLAGS
};
static struct option options[NR_FLAGS + 1] = {
{ 0, "listen", 1 },
{ 0, "plugins", 0 },
{ 0, "show-cursor", 0 },
{ 0, "help", 0 },
{ 0, "version", 0 },
{ 0, NULL, 0 }
};
static const char *usage =
"Usage: %s [OPTION]...\n"
"Curses based music player.\n"
"\n"
" --listen ADDR listen on ADDR instead of $CMUS_SOCKET or $XDG_RUNTIME_DIR/cmus-socket\n"
" ADDR is either a UNIX socket or host[:port]\n"
" WARNING: using TCP/IP is insecure!\n"
" --plugins list available plugins and exit\n"
" --show-cursor always visible cursor\n"
" --help display this help and exit\n"
" --version " VERSION "\n"
"\n"
"Use cmus-remote to control cmus from command line.\n"
"Report bugs to <cmus-devel@lists.sourceforge.net>.\n";
int main(int argc, char *argv[])
{
int list_plugins = 0;
program_name = argv[0];
argv++;
while (1) {
int idx;
char *arg;
idx = get_option(&argv, options, &arg);
if (idx < 0)
break;
switch (idx) {
case FLAG_HELP:
printf(usage, program_name);
return 0;
case FLAG_VERSION:
printf("cmus " VERSION
"\nCopyright 2004-2006 Timo Hirvonen"
"\nCopyright 2008-2016 Various Authors\n");
return 0;
case FLAG_PLUGINS:
list_plugins = 1;
break;
case FLAG_LISTEN:
server_address = xstrdup(arg);
break;
case FLAG_SHOW_CURSOR:
show_cursor = 1;
break;
}
}
setlocale(LC_CTYPE, "");
setlocale(LC_COLLATE, "");
charset = getenv("CMUS_CHARSET");
if (!charset || !charset[0]) {
#ifdef CODESET
charset = nl_langinfo(CODESET);
#else
charset = "ISO-8859-1";
#endif
}
if (strcmp(charset, "UTF-8") == 0)
using_utf8 = 1;
misc_init();
if (server_address == NULL)
server_address = xstrdup(cmus_socket_path);
debug_init();
d_print("charset = '%s'\n", charset);
ip_load_plugins();
op_load_plugins();
if (list_plugins) {
ip_dump_plugins();
op_dump_plugins();
return 0;
}
init_all();
main_loop();
exit_all();
spawn_status_program_inner("exiting", NULL);
return 0;
}