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 "screen_message.h"
24 #include "screen_find.h"
25 #include "config.h"
26 #include "i18n.h"
27 #include "charset.h"
28 #include "options.h"
29 #include "mpdclient.h"
30 #include "utils.h"
31 #include "strfsong.h"
32 #include "wreadln.h"
33 #include "colors.h"
34 #include "screen.h"
35 #include "screen_utils.h"
36 #include "screen_song.h"
37 #include "screen_lyrics.h"
39 #ifndef NCMPC_MINI
40 #include "hscroll.h"
41 #endif
43 #include <mpd/client.h>
45 #include <ctype.h>
46 #include <string.h>
47 #include <glib.h>
49 #define MAX_SONG_LENGTH 512
51 #ifndef NCMPC_MINI
52 typedef struct
53 {
54 GList **list;
55 GList **dir_list;
56 struct mpdclient *c;
57 } completion_callback_data_t;
59 static bool must_scroll;
60 #endif
62 static struct mpdclient_playlist *playlist;
63 static int current_song_id = -1;
64 static int selected_song_id = -1;
65 static list_window_t *lw = NULL;
66 static guint timer_hide_cursor_id;
68 static void
69 screen_playlist_paint(void);
71 static void
72 playlist_repaint(void)
73 {
74 screen_playlist_paint();
75 wrefresh(lw->w);
76 }
78 static const struct mpd_song *
79 playlist_selected_song(void)
80 {
81 return !lw->range_selection &&
82 lw->selected < playlist_length(playlist)
83 ? playlist_get(playlist, lw->selected)
84 : NULL;
85 }
87 static void
88 playlist_save_selection(void)
89 {
90 selected_song_id = playlist_selected_song() != NULL
91 ? (int)mpd_song_get_id(playlist_selected_song())
92 : -1;
93 }
95 static void
96 playlist_restore_selection(void)
97 {
98 const struct mpd_song *song;
99 int pos;
101 if (selected_song_id < 0)
102 /* there was no selection */
103 return;
105 song = playlist_selected_song();
106 if (song != NULL &&
107 mpd_song_get_id(song) == (unsigned)selected_song_id)
108 /* selection is still valid */
109 return;
111 pos = playlist_get_index_from_id(playlist, selected_song_id);
112 if (pos >= 0)
113 lw->selected = pos;
115 list_window_check_selected(lw, playlist_length(playlist));
116 playlist_save_selection();
117 }
119 #ifndef NCMPC_MINI
120 static char *
121 format_duration(unsigned duration)
122 {
123 if (duration == 0)
124 return NULL;
126 return g_strdup_printf("%d:%02d", duration / 60, duration % 60);
127 }
128 #endif
130 static const char *
131 list_callback(unsigned idx, bool *highlight, char **second_column, G_GNUC_UNUSED void *data)
132 {
133 static char songname[MAX_SONG_LENGTH];
134 #ifndef NCMPC_MINI
135 static scroll_state_t st;
136 #endif
137 struct mpd_song *song;
139 if (playlist == NULL || idx >= playlist_length(playlist))
140 return NULL;
142 song = playlist_get(playlist, idx);
143 if ((int)mpd_song_get_id(song) == current_song_id)
144 *highlight = true;
146 strfsong(songname, MAX_SONG_LENGTH, options.list_format, song);
148 #ifndef NCMPC_MINI
149 if(second_column)
150 *second_column = format_duration(mpd_song_get_duration(song));
152 if (idx == lw->selected)
153 {
154 int second_column_len = 0;
155 if (second_column != NULL && *second_column != NULL)
156 second_column_len = strlen(*second_column);
157 if (options.scroll && utf8_width(songname) > (unsigned)(COLS - second_column_len - 1) )
158 {
159 static unsigned current_song;
160 char *tmp;
162 must_scroll = true;
164 if (current_song != lw->selected) {
165 st.offset = 0;
166 current_song = lw->selected;
167 }
169 tmp = strscroll(songname, options.scroll_sep,
170 MAX_SONG_LENGTH, &st);
171 g_strlcpy(songname, tmp, MAX_SONG_LENGTH);
172 g_free(tmp);
173 }
174 else
175 st.offset = 0;
176 }
177 #else
178 (void)second_column;
179 #endif
181 return songname;
182 }
184 static void
185 center_playing_item(struct mpdclient *c, bool center_cursor)
186 {
187 const struct mpd_song *song;
188 unsigned length = c->playlist.list->len;
189 int idx;
191 song = mpdclient_get_current_song(c);
192 if (song == NULL)
193 return;
195 /* try to center the song that are playing */
196 idx = playlist_get_index(&c->playlist, c->song);
197 if (idx < 0)
198 return;
200 if (length < lw->rows)
201 {
202 if (center_cursor)
203 list_window_set_selected(lw, idx);
204 return;
205 }
207 list_window_center(lw, length, idx);
209 if (center_cursor) {
210 list_window_set_selected(lw, idx);
211 return;
212 }
214 /* make sure the cursor is in the window */
215 if (lw->selected < lw->start + options.scroll_offset) {
216 if (lw->start > 0)
217 lw->selected = lw->start + options.scroll_offset;
218 if (lw->range_selection) {
219 lw->selected_start = lw->range_base;
220 lw->selected_end = lw->selected;
221 } else {
222 lw->selected_start = lw->selected;
223 lw->selected_end = lw->selected;
224 }
225 } else if (lw->selected > lw->start + lw->rows - 1 - options.scroll_offset) {
226 if (lw->start + lw->rows < length)
227 lw->selected = lw->start + lw->rows - 1 - options.scroll_offset;
228 if (lw->range_selection) {
229 lw->selected_start = lw->selected;
230 lw->selected_end = lw->range_base;
231 } else {
232 lw->selected_start = lw->selected;
233 lw->selected_end = lw->selected;
234 }
235 }
236 }
238 #ifndef NCMPC_MINI
239 static void
240 save_pre_completion_cb(GCompletion *gcmp, G_GNUC_UNUSED gchar *line,
241 void *data)
242 {
243 completion_callback_data_t *tmp = (completion_callback_data_t *)data;
244 GList **list = tmp->list;
245 struct mpdclient *c = tmp->c;
247 if( *list == NULL ) {
248 /* create completion list */
249 *list = gcmp_list_from_path(c, "", NULL, GCMP_TYPE_PLAYLIST);
250 g_completion_add_items(gcmp, *list);
251 }
252 }
254 static void
255 save_post_completion_cb(G_GNUC_UNUSED GCompletion *gcmp,
256 G_GNUC_UNUSED gchar *line, GList *items,
257 G_GNUC_UNUSED void *data)
258 {
259 if (g_list_length(items) >= 1)
260 screen_display_completion_list(items);
261 }
262 #endif
264 #ifndef NCMPC_MINI
265 /**
266 * Wrapper for strncmp(). We are not allowed to pass &strncmp to
267 * g_completion_set_compare(), because strncmp() takes size_t where
268 * g_completion_set_compare passes a gsize value.
269 */
270 static gint
271 completion_strncmp(const gchar *s1, const gchar *s2, gsize n)
272 {
273 return strncmp(s1, s2, n);
274 }
275 #endif
277 int
278 playlist_save(struct mpdclient *c, char *name, char *defaultname)
279 {
280 gchar *filename, *filename_utf8;
281 #ifndef NCMPC_MINI
282 GCompletion *gcmp;
283 GList *list = NULL;
284 completion_callback_data_t data;
285 #endif
287 #ifdef NCMPC_MINI
288 (void)defaultname;
289 #endif
291 #ifndef NCMPC_MINI
292 if (name == NULL) {
293 /* initialize completion support */
294 gcmp = g_completion_new(NULL);
295 g_completion_set_compare(gcmp, completion_strncmp);
296 data.list = &list;
297 data.dir_list = NULL;
298 data.c = c;
299 wrln_completion_callback_data = &data;
300 wrln_pre_completion_callback = save_pre_completion_cb;
301 wrln_post_completion_callback = save_post_completion_cb;
304 /* query the user for a filename */
305 filename = screen_readln(_("Save playlist as"),
306 defaultname,
307 NULL,
308 gcmp);
310 /* destroy completion support */
311 wrln_completion_callback_data = NULL;
312 wrln_pre_completion_callback = NULL;
313 wrln_post_completion_callback = NULL;
314 g_completion_free(gcmp);
315 list = string_list_free(list);
316 if( filename )
317 filename=g_strstrip(filename);
318 } else
319 #endif
320 filename=g_strdup(name);
322 if (filename == NULL)
323 return -1;
325 /* send save command to mpd */
327 filename_utf8 = locale_to_utf8(filename);
329 if (!mpd_run_save(c->connection, filename_utf8)) {
330 if (mpd_connection_get_error(c->connection) == MPD_ERROR_SERVER &&
331 mpd_connection_get_server_error(c->connection) == MPD_SERVER_ERROR_EXIST &&
332 mpd_connection_clear_error(c->connection)) {
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 if (!mpd_run_rm(c->connection, filename_utf8) ||
349 !mpd_run_save(c->connection, filename_utf8)) {
350 mpdclient_handle_error(c);
351 g_free(filename_utf8);
352 g_free(filename);
353 return -1;
354 }
355 } else {
356 mpdclient_handle_error(c);
357 g_free(filename_utf8);
358 g_free(filename);
359 return -1;
360 }
361 }
363 c->events |= MPD_IDLE_STORED_PLAYLIST;
365 g_free(filename_utf8);
367 /* success */
368 screen_status_printf(_("Saved %s"), filename);
369 g_free(filename);
370 return 0;
371 }
373 #ifndef NCMPC_MINI
374 static void add_dir(GCompletion *gcmp, gchar *dir, GList **dir_list,
375 GList **list, struct mpdclient *c)
376 {
377 g_completion_remove_items(gcmp, *list);
378 *list = string_list_remove(*list, dir);
379 *list = gcmp_list_from_path(c, dir, *list, GCMP_TYPE_RFILE);
380 g_completion_add_items(gcmp, *list);
381 *dir_list = g_list_append(*dir_list, g_strdup(dir));
382 }
384 static void add_pre_completion_cb(GCompletion *gcmp, gchar *line, void *data)
385 {
386 completion_callback_data_t *tmp = (completion_callback_data_t *)data;
387 GList **dir_list = tmp->dir_list;
388 GList **list = tmp->list;
389 struct mpdclient *c = tmp->c;
391 if (*list == NULL) {
392 /* create initial list */
393 *list = gcmp_list_from_path(c, "", NULL, GCMP_TYPE_RFILE);
394 g_completion_add_items(gcmp, *list);
395 } else if (line && line[0] && line[strlen(line)-1]=='/' &&
396 string_list_find(*dir_list, line) == NULL) {
397 /* add directory content to list */
398 add_dir(gcmp, line, dir_list, list, c);
399 }
400 }
402 static void add_post_completion_cb(GCompletion *gcmp, gchar *line,
403 GList *items, void *data)
404 {
405 completion_callback_data_t *tmp = (completion_callback_data_t *)data;
406 GList **dir_list = tmp->dir_list;
407 GList **list = tmp->list;
408 struct mpdclient *c = tmp->c;
410 if (g_list_length(items) >= 1)
411 screen_display_completion_list(items);
413 if (line && line[0] && line[strlen(line) - 1] == '/' &&
414 string_list_find(*dir_list, line) == NULL) {
415 /* add directory content to list */
416 add_dir(gcmp, line, dir_list, list, c);
417 }
418 }
419 #endif
421 static int
422 handle_add_to_playlist(struct mpdclient *c)
423 {
424 gchar *path;
425 #ifndef NCMPC_MINI
426 GCompletion *gcmp;
427 GList *list = NULL;
428 GList *dir_list = NULL;
429 completion_callback_data_t data;
431 /* initialize completion support */
432 gcmp = g_completion_new(NULL);
433 g_completion_set_compare(gcmp, completion_strncmp);
434 data.list = &list;
435 data.dir_list = &dir_list;
436 data.c = c;
437 wrln_completion_callback_data = &data;
438 wrln_pre_completion_callback = add_pre_completion_cb;
439 wrln_post_completion_callback = add_post_completion_cb;
440 #endif
442 /* get path */
443 path = screen_readln(_("Add"),
444 NULL,
445 NULL,
446 #ifdef NCMPC_MINI
447 NULL
448 #else
449 gcmp
450 #endif
451 );
453 /* destroy completion data */
454 #ifndef NCMPC_MINI
455 wrln_completion_callback_data = NULL;
456 wrln_pre_completion_callback = NULL;
457 wrln_post_completion_callback = NULL;
458 g_completion_free(gcmp);
459 string_list_free(list);
460 string_list_free(dir_list);
461 #endif
463 /* add the path to the playlist */
464 if (path != NULL) {
465 char *path_utf8 = locale_to_utf8(path);
466 mpdclient_cmd_add_path(c, path_utf8);
467 g_free(path_utf8);
468 }
470 g_free(path);
471 return 0;
472 }
474 static void
475 screen_playlist_init(WINDOW *w, int cols, int rows)
476 {
477 lw = list_window_init(w, cols, rows);
478 }
480 static gboolean
481 timer_hide_cursor(gpointer data)
482 {
483 struct mpdclient *c = data;
485 assert(options.hide_cursor > 0);
486 assert(timer_hide_cursor_id != 0);
488 timer_hide_cursor_id = 0;
490 /* hide the cursor when mpd is playing and the user is inactive */
492 if (c->status != NULL &&
493 mpd_status_get_state(c->status) == MPD_STATE_PLAY) {
494 lw->hide_cursor = true;
495 playlist_repaint();
496 } else
497 timer_hide_cursor_id = g_timeout_add(options.hide_cursor * 1000,
498 timer_hide_cursor, c);
500 return FALSE;
501 }
503 static void
504 screen_playlist_open(struct mpdclient *c)
505 {
506 playlist = &c->playlist;
508 assert(timer_hide_cursor_id == 0);
509 if (options.hide_cursor > 0) {
510 lw->hide_cursor = false;
511 timer_hide_cursor_id = g_timeout_add(options.hide_cursor * 1000,
512 timer_hide_cursor, c);
513 }
515 playlist_restore_selection();
516 }
518 static void
519 screen_playlist_close(void)
520 {
521 if (timer_hide_cursor_id != 0) {
522 g_source_remove(timer_hide_cursor_id);
523 timer_hide_cursor_id = 0;
524 }
525 }
527 static void
528 screen_playlist_resize(int cols, int rows)
529 {
530 lw->cols = cols;
531 lw->rows = rows;
532 }
535 static void
536 screen_playlist_exit(void)
537 {
538 list_window_free(lw);
539 }
541 static const char *
542 screen_playlist_title(char *str, size_t size)
543 {
544 if (options.host == NULL)
545 return _("Playlist");
547 g_snprintf(str, size, _("Playlist on %s"), options.host);
548 return str;
549 }
551 static void
552 screen_playlist_paint(void)
553 {
554 #ifndef NCMPC_MINI
555 must_scroll = false;
556 #endif
558 list_window_paint(lw, list_callback, NULL);
559 }
561 static void
562 screen_playlist_update(struct mpdclient *c)
563 {
564 static int prev_song_id = -1;
566 if (c->events & MPD_IDLE_PLAYLIST)
567 playlist_restore_selection();
569 current_song_id = c->status != NULL &&
570 (mpd_status_get_state(c->status) == MPD_STATE_PLAY ||
571 mpd_status_get_state(c->status) == MPD_STATE_PAUSE)
572 ? (int)mpd_status_get_song_id(c->status) : -1;
574 if (current_song_id != prev_song_id) {
575 prev_song_id = current_song_id;
577 /* center the cursor */
578 if (options.auto_center && current_song_id != -1 && ! lw->range_selection)
579 center_playing_item(c, false);
581 playlist_repaint();
582 #ifndef NCMPC_MINI
583 } else if (options.scroll && must_scroll) {
584 /* always repaint if horizontal scrolling is
585 enabled */
586 playlist_repaint();
587 #endif
588 } else if (c->events & MPD_IDLE_PLAYLIST) {
589 /* the playlist has changed, we must paint the new
590 version */
591 playlist_repaint();
592 }
593 }
595 #ifdef HAVE_GETMOUSE
596 static bool
597 handle_mouse_event(struct mpdclient *c)
598 {
599 int row;
600 unsigned selected;
601 unsigned long bstate;
603 if (screen_get_mouse_event(c, &bstate, &row) ||
604 list_window_mouse(lw, playlist_length(playlist), bstate, row)) {
605 playlist_repaint();
606 return true;
607 }
609 if (bstate & BUTTON1_DOUBLE_CLICKED) {
610 /* stop */
611 screen_cmd(c, CMD_STOP);
612 return true;
613 }
615 selected = lw->start + row;
617 if (bstate & BUTTON1_CLICKED) {
618 /* play */
619 if (lw->start + row < playlist_length(playlist))
620 mpdclient_cmd_play(c, lw->start + row);
621 } else if (bstate & BUTTON3_CLICKED) {
622 /* delete */
623 if (selected == lw->selected)
624 mpdclient_cmd_delete(c, lw->selected);
625 }
627 lw->selected = selected;
628 list_window_check_selected(lw, playlist_length(playlist));
629 playlist_save_selection();
630 playlist_repaint();
632 return true;
633 }
634 #endif
636 static bool
637 screen_playlist_cmd(struct mpdclient *c, command_t cmd)
638 {
639 static command_t cached_cmd = CMD_NONE;
640 command_t prev_cmd = cached_cmd;
641 cached_cmd = cmd;
643 lw->hide_cursor = false;
645 if (options.hide_cursor > 0) {
646 if (timer_hide_cursor_id != 0)
647 g_source_remove(timer_hide_cursor_id);
648 timer_hide_cursor_id = g_timeout_add(options.hide_cursor * 1000,
649 timer_hide_cursor, c);
650 }
652 if (list_window_cmd(lw, playlist_length(&c->playlist), cmd)) {
653 playlist_save_selection();
654 playlist_repaint();
655 return true;
656 }
658 switch(cmd) {
659 case CMD_SCREEN_UPDATE:
660 center_playing_item(c, prev_cmd == CMD_SCREEN_UPDATE);
661 playlist_repaint();
662 return false;
663 case CMD_SELECT_PLAYING:
664 list_window_set_selected(lw, playlist_get_index(&c->playlist,
665 c->song));
666 playlist_save_selection();
667 return true;
669 case CMD_LIST_FIND:
670 case CMD_LIST_RFIND:
671 case CMD_LIST_FIND_NEXT:
672 case CMD_LIST_RFIND_NEXT:
673 screen_find(lw, playlist_length(&c->playlist),
674 cmd, list_callback, NULL);
675 playlist_save_selection();
676 playlist_repaint();
677 return true;
678 case CMD_LIST_JUMP:
679 screen_jump(lw, list_callback, NULL);
680 playlist_save_selection();
681 playlist_repaint();
682 return true;
684 #ifdef HAVE_GETMOUSE
685 case CMD_MOUSE_EVENT:
686 return handle_mouse_event(c);
687 #endif
689 #ifdef ENABLE_SONG_SCREEN
690 case CMD_SCREEN_SONG:
691 if (playlist_selected_song()) {
692 screen_song_switch(c, playlist_selected_song());
693 return true;
694 }
696 break;
697 #endif
699 #ifdef ENABLE_LYRICS_SCREEN
700 case CMD_SCREEN_LYRICS:
701 if (lw->selected < playlist_length(&c->playlist)) {
702 struct mpd_song *selected = playlist_get(&c->playlist, lw->selected);
703 bool follow = false;
705 if (c->song && selected &&
706 !strcmp(mpd_song_get_uri(selected),
707 mpd_song_get_uri(c->song)))
708 follow = true;
710 screen_lyrics_switch(c, selected, follow);
711 return true;
712 }
714 break;
715 #endif
716 case CMD_SCREEN_SWAP:
717 screen_swap(c, playlist_get(&c->playlist, lw->selected));
718 return true;
720 default:
721 break;
722 }
724 if (!mpdclient_is_connected(c))
725 return false;
727 switch(cmd) {
728 case CMD_PLAY:
729 mpdclient_cmd_play(c, lw->selected);
730 return true;
732 case CMD_DELETE:
733 if (lw->range_selection) {
734 mpdclient_cmd_delete_range(c, lw->selected_start,
735 lw->selected_end + 1);
736 } else {
737 mpdclient_cmd_delete(c, lw->selected);
738 }
740 lw->selected = lw->selected_end = lw->selected_start;
741 lw->range_selection = false;
742 return true;
744 case CMD_SAVE_PLAYLIST:
745 playlist_save(c, NULL, NULL);
746 return true;
748 case CMD_ADD:
749 handle_add_to_playlist(c);
750 return true;
752 case CMD_SHUFFLE:
753 {
754 if(!lw->range_selection)
755 /* No range selection, shuffle all list. */
756 break;
758 if (mpd_run_shuffle_range(c->connection, lw->selected_start,
759 lw->selected_end + 1))
760 screen_status_message(_("Shuffled playlist"));
761 else
762 mpdclient_handle_error(c);
763 return true;
764 }
766 case CMD_LIST_MOVE_UP:
767 if(lw->selected_start == 0)
768 return false;
769 if(lw->range_selection)
770 {
771 unsigned i = lw->selected_start;
772 unsigned last_selected = lw->selected;
773 for(; i <= lw->selected_end; ++i)
774 mpdclient_cmd_move(c, i, i-1);
775 lw->selected_start--;
776 lw->selected_end--;
777 lw->selected = last_selected - 1;
778 lw->range_base--;
779 }
780 else
781 {
782 mpdclient_cmd_move(c, lw->selected, lw->selected-1);
783 lw->selected--;
784 lw->selected_start--;
785 lw->selected_end--;
786 }
788 playlist_save_selection();
789 return true;
791 case CMD_LIST_MOVE_DOWN:
792 if(lw->selected_end+1 >= playlist_length(&c->playlist))
793 return false;
794 if(lw->range_selection)
795 {
796 int i = lw->selected_end;
797 unsigned last_selected = lw->selected;
798 for(; i >= (int)lw->selected_start; --i)
799 mpdclient_cmd_move(c, i, i+1);
800 lw->selected_start++;
801 lw->selected_end++;
802 lw->selected = last_selected + 1;
803 lw->range_base++;
804 }
805 else
806 {
807 mpdclient_cmd_move(c, lw->selected, lw->selected+1);
808 lw->selected++;
809 lw->selected_start++;
810 lw->selected_end++;
811 }
813 playlist_save_selection();
814 return true;
816 case CMD_LOCATE:
817 if (playlist_selected_song()) {
818 screen_file_goto_song(c, playlist_selected_song());
819 return true;
820 }
822 break;
824 default:
825 break;
826 }
828 return false;
829 }
831 const struct screen_functions screen_playlist = {
832 .init = screen_playlist_init,
833 .exit = screen_playlist_exit,
834 .open = screen_playlist_open,
835 .close = screen_playlist_close,
836 .resize = screen_playlist_resize,
837 .paint = screen_playlist_paint,
838 .update = screen_playlist_update,
839 .cmd = screen_playlist_cmd,
840 .get_title = screen_playlist_title,
841 };