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 "screen_file.h"
21 #include "config.h"
22 #include "i18n.h"
23 #include "charset.h"
24 #include "options.h"
25 #include "mpdclient.h"
26 #include "utils.h"
27 #include "strfsong.h"
28 #include "wreadln.h"
29 #include "command.h"
30 #include "colors.h"
31 #include "screen.h"
32 #include "screen_utils.h"
33 #include "screen_play.h"
35 #ifndef NCMPC_MINI
36 #include "hscroll.h"
37 #endif
39 #include <mpd/client.h>
41 #include <ctype.h>
42 #include <stdlib.h>
43 #include <string.h>
44 #include <time.h>
45 #include <glib.h>
47 #define MAX_SONG_LENGTH 512
49 #ifndef NCMPC_MINI
50 typedef struct
51 {
52 GList **list;
53 GList **dir_list;
54 struct mpdclient *c;
55 } completion_callback_data_t;
57 static bool must_scroll;
58 #endif
60 static struct mpdclient_playlist *playlist;
61 static int current_song_id = -1;
62 static int selected_song_id = -1;
63 static list_window_t *lw = NULL;
64 static guint timer_hide_cursor_id;
66 static void
67 play_paint(void);
69 static void
70 playlist_repaint(void)
71 {
72 play_paint();
73 wrefresh(lw->w);
74 }
76 static const struct mpd_song *
77 playlist_selected_song(void)
78 {
79 return !lw->range_selection &&
80 lw->selected < playlist_length(playlist)
81 ? playlist_get(playlist, lw->selected)
82 : NULL;
83 }
85 static void
86 playlist_save_selection(void)
87 {
88 selected_song_id = playlist_selected_song() != NULL
89 ? (int)mpd_song_get_id(playlist_selected_song())
90 : -1;
91 }
93 static void
94 playlist_restore_selection(void)
95 {
96 const struct mpd_song *song;
97 int pos;
99 if (selected_song_id < 0)
100 /* there was no selection */
101 return;
103 song = playlist_selected_song();
104 if (song != NULL &&
105 mpd_song_get_id(song) == (unsigned)selected_song_id)
106 /* selection is still valid */
107 return;
109 pos = playlist_get_index_from_id(playlist, selected_song_id);
110 if (pos >= 0)
111 lw->selected = pos;
113 list_window_check_selected(lw, playlist_length(playlist));
114 playlist_save_selection();
115 }
117 #ifndef NCMPC_MINI
118 static char *
119 format_duration(unsigned duration)
120 {
121 if (duration == 0)
122 return NULL;
124 return g_strdup_printf("%d:%02d", duration / 60, duration % 60);
125 }
126 #endif
128 static const char *
129 list_callback(unsigned idx, bool *highlight, char **second_column, G_GNUC_UNUSED void *data)
130 {
131 static char songname[MAX_SONG_LENGTH];
132 #ifndef NCMPC_MINI
133 static scroll_state_t st;
134 #endif
135 struct mpd_song *song;
137 if (playlist == NULL || idx >= playlist_length(playlist))
138 return NULL;
140 song = playlist_get(playlist, idx);
141 if ((int)mpd_song_get_id(song) == current_song_id)
142 *highlight = true;
144 strfsong(songname, MAX_SONG_LENGTH, options.list_format, song);
146 #ifndef NCMPC_MINI
147 if(second_column)
148 *second_column = format_duration(mpd_song_get_duration(song));
150 if (idx == lw->selected)
151 {
152 int second_column_len = 0;
153 if (second_column != NULL && *second_column != NULL)
154 second_column_len = strlen(*second_column);
155 if (options.scroll && utf8_width(songname) > (unsigned)(COLS - second_column_len - 1) )
156 {
157 static unsigned current_song;
158 char *tmp;
160 must_scroll = true;
162 if (current_song != lw->selected) {
163 st.offset = 0;
164 current_song = lw->selected;
165 }
167 tmp = strscroll(songname, options.scroll_sep,
168 MAX_SONG_LENGTH, &st);
169 g_strlcpy(songname, tmp, MAX_SONG_LENGTH);
170 g_free(tmp);
171 }
172 else
173 st.offset = 0;
174 }
175 #else
176 (void)second_column;
177 #endif
179 return songname;
180 }
182 static void
183 center_playing_item(struct mpdclient *c, bool center_cursor)
184 {
185 unsigned length = c->playlist.list->len;
186 int idx;
188 if (!c->song || c->status == NULL ||
189 IS_STOPPED(mpd_status_get_state(c->status)))
190 return;
192 /* try to center the song that are playing */
193 idx = playlist_get_index(&c->playlist, c->song);
194 if (idx < 0)
195 return;
197 if (length < lw->rows)
198 {
199 if (center_cursor)
200 list_window_set_selected(lw, idx);
201 return;
202 }
204 list_window_center(lw, length, idx);
206 if (center_cursor) {
207 list_window_set_selected(lw, idx);
208 return;
209 }
211 /* make sure the cursor is in the window */
212 if (lw->selected < lw->start + options.scroll_offset) {
213 if (lw->start > 0)
214 lw->selected = lw->start + options.scroll_offset;
215 if (lw->range_selection) {
216 lw->selected_start = lw->range_base;
217 lw->selected_end = lw->selected;
218 } else {
219 lw->selected_start = lw->selected;
220 lw->selected_end = lw->selected;
221 }
222 } else if (lw->selected > lw->start + lw->rows - 1 - options.scroll_offset) {
223 if (lw->start + lw->rows < length)
224 lw->selected = lw->start + lw->rows - 1 - options.scroll_offset;
225 if (lw->range_selection) {
226 lw->selected_start = lw->selected;
227 lw->selected_end = lw->range_base;
228 } else {
229 lw->selected_start = lw->selected;
230 lw->selected_end = lw->selected;
231 }
232 }
233 }
235 #ifndef NCMPC_MINI
236 static void
237 save_pre_completion_cb(GCompletion *gcmp, G_GNUC_UNUSED gchar *line,
238 void *data)
239 {
240 completion_callback_data_t *tmp = (completion_callback_data_t *)data;
241 GList **list = tmp->list;
242 struct mpdclient *c = tmp->c;
244 if( *list == NULL ) {
245 /* create completion list */
246 *list = gcmp_list_from_path(c, "", NULL, GCMP_TYPE_PLAYLIST);
247 g_completion_add_items(gcmp, *list);
248 }
249 }
251 static void
252 save_post_completion_cb(G_GNUC_UNUSED GCompletion *gcmp,
253 G_GNUC_UNUSED gchar *line, GList *items,
254 G_GNUC_UNUSED void *data)
255 {
256 if (g_list_length(items) >= 1)
257 screen_display_completion_list(items);
258 }
259 #endif
261 #ifndef NCMPC_MINI
262 /**
263 * Wrapper for strncmp(). We are not allowed to pass &strncmp to
264 * g_completion_set_compare(), because strncmp() takes size_t where
265 * g_completion_set_compare passes a gsize value.
266 */
267 static gint
268 completion_strncmp(const gchar *s1, const gchar *s2, gsize n)
269 {
270 return strncmp(s1, s2, n);
271 }
272 #endif
274 int
275 playlist_save(struct mpdclient *c, char *name, char *defaultname)
276 {
277 gchar *filename, *filename_utf8;
278 gint error;
279 #ifndef NCMPC_MINI
280 GCompletion *gcmp;
281 GList *list = NULL;
282 completion_callback_data_t data;
283 #endif
285 #ifdef NCMPC_MINI
286 (void)defaultname;
287 #endif
289 #ifndef NCMPC_MINI
290 if (name == NULL) {
291 /* initialize completion support */
292 gcmp = g_completion_new(NULL);
293 g_completion_set_compare(gcmp, completion_strncmp);
294 data.list = &list;
295 data.dir_list = NULL;
296 data.c = c;
297 wrln_completion_callback_data = &data;
298 wrln_pre_completion_callback = save_pre_completion_cb;
299 wrln_post_completion_callback = save_post_completion_cb;
302 /* query the user for a filename */
303 filename = screen_readln(_("Save playlist as"),
304 defaultname,
305 NULL,
306 gcmp);
308 /* destroy completion support */
309 wrln_completion_callback_data = NULL;
310 wrln_pre_completion_callback = NULL;
311 wrln_post_completion_callback = NULL;
312 g_completion_free(gcmp);
313 list = string_list_free(list);
314 if( filename )
315 filename=g_strstrip(filename);
316 } else
317 #endif
318 filename=g_strdup(name);
320 if (filename == NULL)
321 return -1;
323 /* send save command to mpd */
325 filename_utf8 = locale_to_utf8(filename);
326 error = mpdclient_cmd_save_playlist(c, filename_utf8);
328 if (error) {
329 gint code = GET_ACK_ERROR_CODE(error);
331 if (code == MPD_SERVER_ERROR_EXIST) {
332 char *buf;
333 int key;
335 buf = g_strdup_printf(_("Replace %s [%s/%s] ? "),
336 filename, YES, NO);
337 key = tolower(screen_getch(buf));
338 g_free(buf);
340 if (key != YES[0]) {
341 g_free(filename_utf8);
342 g_free(filename);
343 screen_status_printf(_("Aborted"));
344 return -1;
345 }
347 error = mpdclient_cmd_delete_playlist(c, filename_utf8);
348 if (error) {
349 g_free(filename_utf8);
350 g_free(filename);
351 return -1;
352 }
354 error = mpdclient_cmd_save_playlist(c, filename_utf8);
355 if (error) {
356 g_free(filename_utf8);
357 g_free(filename);
358 return error;
359 }
360 } else {
361 g_free(filename_utf8);
362 g_free(filename);
363 return -1;
364 }
365 }
367 g_free(filename_utf8);
369 /* success */
370 screen_status_printf(_("Saved %s"), filename);
371 g_free(filename);
372 return 0;
373 }
375 #ifndef NCMPC_MINI
376 static void add_dir(GCompletion *gcmp, gchar *dir, GList **dir_list,
377 GList **list, struct mpdclient *c)
378 {
379 g_completion_remove_items(gcmp, *list);
380 *list = string_list_remove(*list, dir);
381 *list = gcmp_list_from_path(c, dir, *list, GCMP_TYPE_RFILE);
382 g_completion_add_items(gcmp, *list);
383 *dir_list = g_list_append(*dir_list, g_strdup(dir));
384 }
386 static void add_pre_completion_cb(GCompletion *gcmp, gchar *line, void *data)
387 {
388 completion_callback_data_t *tmp = (completion_callback_data_t *)data;
389 GList **dir_list = tmp->dir_list;
390 GList **list = tmp->list;
391 struct mpdclient *c = tmp->c;
393 if (*list == NULL) {
394 /* create initial list */
395 *list = gcmp_list_from_path(c, "", NULL, GCMP_TYPE_RFILE);
396 g_completion_add_items(gcmp, *list);
397 } else if (line && line[0] && line[strlen(line)-1]=='/' &&
398 string_list_find(*dir_list, line) == NULL) {
399 /* add directory content to list */
400 add_dir(gcmp, line, dir_list, list, c);
401 }
402 }
404 static void add_post_completion_cb(GCompletion *gcmp, gchar *line,
405 GList *items, void *data)
406 {
407 completion_callback_data_t *tmp = (completion_callback_data_t *)data;
408 GList **dir_list = tmp->dir_list;
409 GList **list = tmp->list;
410 struct mpdclient *c = tmp->c;
412 if (g_list_length(items) >= 1)
413 screen_display_completion_list(items);
415 if (line && line[0] && line[strlen(line) - 1] == '/' &&
416 string_list_find(*dir_list, line) == NULL) {
417 /* add directory content to list */
418 add_dir(gcmp, line, dir_list, list, c);
419 }
420 }
421 #endif
423 static int
424 handle_add_to_playlist(struct mpdclient *c)
425 {
426 gchar *path;
427 #ifndef NCMPC_MINI
428 GCompletion *gcmp;
429 GList *list = NULL;
430 GList *dir_list = NULL;
431 completion_callback_data_t data;
433 /* initialize completion support */
434 gcmp = g_completion_new(NULL);
435 g_completion_set_compare(gcmp, completion_strncmp);
436 data.list = &list;
437 data.dir_list = &dir_list;
438 data.c = c;
439 wrln_completion_callback_data = &data;
440 wrln_pre_completion_callback = add_pre_completion_cb;
441 wrln_post_completion_callback = add_post_completion_cb;
442 #endif
444 /* get path */
445 path = screen_readln(_("Add"),
446 NULL,
447 NULL,
448 #ifdef NCMPC_MINI
449 NULL
450 #else
451 gcmp
452 #endif
453 );
455 /* destroy completion data */
456 #ifndef NCMPC_MINI
457 wrln_completion_callback_data = NULL;
458 wrln_pre_completion_callback = NULL;
459 wrln_post_completion_callback = NULL;
460 g_completion_free(gcmp);
461 string_list_free(list);
462 string_list_free(dir_list);
463 #endif
465 /* add the path to the playlist */
466 if (path != NULL) {
467 char *path_utf8 = locale_to_utf8(path);
468 mpdclient_cmd_add_path(c, path_utf8);
469 g_free(path_utf8);
470 }
472 g_free(path);
473 return 0;
474 }
476 static void
477 play_init(WINDOW *w, int cols, int rows)
478 {
479 lw = list_window_init(w, cols, rows);
480 }
482 static gboolean
483 timer_hide_cursor(gpointer data)
484 {
485 struct mpdclient *c = data;
487 assert(options.hide_cursor > 0);
488 assert(timer_hide_cursor_id != 0);
490 timer_hide_cursor_id = 0;
492 /* hide the cursor when mpd is playing and the user is inactive */
494 if (c->status != NULL &&
495 mpd_status_get_state(c->status) == MPD_STATE_PLAY) {
496 lw->hide_cursor = true;
497 playlist_repaint();
498 } else
499 timer_hide_cursor_id = g_timeout_add(options.hide_cursor * 1000,
500 timer_hide_cursor, c);
502 return FALSE;
503 }
505 static void
506 play_open(struct mpdclient *c)
507 {
508 playlist = &c->playlist;
510 assert(timer_hide_cursor_id == 0);
511 if (options.hide_cursor > 0) {
512 lw->hide_cursor = false;
513 timer_hide_cursor_id = g_timeout_add(options.hide_cursor * 1000,
514 timer_hide_cursor, c);
515 }
517 playlist_restore_selection();
518 }
520 static void
521 play_close(void)
522 {
523 if (timer_hide_cursor_id != 0) {
524 g_source_remove(timer_hide_cursor_id);
525 timer_hide_cursor_id = 0;
526 }
527 }
529 static void
530 play_resize(int cols, int rows)
531 {
532 lw->cols = cols;
533 lw->rows = rows;
534 }
537 static void
538 play_exit(void)
539 {
540 list_window_free(lw);
541 }
543 static const char *
544 play_title(char *str, size_t size)
545 {
546 if (options.host == NULL)
547 return _("Playlist");
549 g_snprintf(str, size, _("Playlist on %s"), options.host);
550 return str;
551 }
553 static void
554 play_paint(void)
555 {
556 #ifndef NCMPC_MINI
557 must_scroll = false;
558 #endif
560 list_window_paint(lw, list_callback, NULL);
561 }
563 static void
564 play_update(struct mpdclient *c)
565 {
566 static int prev_song_id = -1;
568 if (c->events & MPD_IDLE_PLAYLIST)
569 playlist_restore_selection();
571 current_song_id = c->status != NULL &&
572 !IS_STOPPED(mpd_status_get_state(c->status))
573 ? (int)mpd_status_get_song_id(c->status) : -1;
575 if (current_song_id != prev_song_id) {
576 prev_song_id = current_song_id;
578 /* center the cursor */
579 if (options.auto_center && current_song_id != -1 && ! lw->range_selection)
580 center_playing_item(c, false);
582 playlist_repaint();
583 #ifndef NCMPC_MINI
584 } else if (options.scroll && must_scroll) {
585 /* always repaint if horizontal scrolling is
586 enabled */
587 playlist_repaint();
588 #endif
589 } else if (c->events & MPD_IDLE_PLAYLIST) {
590 /* the playlist has changed, we must paint the new
591 version */
592 playlist_repaint();
593 }
594 }
596 #ifdef HAVE_GETMOUSE
597 static bool
598 handle_mouse_event(struct mpdclient *c)
599 {
600 int row;
601 unsigned selected;
602 unsigned long bstate;
604 if (screen_get_mouse_event(c, &bstate, &row) ||
605 list_window_mouse(lw, playlist_length(playlist), bstate, row)) {
606 playlist_repaint();
607 return true;
608 }
610 if (bstate & BUTTON1_DOUBLE_CLICKED) {
611 /* stop */
612 screen_cmd(c, CMD_STOP);
613 return true;
614 }
616 selected = lw->start + row;
618 if (bstate & BUTTON1_CLICKED) {
619 /* play */
620 if (lw->start + row < playlist_length(playlist))
621 mpdclient_cmd_play(c, lw->start + row);
622 } else if (bstate & BUTTON3_CLICKED) {
623 /* delete */
624 if (selected == lw->selected)
625 mpdclient_cmd_delete(c, lw->selected);
626 }
628 lw->selected = selected;
629 list_window_check_selected(lw, playlist_length(playlist));
630 playlist_save_selection();
631 playlist_repaint();
633 return true;
634 }
635 #endif
637 static bool
638 play_cmd(struct mpdclient *c, command_t cmd)
639 {
640 static command_t cached_cmd = CMD_NONE;
641 command_t prev_cmd = cached_cmd;
642 cached_cmd = cmd;
644 lw->hide_cursor = false;
646 if (options.hide_cursor > 0) {
647 if (timer_hide_cursor_id != 0)
648 g_source_remove(timer_hide_cursor_id);
649 timer_hide_cursor_id = g_timeout_add(options.hide_cursor * 1000,
650 timer_hide_cursor, c);
651 }
653 if (list_window_cmd(lw, playlist_length(&c->playlist), cmd)) {
654 playlist_save_selection();
655 playlist_repaint();
656 return true;
657 }
659 switch(cmd) {
660 case CMD_PLAY:
661 mpdclient_cmd_play(c, lw->selected);
662 return true;
663 case CMD_DELETE:
664 {
665 int i = lw->selected_end, start = lw->selected_start;
666 for(; i >= start; --i)
667 mpdclient_cmd_delete(c, i);
669 i++;
670 if(i >= (int)playlist_length(&c->playlist))
671 i--;
672 lw->selected = i;
673 lw->selected_start = i;
674 lw->selected_end = i;
675 lw->range_selection = false;
677 playlist_save_selection();
678 return true;
679 }
680 case CMD_SAVE_PLAYLIST:
681 playlist_save(c, NULL, NULL);
682 return true;
683 case CMD_ADD:
684 handle_add_to_playlist(c);
685 return true;
686 case CMD_SCREEN_UPDATE:
687 center_playing_item(c, prev_cmd == CMD_SCREEN_UPDATE);
688 playlist_repaint();
689 return false;
690 case CMD_SELECT_PLAYING:
691 list_window_set_selected(lw, playlist_get_index(&c->playlist,
692 c->song));
693 playlist_save_selection();
694 return true;
695 case CMD_SHUFFLE:
696 {
697 if(!lw->range_selection)
698 /* No range selection, shuffle all list. */
699 break;
701 if (mpdclient_cmd_shuffle_range(c, lw->selected_start, lw->selected_end+1) == 0)
702 screen_status_message(_("Shuffled playlist"));
704 return true;
705 }
706 case CMD_LIST_MOVE_UP:
707 if(lw->selected_start == 0)
708 return false;
709 if(lw->range_selection)
710 {
711 unsigned i = lw->selected_start;
712 unsigned last_selected = lw->selected;
713 for(; i <= lw->selected_end; ++i)
714 mpdclient_cmd_move(c, i, i-1);
715 lw->selected_start--;
716 lw->selected_end--;
717 lw->selected = last_selected - 1;
718 lw->range_base--;
719 }
720 else
721 {
722 mpdclient_cmd_move(c, lw->selected, lw->selected-1);
723 lw->selected--;
724 lw->selected_start--;
725 lw->selected_end--;
726 }
728 playlist_save_selection();
729 return true;
730 case CMD_LIST_MOVE_DOWN:
731 if(lw->selected_end+1 >= playlist_length(&c->playlist))
732 return false;
733 if(lw->range_selection)
734 {
735 int i = lw->selected_end;
736 unsigned last_selected = lw->selected;
737 for(; i >= (int)lw->selected_start; --i)
738 mpdclient_cmd_move(c, i, i+1);
739 lw->selected_start++;
740 lw->selected_end++;
741 lw->selected = last_selected + 1;
742 lw->range_base++;
743 }
744 else
745 {
746 mpdclient_cmd_move(c, lw->selected, lw->selected+1);
747 lw->selected++;
748 lw->selected_start++;
749 lw->selected_end++;
750 }
752 playlist_save_selection();
753 return true;
754 case CMD_LIST_FIND:
755 case CMD_LIST_RFIND:
756 case CMD_LIST_FIND_NEXT:
757 case CMD_LIST_RFIND_NEXT:
758 screen_find(lw, playlist_length(&c->playlist),
759 cmd, list_callback, NULL);
760 playlist_save_selection();
761 playlist_repaint();
762 return true;
763 case CMD_LIST_JUMP:
764 screen_jump(lw, list_callback, NULL);
765 playlist_save_selection();
766 playlist_repaint();
767 return true;
769 #ifdef HAVE_GETMOUSE
770 case CMD_MOUSE_EVENT:
771 return handle_mouse_event(c);
772 #endif
774 #ifdef ENABLE_SONG_SCREEN
775 case CMD_SCREEN_SONG:
776 if (playlist_selected_song()) {
777 screen_song_switch(c, playlist_selected_song());
778 return true;
779 }
781 break;
782 #endif
784 case CMD_LOCATE:
785 if (playlist_selected_song()) {
786 screen_file_goto_song(c, playlist_selected_song());
787 return true;
788 }
790 break;
792 #ifdef ENABLE_LYRICS_SCREEN
793 case CMD_SCREEN_LYRICS:
794 if (lw->selected < playlist_length(&c->playlist)) {
795 struct mpd_song *selected = playlist_get(&c->playlist, lw->selected);
796 bool follow = false;
798 if (c->song && selected &&
799 !strcmp(mpd_song_get_uri(selected),
800 mpd_song_get_uri(c->song)))
801 follow = true;
803 screen_lyrics_switch(c, selected, follow);
804 return true;
805 }
807 break;
808 #endif
809 case CMD_SCREEN_SWAP:
810 screen_swap(c, playlist_get(&c->playlist, lw->selected));
811 return true;
813 default:
814 break;
815 }
817 return false;
818 }
820 const struct screen_functions screen_playlist = {
821 .init = play_init,
822 .exit = play_exit,
823 .open = play_open,
824 .close = play_close,
825 .resize = play_resize,
826 .paint = play_paint,
827 .update = play_update,
828 .cmd = play_cmd,
829 .get_title = play_title,
830 };