Move ncurses related files to curses directory
[ncmpcpp.git] / src / browser.cpp
blob320597b6ad4fadde4d99e6f611cb83045ad9982d
1 /***************************************************************************
2 * Copyright (C) 2008-2016 by Andrzej Rybczak *
3 * electricityispower@gmail.com *
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 *
16 * along with this program; if not, write to the *
17 * Free Software Foundation, Inc., *
18 * 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. *
19 ***************************************************************************/
21 #include <algorithm>
22 #include <boost/algorithm/string/predicate.hpp>
23 #include <boost/filesystem.hpp>
24 #include <boost/locale/conversion.hpp>
25 #include <time.h>
27 #include "browser.h"
28 #include "charset.h"
29 #include "display.h"
30 #include "global.h"
31 #include "helpers.h"
32 #include "playlist.h"
33 #include "curses/menu_impl.h"
34 #include "screen_switcher.h"
35 #include "settings.h"
36 #include "status.h"
37 #include "statusbar.h"
38 #include "tag_editor.h"
39 #include "title.h"
40 #include "tags.h"
41 #include "helpers/song_iterator_maker.h"
42 #include "utility/comparators.h"
43 #include "utility/string.h"
44 #include "configuration.h"
46 using Global::MainHeight;
47 using Global::MainStartY;
48 using Global::myScreen;
50 namespace fs = boost::filesystem;
51 namespace ph = std::placeholders;
53 Browser *myBrowser;
55 namespace {
57 std::set<std::string> lm_supported_extensions;
59 std::string realPath(bool local_browser, std::string path);
60 bool isStringParentDirectory(const std::string &directory);
61 bool isItemParentDirectory(const MPD::Item &item);
62 bool isRootDirectory(const std::string &directory);
63 bool isHidden(const fs::directory_iterator &entry);
64 bool hasSupportedExtension(const fs::directory_entry &entry);
65 MPD::Song getLocalSong(const fs::directory_entry &entry, bool read_tags);
66 void getLocalDirectory(std::vector<MPD::Item> &items, const std::string &directory);
67 void getLocalDirectoryRecursively(std::vector<MPD::Song> &songs, const std::string &directory);
68 void clearDirectory(const std::string &directory);
70 std::string itemToString(const MPD::Item &item);
71 bool browserEntryMatcher(const Regex::Regex &rx, const MPD::Item &item, bool filter);
75 template <>
76 struct SongPropertiesExtractor<MPD::Item>
78 template <typename ItemT>
79 auto &operator()(ItemT &item) const
81 auto s = item.value().type() == MPD::Item::Type::Song
82 ? &item.value().song()
83 : nullptr;
84 m_cache.assign(&item.properties(), s);
85 return m_cache;
88 private:
89 mutable SongProperties m_cache;
92 SongIterator BrowserWindow::currentS()
94 return makeSongIterator(current());
97 ConstSongIterator BrowserWindow::currentS() const
99 return makeConstSongIterator(current());
102 SongIterator BrowserWindow::beginS()
104 return makeSongIterator(begin());
107 ConstSongIterator BrowserWindow::beginS() const
109 return makeConstSongIterator(begin());
112 SongIterator BrowserWindow::endS()
114 return makeSongIterator(end());
117 ConstSongIterator BrowserWindow::endS() const
119 return makeConstSongIterator(end());
122 std::vector<MPD::Song> BrowserWindow::getSelectedSongs()
124 return {}; // TODO
127 /**********************************************************************/
129 Browser::Browser()
130 : m_update_request(true)
131 , m_local_browser(false)
132 , m_scroll_beginning(0)
133 , m_current_directory("/")
135 w = NC::Menu<MPD::Item>(0, MainStartY, COLS, MainHeight, Config.browser_display_mode == DisplayMode::Columns && Config.titles_visibility ? Display::Columns(COLS) : "", Config.main_color, NC::Border());
136 w.setHighlightColor(Config.main_highlight_color);
137 w.cyclicScrolling(Config.use_cyclic_scrolling);
138 w.centeredCursor(Config.centered_cursor);
139 w.setSelectedPrefix(Config.selected_item_prefix);
140 w.setSelectedSuffix(Config.selected_item_suffix);
141 w.setItemDisplayer(std::bind(Display::Items, ph::_1, std::cref(w)));
144 void Browser::resize()
146 size_t x_offset, width;
147 getWindowResizeParams(x_offset, width);
148 w.resize(width, MainHeight);
149 w.moveTo(x_offset, MainStartY);
150 switch (Config.browser_display_mode)
152 case DisplayMode::Columns:
153 if (Config.titles_visibility)
155 w.setTitle(Display::Columns(w.getWidth()));
156 break;
158 case DisplayMode::Classic:
159 w.setTitle("");
160 break;
162 hasToBeResized = 0;
165 void Browser::switchTo()
167 SwitchTo::execute(this);
168 markSongsInPlaylist(w);
169 drawHeader();
172 std::wstring Browser::title()
174 std::wstring result = L"Browse: ";
175 result += Scroller(ToWString(m_current_directory), m_scroll_beginning, COLS-result.length()-(Config.design == Design::Alternative ? 2 : Global::VolumeState.length()));
176 return result;
179 void Browser::update()
181 if (m_update_request)
183 m_update_request = false;
184 bool directory_changed = false;
189 getDirectory(m_current_directory);
190 w.refresh();
192 catch (MPD::ServerError &err)
194 // If current directory doesn't exist, try getting its
195 // parent until we either succeed or reach the root.
196 if (err.code() == MPD_SERVER_ERROR_NO_EXIST)
198 m_current_directory = getParentDirectory(m_current_directory);
199 directory_changed = true;
201 else
202 throw;
205 while (w.empty() && !inRootDirectory());
206 if (directory_changed)
207 drawHeader();
211 void Browser::mouseButtonPressed(MEVENT me)
213 if (w.empty() || !w.hasCoords(me.x, me.y) || size_t(me.y) >= w.size())
214 return;
215 if (me.bstate & (BUTTON1_PRESSED | BUTTON3_PRESSED))
217 w.Goto(me.y);
218 switch (w.current()->value().type())
220 case MPD::Item::Type::Directory:
221 if (me.bstate & BUTTON1_PRESSED)
222 enterDirectory();
223 else
224 addItemToPlaylist(false);
225 break;
226 case MPD::Item::Type::Playlist:
227 case MPD::Item::Type::Song:
229 bool play = me.bstate & BUTTON3_PRESSED;
230 addItemToPlaylist(play);
231 break;
235 else
236 Screen<WindowType>::mouseButtonPressed(me);
239 /***********************************************************************/
241 bool Browser::allowsSearching()
243 return true;
246 const std::string &Browser::searchConstraint()
248 return m_search_predicate.constraint();
251 void Browser::setSearchConstraint(const std::string &constraint)
253 m_search_predicate = Regex::Filter<MPD::Item>(
254 constraint,
255 Config.regex_type,
256 std::bind(browserEntryMatcher, ph::_1, ph::_2, false));
259 void Browser::clearSearchConstraint()
261 m_search_predicate.clear();
264 bool Browser::search(SearchDirection direction, bool wrap, bool skip_current)
266 return ::search(w, m_search_predicate, direction, wrap, skip_current);
269 /***********************************************************************/
271 bool Browser::allowsFiltering()
273 return allowsSearching();
276 std::string Browser::currentFilter()
278 std::string result;
279 if (auto pred = w.filterPredicate<Regex::Filter<MPD::Item>>())
280 result = pred->constraint();
281 return result;
284 void Browser::applyFilter(const std::string &constraint)
286 if (!constraint.empty())
288 w.applyFilter(Regex::Filter<MPD::Item>(
289 constraint,
290 Config.regex_type,
291 std::bind(browserEntryMatcher, ph::_1, ph::_2, true)));
293 else
294 w.clearFilter();
298 /***********************************************************************/
300 bool Browser::itemAvailable()
302 return !w.empty()
303 // ignore parent directory
304 && !isParentDirectory(w.current()->value());
307 bool Browser::addItemToPlaylist(bool play)
309 bool result = false;
311 auto tryToPlay = [] {
312 // Cheap trick that might fail in presence of multiple
313 // clients modifying the playlist at the same time, but
314 // oh well, this approach correctly loads cue playlists
315 // and is much faster in general as it doesn't require
316 // fetching song data.
319 Mpd.Play(Status::State::playlistLength());
321 catch (MPD::ServerError &e)
323 // If not bad index, rethrow.
324 if (e.code() != MPD_SERVER_ERROR_ARG)
325 throw;
329 const MPD::Item &item = w.current()->value();
330 switch (item.type())
332 case MPD::Item::Type::Directory:
334 if (m_local_browser)
336 std::vector<MPD::Song> songs;
337 getLocalDirectoryRecursively(songs, item.directory().path());
338 result = addSongsToPlaylist(songs.begin(), songs.end(), play, -1);
340 else
342 Mpd.Add(item.directory().path());
343 if (play)
344 tryToPlay();
345 result = true;
347 Statusbar::printf("Directory \"%1%\" added%2%",
348 item.directory().path(), withErrors(result));
349 break;
351 case MPD::Item::Type::Song:
352 result = addSongToPlaylist(item.song(), play);
353 break;
354 case MPD::Item::Type::Playlist:
355 Mpd.LoadPlaylist(item.playlist().path());
356 if (play)
357 tryToPlay();
358 Statusbar::printf("Playlist \"%1%\" loaded", item.playlist().path());
359 result = true;
360 break;
362 return result;
365 std::vector<MPD::Song> Browser::getSelectedSongs()
367 std::vector<MPD::Song> songs;
368 auto item_handler = [this, &songs](const MPD::Item &item) {
369 switch (item.type())
371 case MPD::Item::Type::Directory:
372 if (m_local_browser)
373 getLocalDirectoryRecursively(songs, item.directory().path());
374 else
376 std::copy(
377 std::make_move_iterator(Mpd.GetDirectoryRecursive(item.directory().path())),
378 std::make_move_iterator(MPD::SongIterator()),
379 std::back_inserter(songs)
382 break;
383 case MPD::Item::Type::Song:
384 songs.push_back(item.song());
385 break;
386 case MPD::Item::Type::Playlist:
387 std::copy(
388 std::make_move_iterator(Mpd.GetPlaylistContent(item.playlist().path())),
389 std::make_move_iterator(MPD::SongIterator()),
390 std::back_inserter(songs)
392 break;
395 for (const auto &item : w)
396 if (item.isSelected())
397 item_handler(item.value());
398 // if no item is selected, add current one
399 if (songs.empty() && !w.empty())
400 item_handler(w.current()->value());
401 return songs;
404 /***********************************************************************/
406 bool Browser::inRootDirectory()
408 return isRootDirectory(m_current_directory);
411 bool Browser::isParentDirectory(const MPD::Item &item)
413 return isItemParentDirectory(item);
416 const std::string& Browser::currentDirectory()
418 return m_current_directory;
421 void Browser::locateSong(const MPD::Song &s)
423 if (s.getDirectory().empty())
424 throw std::runtime_error("Song's directory is empty");
426 m_local_browser = !s.isFromDatabase();
428 if (myScreen != this)
429 switchTo();
431 w.clearFilter();
433 // change to relevant directory
434 if (m_current_directory != s.getDirectory())
436 getDirectory(s.getDirectory());
437 drawHeader();
440 // highlight the item
441 auto begin = w.beginV(), end = w.endV();
442 auto it = std::find(begin, end, MPD::Item(s));
443 if (it != end)
444 w.highlight(it-begin);
447 bool Browser::enterDirectory()
449 bool result = false;
450 if (!w.empty())
452 const auto &item = w.current()->value();
453 if (item.type() == MPD::Item::Type::Directory)
455 getDirectory(item.directory().path());
456 drawHeader();
457 result = true;
460 return result;
463 void Browser::getDirectory(std::string directory)
465 ScopedUnfilteredMenu<MPD::Item> sunfilter(ReapplyFilter::Yes, w);
467 m_scroll_beginning = 0;
468 w.clear();
470 // reset the position if we change directories
471 if (m_current_directory != directory)
472 w.reset();
474 // check if it's a parent directory
475 if (isStringParentDirectory(directory))
477 directory.resize(directory.length()-3);
478 directory = getParentDirectory(directory);
480 // when we go down to root, it can be empty
481 if (directory.empty())
482 directory = "/";
484 std::vector<MPD::Item> items;
485 if (m_local_browser)
486 getLocalDirectory(items, directory);
487 else
489 std::copy(
490 std::make_move_iterator(Mpd.GetDirectory(directory)),
491 std::make_move_iterator(MPD::ItemIterator()),
492 std::back_inserter(items)
496 // sort items
497 if (Config.browser_sort_mode != SortMode::NoOp)
499 std::sort(items.begin(), items.end(),
500 LocaleBasedItemSorting(std::locale(), Config.ignore_leading_the, Config.browser_sort_mode)
504 // if the requested directory is not root, add parent directory
505 if (!isRootDirectory(directory))
507 // make it so that display function doesn't have to handle special cases
508 w.addItem(MPD::Directory(directory + "/.."), NC::List::Properties::None);
511 for (const auto &item : items)
513 switch (item.type())
515 case MPD::Item::Type::Playlist:
517 w.addItem(std::move(item));
518 break;
520 case MPD::Item::Type::Directory:
522 bool is_current = item.directory().path() == m_current_directory;
523 w.addItem(std::move(item));
524 if (is_current)
525 w.highlight(w.size()-1);
526 break;
528 case MPD::Item::Type::Song:
530 auto properties = NC::List::Properties::Selectable;
531 if (myPlaylist->checkForSong(item.song()))
532 properties |= NC::List::Properties::Bold;
533 w.addItem(std::move(item), properties);
534 break;
538 m_current_directory = directory;
541 void Browser::changeBrowseMode()
543 if (Mpd.GetHostname()[0] != '/')
545 Statusbar::print("For browsing local filesystem connection to MPD via UNIX Socket is required");
546 return;
549 m_local_browser = !m_local_browser;
550 Statusbar::printf("Browse mode: %1%",
551 m_local_browser ? "local filesystem" : "MPD database"
553 if (m_local_browser)
555 m_current_directory = "~";
556 expand_home(m_current_directory);
558 else
559 m_current_directory = "/";
560 w.reset();
561 getDirectory(m_current_directory);
562 drawHeader();
565 void Browser::remove(const MPD::Item &item)
567 if (!Config.allow_for_physical_item_deletion)
568 throw std::runtime_error("physical deletion is forbidden");
569 if (isParentDirectory((item)))
570 throw std::runtime_error("deletion of parent directory is forbidden");
572 std::string path;
573 switch (item.type())
575 case MPD::Item::Type::Directory:
576 path = realPath(m_local_browser, item.directory().path());
577 clearDirectory(path);
578 fs::remove(path);
579 break;
580 case MPD::Item::Type::Song:
581 path = realPath(m_local_browser, item.song().getURI());
582 fs::remove(path);
583 break;
584 case MPD::Item::Type::Playlist:
585 path = item.playlist().path();
586 try {
587 Mpd.DeletePlaylist(path);
588 } catch (MPD::ServerError &e) {
589 // if there is no such mpd playlist, it's a local one
590 if (e.code() == MPD_SERVER_ERROR_NO_EXIST)
592 path = realPath(m_local_browser, std::move(path));
593 fs::remove(path);
595 else
596 throw;
598 break;
602 /***********************************************************************/
604 void Browser::fetchSupportedExtensions()
606 lm_supported_extensions.clear();
607 MPD::StringIterator extension = Mpd.GetSupportedExtensions(), end;
608 for (; extension != end; ++extension)
609 lm_supported_extensions.insert("." + std::move(*extension));
612 /***********************************************************************/
614 namespace {
616 std::string realPath(bool local_browser, std::string path)
618 if (!local_browser)
619 path = Config.mpd_music_dir + path;
620 return path;
623 bool isStringParentDirectory(const std::string &directory)
625 return boost::algorithm::ends_with(directory, "/..");
628 bool isItemParentDirectory(const MPD::Item &item)
630 return item.type() == MPD::Item::Type::Directory
631 && isStringParentDirectory(item.directory().path());
634 bool isRootDirectory(const std::string &directory)
636 return directory == "/";
639 bool isHidden(const fs::directory_iterator &entry)
641 return entry->path().filename().native()[0] == '.';
644 bool hasSupportedExtension(const fs::directory_entry &entry)
646 return lm_supported_extensions.find(entry.path().extension().native())
647 != lm_supported_extensions.end();
650 MPD::Song getLocalSong(const fs::directory_entry &entry, bool read_tags)
652 mpd_pair pair = { "file", entry.path().c_str() };
653 mpd_song *s = mpd_song_begin(&pair);
654 if (s == nullptr)
655 throw std::runtime_error("invalid path: " + entry.path().native());
656 if (read_tags)
658 #ifdef HAVE_TAGLIB_H
659 Tags::setAttribute(s, "Last-Modified",
660 timeFormat("%Y-%m-%dT%H:%M:%SZ", fs::last_write_time(entry.path()))
662 // read tags
663 Tags::read(s);
664 #endif // HAVE_TAGLIB_H
666 return s;
669 void getLocalDirectory(std::vector<MPD::Item> &items, const std::string &directory)
671 for (fs::directory_iterator entry(directory), end; entry != end; ++entry)
673 if (!Config.local_browser_show_hidden_files && isHidden(entry))
674 continue;
676 if (fs::is_directory(*entry))
678 items.push_back(MPD::Directory(
679 entry->path().native(),
680 fs::last_write_time(entry->path())
683 else if (hasSupportedExtension(*entry))
684 items.push_back(getLocalSong(*entry, true));
688 void getLocalDirectoryRecursively(std::vector<MPD::Song> &songs, const std::string &directory)
690 size_t sort_offset = songs.size();
691 for (fs::directory_iterator entry(directory), end; entry != end; ++entry)
693 if (!Config.local_browser_show_hidden_files && isHidden(entry))
694 continue;
696 if (fs::is_directory(*entry))
698 getLocalDirectoryRecursively(songs, entry->path().native());
699 sort_offset = songs.size();
701 else if (hasSupportedExtension(*entry))
702 songs.push_back(getLocalSong(*entry, false));
705 if (Config.browser_sort_mode != SortMode::NoOp)
707 std::sort(songs.begin()+sort_offset, songs.end(),
708 LocaleBasedSorting(std::locale(), Config.ignore_leading_the)
713 void clearDirectory(const std::string &directory)
715 for (fs::directory_iterator entry(directory), end; entry != end; ++entry)
717 if (!fs::is_symlink(*entry) && fs::is_directory(*entry))
718 clearDirectory(entry->path().native());
719 const char msg[] = "Deleting \"%1%\"...";
720 Statusbar::printf(msg, wideShorten(entry->path().native(), COLS-const_strlen(msg)));
721 fs::remove(entry->path());
725 /***********************************************************************/
727 std::string itemToString(const MPD::Item &item)
729 std::string result;
730 switch (item.type())
732 case MPD::Item::Type::Directory:
733 result = "[" + getBasename(item.directory().path()) + "]";
734 break;
735 case MPD::Item::Type::Song:
736 switch (Config.browser_display_mode)
738 case DisplayMode::Classic:
739 result = Format::stringify<char>(Config.song_list_format, &item.song());
740 break;
741 case DisplayMode::Columns:
742 result = Format::stringify<char>(Config.song_columns_mode_format, &item.song());
743 break;
745 break;
746 case MPD::Item::Type::Playlist:
747 result = Config.browser_playlist_prefix.str();
748 result += getBasename(item.playlist().path());
749 break;
751 return result;
754 bool browserEntryMatcher(const Regex::Regex &rx, const MPD::Item &item, bool filter)
756 if (isItemParentDirectory(item))
757 return filter;
758 return Regex::search(itemToString(item), rx);