46ad68eb24dead84bafa53c51bd8fe6c3505d2e6
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->range_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 char *
100 format_duration(int duration)
101 {
102 if (duration == MPD_SONG_NO_TIME)
103 return NULL;
105 return g_strdup_printf("%d:%02d", duration / 60, duration % 60);
106 }
108 static const char *
109 list_callback(unsigned idx, bool *highlight, char **second_column, G_GNUC_UNUSED void *data)
110 {
111 static char songname[MAX_SONG_LENGTH];
112 #ifndef NCMPC_MINI
113 static scroll_state_t st;
114 #endif
115 mpd_Song *song;
117 if (playlist == NULL || idx >= playlist_length(playlist))
118 return NULL;
120 song = playlist_get(playlist, idx);
121 if (song->id == current_song_id)
122 *highlight = true;
124 strfsong(songname, MAX_SONG_LENGTH, options.list_format, song);
126 #ifndef NCMPC_MINI
127 if(second_column)
128 *second_column = format_duration(song->time);
130 if (idx == lw->selected)
131 {
132 if (options.scroll && utf8_width(songname) > (unsigned)(COLS - strlen(*second_column) - 1) )
133 {
134 static unsigned current_song;
135 char *tmp;
137 if (current_song != lw->selected) {
138 st.offset = 0;
139 current_song = lw->selected;
140 }
142 tmp = strscroll(songname, options.scroll_sep,
143 MAX_SONG_LENGTH, &st);
144 g_strlcpy(songname, tmp, MAX_SONG_LENGTH);
145 g_free(tmp);
146 }
147 else
148 st.offset = 0;
149 }
150 #else
151 (void)second_column;
152 #endif
154 return songname;
155 }
157 static void
158 center_playing_item(mpdclient_t *c, bool center_cursor)
159 {
160 unsigned length = c->playlist.list->len;
161 int idx;
163 if (!c->song || c->status == NULL ||
164 IS_STOPPED(c->status->state))
165 return;
167 /* try to center the song that are playing */
168 idx = playlist_get_index(c, c->song);
169 if (idx < 0)
170 return;
172 if (length < lw->rows)
173 {
174 if (center_cursor)
175 list_window_set_selected(lw, idx);
176 return;
177 }
179 list_window_center(lw, length, idx);
181 if (center_cursor) {
182 list_window_set_selected(lw, idx);
183 return;
184 }
186 /* make sure the cursor is in the window */
187 if (lw->selected < lw->start + options.scroll_offset) {
188 if (lw->start > 0)
189 lw->selected = lw->start + options.scroll_offset;
190 if (lw->range_selection) {
191 lw->selected_start = lw->range_base;
192 lw->selected_end = lw->selected;
193 } else {
194 lw->selected_start = lw->selected;
195 lw->selected_end = lw->selected;
196 }
197 } else if (lw->selected > lw->start + lw->rows - 1 - options.scroll_offset) {
198 if (lw->start + lw->rows < length)
199 lw->selected = lw->start + lw->rows - 1 - options.scroll_offset;
200 if (lw->range_selection) {
201 lw->selected_start = lw->selected;
202 lw->selected_end = lw->range_base;
203 } else {
204 lw->selected_start = lw->selected;
205 lw->selected_end = lw->selected;
206 }
207 }
208 }
210 #ifndef NCMPC_MINI
211 static void
212 save_pre_completion_cb(GCompletion *gcmp, G_GNUC_UNUSED gchar *line,
213 void *data)
214 {
215 completion_callback_data_t *tmp = (completion_callback_data_t *)data;
216 GList **list = tmp->list;
217 mpdclient_t *c = tmp->c;
219 if( *list == NULL ) {
220 /* create completion list */
221 *list = gcmp_list_from_path(c, "", NULL, GCMP_TYPE_PLAYLIST);
222 g_completion_add_items(gcmp, *list);
223 }
224 }
226 static void
227 save_post_completion_cb(G_GNUC_UNUSED GCompletion *gcmp,
228 G_GNUC_UNUSED gchar *line, GList *items,
229 G_GNUC_UNUSED void *data)
230 {
231 if (g_list_length(items) >= 1)
232 screen_display_completion_list(items);
233 }
234 #endif
236 #ifndef NCMPC_MINI
237 /**
238 * Wrapper for strncmp(). We are not allowed to pass &strncmp to
239 * g_completion_set_compare(), because strncmp() takes size_t where
240 * g_completion_set_compare passes a gsize value.
241 */
242 static gint
243 completion_strncmp(const gchar *s1, const gchar *s2, gsize n)
244 {
245 return strncmp(s1, s2, n);
246 }
247 #endif
249 int
250 playlist_save(mpdclient_t *c, char *name, char *defaultname)
251 {
252 gchar *filename, *filename_utf8;
253 gint error;
254 #ifndef NCMPC_MINI
255 GCompletion *gcmp;
256 GList *list = NULL;
257 completion_callback_data_t data;
258 #endif
260 #ifdef NCMPC_MINI
261 (void)defaultname;
262 #endif
264 #ifndef NCMPC_MINI
265 if (name == NULL) {
266 /* initialize completion support */
267 gcmp = g_completion_new(NULL);
268 g_completion_set_compare(gcmp, completion_strncmp);
269 data.list = &list;
270 data.dir_list = NULL;
271 data.c = c;
272 wrln_completion_callback_data = &data;
273 wrln_pre_completion_callback = save_pre_completion_cb;
274 wrln_post_completion_callback = save_post_completion_cb;
277 /* query the user for a filename */
278 filename = screen_readln(screen.status_window.w,
279 _("Save playlist as"),
280 defaultname,
281 NULL,
282 gcmp);
284 /* destroy completion support */
285 wrln_completion_callback_data = NULL;
286 wrln_pre_completion_callback = NULL;
287 wrln_post_completion_callback = NULL;
288 g_completion_free(gcmp);
289 list = string_list_free(list);
290 if( filename )
291 filename=g_strstrip(filename);
292 } else
293 #endif
294 filename=g_strdup(name);
296 if (filename == NULL)
297 return -1;
299 /* send save command to mpd */
301 filename_utf8 = locale_to_utf8(filename);
302 error = mpdclient_cmd_save_playlist(c, filename_utf8);
303 g_free(filename_utf8);
305 if (error) {
306 gint code = GET_ACK_ERROR_CODE(error);
308 if (code == MPD_ACK_ERROR_EXIST) {
309 char *buf;
310 int key;
312 buf = g_strdup_printf(_("Replace %s [%s/%s] ? "),
313 filename, YES, NO);
314 key = tolower(screen_getch(screen.status_window.w,
315 buf));
316 g_free(buf);
318 if (key == YES[0]) {
319 filename_utf8 = locale_to_utf8(filename);
320 error = mpdclient_cmd_delete_playlist(c, filename_utf8);
321 g_free(filename_utf8);
323 if (error) {
324 g_free(filename);
325 return -1;
326 }
328 error = playlist_save(c, filename, NULL);
329 g_free(filename);
330 return error;
331 }
333 screen_status_printf(_("Aborted"));
334 }
336 g_free(filename);
337 return -1;
338 }
340 /* success */
341 screen_status_printf(_("Saved %s"), filename);
342 g_free(filename);
343 return 0;
344 }
346 #ifndef NCMPC_MINI
347 static void add_dir(GCompletion *gcmp, gchar *dir, GList **dir_list,
348 GList **list, mpdclient_t *c)
349 {
350 g_completion_remove_items(gcmp, *list);
351 *list = string_list_remove(*list, dir);
352 *list = gcmp_list_from_path(c, dir, *list, GCMP_TYPE_RFILE);
353 g_completion_add_items(gcmp, *list);
354 *dir_list = g_list_append(*dir_list, g_strdup(dir));
355 }
357 static void add_pre_completion_cb(GCompletion *gcmp, gchar *line, void *data)
358 {
359 completion_callback_data_t *tmp = (completion_callback_data_t *)data;
360 GList **dir_list = tmp->dir_list;
361 GList **list = tmp->list;
362 mpdclient_t *c = tmp->c;
364 if (*list == NULL) {
365 /* create initial list */
366 *list = gcmp_list_from_path(c, "", NULL, GCMP_TYPE_RFILE);
367 g_completion_add_items(gcmp, *list);
368 } else if (line && line[0] && line[strlen(line)-1]=='/' &&
369 string_list_find(*dir_list, line) == NULL) {
370 /* add directory content to list */
371 add_dir(gcmp, line, dir_list, list, c);
372 }
373 }
375 static void add_post_completion_cb(GCompletion *gcmp, gchar *line,
376 GList *items, void *data)
377 {
378 completion_callback_data_t *tmp = (completion_callback_data_t *)data;
379 GList **dir_list = tmp->dir_list;
380 GList **list = tmp->list;
381 mpdclient_t *c = tmp->c;
383 if (g_list_length(items) >= 1)
384 screen_display_completion_list(items);
386 if (line && line[0] && line[strlen(line) - 1] == '/' &&
387 string_list_find(*dir_list, line) == NULL) {
388 /* add directory content to list */
389 add_dir(gcmp, line, dir_list, list, c);
390 }
391 }
392 #endif
394 static int
395 handle_add_to_playlist(mpdclient_t *c)
396 {
397 gchar *path;
398 #ifndef NCMPC_MINI
399 GCompletion *gcmp;
400 GList *list = NULL;
401 GList *dir_list = NULL;
402 completion_callback_data_t data;
404 /* initialize completion support */
405 gcmp = g_completion_new(NULL);
406 g_completion_set_compare(gcmp, completion_strncmp);
407 data.list = &list;
408 data.dir_list = &dir_list;
409 data.c = c;
410 wrln_completion_callback_data = &data;
411 wrln_pre_completion_callback = add_pre_completion_cb;
412 wrln_post_completion_callback = add_post_completion_cb;
413 #endif
415 /* get path */
416 path = screen_readln(screen.status_window.w,
417 _("Add"),
418 NULL,
419 NULL,
420 #ifdef NCMPC_MINI
421 NULL
422 #else
423 gcmp
424 #endif
425 );
427 /* destroy completion data */
428 #ifndef NCMPC_MINI
429 wrln_completion_callback_data = NULL;
430 wrln_pre_completion_callback = NULL;
431 wrln_post_completion_callback = NULL;
432 g_completion_free(gcmp);
433 string_list_free(list);
434 string_list_free(dir_list);
435 #endif
437 /* add the path to the playlist */
438 if (path != NULL) {
439 char *path_utf8 = locale_to_utf8(path);
440 mpdclient_cmd_add_path(c, path_utf8);
441 g_free(path_utf8);
442 }
444 g_free(path);
445 return 0;
446 }
448 static void
449 play_init(WINDOW *w, int cols, int rows)
450 {
451 lw = list_window_init(w, cols, rows);
452 }
454 static gboolean
455 timer_hide_cursor(gpointer data)
456 {
457 struct mpdclient *c = data;
459 assert(options.hide_cursor > 0);
460 assert(timer_hide_cursor_id != 0);
462 timer_hide_cursor_id = 0;
464 /* hide the cursor when mpd is playing and the user is inactive */
466 if (c->status != NULL && c->status->state == MPD_STATUS_STATE_PLAY) {
467 lw->hide_cursor = true;
468 playlist_repaint();
469 } else
470 timer_hide_cursor_id = g_timeout_add(options.hide_cursor * 1000,
471 timer_hide_cursor, c);
473 return FALSE;
474 }
476 static void
477 play_open(mpdclient_t *c)
478 {
479 static gboolean install_cb = TRUE;
481 playlist = &c->playlist;
483 assert(timer_hide_cursor_id == 0);
484 if (options.hide_cursor > 0) {
485 lw->hide_cursor = false;
486 timer_hide_cursor_id = g_timeout_add(options.hide_cursor * 1000,
487 timer_hide_cursor, c);
488 }
490 if (install_cb) {
491 mpdclient_install_playlist_callback(c, playlist_changed_callback);
492 install_cb = FALSE;
493 }
494 }
496 static void
497 play_close(void)
498 {
499 if (timer_hide_cursor_id != 0) {
500 g_source_remove(timer_hide_cursor_id);
501 timer_hide_cursor_id = 0;
502 }
503 }
505 static void
506 play_resize(int cols, int rows)
507 {
508 lw->cols = cols;
509 lw->rows = rows;
510 }
513 static void
514 play_exit(void)
515 {
516 list_window_free(lw);
517 }
519 static const char *
520 play_title(char *str, size_t size)
521 {
522 if( strcmp(options.host, "localhost") == 0 )
523 return _("Playlist");
525 g_snprintf(str, size, _("Playlist on %s"), options.host);
526 return str;
527 }
529 static void
530 play_paint(void)
531 {
532 list_window_paint(lw, list_callback, NULL);
533 }
535 static void
536 play_update(mpdclient_t *c)
537 {
538 static int prev_song_id = -1;
540 current_song_id = c->song != NULL && c->status != NULL &&
541 !IS_STOPPED(c->status->state) ? c->song->id : -1;
543 if (current_song_id != prev_song_id) {
544 prev_song_id = current_song_id;
546 /* center the cursor */
547 if (options.auto_center && current_song_id != -1 && ! lw->range_selection)
548 center_playing_item(c, false);
550 playlist_repaint();
551 #ifndef NCMPC_MINI
552 } else if (options.scroll) {
553 /* always repaint if horizontal scrolling is
554 enabled */
555 playlist_repaint();
556 #endif
557 }
558 }
560 #ifdef HAVE_GETMOUSE
561 static bool
562 handle_mouse_event(struct mpdclient *c)
563 {
564 int row;
565 unsigned selected;
566 unsigned long bstate;
568 if (screen_get_mouse_event(c, &bstate, &row) ||
569 list_window_mouse(lw, playlist_length(playlist), bstate, row)) {
570 playlist_repaint();
571 return true;
572 }
574 if (bstate & BUTTON1_DOUBLE_CLICKED) {
575 /* stop */
576 screen_cmd(c, CMD_STOP);
577 return true;
578 }
580 selected = lw->start + row;
582 if (bstate & BUTTON1_CLICKED) {
583 /* play */
584 if (lw->start + row < playlist_length(playlist))
585 mpdclient_cmd_play(c, lw->start + row);
586 } else if (bstate & BUTTON3_CLICKED) {
587 /* delete */
588 if (selected == lw->selected)
589 mpdclient_cmd_delete(c, lw->selected);
590 }
592 lw->selected = selected;
593 list_window_check_selected(lw, playlist_length(playlist));
594 playlist_repaint();
596 return true;
597 }
598 #endif
600 static bool
601 play_cmd(mpdclient_t *c, command_t cmd)
602 {
603 static command_t cached_cmd = CMD_NONE;
604 command_t prev_cmd = cached_cmd;
605 cached_cmd = cmd;
607 lw->hide_cursor = false;
609 if (options.hide_cursor > 0) {
610 if (timer_hide_cursor_id != 0)
611 g_source_remove(timer_hide_cursor_id);
612 timer_hide_cursor_id = g_timeout_add(options.hide_cursor * 1000,
613 timer_hide_cursor, c);
614 }
616 if (list_window_cmd(lw, playlist_length(&c->playlist), cmd)) {
617 playlist_repaint();
618 return true;
619 }
621 switch(cmd) {
622 case CMD_PLAY:
623 mpdclient_cmd_play(c, lw->selected);
624 return true;
625 case CMD_DELETE:
626 {
627 int i = lw->selected_end, start = lw->selected_start;
628 for(; i >= start; --i)
629 mpdclient_cmd_delete(c, i);
631 i++;
632 if(i >= (int)playlist_length(&c->playlist))
633 i--;
634 lw->selected = i;
635 lw->selected_start = i;
636 lw->selected_end = i;
637 lw->range_selection = false;
639 return true;
640 }
641 case CMD_SAVE_PLAYLIST:
642 playlist_save(c, NULL, NULL);
643 return true;
644 case CMD_ADD:
645 handle_add_to_playlist(c);
646 return true;
647 case CMD_SCREEN_UPDATE:
648 center_playing_item(c, prev_cmd == CMD_SCREEN_UPDATE);
649 playlist_repaint();
650 return false;
651 case CMD_SELECT_PLAYING:
652 list_window_set_selected(lw, playlist_get_index(c, c->song));
653 return true;
654 case CMD_SHUFFLE:
655 {
656 if(!lw->range_selection)
657 /* No range selection, shuffle all list. */
658 break;
660 if (mpdclient_cmd_shuffle_range(c, lw->selected_start, lw->selected_end+1) == 0)
661 screen_status_message(_("Shuffled playlist"));
663 return true;
664 }
665 case CMD_LIST_MOVE_UP:
666 if(lw->selected_start == 0)
667 return false;
668 if(lw->range_selection)
669 {
670 unsigned i = lw->selected_start;
671 unsigned last_selected = lw->selected;
672 for(; i <= lw->selected_end; ++i)
673 mpdclient_cmd_move(c, i, i-1);
674 lw->selected_start--;
675 lw->selected_end--;
676 lw->selected = last_selected - 1;
677 lw->range_base--;
678 }
679 else
680 {
681 mpdclient_cmd_move(c, lw->selected, lw->selected-1);
682 lw->selected--;
683 lw->selected_start--;
684 lw->selected_end--;
685 }
686 return true;
687 case CMD_LIST_MOVE_DOWN:
688 if(lw->selected_end+1 >= playlist_length(&c->playlist))
689 return false;
690 if(lw->range_selection)
691 {
692 int i = lw->selected_end;
693 unsigned last_selected = lw->selected;
694 for(; i >= (int)lw->selected_start; --i)
695 mpdclient_cmd_move(c, i, i+1);
696 lw->selected_start++;
697 lw->selected_end++;
698 lw->selected = last_selected + 1;
699 lw->range_base++;
700 }
701 else
702 {
703 mpdclient_cmd_move(c, lw->selected, lw->selected+1);
704 lw->selected++;
705 lw->selected_start++;
706 lw->selected_end++;
707 }
708 return true;
709 case CMD_LIST_FIND:
710 case CMD_LIST_RFIND:
711 case CMD_LIST_FIND_NEXT:
712 case CMD_LIST_RFIND_NEXT:
713 screen_find(lw, playlist_length(&c->playlist),
714 cmd, list_callback, NULL);
715 playlist_repaint();
716 return true;
717 case CMD_LIST_JUMP:
718 screen_jump(lw, list_callback, NULL);
719 playlist_repaint();
720 return true;
722 #ifdef HAVE_GETMOUSE
723 case CMD_MOUSE_EVENT:
724 return handle_mouse_event(c);
725 #endif
727 #ifdef ENABLE_SONG_SCREEN
728 case CMD_SCREEN_SONG:
729 if (lw->selected < playlist_length(&c->playlist)) {
730 screen_song_switch(c, playlist_get(&c->playlist, lw->selected));
731 return true;
732 }
734 break;
735 #endif
737 case CMD_LOCATE:
738 if (lw->selected < playlist_length(&c->playlist)) {
739 screen_file_goto_song(c, playlist_get(&c->playlist, lw->selected));
740 return true;
741 }
743 break;
745 #ifdef ENABLE_LYRICS_SCREEN
746 case CMD_SCREEN_LYRICS:
747 if (lw->selected < playlist_length(&c->playlist)) {
748 screen_lyrics_switch(c, playlist_get(&c->playlist, lw->selected));
749 return true;
750 }
752 break;
753 #endif
754 case CMD_SCREEN_SWAP:
755 screen_swap(c, playlist_get(&c->playlist, lw->selected));
756 return true;
758 default:
759 break;
760 }
762 return false;
763 }
765 const struct screen_functions screen_playlist = {
766 .init = play_init,
767 .exit = play_exit,
768 .open = play_open,
769 .close = play_close,
770 .resize = play_resize,
771 .paint = play_paint,
772 .update = play_update,
773 .cmd = play_cmd,
774 .get_title = play_title,
775 };