2645 lines
59 KiB
C
2645 lines
59 KiB
C
/*
|
||
* 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 (@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 <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;
|
||
}
|