1 /*
2     Hotkeys plugin for DeaDBeeF
3     Copyright (C) 2009-2011 Viktor Semykin <thesame.ml@gmail.com>
4     Copyright (C) 2012-2013 Alexey Yakovenko <waker@users.sourceforge.net>
5 
6     This software is provided 'as-is', without any express or implied
7     warranty.  In no event will the authors be held liable for any damages
8     arising from the use of this software.
9 
10     Permission is granted to anyone to use this software for any purpose,
11     including commercial applications, and to alter it and redistribute it
12     freely, subject to the following restrictions:
13 
14     1. The origin of this software must not be misrepresented; you must not
15      claim that you wrote the original software. If you use this software
16      in a product, an acknowledgment in the product documentation would be
17      appreciated but is not required.
18 
19     2. Altered source versions must be plainly marked as such, and must not be
20      misrepresented as being the original software.
21 
22     3. This notice may not be removed or altered from any source distribution.
23 */
24 #include <stdio.h>
25 #include <stdlib.h>
26 #include <string.h>
27 #include <unistd.h>
28 #ifndef __APPLE__
29 #include <X11/Xlib.h>
30 #endif
31 #include <ctype.h>
32 #ifdef __linux__
33 #include <sys/prctl.h>
34 #endif
35 
36 #include "../libparser/parser.h"
37 #include "hotkeys.h"
38 #include "../../deadbeef.h"
39 #include "actionhandlers.h"
40 
41 //#define trace(...) { fprintf(stderr, __VA_ARGS__); }
42 #define trace(fmt,...)
43 
44 static DB_hotkeys_plugin_t plugin;
45 DB_functions_t *deadbeef;
46 
47 #ifndef __APPLE__
48 static int finished;
49 static Display *disp;
50 static intptr_t loop_tid;
51 static int need_reset = 0;
52 #endif
53 
54 #define MAX_COMMAND_COUNT 256
55 
56 typedef struct {
57     const char *name;
58     int keysym;
59 #ifndef __APPLE__
60     int keycode; // after mapping
61 #endif
62 } xkey_t;
63 
64 #define KEY(kname, kcode) { .name=kname, .keysym=kcode},
65 
66 static xkey_t keys[] = {
67     #include "keysyms.inc"
68 };
69 
70 typedef struct command_s {
71     int keycode;
72 #ifndef __APPLE__
73     int x11_keycode;
74 #endif
75     int modifier;
76     int ctx;
77     int isglobal;
78     DB_plugin_action_t *action;
79 } command_t;
80 
81 static command_t commands [MAX_COMMAND_COUNT];
82 static int command_count = 0;
83 
84 #ifndef __APPLE__
85 static void
init_mapped_keycodes(Display * disp,Atom * syms,int first_kk,int last_kk,int ks_per_kk)86 init_mapped_keycodes (Display *disp, Atom *syms, int first_kk, int last_kk, int ks_per_kk) {
87     int i, ks;
88     for (i = 0; i < last_kk-first_kk; i++)
89     {
90         int sym = * (syms + i*ks_per_kk);
91         for (ks = 0; keys[ks].name; ks++)
92         {
93             if (keys[ ks ].keysym == sym)
94             {
95                 keys[ks].keycode = i+first_kk;
96             }
97         }
98     }
99 }
100 #endif
101 
102 static int
get_keycode(const char * name)103 get_keycode (const char* name) {
104     for (int i = 0; keys[i].name; i++) {
105         if (!strcmp (name, keys[i].name)) {
106             trace ("init: key %s code %x\n", name, keys[i].keysym);
107             return keys[i].keysym;
108         }
109     }
110     return 0;
111 }
112 
113 static char*
trim(char * s)114 trim (char* s)
115 {
116     char *h, *t;
117 
118     for (h = s; *h == ' ' || *h == '\t'; h++);
119     for (t = s + strlen (s); *t == ' ' || *t == '\t'; t--);
120     * (t+1) = 0;
121     return h;
122 }
123 
124 static void
cmd_invoke_plugin_command(DB_plugin_action_t * action,int ctx)125 cmd_invoke_plugin_command (DB_plugin_action_t *action, int ctx)
126 {
127     if (action->callback) {
128         if (ctx == DDB_ACTION_CTX_MAIN) {
129             // collect stuff for 1.4 user data
130 
131             // common action
132             if (action->flags & DB_ACTION_COMMON)
133             {
134                 action->callback (action, NULL);
135                 return;
136             }
137 
138             // playlist action
139             if (action->flags & DB_ACTION_PLAYLIST)
140             {
141                 ddb_playlist_t *plt = deadbeef->plt_get_curr ();
142                 if (plt) {
143                     action->callback (action, plt);
144                     deadbeef->plt_unref (plt);
145                 }
146                 return;
147             }
148 
149             int selected_count = 0;
150             DB_playItem_t *pit = deadbeef->pl_get_first (PL_MAIN);
151             DB_playItem_t *selected = NULL;
152             while (pit) {
153                 if (deadbeef->pl_is_selected (pit))
154                 {
155                     if (!selected)
156                         selected = pit;
157                     selected_count++;
158                 }
159                 DB_playItem_t *next = deadbeef->pl_get_next (pit, PL_MAIN);
160                 deadbeef->pl_item_unref (pit);
161                 pit = next;
162             }
163 
164             //Now we're checking if action is applicable:
165 
166             if (selected_count == 0)
167             {
168                 trace ("No tracks selected\n");
169                 return;
170             }
171             if ((selected_count == 1) && (!(action->flags & DB_ACTION_SINGLE_TRACK)))
172             {
173                 trace ("Hotkeys: action %s not allowed for single track\n", action->name);
174                 return;
175             }
176             if ((selected_count > 1) && (!(action->flags & DB_ACTION_MULTIPLE_TRACKS)))
177             {
178                 trace ("Hotkeys: action %s not allowed for multiple tracks\n", action->name);
179                 return;
180             }
181 
182             //So, action is allowed, do it.
183 
184             if (action->flags & DB_ACTION_CAN_MULTIPLE_TRACKS)
185             {
186                 action->callback (action, NULL);
187             }
188             else {
189                 pit = deadbeef->pl_get_first (PL_MAIN);
190                 while (pit) {
191                     if (deadbeef->pl_is_selected (pit))
192                     {
193                         action->callback (action, pit);
194                     }
195                     DB_playItem_t *next = deadbeef->pl_get_next (pit, PL_MAIN);
196                     deadbeef->pl_item_unref (pit);
197                     pit = next;
198                 }
199             }
200         }
201     }
202     else {
203         action->callback2 (action, ctx);
204     }
205 }
206 
207 static DB_plugin_action_t *
find_action_by_name(const char * command)208 find_action_by_name (const char *command) {
209     // find action with this name, and add to list
210     DB_plugin_action_t *actions = NULL;
211     DB_plugin_t **plugins = deadbeef->plug_get_list ();
212     for (int i = 0; plugins[i]; i++) {
213         DB_plugin_t *p = plugins[i];
214         if (p->get_actions) {
215             actions = p->get_actions (NULL);
216             while (actions) {
217                 if (actions->name && actions->title && !strcasecmp (actions->name, command)) {
218                     break; // found
219                 }
220                 actions = actions->next;
221             }
222             if (actions) {
223                 break;
224             }
225         }
226     }
227     return actions;
228 }
229 
230 #ifndef __APPLE__
231 static int
get_x11_keycode(const char * name,Atom * syms,int first_kk,int last_kk,int ks_per_kk)232 get_x11_keycode (const char *name, Atom *syms, int first_kk, int last_kk, int ks_per_kk) {
233     int i, ks;
234 
235     for (i = 0; i < last_kk-first_kk; i++)
236     {
237         int sym = * (syms + i*ks_per_kk);
238         for (ks = 0; keys[ks].name; ks++)
239         {
240             if ( (keys[ ks ].keysym == sym) && (0 == strcmp (name, keys[ ks ].name)))
241             {
242                 return i+first_kk;
243             }
244         }
245     }
246     return 0;
247 }
248 
249 static int
read_config(Display * disp)250 read_config (Display *disp) {
251     int ks_per_kk;
252     int first_kk, last_kk;
253     Atom* syms;
254 
255     XDisplayKeycodes (disp, &first_kk, &last_kk);
256     syms = XGetKeyboardMapping (disp, first_kk, last_kk - first_kk, &ks_per_kk);
257 #else
258 #define ShiftMask       (1<<0)
259 #define LockMask        (1<<1)
260 #define ControlMask     (1<<2)
261 #define Mod1Mask        (1<<3)
262 #define Mod2Mask        (1<<4)
263 #define Mod3Mask        (1<<5)
264 #define Mod4Mask        (1<<6)
265 #define Mod5Mask        (1<<7)
266     int ks_per_kk = -1;
267     int first_kk = -1, last_kk = -1;
268     int* syms = NULL;
269 static int
270 read_config (void) {
271 #endif
272     DB_conf_item_t *item = deadbeef->conf_find ("hotkey.", NULL);
273     while (item) {
274         if (command_count == MAX_COMMAND_COUNT)
275         {
276             fprintf (stderr, "hotkeys: maximum number (%d) of commands exceeded\n", MAX_COMMAND_COUNT);
277             break;
278         }
279 
280         command_t *cmd_entry = &commands[ command_count ];
281         memset (cmd_entry, 0, sizeof (command_t));
282 
283         char token[MAX_TOKEN];
284         char keycombo[MAX_TOKEN];
285         const char *script = item->value;
286         if ((script = gettoken (script, keycombo)) == 0) {
287             trace ("hotkeys: unexpected eol (keycombo)\n");
288             goto out;
289         }
290         if ((script = gettoken (script, token)) == 0) {
291             trace ("hotkeys: unexpected eol (ctx)\n");
292             goto out;
293         }
294         cmd_entry->ctx = atoi (token);
295         if (cmd_entry->ctx < 0 || cmd_entry->ctx >= DDB_ACTION_CTX_COUNT) {
296             trace ("hotkeys: invalid ctx %d\n", cmd_entry->ctx);
297             goto out;
298         }
299         if ((script = gettoken (script, token)) == 0) {
300             trace ("hotkeys: unexpected eol (isglobal)\n");
301             goto out;
302         }
303         cmd_entry->isglobal = atoi (token);
304         if ((script = gettoken (script, token)) == 0) {
305             trace ("hotkeys: unexpected eol (action)\n");
306             goto out;
307         }
308         cmd_entry->action = find_action_by_name (token);
309         if (!cmd_entry->action) {
310             trace ("hotkeys: action not found %s\n", token);
311             goto out;
312         }
313 
314         // parse key combo
315         int done = 0;
316         char* p;
317         char* space = keycombo;
318         do {
319             p = space;
320             space = strchr (p, ' ');
321             if (space) {
322                 *space = 0;
323                 space++;
324             }
325             else
326                 done = 1;
327 
328             if (0 == strcasecmp (p, "Ctrl"))
329                 cmd_entry->modifier |= ControlMask;
330 
331             else if (0 == strcasecmp (p, "Alt"))
332                 cmd_entry->modifier |= Mod1Mask;
333 
334             else if (0 == strcasecmp (p, "Shift"))
335                 cmd_entry->modifier |= ShiftMask;
336 
337             else if (0 == strcasecmp (p, "Super")) {
338                 cmd_entry->modifier |= Mod4Mask;
339             }
340 
341             else {
342                 if (p[0] == '0' && p[1] == 'x') {
343                     // parse hex keycode
344                     int r = sscanf (p, "0x%x", &cmd_entry->keycode);
345                     if (!r) {
346                         cmd_entry->keycode = 0;
347                     }
348                 }
349                 else {
350                     // lookup name table
351                     cmd_entry->keycode = get_keycode (p);
352 #ifndef __APPLE__
353                     cmd_entry->x11_keycode = get_x11_keycode (p, syms, first_kk, last_kk, ks_per_kk);
354                     trace ("%s: kc=%d, xkc=%d\n", p, cmd_entry->keycode, cmd_entry->x11_keycode);
355 #endif
356                 }
357                 if (!cmd_entry->keycode)
358                 {
359                     trace ("hotkeys: got 0 from get_keycode while adding hotkey: %s %s\n", item->key, item->value);
360                     break;
361                 }
362             }
363         } while (!done);
364 
365         if (done) {
366             if (cmd_entry->keycode == 0) {
367                 trace ("hotkeys: Key not found while parsing %s %s\n", item->key, item->value);
368             }
369             else {
370                 command_count++;
371             }
372         }
373 out:
374         item = deadbeef->conf_find ("hotkey.", item);
375     }
376 #ifndef __APPLE__
377     XFree (syms);
378     int i;
379     // need to grab it here to prevent gdk_x_error from being called while we're
380     // doing it on other thread
381     for (i = 0; i < command_count; i++) {
382         if (!commands[i].isglobal) {
383             continue;
384         }
385         for (int f = 0; f < 16; f++) {
386             uint32_t flags = 0;
387             if (f & 1) {
388                 flags |= LockMask;
389             }
390             if (f & 2) {
391                 flags |= Mod2Mask;
392             }
393             if (f & 4) {
394                 flags |= Mod3Mask;
395             }
396             if (f & 8) {
397                 flags |= Mod5Mask;
398             }
399             trace ("XGrabKey %d %x\n", commands[i].keycode, commands[i].modifier | flags);
400             XGrabKey (disp, commands[i].x11_keycode, commands[i].modifier | flags, DefaultRootWindow (disp), False, GrabModeAsync, GrabModeAsync);
401         }
402     }
403 #endif
404 
405     return 0;
406 }
407 
408 DB_plugin_t *
409 hotkeys_load (DB_functions_t *api) {
410     deadbeef = api;
411     return DB_PLUGIN (&plugin);
412 }
413 
414 static void
415 cleanup () {
416     command_count = 0;
417 #ifndef __APPLE__
418     if (disp) {
419         XCloseDisplay (disp);
420         disp = NULL;
421     }
422 #endif
423 }
424 
425 #ifndef __APPLE__
426 static int
427 x_err_handler (Display *d, XErrorEvent *evt) {
428 #if 0
429     // this code crashes if gtk plugin is active
430     char buffer[1024];
431     XGetErrorText (d, evt->error_code, buffer, sizeof (buffer));
432     trace ("hotkeys: xlib error: %s\n", buffer);
433 #endif
434     return 0;
435 }
436 
437 static void
438 hotkeys_event_loop (void *unused) {
439     int i;
440 #ifdef __linux__
441     prctl (PR_SET_NAME, "deadbeef-hotkeys", 0, 0, 0, 0);
442 #endif
443 
444     while (!finished) {
445         if (need_reset) {
446             trace ("hotkeys: reinitializing\n");
447             XSetErrorHandler (x_err_handler);
448             for (int i = 0; i < command_count; i++) {
449                 for (int f = 0; f < 16; f++) {
450                     uint32_t flags = 0;
451                     if (f & 1) {
452                         flags |= LockMask;
453                     }
454                     if (f & 2) {
455                         flags |= Mod2Mask;
456                     }
457                     if (f & 4) {
458                         flags |= Mod3Mask;
459                     }
460                     if (f & 8) {
461                         flags |= Mod5Mask;
462                     }
463                     XUngrabKey (disp, commands[i].x11_keycode, commands[i].modifier | flags, DefaultRootWindow (disp));
464                 }
465             }
466             memset (commands, 0, sizeof (commands));
467             command_count = 0;
468             read_config (disp);
469             need_reset = 0;
470         }
471 
472         XEvent event;
473         while (XPending (disp))
474         {
475             XNextEvent (disp, &event);
476 
477             if (event.xkey.type == KeyPress)
478             {
479                 int state = event.xkey.state;
480                 // ignore caps/scroll/numlock
481                 state &= (ShiftMask|ControlMask|Mod1Mask|Mod4Mask);
482                 trace ("hotkeys: key %d mods %X (%X)\n", event.xkey.keycode, state, event.xkey.state);
483                 trace ("filtered state=%X\n", state);
484                 for (i = 0; i < command_count; i++) {
485                     if ( (event.xkey.keycode == commands[ i ].x11_keycode) &&
486                          (state == commands[ i ].modifier))
487                     {
488                         trace ("matches to commands[%d]!\n", i);
489                         cmd_invoke_plugin_command (commands[i].action, commands[i].ctx);
490                         break;
491                     }
492                 }
493                 if (i == command_count) {
494                     trace ("keypress doesn't match to any global hotkey\n");
495                 }
496             }
497         }
498         usleep (200 * 1000);
499     }
500 }
501 #endif
502 
503 static int
504 hotkeys_connect (void) {
505 #ifndef __APPLE__
506     finished = 0;
507     loop_tid = 0;
508     disp = XOpenDisplay (NULL);
509     if (!disp)
510     {
511         fprintf (stderr, "hotkeys: could not open display\n");
512         return -1;
513     }
514     XSetErrorHandler (x_err_handler);
515 
516     read_config (disp);
517 
518     int ks_per_kk;
519     int first_kk, last_kk;
520     Atom* syms;
521     XDisplayKeycodes (disp, &first_kk, &last_kk);
522     syms = XGetKeyboardMapping (disp, first_kk, last_kk - first_kk, &ks_per_kk);
523     init_mapped_keycodes (disp, syms, first_kk, last_kk, ks_per_kk);
524     XFree (syms);
525     XSync (disp, 0);
526     loop_tid = deadbeef->thread_start (hotkeys_event_loop, 0);
527 #else
528     read_config ();
529 #endif
530     return 0;
531 }
532 
533 static int
534 hotkeys_disconnect (void) {
535 #ifndef __APPLE__
536     if (loop_tid) {
537         finished = 1;
538         deadbeef->thread_join (loop_tid);
539     }
540 #endif
541     cleanup ();
542     return 0;
543 }
544 
545 const char *
546 hotkeys_get_name_for_keycode (int keycode) {
547     for (int i = 0; keys[i].name; i++) {
548         if (keycode == keys[i].keysym) {
549             return keys[i].name;
550         }
551     }
552     return NULL;
553 }
554 
555 
556 DB_plugin_action_t*
557 hotkeys_get_action_for_keycombo (int key, int mods, int isglobal, int *ctx) {
558     int i;
559     // find mapped keycode
560 
561     if (key < 0x7f && isupper (key)) {
562         key = tolower (key);
563     }
564 
565     int keycode = key;
566 
567     trace ("hotkeys: keysym 0x%X mapped to 0x%X\n", key, keycode);
568 
569 
570     for (i = 0; i < command_count; i++) {
571         trace ("hotkeys: command %s keycode %x mods %x\n", commands[i].action->name, commands[i].keycode, commands[i].modifier);
572         if (commands[i].keycode == keycode && commands[i].modifier == mods && commands[i].isglobal == isglobal) {
573             *ctx = commands[i].ctx;
574             return commands[i].action;
575         }
576     }
577     return NULL;
578 }
579 
580 void
581 hotkeys_reset (void) {
582 #ifndef __APPLE__
583     need_reset = 1;
584     trace ("hotkeys: reset flagged\n");
585 #endif
586 }
587 
588 int
589 action_play_cb (struct DB_plugin_action_s *action, int ctx) {
590     // NOTE: this function is copied as on_playbtn_clicked in gtkui
591     DB_output_t *output = deadbeef->get_output ();
592     if (output->state () == OUTPUT_STATE_PAUSED) {
593         ddb_playlist_t *plt = deadbeef->plt_get_curr ();
594         int cur = deadbeef->plt_get_cursor (plt, PL_MAIN);
595         if (cur != -1) {
596             ddb_playItem_t *it = deadbeef->plt_get_item_for_idx (plt, cur, PL_MAIN);
597             ddb_playItem_t *it_playing = deadbeef->streamer_get_playing_track ();
598             if (it) {
599                 deadbeef->pl_item_unref (it);
600             }
601             if (it_playing) {
602                 deadbeef->pl_item_unref (it_playing);
603             }
604             if (it != it_playing) {
605                 deadbeef->sendmessage (DB_EV_PLAY_NUM, 0, cur, 0);
606             }
607             else {
608                 deadbeef->sendmessage (DB_EV_PLAY_CURRENT, 0, 0, 0);
609             }
610         }
611         else {
612             deadbeef->sendmessage (DB_EV_PLAY_CURRENT, 0, 0, 0);
613         }
614         deadbeef->plt_unref (plt);
615     }
616     else {
617         ddb_playlist_t *plt = deadbeef->plt_get_curr ();
618         int cur = -1;
619         if (plt) {
620             cur = deadbeef->plt_get_cursor (plt, PL_MAIN);
621             deadbeef->plt_unref (plt);
622         }
623         if (cur == -1) {
624             cur = 0;
625         }
626         deadbeef->sendmessage (DB_EV_PLAY_NUM, 0, cur, 0);
627     }
628     return 0;
629 }
630 
631 int
632 action_prev_cb (struct DB_plugin_action_s *action, int ctx) {
633     deadbeef->sendmessage (DB_EV_PREV, 0, 0, 0);
634     return 0;
635 }
636 
637 int
638 action_next_cb (struct DB_plugin_action_s *action, int ctx) {
639     deadbeef->sendmessage (DB_EV_NEXT, 0, 0, 0);
640     return 0;
641 }
642 
643 int
644 action_stop_cb (struct DB_plugin_action_s *action, int ctx) {
645     deadbeef->sendmessage (DB_EV_STOP, 0, 0, 0);
646     return 0;
647 }
648 
649 int
650 action_toggle_pause_cb (struct DB_plugin_action_s *action, int ctx) {
651     deadbeef->sendmessage (DB_EV_TOGGLE_PAUSE, 0, 0, 0);
652     return 0;
653 }
654 
655 int
656 action_play_pause_cb (struct DB_plugin_action_s *action, int ctx) {
657     int state = deadbeef->get_output ()->state ();
658     if (state == OUTPUT_STATE_PLAYING) {
659         deadbeef->sendmessage (DB_EV_PAUSE, 0, 0, 0);
660     }
661     else {
662         deadbeef->sendmessage (DB_EV_PLAY_CURRENT, 0, 0, 0);
663     }
664     return 0;
665 }
666 
667 int
668 action_play_random_cb (struct DB_plugin_action_s *action, int ctx) {
669     deadbeef->sendmessage (DB_EV_PLAY_RANDOM, 0, 0, 0);
670     return 0;
671 }
672 
673 int
674 action_seek_5p_forward_cb (struct DB_plugin_action_s *action, int ctx) {
675     deadbeef->pl_lock ();
676     DB_playItem_t *it = deadbeef->streamer_get_playing_track ();
677     if (it) {
678         float dur = deadbeef->pl_get_item_duration (it);
679         if (dur > 0) {
680             float pos = deadbeef->streamer_get_playpos ();
681             deadbeef->sendmessage (DB_EV_SEEK, 0, (pos + dur * 0.05f) * 1000, 0);
682         }
683         deadbeef->pl_item_unref (it);
684     }
685     deadbeef->pl_unlock ();
686     return 0;
687 }
688 
689 int
690 action_seek_5p_backward_cb (struct DB_plugin_action_s *action, int ctx) {
691     deadbeef->pl_lock ();
692     DB_playItem_t *it = deadbeef->streamer_get_playing_track ();
693     if (it) {
694         float dur = deadbeef->pl_get_item_duration (it);
695         if (dur > 0) {
696             float pos = deadbeef->streamer_get_playpos ();
697             pos = (pos - dur * 0.05f) * 1000;
698             if (pos < 0) {
699                 pos = 0;
700             }
701 
702             deadbeef->sendmessage (DB_EV_SEEK, 0, pos, 0);
703         }
704         deadbeef->pl_item_unref (it);
705     }
706     deadbeef->pl_unlock ();
707     return 0;
708 }
709 
710 int
711 action_seek_1p_forward_cb (struct DB_plugin_action_s *action, int ctx) {
712     deadbeef->pl_lock ();
713     DB_playItem_t *it = deadbeef->streamer_get_playing_track ();
714     if (it) {
715         float dur = deadbeef->pl_get_item_duration (it);
716         if (dur > 0) {
717             float pos = deadbeef->streamer_get_playpos ();
718             deadbeef->sendmessage (DB_EV_SEEK, 0, (pos + dur * 0.01f) * 1000, 0);
719         }
720         deadbeef->pl_item_unref (it);
721     }
722     deadbeef->pl_unlock ();
723     return 0;
724 }
725 
726 int
727 action_seek_1p_backward_cb (struct DB_plugin_action_s *action, int ctx) {
728     deadbeef->pl_lock ();
729     DB_playItem_t *it = deadbeef->streamer_get_playing_track ();
730     if (it) {
731         float dur = deadbeef->pl_get_item_duration (it);
732         if (dur > 0) {
733             float pos = deadbeef->streamer_get_playpos ();
734             pos = (pos - dur * 0.01f) * 1000;
735             if (pos < 0) {
736                 pos = 0;
737             }
738             deadbeef->sendmessage (DB_EV_SEEK, 0, pos, 0);
739         }
740         deadbeef->pl_item_unref (it);
741     }
742     deadbeef->pl_unlock ();
743     return 0;
744 }
745 
746 static int
747 seek_sec (float sec) {
748     deadbeef->pl_lock ();
749     DB_playItem_t *it = deadbeef->streamer_get_playing_track ();
750     if (it) {
751         float dur = deadbeef->pl_get_item_duration (it);
752         if (dur > 0) {
753             float pos = deadbeef->streamer_get_playpos ();
754             pos += sec;
755             if (pos < 0) {
756                 pos = 0;
757             }
758             deadbeef->sendmessage (DB_EV_SEEK, 0, pos * 1000, 0);
759         }
760         deadbeef->pl_item_unref (it);
761     }
762     deadbeef->pl_unlock ();
763     return 0;
764 }
765 
766 int
767 action_seek_1s_forward_cb (struct DB_plugin_action_s *action, int ctx) {
768     return seek_sec (1.f);
769 }
770 
771 int
772 action_seek_1s_backward_cb (struct DB_plugin_action_s *action, int ctx) {
773     return seek_sec (-1.f);
774 }
775 
776 int
777 action_seek_5s_forward_cb (struct DB_plugin_action_s *action, int ctx) {
778     return seek_sec (5.f);
779 }
780 
781 int
782 action_seek_5s_backward_cb (struct DB_plugin_action_s *action, int ctx) {
783     return seek_sec (-5.f);
784 }
785 
786 int
787 action_volume_up_cb (struct DB_plugin_action_s *action, int ctx) {
788     deadbeef->volume_set_db (deadbeef->volume_get_db () + 1);
789     return 0;
790 }
791 
792 int
793 action_volume_down_cb (struct DB_plugin_action_s *action, int ctx) {
794     deadbeef->volume_set_db (deadbeef->volume_get_db () - 1);
795     return 0;
796 }
797 
798 int
799 action_toggle_stop_after_current_cb (struct DB_plugin_action_s *action, int ctx) {
800     int var = deadbeef->conf_get_int ("playlist.stop_after_current", 0);
801     var = 1 - var;
802     deadbeef->conf_set_int ("playlist.stop_after_current", var);
803     deadbeef->sendmessage (DB_EV_CONFIGCHANGED, 0, 0, 0);
804     return 0;
805 }
806 
807 int
808 action_toggle_stop_after_album_cb (struct DB_plugin_action_s *action, int ctx) {
809     int var = deadbeef->conf_get_int ("playlist.stop_after_album", 0);
810     var = 1 - var;
811     deadbeef->conf_set_int ("playlist.stop_after_album", var);
812     deadbeef->sendmessage (DB_EV_CONFIGCHANGED, 0, 0, 0);
813     return 0;
814 }
815 
816 static DB_plugin_action_t action_reload_metadata = {
817     .title = "Reload Metadata",
818     .name = "reload_metadata",
819     .flags = DB_ACTION_MULTIPLE_TRACKS,
820     .callback2 = action_reload_metadata_handler,
821     .next = NULL
822 };
823 
824 static DB_plugin_action_t action_jump_to_current = {
825     .title = "Playback/Jump To Currently Playing Track",
826     .name = "jump_to_current_track",
827     .flags = DB_ACTION_COMMON,
828     .callback2 = action_jump_to_current_handler,
829     .next = &action_reload_metadata
830 };
831 
832 static DB_plugin_action_t action_skip_to_prev_genre = {
833     .title = "Playback/Skip to/Previous genre",
834     .name = "skip_to_prev_genre",
835     .flags = DB_ACTION_COMMON | DB_ACTION_ADD_MENU,
836     .callback2 = action_skip_to_prev_genre_handler,
837     .next = &action_jump_to_current
838 };
839 
840 static DB_plugin_action_t action_skip_to_prev_composer = {
841     .title = "Playback/Skip to/Previous composer",
842     .name = "skip_to_prev_composer",
843     .flags = DB_ACTION_COMMON | DB_ACTION_ADD_MENU,
844     .callback2 = action_skip_to_prev_composer_handler,
845     .next = &action_skip_to_prev_genre
846 };
847 
848 static DB_plugin_action_t action_skip_to_prev_artist = {
849     .title = "Playback/Skip to/Previous artist",
850     .name = "skip_to_prev_artist",
851     .flags = DB_ACTION_COMMON | DB_ACTION_ADD_MENU,
852     .callback2 = action_skip_to_prev_artist_handler,
853     .next = &action_skip_to_prev_composer
854 };
855 
856 static DB_plugin_action_t action_skip_to_prev_album = {
857     .title = "Playback/Skip to/Previous album",
858     .name = "skip_to_prev_album",
859     .flags = DB_ACTION_COMMON | DB_ACTION_ADD_MENU,
860     .callback2 = action_skip_to_prev_album_handler,
861     .next = &action_skip_to_prev_artist
862 };
863 
864 static DB_plugin_action_t action_skip_to_next_genre = {
865     .title = "Playback/Skip to/Next genre",
866     .name = "skip_to_next_genre",
867     .flags = DB_ACTION_COMMON | DB_ACTION_ADD_MENU,
868     .callback2 = action_skip_to_next_genre_handler,
869     .next = &action_skip_to_prev_album
870 };
871 
872 static DB_plugin_action_t action_skip_to_next_composer = {
873     .title = "Playback/Skip to/Next composer",
874     .name = "skip_to_next_composer",
875     .flags = DB_ACTION_COMMON | DB_ACTION_ADD_MENU,
876     .callback2 = action_skip_to_next_composer_handler,
877     .next = &action_skip_to_next_genre
878 };
879 
880 static DB_plugin_action_t action_skip_to_next_artist = {
881     .title = "Playback/Skip to/Next artist",
882     .name = "skip_to_next_artist",
883     .flags = DB_ACTION_COMMON | DB_ACTION_ADD_MENU,
884     .callback2 = action_skip_to_next_artist_handler,
885     .next = &action_skip_to_next_composer
886 };
887 
888 static DB_plugin_action_t action_skip_to_next_album = {
889     .title = "Playback/Skip to/Next album",
890     .name = "skip_to_next_album",
891     .flags = DB_ACTION_COMMON | DB_ACTION_ADD_MENU,
892     .callback2 = action_skip_to_next_album_handler,
893     .next = &action_skip_to_next_artist
894 };
895 
896 static DB_plugin_action_t action_next_playlist = {
897     .title = "Next Playlist",
898     .name = "next_playlist",
899     .flags = DB_ACTION_COMMON,
900     .callback2 = action_next_playlist_handler,
901     .next = &action_skip_to_next_album
902 };
903 
904 static DB_plugin_action_t action_prev_playlist = {
905     .title = "Prev Playlist",
906     .name = "prev_playlist",
907     .flags = DB_ACTION_COMMON,
908     .callback2 = action_prev_playlist_handler,
909     .next = &action_next_playlist
910 };
911 
912 static DB_plugin_action_t action_playlist10 = {
913     .title = "Switch To Playlist 10",
914     .name = "playlist10",
915     .flags = DB_ACTION_COMMON,
916     .callback2 = action_playlist10_handler,
917     .next = &action_prev_playlist
918 };
919 
920 static DB_plugin_action_t action_playlist9 = {
921     .title = "Switch To Playlist 9",
922     .name = "playlist9",
923     .flags = DB_ACTION_COMMON,
924     .callback2 = action_playlist9_handler,
925     .next = &action_playlist10
926 };
927 
928 static DB_plugin_action_t action_playlist8 = {
929     .title = "Switch To Playlist 8",
930     .name = "playlist8",
931     .flags = DB_ACTION_COMMON,
932     .callback2 = action_playlist8_handler,
933     .next = &action_playlist9
934 };
935 
936 static DB_plugin_action_t action_playlist7 = {
937     .title = "Switch To Playlist 7",
938     .name = "playlist7",
939     .flags = DB_ACTION_COMMON,
940     .callback2 = action_playlist7_handler,
941     .next = &action_playlist8
942 };
943 
944 static DB_plugin_action_t action_playlist6 = {
945     .title = "Switch To Playlist 6",
946     .name = "playlist6",
947     .flags = DB_ACTION_COMMON,
948     .callback2 = action_playlist6_handler,
949     .next = &action_playlist7
950 };
951 
952 static DB_plugin_action_t action_playlist5 = {
953     .title = "Switch To Playlist 5",
954     .name = "playlist5",
955     .flags = DB_ACTION_COMMON,
956     .callback2 = action_playlist5_handler,
957     .next = &action_playlist6
958 };
959 
960 static DB_plugin_action_t action_playlist4 = {
961     .title = "Switch To Playlist 4",
962     .name = "playlist4",
963     .flags = DB_ACTION_COMMON,
964     .callback2 = action_playlist4_handler,
965     .next = &action_playlist5
966 };
967 
968 static DB_plugin_action_t action_playlist3 = {
969     .title = "Switch To Playlist 3",
970     .name = "playlist3",
971     .flags = DB_ACTION_COMMON,
972     .callback2 = action_playlist3_handler,
973     .next = &action_playlist4
974 };
975 
976 static DB_plugin_action_t action_playlist2 = {
977     .title = "Switch To Playlist 2",
978     .name = "playlist2",
979     .flags = DB_ACTION_COMMON,
980     .callback2 = action_playlist2_handler,
981     .next = &action_playlist3
982 };
983 
984 static DB_plugin_action_t action_playlist1 = {
985     .title = "Switch To Playlist 1",
986     .name = "playlist1",
987     .flags = DB_ACTION_COMMON,
988     .callback2 = action_playlist1_handler,
989     .next = &action_playlist2
990 };
991 
992 static DB_plugin_action_t action_sort_randomize = {
993     .title = "Edit/Sort Randomize",
994     .name = "sort_randomize",
995     .flags = DB_ACTION_COMMON,
996     .callback2 = action_sort_randomize_handler,
997     .next = &action_playlist1
998 };
999 
1000 static DB_plugin_action_t action_sort_by_date = {
1001     .title = "Edit/Sort By Date",
1002     .name = "sort_date",
1003     .flags = DB_ACTION_COMMON,
1004     .callback2 = action_sort_by_date_handler,
1005     .next = &action_sort_randomize
1006 };
1007 
1008 static DB_plugin_action_t action_sort_by_artist = {
1009     .title = "Edit/Sort By Artist",
1010     .name = "sort_artist",
1011     .flags = DB_ACTION_COMMON,
1012     .callback2 = action_sort_by_artist_handler,
1013     .next = &action_sort_by_date
1014 };
1015 
1016 
1017 static DB_plugin_action_t action_sort_by_album = {
1018     .title = "Edit/Sort By Album",
1019     .name = "sort_album",
1020     .flags = DB_ACTION_COMMON,
1021     .callback2 = action_sort_by_album_handler,
1022     .next = &action_sort_by_artist
1023 };
1024 
1025 static DB_plugin_action_t action_sort_by_tracknr = {
1026     .title = "Edit/Sort By Track Number",
1027     .name = "sort_tracknr",
1028     .flags = DB_ACTION_COMMON,
1029     .callback2 = action_sort_by_tracknr_handler,
1030     .next = &action_sort_by_album
1031 };
1032 
1033 static DB_plugin_action_t action_sort_by_title = {
1034     .title = "Edit/Sort By Title",
1035     .name = "sort_title",
1036     .flags = DB_ACTION_COMMON,
1037     .callback2 = action_sort_by_title_handler,
1038     .next = &action_sort_by_tracknr
1039 };
1040 
1041 static DB_plugin_action_t action_invert_selection = {
1042     .title = "Edit/Invert Selection",
1043     .name = "invert_selection",
1044     .flags = DB_ACTION_COMMON,
1045     .callback2 = action_invert_selection_handler,
1046     .next = &action_sort_by_tracknr
1047 };
1048 
1049 static DB_plugin_action_t action_clear_playlist = {
1050     .title = "Edit/Clear Playlist",
1051     .name = "clear_playlist",
1052     .flags = DB_ACTION_COMMON,
1053     .callback2 = action_clear_playlist_handler,
1054     .next = &action_invert_selection
1055 };
1056 
1057 static DB_plugin_action_t action_remove_from_playqueue = {
1058     .title = "Playback/Remove From Playback Queue",
1059     .name = "remove_from_playback_queue",
1060     .flags = DB_ACTION_MULTIPLE_TRACKS,
1061     .callback2 = action_remove_from_playqueue_handler,
1062     .next = &action_clear_playlist
1063 };
1064 
1065 static DB_plugin_action_t action_add_to_playqueue = {
1066     .title = "Playback/Add To Playback Queue",
1067     .name = "add_to_playback_queue",
1068     .flags = DB_ACTION_MULTIPLE_TRACKS,
1069     .callback2 = action_add_to_playqueue_handler,
1070     .next = &action_remove_from_playqueue
1071 };
1072 
1073 static DB_plugin_action_t action_toggle_mute = {
1074     .title = "Playback/Toggle Mute",
1075     .name = "toggle_mute",
1076     .flags = DB_ACTION_COMMON,
1077     .callback2 = action_toggle_mute_handler,
1078     .next = &action_add_to_playqueue
1079 };
1080 
1081 static DB_plugin_action_t action_play = {
1082     .title = "Playback/Play",
1083     .name = "play",
1084     .flags = DB_ACTION_COMMON,
1085     .callback2 = action_play_cb,
1086     .next = &action_toggle_mute
1087 };
1088 
1089 static DB_plugin_action_t action_stop = {
1090     .title = "Playback/Stop",
1091     .name = "stop",
1092     .flags = DB_ACTION_COMMON,
1093     .callback2 = action_stop_cb,
1094     .next = &action_play
1095 };
1096 
1097 static DB_plugin_action_t action_prev = {
1098     .title = "Playback/Previous",
1099     .name = "prev",
1100     .flags = DB_ACTION_COMMON,
1101     .callback2 = action_prev_cb,
1102     .next = &action_stop
1103 };
1104 
1105 static DB_plugin_action_t action_next = {
1106     .title = "Playback/Next",
1107     .name = "next",
1108     .flags = DB_ACTION_COMMON,
1109     .callback2 = action_next_cb,
1110     .next = &action_prev
1111 };
1112 
1113 static DB_plugin_action_t action_toggle_pause = {
1114     .title = "Playback/Toggle Pause",
1115     .name = "toggle_pause",
1116     .flags = DB_ACTION_COMMON,
1117     .callback2 = action_toggle_pause_cb,
1118     .next = &action_next
1119 };
1120 
1121 static DB_plugin_action_t action_play_pause = {
1122     .title = "Playback/Play\\/Pause",
1123     .name = "play_pause",
1124     .flags = DB_ACTION_COMMON,
1125     .callback2 = action_play_pause_cb,
1126     .next = &action_toggle_pause
1127 };
1128 
1129 static DB_plugin_action_t action_play_random = {
1130     .title = "Playback/Play Random",
1131     .name = "playback_random",
1132     .flags = DB_ACTION_COMMON,
1133     .callback2 = action_play_random_cb,
1134     .next = &action_play_pause
1135 };
1136 
1137 static DB_plugin_action_t action_seek_1s_forward = {
1138     .title = "Playback/Seek 1s Forward",
1139     .name = "seek_1s_fwd",
1140     .flags = DB_ACTION_COMMON,
1141     .callback2 = action_seek_1s_forward_cb,
1142     .next = &action_play_random
1143 };
1144 
1145 static DB_plugin_action_t action_seek_1s_backward = {
1146     .title = "Playback/Seek 1s Backward",
1147     .name = "seek_1s_back",
1148     .flags = DB_ACTION_COMMON,
1149     .callback2 = action_seek_1s_backward_cb,
1150     .next = &action_seek_1s_forward
1151 };
1152 
1153 static DB_plugin_action_t action_seek_5s_forward = {
1154     .title = "Playback/Seek 5s Forward",
1155     .name = "seek_5s_fwd",
1156     .flags = DB_ACTION_COMMON,
1157     .callback2 = action_seek_5s_forward_cb,
1158     .next = &action_seek_1s_backward
1159 };
1160 
1161 static DB_plugin_action_t action_seek_5s_backward = {
1162     .title = "Playback/Seek 5s Backward",
1163     .name = "seek_5s_back",
1164     .flags = DB_ACTION_COMMON,
1165     .callback2 = action_seek_5s_backward_cb,
1166     .next = &action_seek_5s_forward
1167 };
1168 
1169 
1170 static DB_plugin_action_t action_seek_1p_forward = {
1171     .title = "Playback/Seek 1% Forward",
1172     .name = "seek_1p_fwd",
1173     .flags = DB_ACTION_COMMON,
1174     .callback2 = action_seek_1p_forward_cb,
1175     .next = &action_seek_5s_backward
1176 };
1177 
1178 static DB_plugin_action_t action_seek_1p_backward = {
1179     .title = "Playback/Seek 1% Backward",
1180     .name = "seek_1p_back",
1181     .flags = DB_ACTION_COMMON,
1182     .callback2 = action_seek_1p_backward_cb,
1183     .next = &action_seek_1p_forward
1184 };
1185 
1186 static DB_plugin_action_t action_seek_5p_forward = {
1187     .title = "Playback/Seek 5% Forward",
1188     .name = "seek_5p_fwd",
1189     .flags = DB_ACTION_COMMON,
1190     .callback2 = action_seek_5p_forward_cb,
1191     .next = &action_seek_1p_backward
1192 };
1193 
1194 static DB_plugin_action_t action_seek_5p_backward = {
1195     .title = "Playback/Seek 5% Backward",
1196     .name = "seek_5p_back",
1197     .flags = DB_ACTION_COMMON,
1198     .callback2 = action_seek_5p_backward_cb,
1199     .next = &action_seek_5p_forward
1200 };
1201 
1202 static DB_plugin_action_t action_volume_up = {
1203     .title = "Playback/Volume Up",
1204     .name = "volume_up",
1205     .flags = DB_ACTION_COMMON,
1206     .callback2 = action_volume_up_cb,
1207     .next = &action_seek_5p_backward
1208 };
1209 
1210 static DB_plugin_action_t action_volume_down = {
1211     .title = "Playback/Volume Down",
1212     .name = "volume_down",
1213     .flags = DB_ACTION_COMMON,
1214     .callback2 = action_volume_down_cb,
1215     .next = &action_volume_up
1216 };
1217 
1218 static DB_plugin_action_t action_toggle_stop_after_current = {
1219     .title = "Playback/Toggle Stop After Current Track",
1220     .name = "toggle_stop_after_current",
1221     .flags = DB_ACTION_COMMON,
1222     .callback2 = action_toggle_stop_after_current_cb,
1223     .next = &action_volume_down
1224 };
1225 
1226 static DB_plugin_action_t action_toggle_stop_after_album = {
1227     .title = "Playback/Toggle Stop After Current Album",
1228     .name = "toggle_stop_after_album",
1229     .flags = DB_ACTION_COMMON,
1230     .callback2 = action_toggle_stop_after_album_cb,
1231     .next = &action_toggle_stop_after_current
1232 };
1233 
1234 static DB_plugin_action_t *
1235 hotkeys_get_actions (DB_playItem_t *it)
1236 {
1237     return &action_toggle_stop_after_album;
1238 }
1239 
1240 // define plugin interface
1241 static DB_hotkeys_plugin_t plugin = {
1242     .misc.plugin.api_vmajor = 1,
1243     .misc.plugin.api_vminor = 5,
1244     .misc.plugin.version_major = 1,
1245     .misc.plugin.version_minor = 1,
1246     .misc.plugin.type = DB_PLUGIN_MISC,
1247     .misc.plugin.id = "hotkeys",
1248     .misc.plugin.name = "Hotkey manager",
1249     .misc.plugin.descr =
1250         "Manages local and global hotkeys, and executes actions when the assigned key combinations are pressed\n\n"
1251         "This plugin has its own API, to allow 3rd party GUI plugins to reuse the code.\n"
1252         "Check the plugins/hotkeys/hotkeys.h in the source tree if you need this.\n\n"
1253         "Changes in version 1.1\n"
1254         "    * adaptation to new deadbeef 0.6 plugin API\n"
1255         "    * added local hotkeys support\n"
1256     ,
1257     .misc.plugin.copyright =
1258         "Copyright (C) 2012-2013 Alexey Yakovenko <waker@users.sourceforge.net>\n"
1259         "Copyright (C) 2009-2011 Viktor Semykin <thesame.ml@gmail.com>\n"
1260         "\n"
1261         "This program is free software; you can redistribute it and/or\n"
1262         "modify it under the terms of the GNU General Public License\n"
1263         "as published by the Free Software Foundation; either version 2\n"
1264         "of the License, or (at your option) any later version.\n"
1265         "\n"
1266         "This program is distributed in the hope that it will be useful,\n"
1267         "but WITHOUT ANY WARRANTY; without even the implied warranty of\n"
1268         "MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n"
1269         "GNU General Public License for more details.\n"
1270         "\n"
1271         "You should have received a copy of the GNU General Public License\n"
1272         "along with this program; if not, write to the Free Software\n"
1273         "Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.\n"
1274     ,
1275     .misc.plugin.website = "http://deadbeef.sf.net",
1276     .misc.plugin.get_actions = hotkeys_get_actions,
1277     .misc.plugin.start = hotkeys_connect,
1278     .misc.plugin.stop = hotkeys_disconnect,
1279     .get_name_for_keycode = hotkeys_get_name_for_keycode,
1280     .get_action_for_keycombo = hotkeys_get_action_for_keycombo,
1281     .reset = hotkeys_reset,
1282 };
1283 
1284