bindings: add support for defining and executing commands
This commit is contained in:
21
doc/bindings
21
doc/bindings
@@ -95,6 +95,24 @@
|
||||
## selected_items_adder, server_info, song_info,
|
||||
## sort_playlist_dialog, tiny_tag_editor.
|
||||
##
|
||||
## 5) In addition to binding to a key, you can also bind actions
|
||||
## or chains of actions to a command. If it comes to commands,
|
||||
## syntax is very similar to defining keys. Here goes example
|
||||
## definition of a command:
|
||||
##
|
||||
## def_command "quit" [deferred]
|
||||
## stop
|
||||
## quit
|
||||
##
|
||||
## If you execute the above command (which can be done by
|
||||
## invoking action execute_command, typing 'quit' and pressing
|
||||
## enter), ncmpcpp will stop the player and then quit. Note the
|
||||
## presence of word 'deferred' enclosed in square brackets. It
|
||||
## tells ncmpcpp to wait for confirmation (ie. pressing enter)
|
||||
## after you typed quit. Instead of 'deferred', 'immediate'
|
||||
## could be used. Then ncmpcpp will not wait for confirmation
|
||||
## (enter) and will execute the command the moment it sees it.
|
||||
##
|
||||
## Note: Both 'backspace' and 'backspace_2' are used because some
|
||||
## terminals interpret backspace using keycode of 'backspace'
|
||||
## and some the one of 'backspace_2'. You can get away with
|
||||
@@ -191,6 +209,9 @@
|
||||
#def_key "-"
|
||||
# volume_down
|
||||
#
|
||||
#def_key ":"
|
||||
# execute_command
|
||||
#
|
||||
#def_key "tab"
|
||||
# next_screen
|
||||
#
|
||||
|
||||
@@ -972,6 +972,27 @@ void Stop::Run()
|
||||
Mpd.Stop();
|
||||
}
|
||||
|
||||
void ExecuteCommand::Run()
|
||||
{
|
||||
using Global::wFooter;
|
||||
Statusbar::lock();
|
||||
Statusbar::put() << NC::fmtBold << ":" << NC::fmtBoldEnd;
|
||||
wFooter->setGetStringHelper(Statusbar::Helpers::TryExecuteImmediateCommand());
|
||||
std::string name = wFooter->getString();
|
||||
wFooter->setGetStringHelper(Statusbar::Helpers::getString);
|
||||
Statusbar::unlock();
|
||||
if (name.empty())
|
||||
return;
|
||||
auto cmd = Bindings.findCommand(name);
|
||||
if (cmd)
|
||||
{
|
||||
Statusbar::msg(1, "Executing %s...", name.c_str());
|
||||
cmd->binding().execute();
|
||||
}
|
||||
else
|
||||
Statusbar::msg("No command named \"%s\"", name.c_str());
|
||||
}
|
||||
|
||||
bool MoveSortOrderUp::canBeRun() const
|
||||
{
|
||||
return myScreen == mySortPlaylistDialog;
|
||||
@@ -2576,6 +2597,7 @@ void populateActions()
|
||||
insertAction(new NextSong());
|
||||
insertAction(new Pause());
|
||||
insertAction(new Stop());
|
||||
insertAction(new ExecuteCommand());
|
||||
insertAction(new SavePlaylist());
|
||||
insertAction(new MoveSortOrderUp());
|
||||
insertAction(new MoveSortOrderDown());
|
||||
|
||||
@@ -31,7 +31,7 @@ enum ActionType
|
||||
aDummy, aMouseEvent, aScrollUp, aScrollDown, aScrollUpArtist, aScrollUpAlbum, aScrollDownArtist,
|
||||
aScrollDownAlbum, aPageUp, aPageDown, aMoveHome, aMoveEnd, aToggleInterface, aJumpToParentDirectory,
|
||||
aPressEnter, aPressSpace, aPreviousColumn, aNextColumn, aMasterScreen, aSlaveScreen, aVolumeUp,
|
||||
aVolumeDown, aDeletePlaylistItems, aDeleteStoredPlaylist, aDeleteBrowserItems, aReplaySong, aPrevious, aNext, aPause, aStop, aSavePlaylist,
|
||||
aVolumeDown, aDeletePlaylistItems, aDeleteStoredPlaylist, aDeleteBrowserItems, aReplaySong, aPrevious, aNext, aPause, aStop, aExecuteCommand, aSavePlaylist,
|
||||
aMoveSortOrderUp, aMoveSortOrderDown, aMoveSelectedItemsUp, aMoveSelectedItemsDown,
|
||||
aMoveSelectedItemsTo, aAdd, aSeekForward, aSeekBackward, aToggleDisplayMode, aToggleSeparatorsBetweenAlbums,
|
||||
aToggleLyricsFetcher, aToggleFetchingLyricsInBackground, aTogglePlayingSongCentering, aUpdateDatabase,
|
||||
@@ -361,6 +361,14 @@ protected:
|
||||
virtual void Run();
|
||||
};
|
||||
|
||||
struct ExecuteCommand : public Action
|
||||
{
|
||||
ExecuteCommand() : Action(aExecuteCommand, "execute_command") { }
|
||||
|
||||
protected:
|
||||
virtual void Run();
|
||||
};
|
||||
|
||||
struct SavePlaylist : public Action
|
||||
{
|
||||
SavePlaylist() : Action(aSavePlaylist, "save_playlist") { }
|
||||
|
||||
@@ -189,26 +189,52 @@ Key Key::read(NC::Window &w)
|
||||
|
||||
bool BindingsConfiguration::read(const std::string &file)
|
||||
{
|
||||
enum class InProgress { None, Command, Key };
|
||||
|
||||
bool result = true;
|
||||
|
||||
std::ifstream f(file);
|
||||
if (!f.is_open())
|
||||
return result;
|
||||
|
||||
// shared variables
|
||||
InProgress in_progress = InProgress::None;
|
||||
size_t line_no = 0;
|
||||
bool key_def_in_progress = false;
|
||||
Key key = Key::noOp;
|
||||
std::string line;
|
||||
Binding::ActionChain actions;
|
||||
std::string line, strkey;
|
||||
|
||||
// def_key specific variables
|
||||
Key key = Key::noOp;
|
||||
std::string strkey;
|
||||
|
||||
// def_command specific variables
|
||||
bool cmd_immediate = false;
|
||||
std::string cmd_name;
|
||||
|
||||
auto error = [&]() -> std::ostream & {
|
||||
std::cerr << file << ":" << line_no << ": ";
|
||||
key_def_in_progress = false;
|
||||
std::cerr << file << ":" << line_no << ": error: ";
|
||||
in_progress = InProgress::None;
|
||||
result = false;
|
||||
return std::cerr;
|
||||
};
|
||||
|
||||
auto bind_key_def = [&]() -> bool {
|
||||
auto bind_in_progress = [&]() -> bool {
|
||||
if (in_progress == InProgress::Command)
|
||||
{
|
||||
if (!actions.empty())
|
||||
{
|
||||
m_commands.insert(std::make_pair(cmd_name, Command(actions, cmd_immediate)));
|
||||
actions.clear();
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
error() << "definition of command '" << cmd_name << "' cannot be empty\n";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else if (in_progress == InProgress::Key)
|
||||
{
|
||||
if (!actions.empty())
|
||||
{
|
||||
bind(key, actions);
|
||||
@@ -217,25 +243,56 @@ bool BindingsConfiguration::read(const std::string &file)
|
||||
}
|
||||
else
|
||||
{
|
||||
error() << "definition of key '" << strkey << "' cannot be empty.\n";
|
||||
error() << "definition of key '" << strkey << "' cannot be empty\n";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const char def_command[] = "def_command";
|
||||
const char def_key[] = "def_key";
|
||||
|
||||
while (!f.eof() && ++line_no)
|
||||
{
|
||||
getline(f, line);
|
||||
if (line.empty() || line[0] == '#')
|
||||
continue;
|
||||
|
||||
if (!line.compare(0, 7, "def_key")) // beginning of key definition
|
||||
// beginning of command definition
|
||||
if (!line.compare(0, const_strlen(def_command), def_command))
|
||||
{
|
||||
if (key_def_in_progress)
|
||||
if (!bind_in_progress())
|
||||
break;
|
||||
in_progress = InProgress::Command;
|
||||
cmd_name = getEnclosedString(line, '"', '"', 0);
|
||||
if (cmd_name.empty())
|
||||
{
|
||||
if (!bind_key_def())
|
||||
error() << "command must have non-empty name\n";
|
||||
break;
|
||||
}
|
||||
key_def_in_progress = true;
|
||||
if (m_commands.find(cmd_name) != m_commands.end())
|
||||
{
|
||||
error() << "redefinition of command '" << cmd_name << "'\n";
|
||||
break;
|
||||
}
|
||||
std::string cmd_type = getEnclosedString(line, '[', ']', 0);
|
||||
if (cmd_type == "immediate")
|
||||
cmd_immediate = true;
|
||||
else if (cmd_type == "deferred")
|
||||
cmd_immediate = false;
|
||||
else
|
||||
{
|
||||
error() << "invalid type of command: '" << cmd_type << "'\n";
|
||||
break;
|
||||
}
|
||||
}
|
||||
// beginning of key definition
|
||||
else if (!line.compare(0, const_strlen(def_key), def_key))
|
||||
{
|
||||
if (!bind_in_progress())
|
||||
break;
|
||||
in_progress = InProgress::Key;
|
||||
strkey = getEnclosedString(line, '"', '"', 0);
|
||||
key = stringToKey(strkey);
|
||||
if (key == Key::noOp)
|
||||
@@ -262,8 +319,7 @@ bool BindingsConfiguration::read(const std::string &file)
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (key_def_in_progress)
|
||||
bind_key_def();
|
||||
bind_in_progress();
|
||||
f.close();
|
||||
return result;
|
||||
}
|
||||
@@ -318,6 +374,8 @@ void BindingsConfiguration::generateDefaults()
|
||||
}
|
||||
if (notBound(k = stringToKey("-")))
|
||||
bind(k, aVolumeDown);
|
||||
if (notBound(k = stringToKey(":")))
|
||||
bind(k, aExecuteCommand);
|
||||
if (notBound(k = stringToKey("tab")))
|
||||
bind(k, aNextScreen);
|
||||
if (notBound(k = stringToKey("shift_tab")))
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
#define _BINDINGS_H
|
||||
|
||||
#include <cassert>
|
||||
#include <unordered_map>
|
||||
#include "actions.h"
|
||||
#include "macro_utilities.h"
|
||||
|
||||
@@ -85,6 +86,20 @@ struct Binding
|
||||
}
|
||||
}
|
||||
|
||||
bool execute() const {
|
||||
bool result = false;
|
||||
if (m_is_single) {
|
||||
assert(m_action);
|
||||
result = m_action->Execute();
|
||||
} else {
|
||||
for (auto it = m_chain->begin(); it != m_chain->end(); ++it)
|
||||
if (!(*it)->Execute())
|
||||
break;
|
||||
result = true;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
bool isSingle() const {
|
||||
return m_is_single;
|
||||
}
|
||||
@@ -105,16 +120,41 @@ private:
|
||||
};
|
||||
};
|
||||
|
||||
/// Keybindings configuration
|
||||
struct BindingsConfiguration
|
||||
/// Represents executable command
|
||||
struct Command
|
||||
{
|
||||
Command(const Binding &binding_, bool immediate_)
|
||||
: m_binding(binding_), m_immediate(immediate_) { }
|
||||
|
||||
const Binding &binding() const { return m_binding; }
|
||||
bool immediate() const { return m_immediate; }
|
||||
|
||||
private:
|
||||
Binding m_binding;
|
||||
bool m_immediate;
|
||||
};
|
||||
|
||||
/// Keybindings configuration
|
||||
class BindingsConfiguration
|
||||
{
|
||||
typedef std::unordered_map<std::string, Command> CommandsSet;
|
||||
typedef std::multimap<Key, Binding> BindingsMap;
|
||||
|
||||
public:
|
||||
typedef BindingsMap::iterator BindingIterator;
|
||||
typedef BindingsMap::const_iterator ConstBindingIterator;
|
||||
|
||||
bool read(const std::string &file);
|
||||
void generateDefaults();
|
||||
|
||||
const Command *findCommand(const std::string &name) {
|
||||
const Command *ptr = 0;
|
||||
auto it = m_commands.find(name);
|
||||
if (it != m_commands.end())
|
||||
ptr = &it->second;
|
||||
return ptr;
|
||||
}
|
||||
|
||||
std::pair<BindingIterator, BindingIterator> get(const Key &k) {
|
||||
return m_bindings.equal_range(k);
|
||||
}
|
||||
@@ -132,6 +172,7 @@ private:
|
||||
}
|
||||
|
||||
BindingsMap m_bindings;
|
||||
CommandsSet m_commands;
|
||||
};
|
||||
|
||||
extern BindingsConfiguration Bindings;
|
||||
|
||||
@@ -238,6 +238,7 @@ void Help::GetKeybindings()
|
||||
KeyDesc(aSetCrossfade, "Set crossfade");
|
||||
KeyDesc(aUpdateDatabase, "Start music database update");
|
||||
w << '\n';
|
||||
KeyDesc(aExecuteCommand, "Execute command");
|
||||
KeyDesc(aApplyFilter, "Apply filter");
|
||||
KeyDesc(aFindItemForward, "Find item forward");
|
||||
KeyDesc(aFindItemBackward, "Find item backward");
|
||||
|
||||
@@ -237,7 +237,6 @@ int main(int argc, char **argv)
|
||||
drawHeader();
|
||||
past = Timer;
|
||||
}
|
||||
|
||||
// header stuff end
|
||||
|
||||
if (input != Key::noOp)
|
||||
@@ -249,22 +248,8 @@ int main(int argc, char **argv)
|
||||
|
||||
auto k = Bindings.get(input);
|
||||
for (; k.first != k.second; ++k.first)
|
||||
{
|
||||
Binding &b = k.first->second;
|
||||
if (b.isSingle())
|
||||
{
|
||||
if (b.action()->Execute())
|
||||
if (k.first->second.execute())
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
auto chain = b.chain();
|
||||
for (auto it = chain->begin(); it != chain->end(); ++it)
|
||||
if (!(*it)->Execute())
|
||||
break;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (myScreen == myPlaylist)
|
||||
myPlaylist->EnableHighlighting();
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
#include "settings.h"
|
||||
#include "status.h"
|
||||
#include "statusbar.h"
|
||||
#include "bindings.h"
|
||||
|
||||
using Global::wFooter;
|
||||
|
||||
@@ -186,12 +187,13 @@ void Statusbar::Helpers::mpd()
|
||||
Mpd.OrderDataFetching();
|
||||
}
|
||||
|
||||
void Statusbar::Helpers::getString(const std::wstring &)
|
||||
bool Statusbar::Helpers::getString(const std::wstring &)
|
||||
{
|
||||
Status::trace();
|
||||
return true;
|
||||
}
|
||||
|
||||
void Statusbar::Helpers::ApplyFilterImmediately::operator()(const std::wstring &ws)
|
||||
bool Statusbar::Helpers::ApplyFilterImmediately::operator()(const std::wstring &ws)
|
||||
{
|
||||
using Global::myScreen;
|
||||
// if input queue is not empty, we don't want to update filter since next
|
||||
@@ -209,4 +211,19 @@ void Statusbar::Helpers::ApplyFilterImmediately::operator()(const std::wstring &
|
||||
}
|
||||
Status::trace();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Statusbar::Helpers::TryExecuteImmediateCommand::operator()(const std::wstring &ws)
|
||||
{
|
||||
bool continue_ = true;
|
||||
if (m_ws != ws)
|
||||
{
|
||||
m_ws = ws;
|
||||
auto cmd = Bindings.findCommand(ToString(m_ws));
|
||||
if (cmd && cmd->immediate())
|
||||
continue_ = false;
|
||||
}
|
||||
Status::trace();
|
||||
return continue_;
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@ namespace Helpers {//
|
||||
void mpd();
|
||||
|
||||
/// called each time user types another character while inside Window::getString
|
||||
void getString(const std::wstring &);
|
||||
bool getString(const std::wstring &);
|
||||
|
||||
/// called each time user changes current filter (while being inside Window::getString)
|
||||
struct ApplyFilterImmediately
|
||||
@@ -79,13 +79,21 @@ struct ApplyFilterImmediately
|
||||
ApplyFilterImmediately(Filterable *f, const std::wstring &filter)
|
||||
: m_f(f), m_ws(filter) { }
|
||||
|
||||
void operator()(const std::wstring &ws);
|
||||
bool operator()(const std::wstring &ws);
|
||||
|
||||
private:
|
||||
Filterable *m_f;
|
||||
std::wstring m_ws;
|
||||
};
|
||||
|
||||
struct TryExecuteImmediateCommand
|
||||
{
|
||||
bool operator()(const std::wstring &ws);
|
||||
|
||||
private:
|
||||
std::wstring m_ws;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -552,7 +552,10 @@ std::string Window::getString(const std::string &base, size_t length_, size_t wi
|
||||
mvwhline(m_window, y, minx, '*', maxx-minx);
|
||||
|
||||
if (m_get_string_helper)
|
||||
m_get_string_helper(*tmp);
|
||||
{
|
||||
if (!m_get_string_helper(*tmp))
|
||||
break;
|
||||
}
|
||||
|
||||
wmove(m_window, y, x);
|
||||
prefresh(m_window, 0, 0, m_start_y, m_start_x, m_start_y+m_height-1, m_start_x+m_width-1);
|
||||
|
||||
@@ -132,7 +132,7 @@ enum Where { wUp, wDown, wPageUp, wPageDown, wHome, wEnd };
|
||||
/// Helper function that is invoked each time one will want
|
||||
/// to obtain string from Window::getString() function
|
||||
/// @see Window::getString()
|
||||
typedef std::function<void(const std::wstring &)> GetStringHelper;
|
||||
typedef std::function<bool(const std::wstring &)> GetStringHelper;
|
||||
|
||||
/// Initializes curses screen and sets some additional attributes
|
||||
/// @param window_title title of the window (has an effect only if pdcurses lib is used)
|
||||
|
||||
Reference in New Issue
Block a user