openMSX
HotKey.cc
Go to the documentation of this file.
1#include "HotKey.hh"
4#include "CommandException.hh"
5#include "EventDistributor.hh"
6#include "CliComm.hh"
7#include "Event.hh"
8#include "TclArgParser.hh"
9#include "TclObject.hh"
10#include "SettingsConfig.hh"
11#include "one_of.hh"
12#include "outer.hh"
13#include "ranges.hh"
14#include "view.hh"
15#include "build-info.hh"
16#include <array>
17#include <cassert>
18#include <memory>
19
20using std::string;
21
22// This file implements all Tcl key bindings. These are the 'classical' hotkeys
23// (e.g. F12 to (un)mute sound) and the more recent input layers. The idea
24// behind an input layer is something like an OSD widget that (temporarily)
25// takes semi-exclusive access to the input. So while the widget is active
26// keyboard (and joystick) input is no longer passed to the emulated MSX.
27// However the classical hotkeys or the openMSX console still receive input.
28
29namespace openmsx {
30
31static constexpr bool META_HOT_KEYS =
32#ifdef __APPLE__
33 true;
34#else
35 false;
36#endif
37
39 GlobalCommandController& commandController_,
40 EventDistributor& eventDistributor_)
41 : RTSchedulable(rtScheduler)
42 , bindCmd (commandController_, *this, false)
43 , bindDefaultCmd (commandController_, *this, true)
44 , unbindCmd (commandController_, *this, false)
45 , unbindDefaultCmd(commandController_, *this, true)
46 , activateCmd (commandController_)
47 , deactivateCmd (commandController_)
48 , commandController(commandController_)
49 , eventDistributor(eventDistributor_)
50 , listenerHigh(*this, EventDistributor::Priority::HOTKEY_HIGH)
51 , listenerLow (*this, EventDistributor::Priority::HOTKEY_LOW)
52{
53 initDefaultBindings();
54}
55
56HotKey::Listener::Listener(HotKey& hotKey_, EventDistributor::Priority priority_)
57 : hotKey(hotKey_), priority(priority_)
58{
59 auto& distributor = hotKey.eventDistributor;
60 using enum EventType;
61 for (auto type : {KEY_DOWN, KEY_UP,
65 distributor.registerEventListener(type, *this, priority);
66 }
67}
68
69HotKey::Listener::~Listener()
70{
71 auto& distributor = hotKey.eventDistributor;
72 using enum EventType;
76 KEY_UP, KEY_DOWN}) {
77 distributor.unregisterEventListener(type, *this);
78 }
79}
80
81void HotKey::initDefaultBindings()
82{
83 // TODO move to Tcl script?
84
85 if constexpr (META_HOT_KEYS) {
86 // Hot key combos using Mac's Command key.
87 bindDefault(HotKeyInfo(KeyDownEvent::create(SDLK_d, KMOD_GUI),
88 "screenshot -guess-name"));
89 bindDefault(HotKeyInfo(KeyDownEvent::create(SDLK_p, KMOD_GUI),
90 "toggle pause"));
91 bindDefault(HotKeyInfo(KeyDownEvent::create(SDLK_t, KMOD_GUI),
92 "toggle fastforward"));
93 bindDefault(HotKeyInfo(KeyDownEvent::create(SDLK_l, KMOD_GUI),
94 "toggle console"));
95 bindDefault(HotKeyInfo(KeyDownEvent::create(SDLK_u, KMOD_GUI),
96 "toggle mute"));
97 bindDefault(HotKeyInfo(KeyDownEvent::create(SDLK_f, KMOD_GUI),
98 "toggle fullscreen"));
99 bindDefault(HotKeyInfo(KeyDownEvent::create(SDLK_q, KMOD_GUI),
100 "exit"));
101 } else {
102 // Hot key combos for typical PC keyboards.
103 bindDefault(HotKeyInfo(KeyDownEvent::create(SDLK_PRINTSCREEN),
104 "screenshot -guess-name"));
105 bindDefault(HotKeyInfo(KeyDownEvent::create(SDLK_PAUSE),
106 "toggle pause"));
107 bindDefault(HotKeyInfo(KeyDownEvent::create(SDLK_F9),
108 "toggle fastforward"));
109 bindDefault(HotKeyInfo(KeyDownEvent::create(SDLK_F10),
110 "toggle console"));
111 bindDefault(HotKeyInfo(KeyDownEvent::create(SDLK_F11),
112 "toggle fullscreen"));
113 bindDefault(HotKeyInfo(KeyDownEvent::create(SDLK_F12),
114 "toggle mute"));
115 bindDefault(HotKeyInfo(KeyDownEvent::create(SDLK_F4, KMOD_ALT),
116 "exit"));
117 bindDefault(HotKeyInfo(KeyDownEvent::create(SDLK_PAUSE, KMOD_CTRL),
118 "exit"));
119 bindDefault(HotKeyInfo(KeyDownEvent::create(SDLK_RETURN, KMOD_ALT),
120 "toggle fullscreen"));
121 }
122}
123
124static Event createEvent(const TclObject& obj, Interpreter& interp)
125{
126 auto event = InputEventFactory::createInputEvent(obj, interp);
127 using enum EventType;
128 if (getType(event) != one_of(KEY_UP, KEY_DOWN,
132 throw CommandException("Unsupported event type");
133 }
134 return event;
135}
136static Event createEvent(std::string_view str, Interpreter& interp)
137{
138 return createEvent(TclObject(str), interp);
139}
140
142{
143 // restore default bindings
144 unboundKeys.clear();
145 boundKeys.clear();
146 cmdMap = defaultMap;
147}
148
149void HotKey::loadBind(const Data& data)
150{
151 bind(HotKeyInfo(createEvent(data.key, commandController.getInterpreter()),
152 std::string(data.cmd), data.repeat, data.event, data.msx));
153}
154
155void HotKey::loadUnbind(std::string_view key)
156{
157 unbind(createEvent(key, commandController.getInterpreter()));
158}
159
161 explicit EqualEvent(const Event& event_) : event(event_) {}
162 bool operator()(const Event& e) const {
163 return event == e;
164 }
165 bool operator()(const HotKey::HotKeyInfo& info) const {
166 return event == info.event;
167 }
168 const Event& event;
169};
170
171static bool contains(auto&& range, const Event& event)
172{
173 return ranges::any_of(range, EqualEvent(event));
174}
175
176template<typename T>
177static void erase(std::vector<T>& v, const Event& event)
178{
179 if (auto it = ranges::find_if(v, EqualEvent(event)); it != end(v)) {
180 move_pop_back(v, it);
181 }
182}
183
184static void insert(HotKey::KeySet& set, const Event& event)
185{
186 if (auto it = ranges::find_if(set, EqualEvent(event)); it != end(set)) {
187 *it = event;
188 } else {
189 set.push_back(event);
190 }
191}
192
193template<typename HotKeyInfo>
194static void insert(HotKey::BindMap& map, HotKeyInfo&& info)
195{
196 if (auto it = ranges::find_if(map, EqualEvent(info.event)); it != end(map)) {
197 *it = std::forward<HotKeyInfo>(info);
198 } else {
199 map.push_back(std::forward<HotKeyInfo>(info));
200 }
201}
202
203void HotKey::bind(HotKeyInfo&& info)
204{
205 erase(unboundKeys, info.event);
206 erase(defaultMap, info.event);
207 insert(boundKeys, info.event);
208 insert(cmdMap, std::move(info));
209}
210
211void HotKey::unbind(const Event& event)
212{
213 if (auto it1 = ranges::find_if(boundKeys, EqualEvent(event));
214 it1 == end(boundKeys)) {
215 // only when not a regular bound event
216 insert(unboundKeys, event);
217 } else {
218 //erase(boundKeys, *event);
219 move_pop_back(boundKeys, it1);
220 }
221
222 erase(defaultMap, event);
223 erase(cmdMap, event);
224}
225
226void HotKey::bindDefault(HotKeyInfo&& info)
227{
228 if (!contains( boundKeys, info.event) &&
229 !contains(unboundKeys, info.event)) {
230 // not explicitly bound or unbound
231 insert(cmdMap, info);
232 }
233 insert(defaultMap, std::move(info));
234}
235
236void HotKey::unbindDefault(const Event& event)
237{
238 if (!contains( boundKeys, event) &&
239 !contains(unboundKeys, event)) {
240 // not explicitly bound or unbound
241 erase(cmdMap, event);
242 }
243 erase(defaultMap, event);
244}
245
246void HotKey::bindLayer(HotKeyInfo&& info, const string& layer)
247{
248 insert(layerMap[layer], std::move(info));
249}
250
251void HotKey::unbindLayer(const Event& event, const string& layer)
252{
253 erase(layerMap[layer], event);
254}
255
256void HotKey::unbindFullLayer(const string& layer)
257{
258 layerMap.erase(layer);
259}
260
261void HotKey::activateLayer(std::string layer, bool blocking)
262{
263 // Insert new activation record at the end of the list.
264 // (it's not an error if the same layer was already active, in such
265 // as case it will now appear twice in the list of active layer,
266 // and it must also be deactivated twice).
267 activeLayers.emplace_back(std::move(layer), blocking);
268}
269
270void HotKey::deactivateLayer(std::string_view layer)
271{
272 // remove the first matching activation record from the end
273 // (it's not an error if there is no match at all)
274 if (auto it = ranges::find(view::reverse(activeLayers), layer, &LayerInfo::layer);
275 it != activeLayers.rend()) {
276 // 'reverse_iterator' -> 'iterator' conversion is a bit tricky
277 activeLayers.erase((it + 1).base());
278 }
279}
280
281static HotKey::BindMap::const_iterator findMatch(
282 const HotKey::BindMap& map, const Event& event, bool msx)
283{
284 return ranges::find_if(map, [&](auto& p) {
285 return (p.msx == msx) && matches(p.event, event);
286 });
287}
288
289void HotKey::executeRT()
290{
291 if (lastEvent) {
292 executeEvent(*lastEvent, EventDistributor::Priority::HOTKEY_HIGH);
293 executeEvent(*lastEvent, EventDistributor::Priority::HOTKEY_LOW);
294 }
295}
296
297bool HotKey::Listener::signalEvent(const Event& event)
298{
299 return hotKey.signalEvent(event, priority);
300}
301
302bool HotKey::signalEvent(const Event& event, EventDistributor::Priority priority)
303{
304 if (lastEvent && *lastEvent != event) {
305 // If the newly received event is different from the repeating
306 // event, we stop the repeat process.
307 // Except when we're repeating a OsdControlEvent and the
308 // received event was actually the 'generating' event for the
309 // Osd event. E.g. a cursor-keyboard-down event will generate
310 // a corresponding osd event (the osd event is send before the
311 // original event). Without this hack, key-repeat will not work
312 // for osd key bindings.
313 stopRepeat();
314 }
315 return executeEvent(event, priority);
316}
317
318bool HotKey::executeEvent(const Event& event, EventDistributor::Priority priority)
319{
320 bool msx = priority == EventDistributor::Priority::HOTKEY_LOW;
321
322 // First search in active layers (from back to front)
323 bool blocking = false;
324 for (const auto& info : view::reverse(activeLayers)) {
325 auto& cmap = layerMap[info.layer]; // ok, if this entry doesn't exist yet
326 if (auto it = findMatch(cmap, event, msx); it != end(cmap)) {
327 executeBinding(event, *it);
328 // Deny event to lower priority listeners, also don't pass event
329 // to other layers (including the default layer).
330 return true;
331 }
332 blocking = info.blocking;
333 if (blocking) break; // don't try lower layers
334 }
335
336 // If the event was not yet handled, try the default layer.
337 if (auto it = findMatch(cmdMap, event, msx); it != end(cmdMap)) {
338 executeBinding(event, *it);
339 return true; // deny event to lower priority listeners
340 }
341
342 // Event is not handled, only let it pass to the MSX if there was no
343 // blocking layer active.
344 return blocking;
345}
346
347void HotKey::executeBinding(const Event& event, const HotKeyInfo& info)
348{
349 if (info.repeat) {
350 startRepeat(event);
351 }
352 try {
353 // Make a copy of the command string because executing the
354 // command could potentially execute (un)bind commands so
355 // that the original string becomes invalid.
356 // Valgrind complained about this in the following scenario:
357 // - open the OSD menu
358 // - activate the 'Exit openMSX' item
359 // The latter is triggered from e.g. a 'OSDControl A PRESS'
360 // event. The Tcl script bound to that event closes the main
361 // menu and reopens a new quit_menu. This will re-bind the
362 // action for the 'OSDControl A PRESS' event.
363 TclObject command(info.command);
364 if (info.passEvent) {
365 // Add event as the last argument to the command.
366 // (If needed) the current command string is first
367 // converted to a list (thus split in a command name
368 // and arguments).
369 command.addListElement(toTclList(event));
370 }
371
372 // ignore return value
373 command.executeCommand(commandController.getInterpreter());
374 } catch (CommandException& e) {
375 commandController.getCliComm().printWarning(
376 "Error executing hot key command: ", e.getMessage());
377 }
378}
379
380void HotKey::startRepeat(const Event& event)
381{
382 // I initially thought about using the builtin SDL key-repeat feature,
383 // but that won't work for example on joystick buttons. So we have to
384 // code it ourselves.
385
386 // On android, because of the sensitivity of the touch screen it's
387 // very hard to have touches of short durations. So half a second is
388 // too short for the key-repeat-delay. A full second should be fine.
389 static constexpr unsigned DELAY = PLATFORM_ANDROID ? 1000 : 500;
390 // Repeat period.
391 static constexpr unsigned PERIOD = 30;
392
393 unsigned delay = (lastEvent ? PERIOD : DELAY) * 1000;
394 lastEvent = event;
395 scheduleRT(delay);
396}
397
398void HotKey::stopRepeat()
399{
400 lastEvent.reset();
401 cancelRT();
402}
403
404
405// class BindCmd
406
407static constexpr std::string_view getBindCmdName(bool defaultCmd)
408{
409 return defaultCmd ? "bind_default" : "bind";
410}
411
412HotKey::BindCmd::BindCmd(CommandController& commandController_, HotKey& hotKey_,
413 bool defaultCmd_)
414 : Command(commandController_, getBindCmdName(defaultCmd_))
415 , hotKey(hotKey_)
416 , defaultCmd(defaultCmd_)
417{
418}
419
420static string formatBinding(const HotKey::HotKeyInfo& info)
421{
422 return strCat(toString(info.event),
423 (info.msx ? " [msx]" : ""),
424 (info.repeat ? " [repeat]" : ""),
425 (info.passEvent ? " [event]" : ""),
426 ": ", info.command, '\n');
427}
428
429void HotKey::BindCmd::execute(std::span<const TclObject> tokens, TclObject& result)
430{
431 string layer;
432 bool layers = false;
433 bool repeat = false;
434 bool passEvent = false;
435 bool msx = false;
436 std::array parserInfo = {
437 valueArg("-layer", layer),
438 flagArg("-layers", layers),
439 flagArg("-repeat", repeat),
440 flagArg("-event", passEvent),
441 flagArg("-msx", msx),
442 };
443 auto arguments = parseTclArgs(getInterpreter(), tokens.subspan<1>(), parserInfo);
444 if (defaultCmd && !layer.empty()) {
445 throw CommandException("Layers are not supported for default bindings");
446 }
447
448 auto& cMap = defaultCmd
449 ? hotKey.defaultMap
450 : layer.empty() ? hotKey.cmdMap
451 : hotKey.layerMap[layer];
452
453 if (layers) {
454 for (const auto& [layerName, bindings] : hotKey.layerMap) {
455 // An alternative for this test is to always properly
456 // prune layerMap. ATM this approach seems simpler.
457 if (!bindings.empty()) {
458 result.addListElement(layerName);
459 }
460 }
461 return;
462 }
463
464 switch (arguments.size()) {
465 case 0: {
466 // show all bounded keys (for this layer)
467 string r;
468 for (const auto& p : cMap) {
469 r += formatBinding(p);
470 }
471 result = r;
472 break;
473 }
474 case 1: {
475 // show bindings for this key (in this layer)
476 auto it = ranges::find_if(cMap,
477 EqualEvent(createEvent(arguments[0], getInterpreter())));
478 if (it == end(cMap)) {
479 throw CommandException("Key not bound");
480 }
481 result = formatBinding(*it);
482 break;
483 }
484 default: {
485 // make a new binding
486 string command(arguments[1].getString());
487 for (const auto& arg : view::drop(arguments, 2)) {
488 strAppend(command, ' ', arg.getString());
489 }
490 HotKey::HotKeyInfo info(
491 createEvent(arguments[0], getInterpreter()),
492 command, repeat, passEvent, msx);
493 if (defaultCmd) {
494 hotKey.bindDefault(std::move(info));
495 } else if (layer.empty()) {
496 hotKey.bind(std::move(info));
497 } else {
498 hotKey.bindLayer(std::move(info), layer);
499 }
500 break;
501 }
502 }
503}
504string HotKey::BindCmd::help(std::span<const TclObject> /*tokens*/) const
505{
506 auto cmd = getBindCmdName(defaultCmd);
507 return strCat(
508 cmd, " : show all bounded keys\n",
509 cmd, " <key> : show binding for this key\n",
510 cmd, " <key> [-msx] [-repeat] [-event] <cmd> : bind key to command, optionally "
511 "repeat command while key remains pressed and also optionally "
512 "give back the event as argument (a list) to <cmd>.\n"
513 "When the '-msx' flag is given the binding only has effect "
514 "when the msx window has focus (not when the GUI has focus).\n"
515 "These 3 take an optional '-layer <layername>' option, "
516 "see activate_input_layer.\n",
517 cmd, " -layers : show a list of layers with bound keys\n");
518}
519
520
521// class UnbindCmd
522
523static constexpr std::string_view getUnbindCmdName(bool defaultCmd)
524{
525 return defaultCmd ? "unbind_default" : "unbind";
526}
527
528HotKey::UnbindCmd::UnbindCmd(CommandController& commandController_,
529 HotKey& hotKey_, bool defaultCmd_)
530 : Command(commandController_, getUnbindCmdName(defaultCmd_))
531 , hotKey(hotKey_)
532 , defaultCmd(defaultCmd_)
533{
534}
535
536void HotKey::UnbindCmd::execute(std::span<const TclObject> tokens, TclObject& /*result*/)
537{
538 string layer;
539 std::array info = {valueArg("-layer", layer)};
540 auto arguments = parseTclArgs(getInterpreter(), tokens.subspan<1>(), info);
541 if (defaultCmd && !layer.empty()) {
542 throw CommandException("Layers are not supported for default bindings");
543 }
544
545 if ((arguments.size() > 1) || (layer.empty() && (arguments.size() != 1))) {
546 throw SyntaxError();
547 }
548
549 std::optional<Event> event;
550 if (arguments.size() == 1) {
551 event = createEvent(arguments[0], getInterpreter());
552 }
553
554 if (defaultCmd) {
555 assert(event);
556 hotKey.unbindDefault(*event);
557 } else if (layer.empty()) {
558 assert(event);
559 hotKey.unbind(*event);
560 } else {
561 if (event) {
562 hotKey.unbindLayer(*event, layer);
563 } else {
564 hotKey.unbindFullLayer(layer);
565 }
566 }
567}
568string HotKey::UnbindCmd::help(std::span<const TclObject> /*tokens*/) const
569{
570 auto cmd = getUnbindCmdName(defaultCmd);
571 return strCat(
572 cmd, " <key> : unbind this key\n",
573 cmd, " -layer <layername> <key> : unbind key in a specific layer\n",
574 cmd, " -layer <layername> : unbind all keys in this layer\n");
575}
576
577
578// class ActivateCmd
579
580HotKey::ActivateCmd::ActivateCmd(CommandController& commandController_)
581 : Command(commandController_, "activate_input_layer")
582{
583}
584
585void HotKey::ActivateCmd::execute(std::span<const TclObject> tokens, TclObject& result)
586{
587 bool blocking = false;
588 std::array info = {flagArg("-blocking", blocking)};
589 auto args = parseTclArgs(getInterpreter(), tokens.subspan(1), info);
590
591 auto& hotKey = OUTER(HotKey, activateCmd);
592 switch (args.size()) {
593 case 0: {
594 string r;
595 for (const auto& layerInfo : view::reverse(hotKey.activeLayers)) {
596 r += layerInfo.layer;
597 if (layerInfo.blocking) {
598 r += " -blocking";
599 }
600 r += '\n';
601 }
602 result = r;
603 break;
604 }
605 case 1: {
606 std::string_view layer = args[0].getString();
607 hotKey.activateLayer(string(layer), blocking);
608 break;
609 }
610 default:
611 throw SyntaxError();
612 }
613}
614
615string HotKey::ActivateCmd::help(std::span<const TclObject> /*tokens*/) const
616{
617 return "activate_input_layer "
618 ": show list of active layers (most recent on top)\n"
619 "activate_input_layer [-blocking] <layername> "
620 ": activate new layer, optionally in blocking mode\n";
621}
622
623
624// class DeactivateCmd
625
626HotKey::DeactivateCmd::DeactivateCmd(CommandController& commandController_)
627 : Command(commandController_, "deactivate_input_layer")
628{
629}
630
631void HotKey::DeactivateCmd::execute(std::span<const TclObject> tokens, TclObject& /*result*/)
632{
633 checkNumArgs(tokens, 2, "layer");
634 auto& hotKey = OUTER(HotKey, deactivateCmd);
635 hotKey.deactivateLayer(tokens[1].getString());
636}
637
638string HotKey::DeactivateCmd::help(std::span<const TclObject> /*tokens*/) const
639{
640 return "deactivate_input_layer <layername> : deactivate the given input layer";
641}
642
643
644} // namespace openmsx
#define PLATFORM_ANDROID
Definition build-info.hh:17
void printWarning(std::string_view message)
Definition CliComm.cc:12
Priority
Priorities from high to low, higher priority listeners can block events for lower priority listeners.
HotKey(RTScheduler &rtScheduler, GlobalCommandController &commandController, EventDistributor &eventDistributor)
Definition HotKey.cc:38
void loadUnbind(std::string_view key)
Definition HotKey.cc:155
std::vector< HotKeyInfo > BindMap
Definition HotKey.hh:45
void loadBind(const Data &data)
Definition HotKey.cc:149
void loadInit()
Definition HotKey.cc:141
std::vector< Event > KeySet
Definition HotKey.hh:46
static KeyDownEvent create(SDL_Keycode code, SDL_Keymod mod=KMOD_NONE)
Definition Event.hh:81
void scheduleRT(uint64_t delta)
constexpr double e
Definition Math.hh:21
Event createInputEvent(const TclObject &str, Interpreter &interp)
This file implemented 3 utility functions:
Definition Autofire.cc:11
EventType
Definition Event.hh:454
bool matches(const Event &self, const Event &other)
Does this event 'match' the given event.
Definition Event.cc:211
ArgsInfo valueArg(std::string_view name, T &value)
std::vector< TclObject > parseTclArgs(Interpreter &interp, std::span< const TclObject > inArgs, std::span< const ArgsInfo > table)
TclObject toTclList(const Event &event)
Similar to toString(), but retains the structure of the event.
Definition Event.cc:102
EventType getType(const Event &event)
Definition Event.hh:517
std::string toString(const BooleanInput &input)
std::variant< KeyUpEvent, KeyDownEvent, MouseMotionEvent, MouseButtonUpEvent, MouseButtonDownEvent, MouseWheelEvent, JoystickAxisMotionEvent, JoystickHatEvent, JoystickButtonUpEvent, JoystickButtonDownEvent, OsdControlReleaseEvent, OsdControlPressEvent, WindowEvent, TextEvent, FileDropEvent, QuitEvent, FinishFrameEvent, CliCommandEvent, GroupEvent, BootEvent, FrameDrawnEvent, BreakEvent, SwitchRendererEvent, TakeReverseSnapshotEvent, AfterTimedEvent, MachineLoadedEvent, MachineActivatedEvent, MachineDeactivatedEvent, MidiInReaderEvent, MidiInWindowsEvent, MidiInCoreMidiEvent, MidiInCoreMidiVirtualEvent, MidiInALSAEvent, Rs232TesterEvent, Rs232NetEvent, ImGuiDelayedActionEvent, ImGuiActiveEvent > Event
Definition Event.hh:445
ArgsInfo flagArg(std::string_view name, bool &flag)
bool any_of(InputRange &&range, UnaryPredicate pred)
Definition ranges.hh:200
auto find_if(InputRange &&range, UnaryPredicate pred)
Definition ranges.hh:175
auto find(InputRange &&range, const T &value)
Definition ranges.hh:162
Definition view.hh:15
constexpr auto reverse(Range &&range)
Definition view.hh:514
constexpr auto drop(Range &&range, size_t n)
Definition view.hh:502
#define OUTER(type, member)
Definition outer.hh:42
void move_pop_back(VECTOR &v, typename VECTOR::iterator it)
Erase the pointed to element from the given vector.
Definition stl.hh:134
std::string strCat()
Definition strCat.hh:703
void strAppend(std::string &result, Ts &&...ts)
Definition strCat.hh:752
const Event & event
Definition HotKey.cc:168
EqualEvent(const Event &event_)
Definition HotKey.cc:161
bool operator()(const HotKey::HotKeyInfo &info) const
Definition HotKey.cc:165
bool operator()(const Event &e) const
Definition HotKey.cc:162
std::string_view key
Definition HotKey.hh:27
std::string_view cmd
Definition HotKey.hh:28
constexpr void repeat(T n, Op op)
Repeat the given operation 'op' 'n' times.
Definition xrange.hh:147
constexpr auto end(const zstring_view &x)