1 /* ncmpc (Ncurses MPD Client)
2 * (c) 2004-2010 The Music Player Daemon Project
3 * Project homepage: http://musicpd.org
4 *
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.
9 *
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.
14 *
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_queue.h"
21 #include "screen_interface.h"
22 #include "screen_file.h"
23 #include "screen_status.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 "song_paint.h"
34 #include "screen.h"
35 #include "screen_utils.h"
36 #include "screen_song.h"
37 #include "screen_lyrics.h"
38 #include "Compiler.h"
40 #ifndef NCMPC_MINI
41 #include "hscroll.h"
42 #endif
44 #include <mpd/client.h>
46 #include <ctype.h>
47 #include <string.h>
48 #include <glib.h>
50 #define MAX_SONG_LENGTH 512
52 #ifndef NCMPC_MINI
53 typedef struct
54 {
55 GList **list;
56 GList **dir_list;
57 struct mpdclient *c;
58 } completion_callback_data_t;
60 static struct hscroll hscroll;
61 #endif
63 static struct mpdclient_playlist *playlist;
64 static int current_song_id = -1;
65 static int selected_song_id = -1;
66 static struct list_window *lw;
67 static guint timer_hide_cursor_id;
69 static void
70 screen_queue_paint(void);
72 static void
73 screen_queue_repaint(void)
74 {
75 screen_queue_paint();
76 wrefresh(lw->w);
77 }
79 static const struct mpd_song *
80 screen_queue_selected_song(void)
81 {
82 return !lw->range_selection &&
83 lw->selected < playlist_length(playlist)
84 ? playlist_get(playlist, lw->selected)
85 : NULL;
86 }
88 static void
89 screen_queue_save_selection(void)
90 {
91 selected_song_id = screen_queue_selected_song() != NULL
92 ? (int)mpd_song_get_id(screen_queue_selected_song())
93 : -1;
94 }
96 static void
97 screen_queue_restore_selection(void)
98 {
99 const struct mpd_song *song;
100 int pos;
102 list_window_set_length(lw, playlist_length(playlist));
104 if (selected_song_id < 0)
105 /* there was no selection */
106 return;
108 song = screen_queue_selected_song();
109 if (song != NULL &&
110 mpd_song_get_id(song) == (unsigned)selected_song_id)
111 /* selection is still valid */
112 return;
114 pos = playlist_get_index_from_id(playlist, selected_song_id);
115 if (pos >= 0)
116 list_window_set_cursor(lw, pos);
118 screen_queue_save_selection();
119 }
121 static const char *
122 screen_queue_lw_callback(unsigned idx, gcc_unused void *data)
123 {
124 static char songname[MAX_SONG_LENGTH];
125 struct mpd_song *song;
127 assert(playlist != NULL);
128 assert(idx < playlist_length(playlist));
130 song = playlist_get(playlist, idx);
132 strfsong(songname, MAX_SONG_LENGTH, options.list_format, song);
134 return songname;
135 }
137 static void
138 center_playing_item(const struct mpd_status *status, bool center_cursor)
139 {
140 int idx;
142 if (status == NULL ||
143 (mpd_status_get_state(status) != MPD_STATE_PLAY &&
144 mpd_status_get_state(status) != MPD_STATE_PAUSE))
145 return;
147 /* try to center the song that are playing */
148 idx = mpd_status_get_song_pos(status);
149 if (idx < 0)
150 return;
152 list_window_center(lw, idx);
154 if (center_cursor) {
155 list_window_set_cursor(lw, idx);
156 return;
157 }
159 /* make sure the cursor is in the window */
160 list_window_fetch_cursor(lw);
161 }
163 gcc_pure
164 static int
165 get_current_song_id(const struct mpd_status *status)
166 {
167 return status != NULL &&
168 (mpd_status_get_state(status) == MPD_STATE_PLAY ||
169 mpd_status_get_state(status) == MPD_STATE_PAUSE)
170 ? (int)mpd_status_get_song_id(status)
171 : -1;
172 }
174 static bool
175 screen_queue_song_change(const struct mpd_status *status)
176 {
177 if (get_current_song_id(status) == current_song_id)
178 return false;
180 current_song_id = get_current_song_id(status);
182 /* center the cursor */
183 if (options.auto_center && !lw->range_selection)
184 center_playing_item(status, false);
186 return true;
187 }
189 #ifndef NCMPC_MINI
190 static void
191 save_pre_completion_cb(GCompletion *gcmp, gcc_unused gchar *line,
192 void *data)
193 {
194 completion_callback_data_t *tmp = (completion_callback_data_t *)data;
195 GList **list = tmp->list;
196 struct mpdclient *c = tmp->c;
198 if( *list == NULL ) {
199 /* create completion list */
200 *list = gcmp_list_from_path(c, "", NULL, GCMP_TYPE_PLAYLIST);
201 g_completion_add_items(gcmp, *list);
202 }
203 }
205 static void
206 save_post_completion_cb(gcc_unused GCompletion *gcmp,
207 gcc_unused gchar *line, GList *items,
208 gcc_unused void *data)
209 {
210 if (g_list_length(items) >= 1)
211 screen_display_completion_list(items);
212 }
213 #endif
215 #ifndef NCMPC_MINI
216 /**
217 * Wrapper for strncmp(). We are not allowed to pass &strncmp to
218 * g_completion_set_compare(), because strncmp() takes size_t where
219 * g_completion_set_compare passes a gsize value.
220 */
221 static gint
222 completion_strncmp(const gchar *s1, const gchar *s2, gsize n)
223 {
224 return strncmp(s1, s2, n);
225 }
226 #endif
228 int
229 playlist_save(struct mpdclient *c, char *name, char *defaultname)
230 {
231 struct mpd_connection *connection;
232 gchar *filename, *filename_utf8;
233 #ifndef NCMPC_MINI
234 GCompletion *gcmp;
235 GList *list = NULL;
236 completion_callback_data_t data;
237 #endif
239 #ifdef NCMPC_MINI
240 (void)defaultname;
241 #endif
243 #ifndef NCMPC_MINI
244 if (name == NULL) {
245 /* initialize completion support */
246 gcmp = g_completion_new(NULL);
247 g_completion_set_compare(gcmp, completion_strncmp);
248 data.list = &list;
249 data.dir_list = NULL;
250 data.c = c;
251 wrln_completion_callback_data = &data;
252 wrln_pre_completion_callback = save_pre_completion_cb;
253 wrln_post_completion_callback = save_post_completion_cb;
256 /* query the user for a filename */
257 filename = screen_readln(_("Save queue as"),
258 defaultname,
259 NULL,
260 gcmp);
262 /* destroy completion support */
263 wrln_completion_callback_data = NULL;
264 wrln_pre_completion_callback = NULL;
265 wrln_post_completion_callback = NULL;
266 g_completion_free(gcmp);
267 list = string_list_free(list);
268 if( filename )
269 filename=g_strstrip(filename);
270 } else
271 #endif
272 filename=g_strdup(name);
274 if (filename == NULL)
275 return -1;
277 /* send save command to mpd */
279 connection = mpdclient_get_connection(c);
280 if (connection == NULL) {
281 g_free(filename);
282 return -1;
283 }
285 filename_utf8 = locale_to_utf8(filename);
286 if (!mpd_run_save(connection, filename_utf8)) {
287 if (mpd_connection_get_error(connection) == MPD_ERROR_SERVER &&
288 mpd_connection_get_server_error(connection) == MPD_SERVER_ERROR_EXIST &&
289 mpd_connection_clear_error(connection)) {
290 char *buf;
291 bool replace;
293 buf = g_strdup_printf(_("Replace %s [%s/%s] ? "),
294 filename, YES, NO);
295 replace = screen_get_yesno(buf, false);
296 g_free(buf);
298 if (!replace) {
299 g_free(filename_utf8);
300 g_free(filename);
301 screen_status_printf(_("Aborted"));
302 return -1;
303 }
305 if (!mpd_run_rm(connection, filename_utf8) ||
306 !mpd_run_save(connection, filename_utf8)) {
307 mpdclient_handle_error(c);
308 g_free(filename_utf8);
309 g_free(filename);
310 return -1;
311 }
312 } else {
313 mpdclient_handle_error(c);
314 g_free(filename_utf8);
315 g_free(filename);
316 return -1;
317 }
318 }
320 c->events |= MPD_IDLE_STORED_PLAYLIST;
322 g_free(filename_utf8);
324 /* success */
325 screen_status_printf(_("Saved %s"), filename);
326 g_free(filename);
327 return 0;
328 }
330 #ifndef NCMPC_MINI
331 static void add_dir(GCompletion *gcmp, gchar *dir, GList **dir_list,
332 GList **list, struct mpdclient *c)
333 {
334 g_completion_remove_items(gcmp, *list);
335 *list = string_list_remove(*list, dir);
336 *list = gcmp_list_from_path(c, dir, *list, GCMP_TYPE_RFILE);
337 g_completion_add_items(gcmp, *list);
338 *dir_list = g_list_append(*dir_list, g_strdup(dir));
339 }
341 static void add_pre_completion_cb(GCompletion *gcmp, gchar *line, void *data)
342 {
343 completion_callback_data_t *tmp = (completion_callback_data_t *)data;
344 GList **dir_list = tmp->dir_list;
345 GList **list = tmp->list;
346 struct mpdclient *c = tmp->c;
348 if (*list == NULL) {
349 /* create initial list */
350 *list = gcmp_list_from_path(c, "", NULL, GCMP_TYPE_RFILE);
351 g_completion_add_items(gcmp, *list);
352 } else if (line && line[0] && line[strlen(line)-1]=='/' &&
353 string_list_find(*dir_list, line) == NULL) {
354 /* add directory content to list */
355 add_dir(gcmp, line, dir_list, list, c);
356 }
357 }
359 static void add_post_completion_cb(GCompletion *gcmp, gchar *line,
360 GList *items, void *data)
361 {
362 completion_callback_data_t *tmp = (completion_callback_data_t *)data;
363 GList **dir_list = tmp->dir_list;
364 GList **list = tmp->list;
365 struct mpdclient *c = tmp->c;
367 if (g_list_length(items) >= 1)
368 screen_display_completion_list(items);
370 if (line && line[0] && line[strlen(line) - 1] == '/' &&
371 string_list_find(*dir_list, line) == NULL) {
372 /* add directory content to list */
373 add_dir(gcmp, line, dir_list, list, c);
374 }
375 }
376 #endif
378 static int
379 handle_add_to_playlist(struct mpdclient *c)
380 {
381 gchar *path;
382 GCompletion *gcmp;
383 #ifndef NCMPC_MINI
384 GList *list = NULL;
385 GList *dir_list = NULL;
386 completion_callback_data_t data;
388 /* initialize completion support */
389 gcmp = g_completion_new(NULL);
390 g_completion_set_compare(gcmp, completion_strncmp);
391 data.list = &list;
392 data.dir_list = &dir_list;
393 data.c = c;
394 wrln_completion_callback_data = &data;
395 wrln_pre_completion_callback = add_pre_completion_cb;
396 wrln_post_completion_callback = add_post_completion_cb;
397 #else
398 gcmp = NULL;
399 #endif
401 /* get path */
402 path = screen_readln(_("Add"),
403 NULL,
404 NULL,
405 gcmp);
407 /* destroy completion data */
408 #ifndef NCMPC_MINI
409 wrln_completion_callback_data = NULL;
410 wrln_pre_completion_callback = NULL;
411 wrln_post_completion_callback = NULL;
412 g_completion_free(gcmp);
413 string_list_free(list);
414 string_list_free(dir_list);
415 #endif
417 /* add the path to the playlist */
418 if (path != NULL) {
419 char *path_utf8 = locale_to_utf8(path);
420 mpdclient_cmd_add_path(c, path_utf8);
421 g_free(path_utf8);
422 }
424 g_free(path);
425 return 0;
426 }
428 static void
429 screen_queue_init(WINDOW *w, int cols, int rows)
430 {
431 lw = list_window_init(w, cols, rows);
433 #ifndef NCMPC_MINI
434 if (options.scroll)
435 hscroll_init(&hscroll, w, options.scroll_sep);
436 #endif
437 }
439 static gboolean
440 timer_hide_cursor(gpointer data)
441 {
442 struct mpdclient *c = data;
444 assert(options.hide_cursor > 0);
445 assert(timer_hide_cursor_id != 0);
447 timer_hide_cursor_id = 0;
449 /* hide the cursor when mpd is playing and the user is inactive */
451 if (c->status != NULL &&
452 mpd_status_get_state(c->status) == MPD_STATE_PLAY) {
453 lw->hide_cursor = true;
454 screen_queue_repaint();
455 } else
456 timer_hide_cursor_id = g_timeout_add(options.hide_cursor * 1000,
457 timer_hide_cursor, c);
459 return FALSE;
460 }
462 static void
463 screen_queue_open(struct mpdclient *c)
464 {
465 playlist = &c->playlist;
467 assert(timer_hide_cursor_id == 0);
468 if (options.hide_cursor > 0) {
469 lw->hide_cursor = false;
470 timer_hide_cursor_id = g_timeout_add(options.hide_cursor * 1000,
471 timer_hide_cursor, c);
472 }
474 screen_queue_restore_selection();
475 screen_queue_song_change(c->status);
476 }
478 static void
479 screen_queue_close(void)
480 {
481 if (timer_hide_cursor_id != 0) {
482 g_source_remove(timer_hide_cursor_id);
483 timer_hide_cursor_id = 0;
484 }
486 #ifndef NCMPC_MINI
487 if (options.scroll)
488 hscroll_clear(&hscroll);
489 #endif
490 }
492 static void
493 screen_queue_resize(int cols, int rows)
494 {
495 list_window_resize(lw, cols, rows);
496 }
499 static void
500 screen_queue_exit(void)
501 {
502 list_window_free(lw);
503 }
505 static const char *
506 screen_queue_title(char *str, size_t size)
507 {
508 if (options.host == NULL)
509 return _("Queue");
511 g_snprintf(str, size, _("Queue on %s"), options.host);
512 return str;
513 }
515 static void
516 screen_queue_paint_callback(WINDOW *w, unsigned i,
517 unsigned y, unsigned width,
518 bool selected, gcc_unused void *data)
519 {
520 const struct mpd_song *song;
521 struct hscroll *row_hscroll;
523 assert(playlist != NULL);
524 assert(i < playlist_length(playlist));
526 song = playlist_get(playlist, i);
528 #ifdef NCMPC_MINI
529 row_hscroll = NULL;
530 #else
531 row_hscroll = selected && options.scroll && lw->selected == i
532 ? &hscroll : NULL;
533 #endif
535 paint_song_row(w, y, width, selected,
536 (int)mpd_song_get_id(song) == current_song_id,
537 song, row_hscroll);
538 }
540 static void
541 screen_queue_paint(void)
542 {
543 #ifndef NCMPC_MINI
544 if (options.scroll)
545 hscroll_clear(&hscroll);
546 #endif
548 list_window_paint2(lw, screen_queue_paint_callback, NULL);
549 }
551 static void
552 screen_queue_update(struct mpdclient *c)
553 {
554 if (c->events & MPD_IDLE_QUEUE)
555 screen_queue_restore_selection();
556 else
557 /* the queue size may have changed, even if we havn't
558 received the QUEUE idle event yet */
559 list_window_set_length(lw, playlist_length(playlist));
561 if (((c->events & MPD_IDLE_PLAYER) != 0 &&
562 screen_queue_song_change(c->status)) ||
563 c->events & MPD_IDLE_QUEUE)
564 /* the queue or the current song has changed, we must
565 paint the new version */
566 screen_queue_repaint();
567 }
569 #ifdef HAVE_GETMOUSE
570 static bool
571 handle_mouse_event(struct mpdclient *c)
572 {
573 int row;
574 unsigned long bstate;
575 unsigned old_selected;
577 if (screen_get_mouse_event(c, &bstate, &row) ||
578 list_window_mouse(lw, bstate, row)) {
579 screen_queue_repaint();
580 return true;
581 }
583 if (bstate & BUTTON1_DOUBLE_CLICKED) {
584 /* stop */
585 screen_cmd(c, CMD_STOP);
586 return true;
587 }
589 old_selected = lw->selected;
590 list_window_set_cursor(lw, lw->start + row);
592 if (bstate & BUTTON1_CLICKED) {
593 /* play */
594 const struct mpd_song *song = screen_queue_selected_song();
595 if (song != NULL) {
596 struct mpd_connection *connection =
597 mpdclient_get_connection(c);
599 if (connection != NULL &&
600 !mpd_run_play_id(connection,
601 mpd_song_get_id(song)))
602 mpdclient_handle_error(c);
603 }
604 } else if (bstate & BUTTON3_CLICKED) {
605 /* delete */
606 if (lw->selected == old_selected)
607 mpdclient_cmd_delete(c, lw->selected);
609 list_window_set_length(lw, playlist_length(playlist));
610 }
612 screen_queue_save_selection();
613 screen_queue_repaint();
615 return true;
616 }
617 #endif
619 static bool
620 screen_queue_cmd(struct mpdclient *c, command_t cmd)
621 {
622 struct mpd_connection *connection;
623 static command_t cached_cmd = CMD_NONE;
624 command_t prev_cmd = cached_cmd;
625 struct list_window_range range;
626 const struct mpd_song *song;
628 cached_cmd = cmd;
630 lw->hide_cursor = false;
632 if (options.hide_cursor > 0) {
633 if (timer_hide_cursor_id != 0)
634 g_source_remove(timer_hide_cursor_id);
635 timer_hide_cursor_id = g_timeout_add(options.hide_cursor * 1000,
636 timer_hide_cursor, c);
637 }
639 if (list_window_cmd(lw, cmd)) {
640 screen_queue_save_selection();
641 screen_queue_repaint();
642 return true;
643 }
645 switch(cmd) {
646 case CMD_SCREEN_UPDATE:
647 center_playing_item(c->status, prev_cmd == CMD_SCREEN_UPDATE);
648 screen_queue_repaint();
649 return false;
650 case CMD_SELECT_PLAYING:
651 list_window_set_cursor(lw, playlist_get_index(&c->playlist,
652 c->song));
653 screen_queue_save_selection();
654 screen_queue_repaint();
655 return true;
657 case CMD_LIST_FIND:
658 case CMD_LIST_RFIND:
659 case CMD_LIST_FIND_NEXT:
660 case CMD_LIST_RFIND_NEXT:
661 screen_find(lw, cmd, screen_queue_lw_callback, NULL);
662 screen_queue_save_selection();
663 screen_queue_repaint();
664 return true;
665 case CMD_LIST_JUMP:
666 screen_jump(lw, screen_queue_lw_callback, NULL, NULL);
667 screen_queue_save_selection();
668 screen_queue_repaint();
669 return true;
671 #ifdef HAVE_GETMOUSE
672 case CMD_MOUSE_EVENT:
673 return handle_mouse_event(c);
674 #endif
676 #ifdef ENABLE_SONG_SCREEN
677 case CMD_SCREEN_SONG:
678 if (screen_queue_selected_song() != NULL) {
679 screen_song_switch(c, screen_queue_selected_song());
680 return true;
681 }
683 break;
684 #endif
686 #ifdef ENABLE_LYRICS_SCREEN
687 case CMD_SCREEN_LYRICS:
688 if (lw->selected < playlist_length(&c->playlist)) {
689 struct mpd_song *selected = playlist_get(&c->playlist, lw->selected);
690 bool follow = false;
692 if (c->song && selected &&
693 !strcmp(mpd_song_get_uri(selected),
694 mpd_song_get_uri(c->song)))
695 follow = true;
697 screen_lyrics_switch(c, selected, follow);
698 return true;
699 }
701 break;
702 #endif
703 case CMD_SCREEN_SWAP:
704 if (playlist_length(&c->playlist) > 0)
705 screen_swap(c, playlist_get(&c->playlist, lw->selected));
706 else
707 screen_swap(c, NULL);
708 return true;
710 default:
711 break;
712 }
714 if (!mpdclient_is_connected(c))
715 return false;
717 switch(cmd) {
718 case CMD_PLAY:
719 song = screen_queue_selected_song();
720 if (song == NULL)
721 return false;
723 connection = mpdclient_get_connection(c);
724 if (connection != NULL &&
725 !mpd_run_play_id(connection, mpd_song_get_id(song)))
726 mpdclient_handle_error(c);
728 return true;
730 case CMD_DELETE:
731 list_window_get_range(lw, &range);
732 mpdclient_cmd_delete_range(c, range.start, range.end);
734 list_window_set_cursor(lw, range.start);
735 return true;
737 case CMD_SAVE_PLAYLIST:
738 playlist_save(c, NULL, NULL);
739 return true;
741 case CMD_ADD:
742 handle_add_to_playlist(c);
743 return true;
745 case CMD_SHUFFLE:
746 list_window_get_range(lw, &range);
747 if (range.end <= range.start + 1)
748 /* No range selection, shuffle all list. */
749 break;
751 connection = mpdclient_get_connection(c);
752 if (connection == NULL)
753 return true;
755 if (mpd_run_shuffle_range(connection, range.start, range.end))
756 screen_status_message(_("Shuffled queue"));
757 else
758 mpdclient_handle_error(c);
759 return true;
761 case CMD_LIST_MOVE_UP:
762 list_window_get_range(lw, &range);
763 if (range.start == 0 || range.end <= range.start)
764 return false;
766 if (!mpdclient_cmd_move(c, range.end - 1, range.start - 1))
767 return true;
769 lw->selected--;
770 lw->range_base--;
772 if (lw->range_selection)
773 list_window_scroll_to(lw, lw->range_base);
774 list_window_scroll_to(lw, lw->selected);
776 screen_queue_save_selection();
777 return true;
779 case CMD_LIST_MOVE_DOWN:
780 list_window_get_range(lw, &range);
781 if (range.end >= playlist_length(&c->playlist))
782 return false;
784 if (!mpdclient_cmd_move(c, range.start, range.end))
785 return true;
787 lw->selected++;
788 lw->range_base++;
790 if (lw->range_selection)
791 list_window_scroll_to(lw, lw->range_base);
792 list_window_scroll_to(lw, lw->selected);
794 screen_queue_save_selection();
795 return true;
797 case CMD_LOCATE:
798 if (screen_queue_selected_song() != NULL) {
799 screen_file_goto_song(c, screen_queue_selected_song());
800 return true;
801 }
803 break;
805 default:
806 break;
807 }
809 return false;
810 }
812 const struct screen_functions screen_queue = {
813 .init = screen_queue_init,
814 .exit = screen_queue_exit,
815 .open = screen_queue_open,
816 .close = screen_queue_close,
817 .resize = screen_queue_resize,
818 .paint = screen_queue_paint,
819 .update = screen_queue_update,
820 .cmd = screen_queue_cmd,
821 .get_title = screen_queue_title,
822 };