set SIGWINCH handler before initializing ncurses to avoid races
[ncmpcpp.git] / src / status.cpp
bloba6170e2b3b01e3e6141a472efe4b5baed0afe5df
1 /***************************************************************************
2 * Copyright (C) 2008-2014 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 <boost/date_time/posix_time/posix_time.hpp>
22 #include <netinet/tcp.h>
23 #include <netinet/in.h>
25 #include "browser.h"
26 #include "charset.h"
27 #include "global.h"
28 #include "helpers.h"
29 #include "lyrics.h"
30 #include "media_library.h"
31 #include "outputs.h"
32 #include "playlist.h"
33 #include "playlist_editor.h"
34 #include "search_engine.h"
35 #include "sel_items_adder.h"
36 #include "settings.h"
37 #include "status.h"
38 #include "statusbar.h"
39 #include "tag_editor.h"
40 #include "visualizer.h"
41 #include "title.h"
42 #include "utility/string.h"
44 using Global::myScreen;
46 using Global::wFooter;
47 using Global::wHeader;
49 using Global::Timer;
50 using Global::VolumeState;
52 namespace {
54 boost::posix_time::ptime past = boost::posix_time::from_time_t(0);
56 size_t playing_song_scroll_begin = 0;
57 size_t first_line_scroll_begin = 0;
58 size_t second_line_scroll_begin = 0;
60 bool m_status_initialized;
62 char m_consume;
63 char m_crossfade;
64 char m_db_updating;
65 char m_repeat;
66 char m_random;
67 char m_single;
69 int m_current_song_id;
70 int m_current_song_pos;
71 unsigned m_elapsed_time;
72 unsigned m_kbps;
73 MPD::PlayerState m_player_state;
74 unsigned m_playlist_version;
75 unsigned m_playlist_length;
76 unsigned m_total_time;
77 int m_volume;
79 void drawTitle(const MPD::Song &np)
81 assert(!np.empty());
82 windowTitle(np.toString(Config.song_window_title_format, Config.tags_separator));
85 std::string playerStateToString(MPD::PlayerState ps)
87 std::string result;
88 switch (ps)
90 case MPD::psUnknown:
91 result = "[unknown]";
92 break;
93 case MPD::psPlay:
94 if (Config.design == Design::Alternative)
95 result = "[playing]";
96 else
97 result = "Playing: ";
98 break;
99 case MPD::psPause:
100 if (Config.design == Design::Alternative)
101 result = "[paused] ";
102 else
103 result = "[Paused] ";
104 break;
105 case MPD::psStop:
106 if (Config.design == Design::Alternative)
107 result = "[stopped]";
108 break;
109 default:
110 break;
112 return result;
115 void initialize_status()
117 // get full info about new connection
118 Status::update(-1);
120 if (Config.jump_to_now_playing_song_at_start)
122 int curr_pos = Status::State::currentSongPosition();
123 if (curr_pos >= 0)
124 myPlaylist->main().highlight(curr_pos);
127 // Set TCP_NODELAY on the tcp socket as we are using write-write-read pattern
128 // a lot (noidle - write, command - write, then read the result of command),
129 // which kills the performance.
130 int flag = 1;
131 setsockopt(Mpd.GetFD(), IPPROTO_TCP, TCP_NODELAY, &flag, sizeof(flag));
133 // go to startup screen
134 if (Config.startup_screen_type != myScreen->type())
135 toScreen(Config.startup_screen_type)->switchTo();
136 myScreen->refresh();
138 myBrowser->fetchSupportedExtensions();
139 # ifdef ENABLE_OUTPUTS
140 myOutputs->FetchList();
141 # endif // ENABLE_OUTPUTS
142 # ifdef ENABLE_VISUALIZER
143 myVisualizer->ResetFD();
144 if (myScreen == myVisualizer)
145 myVisualizer->SetFD();
146 myVisualizer->FindOutputID();
147 # endif // ENABLE_VISUALIZER
149 m_status_initialized = true;
150 wFooter->addFDCallback(Mpd.GetFD(), Statusbar::Helpers::mpd);
151 Statusbar::printf("Connected to %1%", Mpd.GetHostname());
156 /*************************************************************************/
158 void Status::handleClientError(MPD::ClientError &e)
160 if (!e.clearable())
161 Mpd.Disconnect();
162 Statusbar::printf("ncmpcpp: %1%", e.what());
165 void Status::handleServerError(MPD::ServerError &e)
167 Statusbar::printf("MPD: %1%", e.what());
168 if (e.code() == MPD_SERVER_ERROR_PERMISSION)
170 wFooter->setGetStringHelper(nullptr);
171 Statusbar::put() << "Password: ";
172 Mpd.SetPassword(wFooter->getString(0, true));
173 try {
174 Mpd.SendPassword();
175 Statusbar::print("Password accepted");
176 } catch (MPD::ServerError &e_prim) {
177 handleServerError(e_prim);
179 wFooter->setGetStringHelper(Statusbar::Helpers::getString);
181 else if (e.code() == MPD_SERVER_ERROR_NO_EXIST && myScreen == myBrowser)
183 myBrowser->GetDirectory(getParentDirectory(myBrowser->CurrentDir()));
184 myBrowser->refresh();
188 /*************************************************************************/
190 void Status::trace(bool update_timer, bool update_window_timeout)
192 if (update_timer)
193 Timer = boost::posix_time::microsec_clock::local_time();
194 if (update_window_timeout)
196 // set appropriate window timeout
197 int nc_wtimeout = std::numeric_limits<int>::max();
198 applyToVisibleWindows([&nc_wtimeout](BaseScreen *s) {
199 nc_wtimeout = std::min(nc_wtimeout, s->windowTimeout());
201 wFooter->setTimeout(nc_wtimeout);
203 if (Mpd.Connected())
205 if (!m_status_initialized)
206 initialize_status();
208 if (m_player_state == MPD::psPlay
209 && Global::Timer - past > boost::posix_time::seconds(1))
211 // update elapsed time/bitrate of the current song
212 Status::Changes::elapsedTime(true);
213 wFooter->refresh();
214 past = Timer;
217 applyToVisibleWindows(&BaseScreen::update);
218 Statusbar::tryRedraw();
220 Mpd.idle();
224 void Status::update(int event)
226 auto st = Mpd.getStatus();
227 m_current_song_pos = st.currentSongPosition();
228 m_elapsed_time = st.elapsedTime();
229 m_kbps = st.kbps();
230 m_player_state = st.playerState();
231 m_playlist_length = st.playlistLength();
232 m_total_time = st.totalTime();
233 m_volume = st.volume();
235 if (event & MPD_IDLE_DATABASE)
236 Changes::database();
237 if (event & MPD_IDLE_STORED_PLAYLIST)
238 Changes::storedPlaylists();
239 if (event & MPD_IDLE_PLAYLIST)
241 Changes::playlist(m_playlist_version);
242 m_playlist_version = st.playlistVersion();
244 if (event & MPD_IDLE_PLAYER)
246 Changes::playerState();
247 if (m_current_song_id != st.currentSongID())
249 Changes::songID(st.currentSongID());
250 m_current_song_id = st.currentSongID();
253 if (event & MPD_IDLE_MIXER)
254 Changes::mixer();
255 if (event & MPD_IDLE_OUTPUT)
256 Changes::outputs();
257 if (event & (MPD_IDLE_UPDATE | MPD_IDLE_OPTIONS))
259 if (event & MPD_IDLE_UPDATE)
261 m_db_updating = st.updateID() ? 'U' : 0;
262 if (m_status_initialized)
263 Statusbar::printf("Database update %1%", m_db_updating ? "started" : "finished");
265 if (event & MPD_IDLE_OPTIONS)
267 if (('r' == m_repeat) != st.repeat())
269 m_repeat = st.repeat() ? 'r' : 0;
270 if (m_status_initialized)
271 Statusbar::printf("Repeat mode is %1%", !m_repeat ? "off" : "on");
273 if (('z' == m_random) != st.random())
275 m_random = st.random() ? 'z' : 0;
276 if (m_status_initialized)
277 Statusbar::printf("Random mode is %1%", !m_random ? "off" : "on");
279 if (('s' == m_single) != st.single())
281 m_single = st.single() ? 's' : 0;
282 if (m_status_initialized)
283 Statusbar::printf("Single mode is %1%", !m_single ? "off" : "on");
285 if (('c' == m_consume) != st.consume())
287 m_consume = st.consume() ? 'c' : 0;
288 if (m_status_initialized)
289 Statusbar::printf("Consume mode is %1%", !m_consume ? "off" : "on");
291 if (('x' == m_crossfade) != (st.crossfade() != 0))
293 int crossfade = st.crossfade();
294 m_crossfade = crossfade ? 'x' : 0;
295 if (m_status_initialized)
296 Statusbar::printf("Crossfade set to %1% seconds", crossfade);
299 Changes::flags();
301 m_status_initialized = true;
303 if (event & MPD_IDLE_PLAYER)
304 wFooter->refresh();
306 if (event & (MPD_IDLE_PLAYLIST | MPD_IDLE_DATABASE | MPD_IDLE_PLAYER))
307 applyToVisibleWindows(&BaseScreen::refreshWindow);
310 void Status::clear()
312 // reset local variables
313 m_status_initialized = false;
314 m_repeat = 0;
315 m_random = 0;
316 m_single = 0;
317 m_consume = 0;
318 m_crossfade = 0;
319 m_db_updating = 0;
320 m_current_song_id = -1;
321 m_current_song_pos = -1;
322 m_kbps = 0;
323 m_player_state = MPD::psUnknown;
324 m_playlist_length = 0;
325 m_playlist_version = 0;
326 m_total_time = 0;
327 m_volume = -1;
330 /*************************************************************************/
332 bool Status::State::consume()
334 return m_consume != 0;
337 bool Status::State::crossfade()
339 return m_crossfade != 0;
342 bool Status::State::repeat()
344 return m_repeat != 0;
347 bool Status::State::random()
349 return m_random != 0;
352 bool Status::State::single()
354 return m_single != 0;
357 int Status::State::currentSongID()
359 return m_current_song_id;
362 int Status::State::currentSongPosition()
364 return m_current_song_pos;
367 unsigned Status::State::elapsedTime()
369 return m_elapsed_time;
372 MPD::PlayerState Status::State::player()
374 return m_player_state;
377 unsigned Status::State::totalTime()
379 return m_total_time;
382 int Status::State::volume()
384 return m_volume;
387 /*************************************************************************/
389 void Status::Changes::playlist(unsigned previous_version)
391 myPlaylist->main().clearSearchResults();
392 withUnfilteredMenuReapplyFilter(myPlaylist->main(), [previous_version]() {
393 if (m_playlist_length < myPlaylist->main().size())
395 auto it = myPlaylist->main().begin()+m_playlist_length;
396 auto end = myPlaylist->main().end();
397 for (; it != end; ++it)
398 myPlaylist->unregisterSong(it->value());
399 myPlaylist->main().resizeList(m_playlist_length);
402 Mpd.GetPlaylistChanges(previous_version, [](MPD::Song s) {
403 size_t pos = s.getPosition();
404 if (pos < myPlaylist->main().size())
406 // if song's already in playlist, replace it with a new one
407 MPD::Song &old_s = myPlaylist->main()[pos].value();
408 myPlaylist->unregisterSong(old_s);
409 old_s = s;
411 else // otherwise just add it to playlist
412 myPlaylist->main().addItem(s);
413 myPlaylist->registerSong(s);
417 myPlaylist->reloadTotalLength();
418 myPlaylist->reloadRemaining();
420 if (isVisible(myBrowser))
421 markSongsInPlaylist(myBrowser->proxySongList());
422 if (isVisible(mySearcher))
423 markSongsInPlaylist(mySearcher->proxySongList());
424 if (isVisible(myLibrary))
426 markSongsInPlaylist(myLibrary->songsProxyList());
427 myLibrary->Songs.refresh();
429 if (isVisible(myPlaylistEditor))
431 markSongsInPlaylist(myPlaylistEditor->contentProxyList());
432 myPlaylistEditor->Content.refresh();
436 void Status::Changes::storedPlaylists()
438 myPlaylistEditor->requestPlaylistsUpdate();
439 myPlaylistEditor->requestContentsUpdate();
440 if (myBrowser->CurrentDir() == "/")
442 myBrowser->GetDirectory("/");
443 if (isVisible(myBrowser))
444 myBrowser->refresh();
448 void Status::Changes::database()
450 if (isVisible(myBrowser))
451 myBrowser->GetDirectory(myBrowser->CurrentDir());
452 else
453 myBrowser->main().clear();
454 # ifdef HAVE_TAGLIB_H
455 myTagEditor->Dirs->clear();
456 # endif // HAVE_TAGLIB_H
457 myLibrary->requestTagsUpdate();
458 myLibrary->requestAlbumsUpdate();
459 myLibrary->requestSongsUpdate();
462 void Status::Changes::playerState()
464 switch (m_player_state)
466 case MPD::psPlay:
467 drawTitle(myPlaylist->nowPlayingSong());
468 myPlaylist->reloadRemaining();
469 break;
470 case MPD::psStop:
471 windowTitle("ncmpcpp " VERSION);
472 if (Progressbar::isUnlocked())
473 Progressbar::draw(0, 0);
474 myPlaylist->reloadRemaining();
475 if (Config.design == Design::Alternative)
477 *wHeader << NC::XY(0, 0) << wclrtoeol << NC::XY(0, 1) << wclrtoeol;
478 mixer();
479 flags();
481 # ifdef ENABLE_VISUALIZER
482 if (isVisible(myVisualizer))
483 myVisualizer->main().clear();
484 # endif // ENABLE_VISUALIZER
485 break;
486 default:
487 break;
490 std::string state = playerStateToString(m_player_state);
491 if (Config.design == Design::Alternative)
493 *wHeader << NC::XY(0, 1) << NC::Format::Bold << state << NC::Format::NoBold;
494 wHeader->refresh();
496 else if (Statusbar::isUnlocked() && Config.statusbar_visibility)
498 *wFooter << NC::XY(0, 1);
499 if (state.empty())
500 *wFooter << wclrtoeol;
501 else
502 *wFooter << NC::Format::Bold << state << NC::Format::NoBold;
505 // needed for immediate display after starting
506 // player from stopped state or seeking
507 elapsedTime(false);
510 void Status::Changes::songID(int song_id)
512 // update information about current song
513 myPlaylist->reloadRemaining();
514 playing_song_scroll_begin = 0;
515 first_line_scroll_begin = 0;
516 second_line_scroll_begin = 0;
517 if (m_player_state != MPD::psStop)
519 auto &pl = myPlaylist->main();
521 // try to find the song with new id in the playlist
522 auto it = std::find_if(pl.beginV(), pl.endV(), [song_id](const MPD::Song &s) {
523 return s.getID() == unsigned(song_id);
525 // if it's not there (playlist may be outdated), fetch it
526 const auto &s = it != pl.endV() ? *it : Mpd.GetCurrentSong();
528 GNUC_UNUSED int res;
529 if (!Config.execute_on_song_change.empty())
530 res = system(Config.execute_on_song_change.c_str());
532 # ifdef HAVE_CURL_CURL_H
533 if (Config.fetch_lyrics_in_background)
534 Lyrics::DownloadInBackground(s);
535 # endif // HAVE_CURL_CURL_H
537 drawTitle(s);
539 if (Config.autocenter_mode && !pl.isFiltered())
540 pl.highlight(Status::State::currentSongPosition());
542 if (Config.now_playing_lyrics && isVisible(myLyrics) && myLyrics->previousScreen() == myPlaylist)
544 if (myLyrics->SetSong(s))
545 myLyrics->Reload = 1;
548 elapsedTime(false);
551 void Status::Changes::elapsedTime(bool update_elapsed)
553 if (update_elapsed)
555 auto st = Mpd.getStatus();
556 m_elapsed_time = st.elapsedTime();
557 m_kbps = st.kbps();
560 if (m_player_state == MPD::psStop)
562 if (Statusbar::isUnlocked() && Config.statusbar_visibility)
563 *wFooter << NC::XY(0, 1) << wclrtoeol;
564 return;
567 std::string ps = playerStateToString(m_player_state);
568 MPD::Song np = myPlaylist->nowPlayingSong();
569 drawTitle(np);
571 std::string tracklength;
572 switch (Config.design)
574 case Design::Classic:
575 if (Statusbar::isUnlocked() && Config.statusbar_visibility)
577 if (Config.display_bitrate && m_kbps)
579 tracklength += " [";
580 tracklength += boost::lexical_cast<std::string>(m_kbps);
581 tracklength += " kbps]";
583 tracklength += " [";
584 if (m_total_time)
586 if (Config.display_remaining_time)
588 tracklength += "-";
589 tracklength += MPD::Song::ShowTime(m_total_time-m_elapsed_time);
591 else
592 tracklength += MPD::Song::ShowTime(m_elapsed_time);
593 tracklength += "/";
594 tracklength += MPD::Song::ShowTime(m_total_time);
595 tracklength += "]";
597 else
599 tracklength += MPD::Song::ShowTime(m_elapsed_time);
600 tracklength += "]";
602 NC::WBuffer np_song;
603 stringToBuffer(ToWString(Charset::utf8ToLocale(np.toString(Config.song_status_format, Config.tags_separator, "$"))), np_song);
604 *wFooter << NC::XY(0, 1) << wclrtoeol << NC::Format::Bold << ps << NC::Format::NoBold;
605 writeCyclicBuffer(np_song, *wFooter, playing_song_scroll_begin, wFooter->getWidth()-ps.length()-tracklength.length(), L" ** ");
606 *wFooter << NC::Format::Bold << NC::XY(wFooter->getWidth()-tracklength.length(), 1) << tracklength << NC::Format::NoBold;
608 break;
609 case Design::Alternative:
610 if (Config.display_remaining_time)
612 tracklength = "-";
613 tracklength += MPD::Song::ShowTime(m_total_time-m_elapsed_time);
615 else
616 tracklength = MPD::Song::ShowTime(m_elapsed_time);
617 if (m_total_time)
619 tracklength += "/";
620 tracklength += MPD::Song::ShowTime(m_total_time);
622 // bitrate here doesn't look good, but it can be moved somewhere else later
623 if (Config.display_bitrate && m_kbps)
625 tracklength += " ";
626 tracklength += boost::lexical_cast<std::string>(m_kbps);
627 tracklength += " kbps";
630 NC::WBuffer first, second;
631 stringToBuffer(ToWString(Charset::utf8ToLocale(np.toString(Config.new_header_first_line, Config.tags_separator, "$"))), first);
632 stringToBuffer(ToWString(Charset::utf8ToLocale(np.toString(Config.new_header_second_line, Config.tags_separator, "$"))), second);
634 size_t first_len = wideLength(first.str());
635 size_t first_margin = (std::max(tracklength.length()+1, VolumeState.length()))*2;
636 size_t first_start = first_len < COLS-first_margin ? (COLS-first_len)/2 : tracklength.length()+1;
638 size_t second_len = wideLength(second.str());
639 size_t second_margin = (std::max(ps.length(), size_t(8))+1)*2;
640 size_t second_start = second_len < COLS-second_margin ? (COLS-second_len)/2 : ps.length()+1;
642 if (!Global::SeekingInProgress)
643 *wHeader << NC::XY(0, 0) << wclrtoeol << tracklength;
644 *wHeader << NC::XY(first_start, 0);
645 writeCyclicBuffer(first, *wHeader, first_line_scroll_begin, COLS-tracklength.length()-VolumeState.length()-1, L" ** ");
647 *wHeader << NC::XY(0, 1) << wclrtoeol << NC::Format::Bold << ps << NC::Format::NoBold;
648 *wHeader << NC::XY(second_start, 1);
649 writeCyclicBuffer(second, *wHeader, second_line_scroll_begin, COLS-ps.length()-8-2, L" ** ");
651 *wHeader << NC::XY(wHeader->getWidth()-VolumeState.length(), 0) << Config.volume_color << VolumeState << NC::Color::End;
653 flags();
655 if (Progressbar::isUnlocked())
656 Progressbar::draw(m_elapsed_time, m_total_time);
659 void Status::Changes::flags()
661 if (!Config.header_visibility && Config.design == Design::Classic)
662 return;
664 std::string switch_state;
665 switch (Config.design)
667 case Design::Classic:
668 if (m_repeat)
669 switch_state += m_repeat;
670 if (m_random)
671 switch_state += m_random;
672 if (m_single)
673 switch_state += m_single;
674 if (m_consume)
675 switch_state += m_consume;
676 if (m_crossfade)
677 switch_state += m_crossfade;
678 if (m_db_updating)
679 switch_state += m_db_updating;
681 // this is done by raw ncurses because creating another
682 // window only for handling this is quite silly
683 attrset(A_BOLD|COLOR_PAIR(int(Config.state_line_color)));
684 mvhline(1, 0, 0, COLS);
685 if (!switch_state.empty())
687 mvprintw(1, COLS-switch_state.length()-3, "[");
688 attroff(COLOR_PAIR(int(Config.state_line_color)));
689 attron(COLOR_PAIR(int(Config.state_flags_color)));
690 mvprintw(1, COLS-switch_state.length()-2, "%s", switch_state.c_str());
691 attroff(COLOR_PAIR(int(Config.state_flags_color)));
692 attron(COLOR_PAIR(int(Config.state_line_color)));
693 mvprintw(1, COLS-2, "]");
695 attrset(0);
696 refresh();
697 break;
698 case Design::Alternative:
699 switch_state += '[';
700 switch_state += m_repeat ? m_repeat : '-';
701 switch_state += m_random ? m_random : '-';
702 switch_state += m_single ? m_single : '-';
703 switch_state += m_consume ? m_consume : '-';
704 switch_state += m_crossfade ? m_crossfade : '-';
705 switch_state += m_db_updating ? m_db_updating : '-';
706 switch_state += ']';
707 *wHeader << NC::XY(COLS-switch_state.length(), 1) << NC::Format::Bold << Config.state_flags_color << switch_state << NC::Color::End << NC::Format::NoBold;
708 if (!Config.header_visibility) // in this case also draw separator
710 *wHeader << NC::Format::Bold << NC::Color::Black;
711 mvwhline(wHeader->raw(), 2, 0, 0, COLS);
712 *wHeader << NC::Color::End << NC::Format::NoBold;
714 wHeader->refresh();
715 break;
719 void Status::Changes::mixer()
721 if (!Config.display_volume_level || (!Config.header_visibility && Config.design == Design::Classic))
722 return;
724 switch (Config.design)
726 case Design::Classic:
727 VolumeState = " " "Volume" ": ";
728 break;
729 case Design::Alternative:
730 VolumeState = " " "Vol" ": ";
731 break;
733 if (m_volume < 0)
734 VolumeState += "n/a";
735 else
737 VolumeState += boost::lexical_cast<std::string>(m_volume);
738 VolumeState += "%";
740 *wHeader << Config.volume_color;
741 *wHeader << NC::XY(wHeader->getWidth()-VolumeState.length(), 0) << VolumeState;
742 *wHeader << NC::Color::End;
743 wHeader->refresh();
746 void Status::Changes::outputs()
748 # ifdef ENABLE_OUTPUTS
749 myOutputs->FetchList();
750 # endif // ENABLE_OUTPUTS