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_play.h"
21 #include "screen_interface.h"
22 #include "screen_file.h"
23 #include "config.h"
24 #include "i18n.h"
25 #include "charset.h"
26 #include "options.h"
27 #include "mpdclient.h"
28 #include "utils.h"
29 #include "strfsong.h"
30 #include "wreadln.h"
31 #include "colors.h"
32 #include "screen.h"
33 #include "screen_utils.h"
34 #include "screen_song.h"
35 #include "screen_lyrics.h"
37 #ifndef NCMPC_MINI
38 #include "hscroll.h"
39 #endif
41 #include <mpd/client.h>
43 #include <ctype.h>
44 #include <string.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 const struct mpd_song *song;
186 unsigned length = c->playlist.list->len;
187 int idx;
189 song = mpdclient_get_current_song(c);
190 if (song == NULL)
191 return;
193 /* try to center the song that are playing */
194 idx = playlist_get_index(&c->playlist, c->song);
195 if (idx < 0)
196 return;
198 if (length < lw->rows)
199 {
200 if (center_cursor)
201 list_window_set_selected(lw, idx);
202 return;
203 }
205 list_window_center(lw, length, idx);
207 if (center_cursor) {
208 list_window_set_selected(lw, idx);
209 return;
210 }
212 /* make sure the cursor is in the window */
213 if (lw->selected < lw->start + options.scroll_offset) {
214 if (lw->start > 0)
215 lw->selected = lw->start + options.scroll_offset;
216 if (lw->range_selection) {
217 lw->selected_start = lw->range_base;
218 lw->selected_end = lw->selected;
219 } else {
220 lw->selected_start = lw->selected;
221 lw->selected_end = lw->selected;
222 }
223 } else if (lw->selected > lw->start + lw->rows - 1 - options.scroll_offset) {
224 if (lw->start + lw->rows < length)
225 lw->selected = lw->start + lw->rows - 1 - options.scroll_offset;
226 if (lw->range_selection) {
227 lw->selected_start = lw->selected;
228 lw->selected_end = lw->range_base;
229 } else {
230 lw->selected_start = lw->selected;
231 lw->selected_end = lw->selected;
232 }
233 }
234 }
236 #ifndef NCMPC_MINI
237 static void
238 save_pre_completion_cb(GCompletion *gcmp, G_GNUC_UNUSED gchar *line,
239 void *data)
240 {
241 completion_callback_data_t *tmp = (completion_callback_data_t *)data;
242 GList **list = tmp->list;
243 struct mpdclient *c = tmp->c;
245 if( *list == NULL ) {
246 /* create completion list */
247 *list = gcmp_list_from_path(c, "", NULL, GCMP_TYPE_PLAYLIST);
248 g_completion_add_items(gcmp, *list);
249 }
250 }
252 static void
253 save_post_completion_cb(G_GNUC_UNUSED GCompletion *gcmp,
254 G_GNUC_UNUSED gchar *line, GList *items,
255 G_GNUC_UNUSED void *data)
256 {
257 if (g_list_length(items) >= 1)
258 screen_display_completion_list(items);
259 }
260 #endif
262 #ifndef NCMPC_MINI
263 /**
264 * Wrapper for strncmp(). We are not allowed to pass &strncmp to
265 * g_completion_set_compare(), because strncmp() takes size_t where
266 * g_completion_set_compare passes a gsize value.
267 */
268 static gint
269 completion_strncmp(const gchar *s1, const gchar *s2, gsize n)
270 {
271 return strncmp(s1, s2, n);
272 }
273 #endif
275 int
276 playlist_save(struct mpdclient *c, char *name, char *defaultname)
277 {
278 gchar *filename, *filename_utf8;
279 gint error;
280 #ifndef NCMPC_MINI
281 GCompletion *gcmp;
282 GList *list = NULL;
283 completion_callback_data_t data;
284 #endif
286 #ifdef NCMPC_MINI
287 (void)defaultname;
288 #endif
290 #ifndef NCMPC_MINI
291 if (name == NULL) {
292 /* initialize completion support */
293 gcmp = g_completion_new(NULL);
294 g_completion_set_compare(gcmp, completion_strncmp);
295 data.list = &list;
296 data.dir_list = NULL;
297 data.c = c;
298 wrln_completion_callback_data = &data;
299 wrln_pre_completion_callback = save_pre_completion_cb;
300 wrln_post_completion_callback = save_post_completion_cb;
303 /* query the user for a filename */
304 filename = screen_readln(_("Save playlist as"),
305 defaultname,
306 NULL,
307 gcmp);
309 /* destroy completion support */
310 wrln_completion_callback_data = NULL;
311 wrln_pre_completion_callback = NULL;
312 wrln_post_completion_callback = NULL;
313 g_completion_free(gcmp);
314 list = string_list_free(list);
315 if( filename )
316 filename=g_strstrip(filename);
317 } else
318 #endif
319 filename=g_strdup(name);
321 if (filename == NULL)
322 return -1;
324 /* send save command to mpd */
326 filename_utf8 = locale_to_utf8(filename);
327 error = mpdclient_cmd_save_playlist(c, filename_utf8);
329 if (error) {
330 gint code = GET_ACK_ERROR_CODE(error);
332 if (code == MPD_SERVER_ERROR_EXIST) {
333 char *buf;
334 int key;
336 buf = g_strdup_printf(_("Replace %s [%s/%s] ? "),
337 filename, YES, NO);
338 key = tolower(screen_getch(buf));
339 g_free(buf);
341 if (key != YES[0]) {
342 g_free(filename_utf8);
343 g_free(filename);
344 screen_status_printf(_("Aborted"));
345 return -1;
346 }
348 error = mpdclient_cmd_delete_playlist(c, filename_utf8);
349 if (error) {
350 g_free(filename_utf8);
351 g_free(filename);
352 return -1;
353 }
355 error = mpdclient_cmd_save_playlist(c, filename_utf8);
356 if (error) {
357 g_free(filename_utf8);
358 g_free(filename);
359 return error;
360 }
361 } else {
362 g_free(filename_utf8);
363 g_free(filename);
364 return -1;
365 }
366 }
368 g_free(filename_utf8);
370 /* success */
371 screen_status_printf(_("Saved %s"), filename);
372 g_free(filename);
373 return 0;
374 }
376 #ifndef NCMPC_MINI
377 static void add_dir(GCompletion *gcmp, gchar *dir, GList **dir_list,
378 GList **list, struct mpdclient *c)
379 {
380 g_completion_remove_items(gcmp, *list);
381 *list = string_list_remove(*list, dir);
382 *list = gcmp_list_from_path(c, dir, *list, GCMP_TYPE_RFILE);
383 g_completion_add_items(gcmp, *list);
384 *dir_list = g_list_append(*dir_list, g_strdup(dir));
385 }
387 static void add_pre_completion_cb(GCompletion *gcmp, gchar *line, void *data)
388 {
389 completion_callback_data_t *tmp = (completion_callback_data_t *)data;
390 GList **dir_list = tmp->dir_list;
391 GList **list = tmp->list;
392 struct mpdclient *c = tmp->c;
394 if (*list == NULL) {
395 /* create initial list */
396 *list = gcmp_list_from_path(c, "", NULL, GCMP_TYPE_RFILE);
397 g_completion_add_items(gcmp, *list);
398 } else if (line && line[0] && line[strlen(line)-1]=='/' &&
399 string_list_find(*dir_list, line) == NULL) {
400 /* add directory content to list */
401 add_dir(gcmp, line, dir_list, list, c);
402 }
403 }
405 static void add_post_completion_cb(GCompletion *gcmp, gchar *line,
406 GList *items, void *data)
407 {
408 completion_callback_data_t *tmp = (completion_callback_data_t *)data;
409 GList **dir_list = tmp->dir_list;
410 GList **list = tmp->list;
411 struct mpdclient *c = tmp->c;
413 if (g_list_length(items) >= 1)
414 screen_display_completion_list(items);
416 if (line && line[0] && line[strlen(line) - 1] == '/' &&
417 string_list_find(*dir_list, line) == NULL) {
418 /* add directory content to list */
419 add_dir(gcmp, line, dir_list, list, c);
420 }
421 }
422 #endif
424 static int
425 handle_add_to_playlist(struct mpdclient *c)
426 {
427 gchar *path;
428 #ifndef NCMPC_MINI
429 GCompletion *gcmp;
430 GList *list = NULL;
431 GList *dir_list = NULL;
432 completion_callback_data_t data;
434 /* initialize completion support */
435 gcmp = g_completion_new(NULL);
436 g_completion_set_compare(gcmp, completion_strncmp);
437 data.list = &list;
438 data.dir_list = &dir_list;
439 data.c = c;
440 wrln_completion_callback_data = &data;
441 wrln_pre_completion_callback = add_pre_completion_cb;
442 wrln_post_completion_callback = add_post_completion_cb;
443 #endif
445 /* get path */
446 path = screen_readln(_("Add"),
447 NULL,
448 NULL,
449 #ifdef NCMPC_MINI
450 NULL
451 #else
452 gcmp
453 #endif
454 );
456 /* destroy completion data */
457 #ifndef NCMPC_MINI
458 wrln_completion_callback_data = NULL;
459 wrln_pre_completion_callback = NULL;
460 wrln_post_completion_callback = NULL;
461 g_completion_free(gcmp);
462 string_list_free(list);
463 string_list_free(dir_list);
464 #endif
466 /* add the path to the playlist */
467 if (path != NULL) {
468 char *path_utf8 = locale_to_utf8(path);
469 mpdclient_cmd_add_path(c, path_utf8);
470 g_free(path_utf8);
471 }
473 g_free(path);
474 return 0;
475 }
477 static void
478 play_init(WINDOW *w, int cols, int rows)
479 {
480 lw = list_window_init(w, cols, rows);
481 }
483 static gboolean
484 timer_hide_cursor(gpointer data)
485 {
486 struct mpdclient *c = data;
488 assert(options.hide_cursor > 0);
489 assert(timer_hide_cursor_id != 0);
491 timer_hide_cursor_id = 0;
493 /* hide the cursor when mpd is playing and the user is inactive */
495 if (c->status != NULL &&
496 mpd_status_get_state(c->status) == MPD_STATE_PLAY) {
497 lw->hide_cursor = true;
498 playlist_repaint();
499 } else
500 timer_hide_cursor_id = g_timeout_add(options.hide_cursor * 1000,
501 timer_hide_cursor, c);
503 return FALSE;
504 }
506 static void
507 play_open(struct mpdclient *c)
508 {
509 playlist = &c->playlist;
511 assert(timer_hide_cursor_id == 0);
512 if (options.hide_cursor > 0) {
513 lw->hide_cursor = false;
514 timer_hide_cursor_id = g_timeout_add(options.hide_cursor * 1000,
515 timer_hide_cursor, c);
516 }
518 playlist_restore_selection();
519 }
521 static void
522 play_close(void)
523 {
524 if (timer_hide_cursor_id != 0) {
525 g_source_remove(timer_hide_cursor_id);
526 timer_hide_cursor_id = 0;
527 }
528 }
530 static void
531 play_resize(int cols, int rows)
532 {
533 lw->cols = cols;
534 lw->rows = rows;
535 }
538 static void
539 play_exit(void)
540 {
541 list_window_free(lw);
542 }
544 static const char *
545 play_title(char *str, size_t size)
546 {
547 if (options.host == NULL)
548 return _("Playlist");
550 g_snprintf(str, size, _("Playlist on %s"), options.host);
551 return str;
552 }
554 static void
555 play_paint(void)
556 {
557 #ifndef NCMPC_MINI
558 must_scroll = false;
559 #endif
561 list_window_paint(lw, list_callback, NULL);
562 }
564 static void
565 play_update(struct mpdclient *c)
566 {
567 static int prev_song_id = -1;
569 if (c->events & MPD_IDLE_PLAYLIST)
570 playlist_restore_selection();
572 current_song_id = c->status != NULL &&
573 !IS_STOPPED(mpd_status_get_state(c->status))
574 ? (int)mpd_status_get_song_id(c->status) : -1;
576 if (current_song_id != prev_song_id) {
577 prev_song_id = current_song_id;
579 /* center the cursor */
580 if (options.auto_center && current_song_id != -1 && ! lw->range_selection)
581 center_playing_item(c, false);
583 playlist_repaint();
584 #ifndef NCMPC_MINI
585 } else if (options.scroll && must_scroll) {
586 /* always repaint if horizontal scrolling is
587 enabled */
588 playlist_repaint();
589 #endif
590 } else if (c->events & MPD_IDLE_PLAYLIST) {
591 /* the playlist has changed, we must paint the new
592 version */
593 playlist_repaint();
594 }
595 }
597 #ifdef HAVE_GETMOUSE
598 static bool
599 handle_mouse_event(struct mpdclient *c)
600 {
601 int row;
602 unsigned selected;
603 unsigned long bstate;
605 if (screen_get_mouse_event(c, &bstate, &row) ||
606 list_window_mouse(lw, playlist_length(playlist), bstate, row)) {
607 playlist_repaint();
608 return true;
609 }
611 if (bstate & BUTTON1_DOUBLE_CLICKED) {
612 /* stop */
613 screen_cmd(c, CMD_STOP);
614 return true;
615 }
617 selected = lw->start + row;
619 if (bstate & BUTTON1_CLICKED) {
620 /* play */
621 if (lw->start + row < playlist_length(playlist))
622 mpdclient_cmd_play(c, lw->start + row);
623 } else if (bstate & BUTTON3_CLICKED) {
624 /* delete */
625 if (selected == lw->selected)
626 mpdclient_cmd_delete(c, lw->selected);
627 }
629 lw->selected = selected;
630 list_window_check_selected(lw, playlist_length(playlist));
631 playlist_save_selection();
632 playlist_repaint();
634 return true;
635 }
636 #endif
638 static bool
639 play_cmd(struct mpdclient *c, command_t cmd)
640 {
641 static command_t cached_cmd = CMD_NONE;
642 command_t prev_cmd = cached_cmd;
643 cached_cmd = cmd;
645 lw->hide_cursor = false;
647 if (options.hide_cursor > 0) {
648 if (timer_hide_cursor_id != 0)
649 g_source_remove(timer_hide_cursor_id);
650 timer_hide_cursor_id = g_timeout_add(options.hide_cursor * 1000,
651 timer_hide_cursor, c);
652 }
654 if (list_window_cmd(lw, playlist_length(&c->playlist), cmd)) {
655 playlist_save_selection();
656 playlist_repaint();
657 return true;
658 }
660 switch(cmd) {
661 case CMD_PLAY:
662 mpdclient_cmd_play(c, lw->selected);
663 return true;
664 case CMD_DELETE:
665 if (lw->range_selection) {
666 mpdclient_cmd_delete_range(c, lw->selected_start,
667 lw->selected_end + 1);
668 } else {
669 mpdclient_cmd_delete(c, lw->selected);
670 }
672 lw->selected = lw->selected_end = lw->selected_start;
673 lw->range_selection = false;
674 return true;
676 case CMD_SAVE_PLAYLIST:
677 playlist_save(c, NULL, NULL);
678 return true;
679 case CMD_ADD:
680 handle_add_to_playlist(c);
681 return true;
682 case CMD_SCREEN_UPDATE:
683 center_playing_item(c, prev_cmd == CMD_SCREEN_UPDATE);
684 playlist_repaint();
685 return false;
686 case CMD_SELECT_PLAYING:
687 list_window_set_selected(lw, playlist_get_index(&c->playlist,
688 c->song));
689 playlist_save_selection();
690 return true;
691 case CMD_SHUFFLE:
692 {
693 if(!lw->range_selection)
694 /* No range selection, shuffle all list. */
695 break;
697 if (mpdclient_cmd_shuffle_range(c, lw->selected_start, lw->selected_end+1) == 0)
698 screen_status_message(_("Shuffled playlist"));
700 return true;
701 }
702 case CMD_LIST_MOVE_UP:
703 if(lw->selected_start == 0)
704 return false;
705 if(lw->range_selection)
706 {
707 unsigned i = lw->selected_start;
708 unsigned last_selected = lw->selected;
709 for(; i <= lw->selected_end; ++i)
710 mpdclient_cmd_move(c, i, i-1);
711 lw->selected_start--;
712 lw->selected_end--;
713 lw->selected = last_selected - 1;
714 lw->range_base--;
715 }
716 else
717 {
718 mpdclient_cmd_move(c, lw->selected, lw->selected-1);
719 lw->selected--;
720 lw->selected_start--;
721 lw->selected_end--;
722 }
724 playlist_save_selection();
725 return true;
726 case CMD_LIST_MOVE_DOWN:
727 if(lw->selected_end+1 >= playlist_length(&c->playlist))
728 return false;
729 if(lw->range_selection)
730 {
731 int i = lw->selected_end;
732 unsigned last_selected = lw->selected;
733 for(; i >= (int)lw->selected_start; --i)
734 mpdclient_cmd_move(c, i, i+1);
735 lw->selected_start++;
736 lw->selected_end++;
737 lw->selected = last_selected + 1;
738 lw->range_base++;
739 }
740 else
741 {
742 mpdclient_cmd_move(c, lw->selected, lw->selected+1);
743 lw->selected++;
744 lw->selected_start++;
745 lw->selected_end++;
746 }
748 playlist_save_selection();
749 return true;
750 case CMD_LIST_FIND:
751 case CMD_LIST_RFIND:
752 case CMD_LIST_FIND_NEXT:
753 case CMD_LIST_RFIND_NEXT:
754 screen_find(lw, playlist_length(&c->playlist),
755 cmd, list_callback, NULL);
756 playlist_save_selection();
757 playlist_repaint();
758 return true;
759 case CMD_LIST_JUMP:
760 screen_jump(lw, list_callback, NULL);
761 playlist_save_selection();
762 playlist_repaint();
763 return true;
765 #ifdef HAVE_GETMOUSE
766 case CMD_MOUSE_EVENT:
767 return handle_mouse_event(c);
768 #endif
770 #ifdef ENABLE_SONG_SCREEN
771 case CMD_SCREEN_SONG:
772 if (playlist_selected_song()) {
773 screen_song_switch(c, playlist_selected_song());
774 return true;
775 }
777 break;
778 #endif
780 case CMD_LOCATE:
781 if (playlist_selected_song()) {
782 screen_file_goto_song(c, playlist_selected_song());
783 return true;
784 }
786 break;
788 #ifdef ENABLE_LYRICS_SCREEN
789 case CMD_SCREEN_LYRICS:
790 if (lw->selected < playlist_length(&c->playlist)) {
791 struct mpd_song *selected = playlist_get(&c->playlist, lw->selected);
792 bool follow = false;
794 if (c->song && selected &&
795 !strcmp(mpd_song_get_uri(selected),
796 mpd_song_get_uri(c->song)))
797 follow = true;
799 screen_lyrics_switch(c, selected, follow);
800 return true;
801 }
803 break;
804 #endif
805 case CMD_SCREEN_SWAP:
806 screen_swap(c, playlist_get(&c->playlist, lw->selected));
807 return true;
809 default:
810 break;
811 }
813 return false;
814 }
816 const struct screen_functions screen_playlist = {
817 .init = play_init,
818 .exit = play_exit,
819 .open = play_open,
820 .close = play_close,
821 .resize = play_resize,
822 .paint = play_paint,
823 .update = play_update,
824 .cmd = play_cmd,
825 .get_title = play_title,
826 };