1 /* ncmpc (Ncurses MPD Client)
2 * (c) 2004-2009 The Music Player Daemon Project
3 * Project homepage: http://musicpd.org
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
15 * You should have received a copy of the GNU General Public License along
16 * with this program; if not, write to the Free Software Foundation, Inc.,
17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
18 */
20 #include "config.h"
21 #include "i18n.h"
22 #include "charset.h"
23 #include "options.h"
24 #include "mpdclient.h"
25 #include "utils.h"
26 #include "strfsong.h"
27 #include "wreadln.h"
28 #include "command.h"
29 #include "colors.h"
30 #include "screen.h"
31 #include "screen_utils.h"
32 #include "screen_play.h"
34 #ifndef NCMPC_MINI
35 #include "hscroll.h"
36 #endif
38 #include <ctype.h>
39 #include <stdlib.h>
40 #include <string.h>
41 #include <time.h>
42 #include <glib.h>
44 #define MAX_SONG_LENGTH 512
46 #ifndef NCMPC_MINI
47 typedef struct
48 {
49 GList **list;
50 GList **dir_list;
51 mpdclient_t *c;
52 } completion_callback_data_t;
53 #endif
55 static struct mpdclient_playlist *playlist;
56 static int current_song_id = -1;
57 static list_window_t *lw = NULL;
58 static guint timer_hide_cursor_id;
60 static void
61 play_paint(void);
63 static void
64 playlist_repaint(void)
65 {
66 play_paint();
67 wrefresh(lw->w);
68 }
70 static void
71 playlist_repaint_if_active(void)
72 {
73 if (screen_is_visible(&screen_playlist))
74 playlist_repaint();
75 }
77 static void
78 playlist_changed_callback(mpdclient_t *c, int event, gpointer data)
79 {
80 switch (event) {
81 case PLAYLIST_EVENT_DELETE:
82 break;
83 case PLAYLIST_EVENT_MOVE:
84 if(lw->visual_selection < 0)
85 {
86 lw->selected = *((int *) data);
87 if (lw->selected < lw->start)
88 lw->start--;
89 }
90 break;
91 default:
92 break;
93 }
95 list_window_check_selected(lw, c->playlist.list->len);
96 playlist_repaint_if_active();
97 }
99 static const char *
100 list_callback(unsigned idx, bool *highlight, G_GNUC_UNUSED void *data)
101 {
102 static char songname[MAX_SONG_LENGTH];
103 #ifndef NCMPC_MINI
104 static scroll_state_t st;
105 #endif
106 mpd_Song *song;
108 if (playlist == NULL || idx >= playlist_length(playlist))
109 return NULL;
111 song = playlist_get(playlist, idx);
112 if (song->id == current_song_id)
113 *highlight = true;
115 strfsong(songname, MAX_SONG_LENGTH, options.list_format, song);
117 #ifndef NCMPC_MINI
118 if (options.scroll && (unsigned)song->pos == lw->selected &&
119 utf8_width(songname) > (unsigned)COLS) {
120 static unsigned current_song;
121 char *tmp;
123 if (current_song != lw->selected) {
124 st.offset = 0;
125 current_song = lw->selected;
126 }
128 tmp = strscroll(songname, options.scroll_sep,
129 MAX_SONG_LENGTH, &st);
130 g_strlcpy(songname, tmp, MAX_SONG_LENGTH);
131 g_free(tmp);
132 } else if ((unsigned)song->pos == lw->selected)
133 st.offset = 0;
134 #endif
136 return songname;
137 }
139 static void
140 center_playing_item(mpdclient_t *c)
141 {
142 unsigned length = c->playlist.list->len;
143 unsigned offset = lw->selected - lw->start;
144 int idx;
146 if (!c->song || length < lw->rows ||
147 c->status == NULL || IS_STOPPED(c->status->state))
148 return;
150 /* try to center the song that are playing */
151 idx = playlist_get_index(c, c->song);
152 if (idx < 0)
153 return;
155 list_window_center(lw, length, idx);
157 /* make sure the cursor is in the window */
158 lw->selected = lw->start+offset;
159 lw->selected_start = lw->selected;
160 lw->selected_end = lw->selected;
161 list_window_check_selected(lw, length);
162 }
164 #ifndef NCMPC_MINI
165 static void
166 save_pre_completion_cb(GCompletion *gcmp, G_GNUC_UNUSED gchar *line,
167 void *data)
168 {
169 completion_callback_data_t *tmp = (completion_callback_data_t *)data;
170 GList **list = tmp->list;
171 mpdclient_t *c = tmp->c;
173 if( *list == NULL ) {
174 /* create completion list */
175 *list = gcmp_list_from_path(c, "", NULL, GCMP_TYPE_PLAYLIST);
176 g_completion_add_items(gcmp, *list);
177 }
178 }
180 static void
181 save_post_completion_cb(G_GNUC_UNUSED GCompletion *gcmp,
182 G_GNUC_UNUSED gchar *line, GList *items,
183 G_GNUC_UNUSED void *data)
184 {
185 if (g_list_length(items) >= 1)
186 screen_display_completion_list(items);
187 }
188 #endif
190 #ifndef NCMPC_MINI
191 /**
192 * Wrapper for strncmp(). We are not allowed to pass &strncmp to
193 * g_completion_set_compare(), because strncmp() takes size_t where
194 * g_completion_set_compare passes a gsize value.
195 */
196 static gint
197 completion_strncmp(const gchar *s1, const gchar *s2, gsize n)
198 {
199 return strncmp(s1, s2, n);
200 }
201 #endif
203 int
204 playlist_save(mpdclient_t *c, char *name, char *defaultname)
205 {
206 gchar *filename, *filename_utf8;
207 gint error;
208 #ifndef NCMPC_MINI
209 GCompletion *gcmp;
210 GList *list = NULL;
211 completion_callback_data_t data;
212 #endif
214 #ifdef NCMPC_MINI
215 (void)defaultname;
216 #endif
218 #ifndef NCMPC_MINI
219 if (name == NULL) {
220 /* initialize completion support */
221 gcmp = g_completion_new(NULL);
222 g_completion_set_compare(gcmp, completion_strncmp);
223 data.list = &list;
224 data.dir_list = NULL;
225 data.c = c;
226 wrln_completion_callback_data = &data;
227 wrln_pre_completion_callback = save_pre_completion_cb;
228 wrln_post_completion_callback = save_post_completion_cb;
231 /* query the user for a filename */
232 filename = screen_readln(screen.status_window.w,
233 _("Save playlist as: "),
234 defaultname,
235 NULL,
236 gcmp);
238 /* destroy completion support */
239 wrln_completion_callback_data = NULL;
240 wrln_pre_completion_callback = NULL;
241 wrln_post_completion_callback = NULL;
242 g_completion_free(gcmp);
243 list = string_list_free(list);
244 if( filename )
245 filename=g_strstrip(filename);
246 } else
247 #endif
248 filename=g_strdup(name);
250 if (filename == NULL)
251 return -1;
253 /* send save command to mpd */
255 filename_utf8 = locale_to_utf8(filename);
256 error = mpdclient_cmd_save_playlist(c, filename_utf8);
257 g_free(filename_utf8);
259 if (error) {
260 gint code = GET_ACK_ERROR_CODE(error);
262 if (code == MPD_ACK_ERROR_EXIST) {
263 char *buf;
264 int key;
266 buf = g_strdup_printf(_("Replace %s [%s/%s] ? "),
267 filename, YES, NO);
268 key = tolower(screen_getch(screen.status_window.w,
269 buf));
270 g_free(buf);
272 if (key == YES[0]) {
273 filename_utf8 = locale_to_utf8(filename);
274 error = mpdclient_cmd_delete_playlist(c, filename_utf8);
275 g_free(filename_utf8);
277 if (error) {
278 g_free(filename);
279 return -1;
280 }
282 error = playlist_save(c, filename, NULL);
283 g_free(filename);
284 return error;
285 }
287 screen_status_printf(_("Aborted"));
288 }
290 g_free(filename);
291 return -1;
292 }
294 /* success */
295 screen_status_printf(_("Saved %s"), filename);
296 g_free(filename);
297 return 0;
298 }
300 #ifndef NCMPC_MINI
301 static void add_dir(GCompletion *gcmp, gchar *dir, GList **dir_list,
302 GList **list, mpdclient_t *c)
303 {
304 g_completion_remove_items(gcmp, *list);
305 *list = string_list_remove(*list, dir);
306 *list = gcmp_list_from_path(c, dir, *list, GCMP_TYPE_RFILE);
307 g_completion_add_items(gcmp, *list);
308 *dir_list = g_list_append(*dir_list, g_strdup(dir));
309 }
311 static void add_pre_completion_cb(GCompletion *gcmp, gchar *line, void *data)
312 {
313 completion_callback_data_t *tmp = (completion_callback_data_t *)data;
314 GList **dir_list = tmp->dir_list;
315 GList **list = tmp->list;
316 mpdclient_t *c = tmp->c;
318 if (*list == NULL) {
319 /* create initial list */
320 *list = gcmp_list_from_path(c, "", NULL, GCMP_TYPE_RFILE);
321 g_completion_add_items(gcmp, *list);
322 } else if (line && line[0] && line[strlen(line)-1]=='/' &&
323 string_list_find(*dir_list, line) == NULL) {
324 /* add directory content to list */
325 add_dir(gcmp, line, dir_list, list, c);
326 }
327 }
329 static void add_post_completion_cb(GCompletion *gcmp, gchar *line,
330 GList *items, void *data)
331 {
332 completion_callback_data_t *tmp = (completion_callback_data_t *)data;
333 GList **dir_list = tmp->dir_list;
334 GList **list = tmp->list;
335 mpdclient_t *c = tmp->c;
337 if (g_list_length(items) >= 1)
338 screen_display_completion_list(items);
340 if (line && line[0] && line[strlen(line) - 1] == '/' &&
341 string_list_find(*dir_list, line) == NULL) {
342 /* add directory content to list */
343 add_dir(gcmp, line, dir_list, list, c);
344 }
345 }
346 #endif
348 static int
349 handle_add_to_playlist(mpdclient_t *c)
350 {
351 gchar *path;
352 #ifndef NCMPC_MINI
353 GCompletion *gcmp;
354 GList *list = NULL;
355 GList *dir_list = NULL;
356 completion_callback_data_t data;
358 /* initialize completion support */
359 gcmp = g_completion_new(NULL);
360 g_completion_set_compare(gcmp, completion_strncmp);
361 data.list = &list;
362 data.dir_list = &dir_list;
363 data.c = c;
364 wrln_completion_callback_data = &data;
365 wrln_pre_completion_callback = add_pre_completion_cb;
366 wrln_post_completion_callback = add_post_completion_cb;
367 #endif
369 /* get path */
370 path = screen_readln(screen.status_window.w,
371 _("Add: "),
372 NULL,
373 NULL,
374 #ifdef NCMPC_MINI
375 NULL
376 #else
377 gcmp
378 #endif
379 );
381 /* destroy completion data */
382 #ifndef NCMPC_MINI
383 wrln_completion_callback_data = NULL;
384 wrln_pre_completion_callback = NULL;
385 wrln_post_completion_callback = NULL;
386 g_completion_free(gcmp);
387 string_list_free(list);
388 string_list_free(dir_list);
389 #endif
391 /* add the path to the playlist */
392 if (path != NULL) {
393 char *path_utf8 = locale_to_utf8(path);
394 mpdclient_cmd_add_path(c, path_utf8);
395 g_free(path_utf8);
396 }
398 g_free(path);
399 return 0;
400 }
402 static void
403 play_init(WINDOW *w, int cols, int rows)
404 {
405 lw = list_window_init(w, cols, rows);
406 }
408 static gboolean
409 timer_hide_cursor(gpointer data)
410 {
411 struct mpdclient *c = data;
413 assert(options.hide_cursor > 0);
414 assert(timer_hide_cursor_id != 0);
416 timer_hide_cursor_id = 0;
418 /* hide the cursor when mpd is playing and the user is inactive */
420 if (c->status != NULL && c->status->state == MPD_STATUS_STATE_PLAY) {
421 lw->hide_cursor = true;
422 playlist_repaint();
423 } else
424 timer_hide_cursor_id = g_timeout_add(options.hide_cursor * 1000,
425 timer_hide_cursor, c);
427 return FALSE;
428 }
430 static void
431 play_open(mpdclient_t *c)
432 {
433 static gboolean install_cb = TRUE;
435 playlist = &c->playlist;
437 assert(timer_hide_cursor_id == 0);
438 if (options.hide_cursor > 0) {
439 lw->hide_cursor = false;
440 timer_hide_cursor_id = g_timeout_add(options.hide_cursor * 1000,
441 timer_hide_cursor, c);
442 }
444 if (install_cb) {
445 mpdclient_install_playlist_callback(c, playlist_changed_callback);
446 install_cb = FALSE;
447 }
448 }
450 static void
451 play_close(void)
452 {
453 if (timer_hide_cursor_id != 0) {
454 g_source_remove(timer_hide_cursor_id);
455 timer_hide_cursor_id = 0;
456 }
457 }
459 static void
460 play_resize(int cols, int rows)
461 {
462 lw->cols = cols;
463 lw->rows = rows;
464 }
467 static void
468 play_exit(void)
469 {
470 list_window_free(lw);
471 }
473 static const char *
474 play_title(char *str, size_t size)
475 {
476 if( strcmp(options.host, "localhost") == 0 )
477 return _("Playlist");
479 g_snprintf(str, size, _("Playlist on %s"), options.host);
480 return str;
481 }
483 static void
484 play_paint(void)
485 {
486 list_window_paint(lw, list_callback, NULL);
487 }
489 static void
490 play_update(mpdclient_t *c)
491 {
492 static int prev_song_id = -1;
494 current_song_id = c->song != NULL && c->status != NULL &&
495 !IS_STOPPED(c->status->state) ? c->song->id : -1;
497 if (current_song_id != prev_song_id) {
498 prev_song_id = current_song_id;
500 /* center the cursor */
501 if (options.auto_center && current_song_id != -1)
502 center_playing_item(c);
504 playlist_repaint();
505 #ifndef NCMPC_MINI
506 } else if (options.scroll) {
507 /* always repaint if horizontal scrolling is
508 enabled */
509 playlist_repaint();
510 #endif
511 }
512 }
514 #ifdef HAVE_GETMOUSE
515 static bool
516 handle_mouse_event(struct mpdclient *c)
517 {
518 int row;
519 unsigned selected;
520 unsigned long bstate;
522 if (screen_get_mouse_event(c, &bstate, &row) ||
523 list_window_mouse(lw, playlist_length(playlist), bstate, row)) {
524 playlist_repaint();
525 return true;
526 }
528 if (bstate & BUTTON1_DOUBLE_CLICKED) {
529 /* stop */
530 screen_cmd(c, CMD_STOP);
531 return true;
532 }
534 selected = lw->start + row;
536 if (bstate & BUTTON1_CLICKED) {
537 /* play */
538 if (lw->start + row < playlist_length(playlist))
539 mpdclient_cmd_play(c, lw->start + row);
540 } else if (bstate & BUTTON3_CLICKED) {
541 /* delete */
542 if (selected == lw->selected)
543 mpdclient_cmd_delete(c, lw->selected);
544 }
546 lw->selected = selected;
547 list_window_check_selected(lw, playlist_length(playlist));
548 playlist_repaint();
550 return true;
551 }
552 #endif
554 static bool
555 play_cmd(mpdclient_t *c, command_t cmd)
556 {
557 lw->hide_cursor = false;
559 if (options.hide_cursor > 0) {
560 if (timer_hide_cursor_id != 0)
561 g_source_remove(timer_hide_cursor_id);
562 timer_hide_cursor_id = g_timeout_add(options.hide_cursor * 1000,
563 timer_hide_cursor, c);
564 }
566 if (list_window_cmd(lw, playlist_length(&c->playlist), cmd)) {
567 playlist_repaint();
568 return true;
569 }
571 switch(cmd) {
572 case CMD_PLAY:
573 mpdclient_cmd_play(c, lw->selected);
574 return true;
575 case CMD_DELETE:
576 {
577 int i = lw->selected_end, start = lw->selected_start;
578 for(; i >= start; --i)
579 mpdclient_cmd_delete(c, i);
581 i++;
582 if(i >= (int)playlist_length(&c->playlist))
583 i--;
584 lw->selected = i;
585 lw->selected_start = i;
586 lw->selected_end = i;
587 lw->visual_selection = false;
589 return true;
590 }
591 case CMD_SAVE_PLAYLIST:
592 playlist_save(c, NULL, NULL);
593 return true;
594 case CMD_ADD:
595 handle_add_to_playlist(c);
596 return true;
597 case CMD_SCREEN_UPDATE:
598 center_playing_item(c);
599 playlist_repaint();
600 return false;
601 case CMD_SHUFFLE:
602 {
603 unsigned i = lw->selected_start + 1;
604 unsigned last_selected = lw->selected;
605 if(!lw->visual_selection)
606 /* No visual selection, shuffle all list. */
607 break;
608 for(; i <= lw->selected_end; ++i)
609 mpdclient_cmd_move(c, i, lw->selected_start + (rand() % ((i - lw->selected_start) + 1)));
611 lw->selected = last_selected;
612 screen_status_printf(_("Shuffled selection!"));
614 return true;
615 }
616 case CMD_LIST_MOVE_UP:
617 if(lw->selected_start == 0)
618 return false;
619 if(lw->visual_selection)
620 {
621 unsigned i = lw->selected_start;
622 unsigned last_selected = lw->selected;
623 for(; i <= lw->selected_end; ++i)
624 mpdclient_cmd_move(c, i, i-1);
625 lw->selected_start--;
626 lw->selected_end--;
627 lw->selected = last_selected - 1;
628 lw->visual_base--;
629 }
630 else
631 mpdclient_cmd_move(c, lw->selected, lw->selected-1);
632 return true;
633 case CMD_LIST_MOVE_DOWN:
634 if(lw->selected_end+1 >= playlist_length(&c->playlist))
635 return false;
636 if(lw->visual_selection)
637 {
638 int i = lw->selected_end;
639 unsigned last_selected = lw->selected;
640 for(; i >= (int)lw->selected_start; --i)
641 mpdclient_cmd_move(c, i, i+1);
642 lw->selected_start++;
643 lw->selected_end++;
644 lw->selected = last_selected + 1;
645 lw->visual_base++;
646 }
647 else
648 mpdclient_cmd_move(c, lw->selected, lw->selected+1);
649 return true;
650 case CMD_LIST_FIND:
651 case CMD_LIST_RFIND:
652 case CMD_LIST_FIND_NEXT:
653 case CMD_LIST_RFIND_NEXT:
654 screen_find(lw, playlist_length(&c->playlist),
655 cmd, list_callback, NULL);
656 playlist_repaint();
657 return true;
659 #ifdef HAVE_GETMOUSE
660 case CMD_MOUSE_EVENT:
661 return handle_mouse_event(c);
662 #endif
664 #ifdef ENABLE_SONG_SCREEN
665 case CMD_VIEW:
666 if (lw->selected < playlist_length(&c->playlist)) {
667 screen_song_switch(c, playlist_get(&c->playlist, lw->selected));
668 return true;
669 }
671 break;
672 #endif
674 case CMD_LOCATE:
675 if (lw->selected < playlist_length(&c->playlist)) {
676 screen_file_goto_song(c, playlist_get(&c->playlist, lw->selected));
677 return true;
678 }
680 break;
682 #ifdef ENABLE_LYRICS_SCREEN
683 case CMD_SCREEN_LYRICS:
684 if (lw->selected < playlist_length(&c->playlist)) {
685 screen_lyrics_switch(c, playlist_get(&c->playlist, lw->selected));
686 return true;
687 }
689 break;
690 #endif
692 default:
693 break;
694 }
696 return false;
697 }
699 const struct screen_functions screen_playlist = {
700 .init = play_init,
701 .exit = play_exit,
702 .open = play_open,
703 .close = play_close,
704 .resize = play_resize,
705 .paint = play_paint,
706 .update = play_update,
707 .cmd = play_cmd,
708 .get_title = play_title,
709 };