1 /*
2  * Copyright (c) 2010, 2011 Ryan Flannery <ryan.flannery@gmail.com>
3  *
4  * Permission to use, copy, modify, and distribute this software for any
5  * purpose with or without fee is hereby granted, provided that the above
6  * copyright notice and this permission notice appear in all copies.
7  *
8  * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
9  * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
10  * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
11  * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
12  * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
13  * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
14  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
15  */
16 
17 #include "keybindings.h"
18 
19 
20 /* This table maps KeyActions to their string representations */
21 typedef struct {
22    KeyAction   action;
23    char       *name;
24 } KeyActionName;
25 
26 const KeyActionName KeyActionNames[] = {
27    { scroll_up,               "scroll_up" },
28    { scroll_down,             "scroll_down" },
29    { scroll_up_page,          "scroll_up_page" },
30    { scroll_down_page,        "scroll_down_page" },
31    { scroll_up_halfpage,      "scroll_up_halfpage" },
32    { scroll_down_halfpage,    "scroll_down_halfpage" },
33    { scroll_up_wholepage,     "scroll_up_wholepage" },
34    { scroll_down_wholepage,   "scroll_down_wholepage" },
35    { scroll_left,             "scroll_left" },
36    { scroll_right,            "scroll_right" },
37    { scroll_leftmost,         "scroll_leftmost" },
38    { scroll_rightmost,        "scroll_rightmost" },
39    { jumpto_screen_top,       "jumpto_screen_top" },
40    { jumpto_screen_middle,    "jumpto_screen_middle" },
41    { jumpto_screen_bottom,    "jumpto_screen_bottom" },
42    { jumpto_line,             "jumpto_line" },
43    { jumpto_percent,          "jumpto_percent" },
44    { search_forward,          "search_forward" },
45    { search_backward,         "search_backward" },
46    { find_next_forward,       "find_next_forward" },
47    { find_next_backward,      "find_next_backward" },
48    { visual,                  "visual" },
49    { cut,                     "cut" },
50    { yank,                    "yank" },
51    { paste_after,             "paste_after" },
52    { paste_before,            "paste_before" },
53    { undo,                    "undo" },
54    { redo,                    "redo" },
55    { go,                      "go" },
56    { quit,                    "quit" },
57    { redraw,                  "redraw" },
58    { command_mode,            "command_mode" },
59    { shell,                   "shell" },
60    { switch_windows,          "switch_window" },
61    { show_file_info,          "show_file_info" },
62    { load_playlist,           "load_playlist" },
63    { media_play,              "media_play" },
64    { media_pause,             "media_pause" },
65    { media_stop,              "media_stop" },
66    { media_next,              "media_next" },
67    { media_prev,              "media_prev" },
68    { volume_increase,         "volume_increase" },
69    { volume_decrease,         "volume_decrease" },
70    { seek_forward_seconds,    "seek_forward_seconds" },
71    { seek_backward_seconds,   "seek_backward_seconds" },
72    { seek_forward_minutes,    "seek_forward_minutes" },
73    { seek_backward_minutes,   "seek_backward_minutes" },
74    { toggle_forward,          "toggle_forward" },
75    { toggle_backward,         "toggle_backward" }
76 };
77 const size_t KeyActionNamesSize = sizeof(KeyActionNames) / sizeof(KeyActionName);
78 
79 
80 /* This table maps KeyActions to their handler-functions (with arguments) */
81 typedef struct {
82    KeyAction      action;
83    ActionHandler  handler;
84    bool           visual;
85    KbaArgs        args;
86 } KeyActionHandler;
87 
88 #define ARG_NOT_USED { .num = 0 }
89 const KeyActionHandler KeyActionHandlers[] = {
90 { scroll_up,             kba_scroll_row,     true,  { .direction = UP }},
91 { scroll_down,           kba_scroll_row,     true,  { .direction = DOWN }},
92 { scroll_up_page,        kba_scroll_page,    true,  { .direction = UP,   .amount = SINGLE }},
93 { scroll_down_page,      kba_scroll_page,    true,  { .direction = DOWN, .amount = SINGLE }},
94 { scroll_up_halfpage,    kba_scroll_page,    true,  { .direction = UP,   .amount = HALF }},
95 { scroll_down_halfpage,  kba_scroll_page,    true,  { .direction = DOWN, .amount = HALF }},
96 { scroll_up_wholepage,   kba_scroll_page,    true,  { .direction = UP,   .amount = WHOLE }},
97 { scroll_down_wholepage, kba_scroll_page,    true,  { .direction = DOWN, .amount = WHOLE }},
98 { scroll_left,           kba_scroll_col,     true,  { .direction = LEFT,  .amount = SINGLE }},
99 { scroll_right,          kba_scroll_col,     true,  { .direction = RIGHT, .amount = SINGLE }},
100 { scroll_leftmost,       kba_scroll_col,     true,  { .direction = LEFT,  .amount = WHOLE }},
101 { scroll_rightmost,      kba_scroll_col,     true,  { .direction = RIGHT, .amount = WHOLE }},
102 { jumpto_screen_top,     kba_jumpto_screen,  true,  { .placement = TOP }},
103 { jumpto_screen_middle,  kba_jumpto_screen,  true,  { .placement = MIDDLE }},
104 { jumpto_screen_bottom,  kba_jumpto_screen,  true,  { .placement = BOTTOM }},
105 { jumpto_line,           kba_jumpto_file,    true,  { .scale = NUMBER, .num = 'G' }},
106 { jumpto_percent,        kba_jumpto_file,    true,  { .scale = PERCENT }},
107 { search_forward,        kba_search,         true,  { .direction = FORWARDS }},
108 { search_backward,       kba_search,         true,  { .direction = BACKWARDS }},
109 { find_next_forward,     kba_search_find,    true,  { .direction = SAME }},
110 { find_next_backward,    kba_search_find,    true,  { .direction = OPPOSITE }},
111 { visual,                kba_visual,         true,  ARG_NOT_USED },
112 { cut,                   kba_cut,            true,  ARG_NOT_USED },
113 { yank,                  kba_yank,           true,  ARG_NOT_USED },
114 { paste_after,           kba_paste,          false, { .placement = AFTER }},
115 { paste_before,          kba_paste,          false, { .placement = BEFORE }},
116 { undo,                  kba_undo,           false, ARG_NOT_USED },
117 { redo,                  kba_redo,           false, ARG_NOT_USED },
118 { go,                    kba_go,             true,  ARG_NOT_USED },
119 { quit,                  kba_quit,           false, ARG_NOT_USED },
120 { redraw,                kba_redraw,         false, ARG_NOT_USED },
121 { command_mode,          kba_command_mode,   false, ARG_NOT_USED },
122 { shell,                 kba_shell,          false, ARG_NOT_USED },
123 { switch_windows,        kba_switch_windows, false, ARG_NOT_USED },
124 { show_file_info,        kba_show_file_info, false, ARG_NOT_USED },
125 { load_playlist,         kba_load_playlist,  false, ARG_NOT_USED },
126 { media_play,            kba_play,           false, ARG_NOT_USED },
127 { media_pause,           kba_pause,          false, ARG_NOT_USED },
128 { media_stop,            kba_stop,           false, ARG_NOT_USED },
129 { media_next,            kba_play_next,      false, ARG_NOT_USED },
130 { media_prev,            kba_play_prev,      false, ARG_NOT_USED },
131 { volume_increase,       kba_volume,         false, { .direction = FORWARDS }},
132 { volume_decrease,       kba_volume,         false, { .direction = BACKWARDS }},
133 { seek_forward_seconds,  kba_seek,           false, { .direction = FORWARDS,  .scale = SECONDS, .num = 10 }},
134 { seek_backward_seconds, kba_seek,           false, { .direction = BACKWARDS, .scale = SECONDS, .num = 10 }},
135 { seek_forward_minutes,  kba_seek,           false, { .direction = FORWARDS,  .scale = MINUTES, .num = 1 }},
136 { seek_backward_minutes, kba_seek,           false, { .direction = BACKWARDS, .scale = MINUTES, .num = 1 }},
137 { toggle_forward,        kba_toggle,         false, { .direction = FORWARDS }},
138 { toggle_backward,       kba_toggle,         false, { .direction = BACKWARDS }}
139 };
140 const size_t KeyActionHandlersSize = sizeof(KeyActionHandlers) / sizeof(KeyActionHandler);
141 
142 
143 /* This table contains the default keybindings */
144 typedef struct {
145    KeyCode     key;
146    KeyAction   action;
147 } KeyBinding;
148 
149 #define MY_K_TAB     9
150 #define MY_K_ENTER  13
151 #define MY_K_ESCAPE 27
152 #define kb_CONTROL(x)   ((x) - 'a' + 1)
153 
154 const KeyBinding DefaultKeyBindings[] = {
155    { 'k',               scroll_up },
156    { '-',               scroll_up },
157    { KEY_UP,            scroll_up },
158    { 'j',               scroll_down },
159    { KEY_DOWN,          scroll_down },
160    { kb_CONTROL('y'),   scroll_up_page },
161    { kb_CONTROL('e'),   scroll_down_page },
162    { kb_CONTROL('u'),   scroll_up_halfpage },
163    { kb_CONTROL('d'),   scroll_down_halfpage },
164    { kb_CONTROL('b'),   scroll_up_wholepage },
165    { KEY_PPAGE,         scroll_up_wholepage },
166    { kb_CONTROL('f'),   scroll_down_wholepage },
167    { KEY_NPAGE,         scroll_down_wholepage },
168    { 'h',               scroll_left },
169    { KEY_LEFT,          scroll_left },
170    { KEY_BACKSPACE,     scroll_left },
171    { 'l',               scroll_right },
172    { KEY_RIGHT,         scroll_right },
173    { ' ',               scroll_right },
174    { '^',               scroll_leftmost },
175    { '0',               scroll_leftmost },
176    { '|',               scroll_leftmost },
177    { '$',               scroll_rightmost },
178    { 'H',               jumpto_screen_top },
179    { 'M',               jumpto_screen_middle },
180    { 'L',               jumpto_screen_bottom },
181    { 'G',               jumpto_line },
182    { '%',               jumpto_percent },
183    { '/',               search_forward },
184    { '?',               search_backward },
185    { 'n',               find_next_forward },
186    { 'N',               find_next_backward },
187    { 'v',               visual },
188    { 'V',               visual },
189    { 'd',               cut },
190    { 'y',               yank },
191    { 'p',               paste_after },
192    { 'P',               paste_before },
193    { 'u',               undo },
194    { kb_CONTROL('r'),   redo },
195    { 'g',               go },
196    { kb_CONTROL('c'),   quit },
197    { kb_CONTROL('/'),   quit },
198    { kb_CONTROL('l'),   redraw },
199    { ':',               command_mode },
200    { '!',               shell },
201    { MY_K_TAB,          switch_windows },
202    { 'm',               show_file_info },
203    { MY_K_ENTER,        load_playlist },
204    { MY_K_ENTER,        media_play },
205    { 'z',               media_pause },
206    { 's',               media_stop },
207    { 'f',               seek_forward_seconds },
208    { ']',               seek_forward_seconds },
209    { 'b',               seek_backward_seconds },
210    { '[',               seek_backward_seconds },
211    { 'F',               seek_forward_minutes },
212    { '}',               seek_forward_minutes },
213    { 'B',               seek_backward_minutes },
214    { '{',               seek_backward_minutes },
215    { '(',               media_prev },
216    { ')',               media_next },
217    { '<',               volume_decrease },
218    { '>',               volume_increase },
219    { 't',               toggle_forward },
220    { 'T',               toggle_backward }
221 };
222 const size_t DefaultKeyBindingsSize = sizeof(DefaultKeyBindings) / sizeof(KeyBinding);
223 
224 
225 /* Mapping of special keys to their values */
226 typedef struct {
227    char    *str;
228    KeyCode  code;
229 } SpecialKeyCode;
230 SpecialKeyCode SpecialKeys[] = {
231    { "BACKSPACE", KEY_BACKSPACE },
232    { "ENTER",     MY_K_ENTER },
233    { "SPACE",     ' ' },
234    { "TAB",       MY_K_TAB },
235    { "UP",        KEY_UP },
236    { "DOWN",      KEY_DOWN },
237    { "LEFT",      KEY_LEFT },
238    { "RIGHT",     KEY_RIGHT },
239    { "PAGEUP",    KEY_PPAGE },
240    { "PAGEDOWN",  KEY_NPAGE }
241 };
242 const size_t SpecialKeysSize = sizeof(SpecialKeys) / sizeof(SpecialKeyCode);
243 
244 
245 /*
246  * This is the actual keybinding table mapping KeyCodes to actions.  It is
247  * loaded at initial runtime (kb_init()), and modified thereafter, either via
248  * "bind" commands in the config file or issued during runtime.
249  */
250 #define KEYBINDINGS_CHUNK_SIZE 100
251 KeyBinding *KeyBindings;
252 size_t KeyBindingsSize;
253 size_t KeyBindingsCapacity;
254 
255 void
kb_increase_capacity()256 kb_increase_capacity()
257 {
258    KeyBinding  *new_buffer;
259    size_t       nbytes;
260 
261    KeyBindingsCapacity += KEYBINDINGS_CHUNK_SIZE;
262    nbytes = KeyBindingsCapacity * sizeof(KeyBinding);
263    if ((new_buffer = realloc(KeyBindings, nbytes)) == NULL)
264       err(1, "%s: failed to realloc(3) keybindings", __FUNCTION__);
265 
266    KeyBindings = new_buffer;
267 }
268 
269 
270 /****************************************************************************
271  *
272  *        Routines for initializing, free'ing, manipulating,
273  *                    and retrieving keybindings.
274  *
275  ****************************************************************************/
276 
277 void
kb_init()278 kb_init()
279 {
280    size_t i;
281 
282    /* setup empty buffer */
283    KeyBindingsSize = 0;
284    KeyBindingsCapacity = 0;
285    kb_increase_capacity();
286 
287    /* install default bindings */
288    for (i = 0; i < DefaultKeyBindingsSize; i++)
289       kb_bind(DefaultKeyBindings[i].action, DefaultKeyBindings[i].key);
290 }
291 
292 void
kb_free()293 kb_free()
294 {
295    free(KeyBindings);
296    KeyBindingsSize = 0;
297 }
298 
299 void
kb_bind(KeyAction action,KeyCode key)300 kb_bind(KeyAction action, KeyCode key)
301 {
302    kb_unbind_key(key);
303 
304    if (KeyBindingsSize == KeyBindingsCapacity)
305       kb_increase_capacity();
306 
307    KeyBindings[KeyBindingsSize].action = action;
308    KeyBindings[KeyBindingsSize].key = key;
309    KeyBindingsSize++;
310 }
311 
312 void
kb_unbind_action(KeyAction action)313 kb_unbind_action(KeyAction action)
314 {
315    size_t i, j;
316 
317    for (i = 0; i < KeyBindingsSize; i++) {
318       if (KeyBindings[i].action == action) {
319          for (j = i; j < KeyBindingsSize - 1; j++)
320             KeyBindings[j] = KeyBindings[j + 1];
321 
322          KeyBindingsSize--;
323       }
324    }
325 }
326 
327 void
kb_unbind_key(KeyCode key)328 kb_unbind_key(KeyCode key)
329 {
330    size_t i, j;
331 
332    for (i = 0; i < KeyBindingsSize; i++) {
333       if (KeyBindings[i].key == key) {
334          for (j = i; j < KeyBindingsSize - 1; j++)
335             KeyBindings[j] = KeyBindings[j + 1];
336 
337          KeyBindingsSize--;
338       }
339    }
340 }
341 
342 void
kb_unbind_all()343 kb_unbind_all()
344 {
345    KeyBindingsSize = 0;
346 }
347 
348 bool
kb_str2action(const char * s,KeyAction * action)349 kb_str2action(const char *s, KeyAction *action)
350 {
351    size_t i;
352 
353    for (i = 0; i < KeyActionNamesSize; i++) {
354       if (strcasecmp(KeyActionNames[i].name, s) == 0) {
355          *action = KeyActionNames[i].action;
356          return true;
357       }
358    }
359 
360    return false;
361 }
362 
363 KeyCode
kb_str2specialKey(char * s)364 kb_str2specialKey(char *s)
365 {
366    size_t i;
367 
368    for (i = 0; i < SpecialKeysSize; i++) {
369       if (strcasecmp(SpecialKeys[i].str, s) == 0)
370          return SpecialKeys[i].code;
371    }
372 
373    return -1;
374 }
375 
376 KeyCode
kb_str2keycode(char * s)377 kb_str2keycode(char *s)
378 {
379    KeyCode val;
380 
381    if (strlen(s) == 1)
382       val = s[0];
383    else
384       val = kb_str2specialKey(s);
385 
386    return val;
387 }
388 
389 KeyCode
kb_str2keycode2(char * control,char * key)390 kb_str2keycode2(char *control, char *key)
391 {
392    KeyCode val;
393 
394    if (strcasecmp(control, "control") != 0)
395       return -1;
396 
397    if ((val = kb_str2keycode(key)) > 0)
398       return kb_CONTROL(val);
399    else
400       return -1;
401 }
402 
403 bool
kb_execute(KeyCode k)404 kb_execute(KeyCode k)
405 {
406    KeyAction action;
407    size_t i;
408    bool   found;
409 
410    /* visual mode and ESC pressed ?*/
411    if (visual_mode_start != -1 && k == MY_K_ESCAPE) {
412 	   visual_mode_start = -1;
413 	   return true;
414    }
415 
416    /* Is the key bound? */
417    found  = false;
418    action = -1;
419    for (i = 0; i < KeyBindingsSize; i++) {
420       if (KeyBindings[i].key == k) {
421          action = KeyBindings[i].action;
422          found = true;
423       }
424    }
425 
426    if (!found)
427       return false;
428 
429    /* Execute theaction handler. */
430    for (i = 0; i < KeyActionHandlersSize; i++) {
431       if (KeyActionHandlers[i].action == action) {
432          if (visual_mode_start == -1 || KeyActionHandlers[i].visual) {
433             ((KeyActionHandlers[i].handler)(KeyActionHandlers[i].args));
434             return true;
435          }
436       }
437    }
438 
439    return false;
440 }
441 
442 bool
kb_execute_by_name(const char * name)443 kb_execute_by_name(const char *name)
444 {
445    KeyAction   a;
446    size_t      x;
447 
448    if (!kb_str2action(name, &a))
449       return false;
450 
451    for(x = 0; x < KeyActionHandlersSize; x++) {
452       if(a == KeyActionHandlers[x].action) {
453          KeyActionHandlers[x].handler(KeyActionHandlers[x].args);
454          return true;
455       }
456    }
457    return false;
458 }
459 
460 
461 /*****************************************************************************
462  *
463  *                     Individual Keybinding Handlers
464  *
465  ****************************************************************************/
466 
467 KbaArgs
get_dummy_args()468 get_dummy_args()
469 {
470    KbaArgs dummy = { .direction = -1, .scale = -1, .amount = -1,
471       .placement = -1, .num = -1 };
472 
473    return dummy;
474 }
475 
476 void
kba_scroll_row(KbaArgs a)477 kba_scroll_row(KbaArgs a)
478 {
479    int n = gnum_retrieve();
480 
481    /* update current row */
482    switch (a.direction) {
483    case DOWN:
484       ui.active->crow += n;
485       break;
486    case UP:
487       ui.active->crow -= n;
488       break;
489    default:
490       errx(1, "%s: invalid direction", __FUNCTION__);
491    }
492 
493    /* handle off-the-edge cases */
494    if (ui.active->crow < 0) {
495       swindow_scroll(ui.active, UP, -1 * ui.active->crow);
496       ui.active->crow = 0;
497    }
498 
499    if (ui.active->voffset + ui.active->crow >= ui.active->nrows)
500       ui.active->crow = ui.active->nrows - ui.active->voffset - 1;
501 
502    if (ui.active->crow >= ui.active->h) {
503       swindow_scroll(ui.active, DOWN, ui.active->crow - ui.active->h + 1);
504       ui.active->crow = ui.active->h - 1;
505    }
506 
507    if (ui.active->crow < 0)
508       ui.active->crow = 0;
509 
510    /* redraw */
511    redraw_active();
512    paint_status_bar();
513 }
514 
515 void
kba_scroll_page(KbaArgs a)516 kba_scroll_page(KbaArgs a)
517 {
518    bool maintain_row_idx;
519    int  voffset_original;
520    int  max_row;
521    int  diff;
522    int  n;
523 
524    voffset_original = ui.active->voffset;
525 
526    /* determine max row number */
527    if (ui.active->voffset + ui.active->h >= ui.active->nrows)
528       max_row = ui.active->nrows - ui.active->voffset - 1;
529    else
530       max_row = ui.active->h - 1;
531 
532    /* determine how many pages to scroll */
533    n = 1;
534    if (gnum_get() > 0) {
535       n = gnum_get();
536       gnum_clear();
537    }
538 
539    /* determine how much crow should change */
540    switch (a.amount) {
541    case SINGLE:
542       diff = 1 * n;
543       maintain_row_idx = true;
544       break;
545    case HALF:
546       diff = (ui.active->h / 2) * n;
547       maintain_row_idx = false;
548       break;
549    case WHOLE:
550       diff = ui.active->h * n;
551       maintain_row_idx = false;
552       break;
553    default:
554       errx(1, "scroll_page: invalid amount");
555    }
556    swindow_scroll(ui.active, a.direction, diff);
557 
558    /* handle updating the current row */
559    if (maintain_row_idx) {
560 
561       if (a.direction == DOWN)
562          ui.active->crow -= diff;
563       else
564          ui.active->crow += diff;
565 
566       /* handle off-the-edge cases */
567       if (ui.active->crow < 0)
568          ui.active->crow = 0;
569 
570       if (ui.active->voffset + ui.active->crow >= ui.active->nrows)
571          ui.active->crow = ui.active->nrows - ui.active->voffset - 1;
572 
573       if (ui.active->crow >= ui.active->h)
574          ui.active->crow = ui.active->h - 1;
575 
576       if (ui.active->crow < 0)
577          ui.active->crow = 0;
578 
579    } else {
580       /*
581        * We only move current row here if there was a change this time
582        * in the vertical offset
583        */
584 
585       if (a.direction == DOWN
586       &&  ui.active->voffset == voffset_original) {
587          ui.active->crow += diff;
588          if (ui.active->crow > max_row)
589             ui.active->crow = max_row;
590       }
591 
592       if (a.direction == UP
593       &&  ui.active->voffset == 0 && voffset_original == 0) {
594          ui.active->crow -= diff;
595          if (ui.active->crow < 0)
596             ui.active->crow = 0;
597       }
598    }
599 
600    redraw_active();
601    paint_status_bar();
602 }
603 
604 void
kba_scroll_col(KbaArgs a)605 kba_scroll_col(KbaArgs a)
606 {
607    int   maxlen;
608    int   maxhoff;
609    int   n;
610    int   i;
611 
612    /* determine how many cols to scroll */
613    n = 1;
614    if (gnum_get() > 0) {
615       n = gnum_get();
616       gnum_clear();
617    }
618 
619    /* determine maximum horizontal offset */
620    maxhoff = 0;
621    if (ui.active == ui.playlist) {
622       if (mi_display_getwidth() > ui.active->w)
623          maxhoff = mi_display_getwidth() - ui.active->w;
624    } else {
625       maxlen = 0;
626       for (i = 0; i < mdb.nplaylists; i++) {
627          if ((int) strlen(mdb.playlists[i]->name) > maxlen)
628             maxlen = strlen(mdb.playlists[i]->name);
629       }
630       if (maxlen > ui.active->w)
631          maxhoff = maxlen - ui.active->w;
632    }
633 
634    /* scroll */
635    switch (a.amount) {
636    case SINGLE:
637       swindow_scroll(ui.active, a.direction, n);
638       if (ui.active->hoffset > maxhoff)
639          ui.active->hoffset = maxhoff;
640       break;
641    case WHOLE:
642       switch (a.direction) {
643          case LEFT:
644             ui.active->hoffset = 0;
645             break;
646          case RIGHT:
647             ui.active->hoffset = maxhoff;
648             break;
649          default:
650             errx(1, "scroll_col: invalid direction");
651       }
652       break;
653    default:
654       errx(1, "scroll_col: invalid amount");
655    }
656 
657    /* redraw */
658    redraw_active();
659    paint_status_bar();
660 }
661 
662 void
kba_jumpto_screen(KbaArgs a)663 kba_jumpto_screen(KbaArgs a)
664 {
665    int n;
666    int max_row;
667 
668    /* get offset */
669    n = 0;
670    if (gnum_get() > 0) {
671       n = gnum_get();
672       gnum_clear();
673    }
674 
675    /* determine max row number */
676    if (ui.active->voffset + ui.active->h >= ui.active->nrows)
677       max_row = ui.active->nrows - ui.active->voffset - 1;
678    else
679       max_row = ui.active->h - 1;
680 
681    /* update current row */
682    switch (a.placement) {
683       case TOP:
684          ui.active->crow = n - 1;
685          break;
686       case MIDDLE:
687          ui.active->crow = max_row / 2;
688          break;
689       case BOTTOM:
690          ui.active->crow = max_row - n + 1;
691          break;
692       default:
693          errx(1, "jumpto_page: invalid location");
694    }
695 
696    /* sanitize current row */
697    if (ui.active->crow < 0)
698       ui.active->crow = 0;
699 
700    if (ui.active->crow >= ui.active->h)
701       ui.active->crow = ui.active->h - 1;
702 
703    if (ui.active->crow > max_row)
704       ui.active->crow = max_row;
705 
706    /* redraw */
707    redraw_active();
708    paint_status_bar();
709 }
710 
711 void
kba_jumpto_file(KbaArgs a)712 kba_jumpto_file(KbaArgs a)
713 {
714    float pct;
715    int   n, line, input;
716 
717    /* determine line/percent to jump to */
718    n = -1;
719    if (gnum_get() > 0) {
720       n = gnum_get();
721       gnum_clear();
722    }
723 
724    /* get line number to jump to */
725    switch (a.scale) {
726    case NUMBER:
727       switch (a.num) {
728          case 'g':
729             /* retrieve second 'g' (or cancel) and determine jump-point */
730             while ((input = getch()) && input != 'g') {
731                if (input != ERR) {
732                   ungetch(input);
733                   return;
734                }
735             }
736 
737             if (n < 0)
738                line = 1;
739             else if (n >= ui.active->nrows)
740                line = ui.active->nrows;
741             else
742                line = n;
743 
744             break;
745 
746          case 'G':
747             /* determine jump-point */
748             if (n < 0 || n >= ui.active->nrows)
749                line = ui.active->nrows;
750             else
751                line = n;
752 
753             break;
754 
755          default:
756             errx(1, "jumpto_file: NUMBER type with no num!");
757       }
758 
759       break;
760 
761    case PERCENT:
762       if (n <= 0 || n > 100)
763          return;
764 
765       pct = (float) n / 100;
766       line = pct * (float) ui.active->nrows;
767       break;
768 
769    default:
770       errx(1, "jumpto_file: invalid scale");
771    }
772 
773    /* jump */
774    if (line <= ui.active->voffset) {
775       swindow_scroll(ui.active, UP, ui.active->voffset - line + 1);
776       ui.active->crow = 0;
777    } else if (line > ui.active->voffset + ui.active->h) {
778       swindow_scroll(ui.active, DOWN, line - (ui.active->voffset + ui.active->h));
779       ui.active->crow = ui.active->h - 1;
780    } else {
781       ui.active->crow = line - ui.active->voffset - 1;
782    }
783 
784    /* redraw */
785    redraw_active();
786 
787    if (a.num != -1)   /* XXX a damn dirty hack for now. see search_find */
788       paint_status_bar();
789 }
790 
791 void
kba_go(KbaArgs a UNUSED)792 kba_go(KbaArgs a UNUSED)
793 {
794    /* NOTE: While I intend to support most of vim's g* commands, currently
795     *       this only supports 'gg' ... simply because I use it.
796     */
797    KbaArgs args = get_dummy_args();
798 
799    args.scale = NUMBER;
800    args.num   = 'g';
801    kba_jumpto_file(args);
802 }
803 
804 void
kba_search(KbaArgs a)805 kba_search(KbaArgs a)
806 {
807    const char *errmsg = NULL;
808    KbaArgs   find_args;
809    char  *search_phrase;
810    char  *prompt = NULL;
811    char **argv  = NULL;
812    int    argc = 0;
813    int    i;
814 
815    /* determine prompt to use */
816    switch (a.direction) {
817       case FORWARDS:
818          prompt = "/";
819          break;
820       case BACKWARDS:
821          prompt = "?";
822          break;
823       default:
824          errx(1, "search: invalid direction");
825    }
826 
827    /* get search phrase from user */
828    if (user_getstr(prompt, &search_phrase) != 0) {
829       wclear(ui.command);
830       wrefresh(ui.command);
831       return;
832    }
833 
834    /* set the global query description and the search direction */
835    if (str2argv(search_phrase, &argc, &argv, &errmsg) != 0) {
836       free(search_phrase);
837       paint_error("parse error: %s in '%s'", errmsg, search_phrase);
838       return;
839    }
840 
841    /* setup query */
842    mi_query_clear();
843    mi_query_setraw(search_phrase);
844    for (i = 0; i < argc; i++)
845       mi_query_add_token(argv[i]);
846 
847    search_dir_set(a.direction);
848    argv_free(&argc, &argv);
849    free(search_phrase);
850 
851    /* do the search */
852    find_args = get_dummy_args();
853    find_args.direction = SAME;
854    kba_search_find(find_args);
855 }
856 
857 void
kba_search_find(KbaArgs a)858 kba_search_find(KbaArgs a)
859 {
860    KbaArgs  foo;
861    bool  matches;
862    char *msg;
863    int   dir;
864    int   start_idx;
865    int   idx;
866    int   c;
867 
868    /* determine direction to do the search */
869    switch (a.direction) {
870       case SAME:
871          dir = search_dir_get();
872          break;
873 
874       case OPPOSITE:
875          if (search_dir_get() == FORWARDS)
876             dir = BACKWARDS;
877          else
878             dir = FORWARDS;
879          break;
880 
881       default:
882          errx(1, "search_find: invalid direction");
883    }
884 
885    /* start looking from current row */
886    start_idx = ui.active->voffset + ui.active->crow;
887    msg = NULL;
888    for (c = 1; c < ui.active->nrows + 1; c++) {
889 
890       /* get idx of record */
891       if (dir == FORWARDS)
892          idx = start_idx + c;
893       else
894          idx = start_idx - c;
895 
896       /* normalize idx */
897       if (idx < 0) {
898          idx = ui.active->nrows + idx;
899          msg = "search hit TOP, continuing at BOTTOM";
900 
901       } else if (idx >= ui.active->nrows) {
902          idx %= ui.active->nrows;
903          msg = "search hit BOTTOM, continuing at TOP";
904       }
905 
906       /* check if record at idx matches */
907       if (ui.active == ui.library)
908          matches = str_match_query(mdb.playlists[idx]->name);
909       else
910          matches = mi_match(viewing_playlist->files[idx]);
911 
912       /* found one, jump to it */
913       if (matches) {
914          if (msg != NULL)
915             paint_message(msg);
916 
917          gnum_set(idx + 1);
918          foo = get_dummy_args();
919          foo.scale = NUMBER;
920          foo.num = 'G';
921          kba_jumpto_file(foo);
922          return;
923       }
924    }
925 
926    paint_error("Pattern not found: %s", mi_query_getraw());
927 }
928 
929 
930 void
kba_visual(KbaArgs a UNUSED)931 kba_visual(KbaArgs a UNUSED)
932 {
933    if (ui.active == ui.library) {
934       paint_message("No visual mode in library window.  Sorry.");
935       return;
936    }
937 
938    if (visual_mode_start == -1)
939       visual_mode_start = ui.active->voffset + ui.active->crow;
940    else
941       visual_mode_start = -1;
942 
943    paint_playlist();
944 }
945 
946 /*
947  * TODO so much of the logic in cut() is similar with that of yank() above.
948  * combine the two, use an Args parameter to determine if the operation is
949  * a yank or a delete
950  */
951 void
kba_cut(KbaArgs a UNUSED)952 kba_cut(KbaArgs a UNUSED)
953 {
954    playlist *p;
955    char *warning;
956    bool  got_target;
957    int   start, end;
958    int   response;
959    int   input;
960    int   n;
961 
962    if (visual_mode_start != -1) {
963       start = visual_mode_start;
964       end = ui.active->voffset + ui.active->crow;
965       visual_mode_start = -1;
966       if (start > end) {
967          int tmp = end;
968          end = start;
969          start = tmp;
970       }
971       end++;
972    } else {
973       /* determine range */
974       n = 1;
975       if (gnum_get() > 0) {
976          n = gnum_get();
977          gnum_clear();
978       }
979 
980       /* get range */
981       got_target = false;
982       start = 0;
983       end = 0;
984       while ((input = getch()) && !got_target) {
985          if (input == ERR)
986             continue;
987 
988          switch (input) {
989             case 'd':   /* delete next n lines */
990                start = ui.active->voffset + ui.active->crow;
991                end = start + n;
992                got_target = true;
993                break;
994 
995             case 'G':   /* delete to end of current playlist */
996                start = ui.active->voffset + ui.active->crow;
997                end = ui.active->nrows;
998                got_target = true;
999                break;
1000 
1001             default:
1002                ungetch(input);
1003                return;
1004          }
1005       }
1006    }
1007 
1008    if (start >= ui.active->nrows) {
1009       paint_message("nothing to delete here!");
1010       return;
1011    }
1012 
1013    /* handle deleting of a whole playlist */
1014    if (ui.active == ui.library) {
1015       /* get playlist to delete */
1016       n = ui.active->voffset + ui.active->crow;
1017       p = mdb.playlists[n];
1018 
1019       /*
1020        * XXX i simply do NOT want to be able to delete multiple playlists
1021        * while drunk.
1022        */
1023       if (end != start + 1) {
1024          paint_error("cannot delete multiple playlists");
1025          return;
1026       }
1027       if (p == mdb.library || p == mdb.filter_results) {
1028          paint_error("cannot delete pseudo-playlists like LIBRARY or FILTER");
1029          return;
1030       }
1031 
1032       asprintf(&warning, "Are you sure you want to delete '%s'?", p->name);
1033       if (warning == NULL)
1034          err(1, "cut: asprintf(3) failed");
1035 
1036       /* make sure user wants this */
1037       if (user_get_yesno(warning, &response) != 0) {
1038          paint_message("delete of '%s' cancelled", p->name);
1039          return;
1040       }
1041       if (response != 1) {
1042          paint_message("playlist '%s' not deleted", p->name);
1043          return;
1044       }
1045 
1046       /* delete playlist and redraw library window */
1047       if (viewing_playlist == p) {
1048          viewing_playlist = mdb.library;
1049          ui.playlist->nrows = mdb.library->nfiles;
1050          ui.playlist->crow = 0;
1051          ui.playlist->voffset = 0;
1052          ui.playlist->hoffset = 0;
1053          paint_playlist();
1054       }
1055       ui.library->nrows--;
1056       if (ui.library->voffset + ui.library->crow >= ui.library->nrows)
1057          ui.library->crow = ui.library->nrows - ui.library->voffset - 1;
1058 
1059       medialib_playlist_remove(n);
1060       paint_library();
1061       free(warning);
1062       return;
1063    }
1064 
1065    /* return to handling of deleting files in a playlist... */
1066 
1067    /* can't delete from library */
1068    if (viewing_playlist == mdb.library) {
1069       paint_error("cannot delete from library");
1070       return;
1071    }
1072 
1073    /* sanitize start and end */
1074    if (end > ui.active->nrows)
1075       end = ui.active->nrows;
1076 
1077    /* clear existing yank buffer and add new stuff */
1078    ybuffer_clear();
1079    for (n = start; n < end; n++)
1080       ybuffer_add(viewing_playlist->files[n]);
1081 
1082    /* delete files */
1083    playlist_files_remove(viewing_playlist, start, end - start, true);
1084 
1085    /* update ui appropriately */
1086    viewing_playlist->needs_saving = true;
1087    ui.active->nrows = viewing_playlist->nfiles;
1088    if (ui.active->voffset + ui.active->crow >= ui.active->nrows)
1089       ui.active->crow = ui.active->nrows - ui.active->voffset - 1;
1090 
1091 
1092    /* redraw */
1093    paint_playlist();
1094    paint_library();
1095    paint_message("%d fewer files.", end - start);
1096 }
1097 
1098 void
kba_yank(KbaArgs a UNUSED)1099 kba_yank(KbaArgs a UNUSED)
1100 {
1101    bool got_target;
1102    int  start, end;
1103    int  input;
1104    int  n;
1105 
1106    if (ui.active == ui.library) {
1107       paint_error("cannot yank in library window");
1108       return;
1109    }
1110 
1111    if (viewing_playlist->nfiles == 0) {
1112       paint_error("nothing to yank!");
1113       return;
1114    }
1115 
1116    if (visual_mode_start != -1) {
1117       start = visual_mode_start;
1118       end = ui.active->voffset + ui.active->crow;
1119       visual_mode_start = -1;
1120       if (start > end) {
1121          int tmp = end;
1122          end = start;
1123          start = tmp;
1124       }
1125       end++;
1126    } else {
1127       /* determine range */
1128       n = 1;
1129       if (gnum_get() > 0) {
1130          n = gnum_get();
1131          gnum_clear();
1132       }
1133       /* get next input from user */
1134       got_target = false;
1135       start = 0;
1136       end = 0;
1137       while ((input = getch()) && !got_target) {
1138          if (input == ERR)
1139             continue;
1140 
1141          switch (input) {
1142             case 'y':   /* yank next n lines */
1143                start = ui.active->voffset + ui.active->crow;
1144                end = start + n;
1145                got_target = true;
1146                break;
1147 
1148             case 'G':   /* yank to end of current playlist */
1149                start = ui.active->voffset + ui.active->crow;
1150                end = ui.active->nrows;
1151                got_target = true;
1152                break;
1153 
1154             /*
1155              * TODO handle other directions ( j/k/H/L/M/^u/^d/ etc. )
1156              * here.  this will be ... tricky.
1157              * might want to re-organize other stuff?
1158              */
1159 
1160             default:
1161                ungetch(input);
1162                return;
1163          }
1164       }
1165    }
1166 
1167    /* sanitize start and end */
1168    if (end > ui.active->nrows)
1169       end = ui.active->nrows;
1170 
1171    /* clear existing yank buffer and add new stuff */
1172    ybuffer_clear();
1173    for (n = start; n < end; n++)
1174       ybuffer_add(viewing_playlist->files[n]);
1175 
1176    paint_playlist();
1177    /* notify user # of rows yanked */
1178    paint_message("Yanked %d files.", end - start);
1179 }
1180 
1181 void
kba_paste(KbaArgs a)1182 kba_paste(KbaArgs a)
1183 {
1184    playlist *p;
1185    int start = 0;
1186    int i;
1187 
1188    if (_yank_buffer.nfiles == 0) {
1189       paint_error("nothing to paste");
1190       return;
1191    }
1192 
1193    /* determine the playlist we're pasting into */
1194    if (ui.active == ui.library) {
1195       i = ui.active->voffset + ui.active->crow;
1196       p = mdb.playlists[i];
1197    } else {
1198       p = viewing_playlist;
1199    }
1200 
1201    /* can't alter library */
1202    if (p == mdb.library) {
1203       paint_error("Cannot alter %s psuedo-playlist", mdb.library->name);
1204       return;
1205    }
1206 
1207    if (ui.active == ui.library) {
1208       /* figure out where to paste into playlist */
1209       switch (a.placement) {
1210          case BEFORE:
1211             start = 0;
1212             break;
1213          case AFTER:
1214             start = p->nfiles;
1215             break;
1216          default:
1217             errx(1, "paste: invalid placement [if]");
1218       }
1219 
1220    } else {
1221       /* determine index in playlist to insert new files after */
1222       switch (a.placement) {
1223          case BEFORE:
1224             start = ui.active->voffset + ui.active->crow;
1225             break;
1226          case AFTER:
1227             start = ui.active->voffset + ui.active->crow + 1;
1228             if (start > p->nfiles) start = p->nfiles;
1229             break;
1230          default:
1231             errx(1, "paste: invalid placement [else]");
1232       }
1233    }
1234 
1235    /* add files */
1236    playlist_files_add(p, _yank_buffer.files, start, _yank_buffer.nfiles, true);
1237 
1238    if (p == viewing_playlist)
1239       ui.playlist->nrows = p->nfiles;
1240 
1241    p->needs_saving = true;
1242 
1243    /* redraw */
1244    paint_library();
1245    paint_playlist();
1246    if (ui.active == ui.library)
1247       paint_message("Pasted %d files to '%s'", _yank_buffer.nfiles, p->name);
1248    else
1249       paint_message("Pasted %d files.", _yank_buffer.nfiles);
1250 }
1251 
1252 
1253 void
kba_undo(KbaArgs a UNUSED)1254 kba_undo(KbaArgs a UNUSED)
1255 {
1256    if (ui.active == ui.library) {
1257       paint_message("Cannot undo in library window.");
1258       return;
1259    }
1260 
1261    if (playlist_undo(viewing_playlist) != 0)
1262       paint_message("Nothing to undo.");
1263    else
1264       paint_message("Undo successfull.");
1265 
1266    /* TODO more informative message like in vim */
1267 
1268    ui.playlist->nrows = viewing_playlist->nfiles;
1269    if (ui.playlist->voffset + ui.playlist->crow >= ui.playlist->nrows)
1270       ui.playlist->crow = ui.playlist->nrows - ui.playlist->voffset - 1;
1271 
1272    paint_playlist();
1273 }
1274 
1275 void
kba_redo(KbaArgs a UNUSED)1276 kba_redo(KbaArgs a UNUSED)
1277 {
1278    if (ui.active == ui.library) {
1279       paint_message("Cannot redo in library window.");
1280       return;
1281    }
1282 
1283    if (playlist_redo(viewing_playlist) != 0)
1284       paint_message("Nothing to redo.");
1285    else
1286       paint_message("Redo successfull.");
1287 
1288    /* TODO */
1289 
1290    ui.playlist->nrows = viewing_playlist->nfiles;
1291    if (ui.playlist->voffset + ui.playlist->crow >= ui.playlist->nrows)
1292       ui.playlist->crow = ui.playlist->nrows - ui.playlist->voffset - 1;
1293 
1294    paint_playlist();
1295 }
1296 
1297 void
kba_command_mode(KbaArgs a UNUSED)1298 kba_command_mode(KbaArgs a UNUSED)
1299 {
1300    char  *cmd;
1301 
1302    /* get command from user */
1303    if (user_getstr(":", &cmd) != 0 || strlen(cmd) == 0) {
1304       werase(ui.command);
1305       wrefresh(ui.command);
1306       return;
1307    }
1308 
1309    /* check for '!' used for executing external commands */
1310    if (cmd[0] == '!') {
1311       execute_external_command(cmd + 1);
1312       free(cmd);
1313       return;
1314    }
1315 
1316    cmd_execute(cmd);
1317    free(cmd);
1318    return;
1319 }
1320 
1321 void
kba_shell(KbaArgs a UNUSED)1322 kba_shell(KbaArgs a UNUSED)
1323 {
1324    char  *cmd;
1325 
1326    /* get command from user */
1327    if (user_getstr("!", &cmd) != 0) {
1328       werase(ui.command);
1329       wrefresh(ui.command);
1330       return;
1331    }
1332 
1333    execute_external_command(cmd);
1334    free(cmd);
1335 }
1336 
1337 void
kba_quit(KbaArgs a UNUSED)1338 kba_quit(KbaArgs a UNUSED)
1339 {
1340    VSIG_QUIT = 1;
1341 }
1342 
1343 void
kba_redraw(KbaArgs a UNUSED)1344 kba_redraw(KbaArgs a UNUSED)
1345 {
1346    ui_clear();
1347    paint_all();
1348 }
1349 
1350 void
kba_switch_windows(KbaArgs a UNUSED)1351 kba_switch_windows(KbaArgs a UNUSED)
1352 {
1353    if (ui.active == ui.library) {
1354       ui.active = ui.playlist;
1355       if (ui.lhide)
1356          ui_hide_library();
1357    } else {
1358       ui.active = ui.library;
1359       if (ui.lhide)
1360          ui_unhide_library();
1361    }
1362 
1363    paint_all();
1364    paint_status_bar();
1365 }
1366 
1367 void
kba_show_file_info(KbaArgs a UNUSED)1368 kba_show_file_info(KbaArgs a UNUSED)
1369 {
1370    int idx;
1371 
1372    if (ui.active == ui.library)
1373       return;
1374 
1375    if (ui.active->crow >= ui.active->nrows) {
1376       paint_message("no file here");
1377       return;
1378    }
1379 
1380    if (showing_file_info)
1381       paint_playlist();
1382    else {
1383       /* get file index and show */
1384       idx = ui.active->voffset + ui.active->crow;
1385       paint_playlist_file_info(viewing_playlist->files[idx]);
1386    }
1387 }
1388 
1389 void
kba_load_playlist(KbaArgs a UNUSED)1390 kba_load_playlist(KbaArgs a UNUSED)
1391 {
1392    int  idx;
1393 
1394    if (ui.active == ui.library) {
1395       /* load playlist & switch focus */
1396       idx = ui.library->voffset + ui.library->crow;
1397       viewing_playlist = mdb.playlists[idx];
1398       ui.playlist->nrows = mdb.playlists[idx]->nfiles;
1399       ui.playlist->crow  = 0;
1400       ui.playlist->voffset = 0;
1401       ui.playlist->hoffset = 0;
1402 
1403       paint_playlist();
1404       kba_switch_windows(get_dummy_args());
1405    } else {
1406       /* play song */
1407       if (ui.active->crow >= ui.active->nrows) {
1408          paint_message("no file here");
1409          return;
1410       }
1411       player_set_queue(viewing_playlist, ui.active->voffset + ui.active->crow);
1412       playing_playlist = viewing_playlist;
1413       player_play();
1414    }
1415 }
1416 
1417 void
kba_play(KbaArgs a UNUSED)1418 kba_play(KbaArgs a UNUSED)
1419 {
1420    int  idx;
1421 
1422    if (ui.active == ui.library) {
1423       /* load playlist & switch focus */
1424       idx = ui.library->voffset + ui.library->crow;
1425       viewing_playlist = mdb.playlists[idx];
1426       ui.playlist->nrows = mdb.playlists[idx]->nfiles;
1427       ui.playlist->crow  = 0;
1428       ui.playlist->voffset = 0;
1429       ui.playlist->hoffset = 0;
1430 
1431       paint_playlist();
1432       kba_switch_windows(get_dummy_args());
1433    } else {
1434       /* play song */
1435       if (ui.active->crow >= ui.active->nrows) {
1436          paint_message("no file here");
1437          return;
1438       }
1439       player_set_queue(viewing_playlist, ui.active->voffset + ui.active->crow);
1440       playing_playlist = viewing_playlist;
1441       player_play();
1442    }
1443 }
1444 
1445 void
kba_pause(KbaArgs a UNUSED)1446 kba_pause(KbaArgs a UNUSED)
1447 {
1448    player_pause();
1449 }
1450 
1451 void
kba_stop(KbaArgs a UNUSED)1452 kba_stop(KbaArgs a UNUSED)
1453 {
1454    player_stop();
1455    playing_playlist = NULL;
1456 }
1457 
1458 void
kba_play_next(KbaArgs a UNUSED)1459 kba_play_next(KbaArgs a UNUSED)
1460 {
1461    int n = 1;
1462 
1463    /* is there a multiplier? */
1464    if (gnum_get() > 0) {
1465       n = gnum_get();
1466       gnum_clear();
1467    }
1468 
1469    player_skip_song(n);
1470 }
1471 
1472 void
kba_play_prev(KbaArgs a UNUSED)1473 kba_play_prev(KbaArgs a UNUSED)
1474 {
1475    int n = 1;
1476 
1477    /* is there a multiplier? */
1478    if (gnum_get() > 0) {
1479       n = gnum_get();
1480       gnum_clear();
1481    }
1482 
1483    player_skip_song(n * -1);
1484 }
1485 
1486 void
kba_volume(KbaArgs a)1487 kba_volume(KbaArgs a)
1488 {
1489    float pcnt;
1490 
1491    if (gnum_get() > 0)
1492       pcnt = gnum_retrieve();
1493    else
1494       pcnt = 1;
1495 
1496    switch (a.direction) {
1497    case FORWARDS:
1498       break;
1499    case BACKWARDS:
1500       pcnt *= -1;
1501       break;
1502    default:
1503       errx(1, "kba_volume: invalid direction");
1504    }
1505 
1506    player_volume_step(pcnt);
1507 }
1508 
1509 void
kba_seek(KbaArgs a)1510 kba_seek(KbaArgs a)
1511 {
1512    int n, secs;
1513 
1514    /* determine number of seconds to seek */
1515    switch (a.scale) {
1516    case SECONDS:
1517       secs = a.num;
1518       break;
1519    case MINUTES:
1520       secs = a.num * 60;
1521       break;
1522    default:
1523       errx(1, "seek_playback: invalid scale");
1524    }
1525 
1526    /* adjust for direction */
1527    switch (a.direction) {
1528    case FORWARDS:
1529       /* no change */
1530       break;
1531    case BACKWARDS:
1532       secs *= -1;
1533       break;
1534    default:
1535       errx(1, "seek_playback: invalid direction");
1536    }
1537 
1538    /* is there a multiplier? */
1539    n = 1;
1540    if (gnum_get() > 0) {
1541       n = gnum_get();
1542       gnum_clear();
1543    }
1544 
1545    /* apply n & seek */
1546    player_seek(secs * n);
1547 }
1548 
1549 void
kba_toggle(KbaArgs a)1550 kba_toggle(KbaArgs a)
1551 {
1552    toggle_list *t;
1553    char  *cmd;
1554    bool   got_register;
1555    int    n, input, registr;
1556 
1557    /* is there a multiplier? */
1558    n = 1;
1559    if (gnum_get() > 0) {
1560       n = gnum_get();
1561       gnum_clear();
1562    }
1563 
1564    /* get the register */
1565    got_register = false;
1566    registr = -1;
1567    while ((input = getch()) && !got_register) {
1568       if (input == ERR)
1569          continue;
1570 
1571       if (('a' <= input && input <= 'z')
1572       ||  ('A' <= input && input <= 'Z')) {
1573          got_register = true;
1574          registr = input;
1575       }
1576    }
1577 
1578    /* get the command to execute */
1579    if ((t = toggle_get(registr)) == NULL) {
1580       paint_error("No toggle list in register %c (%i).", registr, registr);
1581       return;
1582    }
1583 
1584    /* update index */
1585    n %= t->size;
1586    switch (a.direction) {
1587       case FORWARDS:
1588          t->index += n;
1589          t->index %= t->size;
1590          break;
1591       case BACKWARDS:
1592          if (n <= (int)t->index) {
1593             t->index -= n;
1594             t->index %= t->size;
1595          } else {
1596             t->index = t->size - (n - t->index);
1597          }
1598          break;
1599       default:
1600          errx(1, "%s: invalid direction", __FUNCTION__);
1601    }
1602 
1603    /* execute */
1604    cmd = t->commands[t->index];
1605    cmd_execute(cmd);
1606 }
1607 
1608 
1609 /*****************************************************************************
1610  *
1611  *                       Routines for Working with 'gnum'
1612  *
1613  ****************************************************************************/
1614 
1615 int _global_input_num = 0;
1616 
gnum_clear()1617 void gnum_clear()
1618 { _global_input_num = 0; }
1619 
gnum_get()1620 int  gnum_get()
1621 { return _global_input_num; }
1622 
gnum_set(int x)1623 void gnum_set(int x)
1624 { _global_input_num = x; }
1625 
gnum_add(int x)1626 void gnum_add(int x)
1627 {
1628    _global_input_num = _global_input_num * 10 + x;
1629 }
1630 
1631 int
gnum_retrieve()1632 gnum_retrieve()
1633 {
1634    int n = 1;
1635    if (gnum_get() > 0) {
1636       n = gnum_get();
1637       gnum_clear();
1638    }
1639    return n;
1640 }
1641 
1642 
1643 /*****************************************************************************
1644  *
1645  *                Routines for Working with Search Direction
1646  *
1647  ****************************************************************************/
1648 
1649 Direction _global_search_dir = FORWARDS;
1650 
search_dir_get()1651 Direction search_dir_get()
1652 { return _global_search_dir; }
1653 
search_dir_set(Direction dir)1654 void search_dir_set(Direction dir)
1655 { _global_search_dir = dir; }
1656 
1657 
1658 /*****************************************************************************
1659  *
1660  *               Routines for Working with Copy/Cut/Paste Buffer
1661  *
1662  ****************************************************************************/
1663 
1664 yank_buffer _yank_buffer;
1665 
1666 void
ybuffer_init()1667 ybuffer_init()
1668 {
1669    _yank_buffer.files = calloc(YANK_BUFFER_CHUNK_SIZE, sizeof(meta_info*));
1670    if (_yank_buffer.files == NULL)
1671       err(1, "ybuffer_init: calloc(3) failed");
1672 
1673    _yank_buffer.capacity = YANK_BUFFER_CHUNK_SIZE;
1674    _yank_buffer.nfiles = 0;
1675 }
1676 
1677 void
ybuffer_clear()1678 ybuffer_clear()
1679 {
1680    _yank_buffer.nfiles = 0;
1681 }
1682 
1683 void
ybuffer_free()1684 ybuffer_free()
1685 {
1686    free(_yank_buffer.files);
1687    _yank_buffer.capacity = 0;
1688    _yank_buffer.nfiles = 0;
1689 }
1690 
1691 void
ybuffer_add(meta_info * f)1692 ybuffer_add(meta_info *f)
1693 {
1694    meta_info **new_buff;
1695    int   new_capacity;
1696 
1697    /* do we need to realloc()? */
1698    if (_yank_buffer.nfiles == _yank_buffer.capacity) {
1699       _yank_buffer.capacity += YANK_BUFFER_CHUNK_SIZE;
1700       new_capacity = _yank_buffer.capacity * sizeof(meta_info*);
1701       if ((new_buff = realloc(_yank_buffer.files, new_capacity)) == NULL)
1702          err(1, "ybuffer_add: realloc(3) failed [%i]", new_capacity);
1703 
1704       _yank_buffer.files = new_buff;
1705    }
1706 
1707    /* add the file */
1708    _yank_buffer.files[ _yank_buffer.nfiles++ ] = f;
1709 }
1710 
1711 
1712 /*****************************************************************************
1713  *
1714  *                           Misc. Handy Routines
1715  *
1716  ****************************************************************************/
1717 
1718 void
redraw_active()1719 redraw_active()
1720 {
1721    if (ui.active == ui.library)
1722       paint_library();
1723    else
1724       paint_playlist();
1725 }
1726 
1727 /*
1728  * Given string input from user (argv[0]) and a command name, check if the
1729  * input matches the command name, taking into acount '!' weirdness and
1730  * abbreviations.
1731  */
1732 bool
match_command_name(const char * input,const char * cmd)1733 match_command_name(const char *input, const char *cmd)
1734 {
1735    bool  found;
1736    char *icopy;
1737 
1738    if (input == NULL || strlen(input) == 0)
1739       return false;
1740 
1741    if (strcmp(input, cmd) == 0)
1742       return true;
1743 
1744    /* check for '!' weirdness and abbreviations */
1745 
1746    if ((icopy = strdup(input)) == NULL)
1747       err(1, "match_command_name: strdup(3) failed");
1748 
1749    /* remove '!' from input, if present */
1750    if (strstr(icopy, "!") != NULL)
1751       *strstr(icopy, "!") = '\0';
1752 
1753    /* now check against command & abbreviation */
1754    if (strstr(cmd, icopy) == cmd)
1755       found = true;
1756    else
1757       found = false;
1758 
1759    free(icopy);
1760    return found;
1761 }
1762 
1763 void
execute_external_command(const char * cmd)1764 execute_external_command(const char *cmd)
1765 {
1766    int   input;
1767 
1768    def_prog_mode();
1769    endwin();
1770 
1771    system(cmd);
1772    printf("\nPress ENTER or type command to continue");
1773    fflush(stdout);
1774    raw();
1775    while(!VSIG_QUIT) {
1776       if ((input = getch()) && input != ERR) {
1777          if (input != '\r')
1778             ungetch(input);
1779          break;
1780       }
1781    }
1782    reset_prog_mode();
1783    paint_all();
1784 }
1785 
1786