/* * 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 . */ #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 #include #include #include #include #include #include #include #include #include #include #ifdef HAVE_ICONV #include #endif #include #include #include #include #if defined(__sun__) || defined(__CYGWIN__) /* TIOCGWINSZ */ #include #include #else #include #endif /* defined in 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 ) 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 (@A–Z[\]^_`a–z{|}~) // 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 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 .\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; }