]> git.neil.brown.name Git - edlib.git/blob - lib-input.c
autospell: fix detection of existing spell range handler
[edlib.git] / lib-input.c
1 /*
2  * Copyright Neil Brown ©2015-2023 <neil@brown.name>
3  * May be distributed under terms of GPLv2 - see file:COPYING
4  *
5  * Core input translation.
6  * This module transalates keystrokes and mouse events into commands.
7  * This involves tracking the current 'mode' state.
8  *
9  */
10
11 #define _GNU_SOURCE /*  for asprintf */
12 #include <unistd.h>
13 #include <stdlib.h>
14 #include <string.h>
15 #include <time.h>
16 #include <stdio.h>
17
18 #define PANE_DATA_TYPE struct input_mode
19 #include "core.h"
20
21 #define LOGSIZE 128
22 struct input_mode {
23         const char      *mode safe;
24         const char      *context safe; /* for status lines */
25         int             num, num2;
26         struct pane     *focus, *source, *grab;
27         struct mark     *point;
28         struct mouse_state {
29                 struct timespec last_action;
30                 int             last_x, last_y;
31                 int             click_count;
32                 int             click_on_up;
33                 char            *mod;
34         } buttons[3];
35
36         char            *log[LOGSIZE];
37         int             head;
38 };
39 #include "core-pane.h"
40
41 /* 'head' is 1 more than the last key added. */
42 static void log_add(struct input_mode *im safe,
43                     const char *type safe, const char *key safe)
44 {
45         free(im->log[im->head]);
46         im->log[im->head] = strconcat(NULL, type, key);
47         im->head = (im->head + 1) % LOGSIZE;
48 }
49
50 static void log_dump(struct pane *p safe)
51 {
52         struct input_mode *im = p->data;
53         int i;
54         int cnt = 0;
55         char *line = NULL;
56
57         i = im->head;
58         do {
59                 if (!im->log[i])
60                         continue;
61                 if (line)
62                         line = strconcat(p, line, ", ", im->log[i]);
63                 else
64                         line = strconcat(p, im->log[i]);
65                 cnt += 1;
66                 if (cnt % 10 == 0) {
67                         LOG("Input %d: %s", cnt/10, line);
68                         line = NULL;
69                 }
70         } while ((i = (i+1) % LOGSIZE) != im->head);
71
72         if (line)
73                 LOG("Input: %s", line);
74 }
75
76 DEF_CMD(log_input)
77 {
78         log_dump(ci->home);
79         return 1;
80 }
81
82 static void report_status(struct pane *focus safe, struct input_mode *im safe)
83 {
84         char *st = NULL;
85
86         if (im->num == NO_NUMERIC && im->mode[0] == 0)
87                 return;
88
89         if (im->num == NO_NUMERIC)
90                 asprintf(&st, " %s", im->mode);
91         else if (im->num == -NO_NUMERIC)
92                 asprintf(&st, " - %s", im->mode);
93         else
94                 asprintf(&st, " %d %s", im->num, im->mode);
95         call("Message:modal", focus, 0, NULL, st);
96 }
97
98 DEF_CMD(set_mode)
99 {
100         struct input_mode *im = ci->home->data;
101
102         if (ci->str) {
103                 free((void*)im->mode);
104                 im->mode = strdup(ci->str);
105         }
106         if (ci->str2) {
107                 free((void*)im->context);
108                 im->context = strdup(ci->str2);
109                 attr_set_str(&ci->home->attrs, "display-context", im->context);
110                 call("window:notify:display-context", ci->home);
111         }
112         report_status(ci->focus, im);
113         return 1;
114 }
115
116 DEF_CMD(set_num)
117 {
118         struct input_mode *im = ci->home->data;
119
120         im->num = ci->num;
121         report_status(ci->focus, im);
122         return 1;
123 }
124
125 DEF_CMD(set_num2)
126 {
127         struct input_mode *im = ci->home->data;
128
129         im->num2 = ci->num;
130         return 1;
131 }
132
133 DEF_CMD(set_all)
134 {
135         struct input_mode *im = ci->home->data;
136
137         if (ci->str) {
138                 free((void*)im->mode);
139                 im->mode = strdup(ci->str);
140         }
141         if (ci->str2) {
142                 free((void*)im->context);
143                 im->context = strdup(ci->str2);
144                 attr_set_str(&ci->home->attrs, "display-context", im->context);
145                 call("window:notify:display-context", ci->home);
146         }
147         im->num = ci->num;
148         im->num2 = ci->num;
149         report_status(ci->focus, im);
150         return 1;
151 }
152
153 static const char *safe ctrl_map[][2] = {
154         { ":Backspace", ":C-H" },
155         { ":Enter",     ":C-M" },
156         { ":ESC",       ":C-[" },
157         { ":LF",        ":C-J" },
158         { ":Tab",       ":C-I" },
159         { ":Del",       ":C-?" },
160         { ":A:Backspace",":A:C-H" },
161         { ":A:Enter",   ":A:C-M" },
162         { ":A:ESC",     ":A:C-[" },
163         { ":A:LF",      ":A:C-J" },
164         { ":A:Tab",     ":A:C-I" },
165         { ":A:Del",     ":A:C-?" },
166         { ":SPC",       "- " },
167 };
168
169 static const char *map_key(const char *key safe)
170 {
171         unsigned int i;
172
173         for (i = 0; i < ARRAY_SIZE(ctrl_map) ; i++) {
174                 if (strcmp(key, ctrl_map[i][0]) == 0)
175                         return ctrl_map[i][1];
176         }
177         return NULL;
178 }
179
180 DEF_CMD(keystroke)
181 {
182         const char *key;
183         struct input_mode *im = ci->home->data;
184         struct pane *p;
185         int ret;
186         int num = im->num;
187         int num2 = im->num2;
188         const char *mode = im->mode;
189         const char *alt;
190         struct mark *m;
191
192         if (!ci->str)
193                 return Enoarg;
194
195         call("window:notify:Keystroke-notify", ci->home, 0, NULL, ci->str);
196         log_add(im, "K", ci->str);
197
198         im->mode = strdup("");
199         im->num = NO_NUMERIC;
200         im->num2 = 0;
201
202         if (im->source != ci->focus) {
203                 im->source = ci->focus;
204                 im->focus = NULL;
205                 im->point = NULL;
206         }
207
208         if (!im->focus || im->focus->focus) {
209                 p = pane_focus(ci->focus);
210                 im->focus = p;
211                 pane_add_notify(ci->home, p, "Notify:Close");
212         }
213         p = im->focus;
214
215         if (!im->point)
216                 im->point = call_ret(mark, "doc:point", p);
217         m = im->point;
218
219         key = strconcat(ci->home, "K", mode, ci->str);
220         time_starts(ci->home);
221         ret = call(key, p, num, m, NULL, num2);
222         if (ret == Efallthrough && (alt = map_key(ci->str)) != NULL) {
223                 key = strconcat(ci->home, "K", mode, alt);
224                 ret = call(key, p, num, m, NULL, num2);
225         }
226         time_ends(ci->home);
227         if (ret <= Efail)
228                 call("Message:default", p, 0, NULL,
229                      strconcat(ci->home, "** Command ", key, " Failed **"));
230         else if (ret == Efallthrough) {
231                 key = strconcat(ci->home, "K", mode, ci->str);
232                 call("Message:modal", p, 0, NULL,
233                      strconcat(ci->home, "** Command ", key, " not known **"));
234         }
235         return Efallthrough;
236 }
237
238 DEF_CMD(keystroke_sequence)
239 {
240         struct pane *home = ci->home;
241         const char *c = ci->str;
242         const char *e, *dash;
243         int ret;
244
245         if (!c)
246                 return Enoarg;
247         while ((e = strchr(c, ' ')) != NULL) {
248                 dash = "";
249                 if (*c != ':' || e == c+1)
250                         dash = "-";
251                 ret = call("Keystroke", home, 0, NULL,
252                            strconcat(home, dash,
253                                      strnsave(home, c, e - c)));
254                 if (ret < 0)
255                         return Efail;
256                 c = e+1;
257         }
258         if (!*c)
259                 return 1;
260         dash = "";
261         if (*c != ':' || c[1] == '\0')
262                 dash = "-";
263         ret = call("Keystroke", home, 0, NULL, strconcat(home, dash, c));
264         if (ret < 0)
265                 return Efail;
266         return 1;
267 }
268
269 static int tspec_diff_ms(struct timespec *a safe, struct timespec *b safe)
270 {
271         return ((a->tv_sec - b->tv_sec) * 1000 +
272                 (a->tv_nsec - b->tv_nsec) / 1000000);
273 }
274
275 DEF_CMD(mouse_event)
276 {
277         struct input_mode *im = ci->home->data;
278         struct xy xy;
279         int num, ex;
280         struct pane *focus;
281         char *key;
282         struct timespec now;
283         unsigned int b;
284         int press;
285         int r;
286         const char *mode;
287         const char *cmd;
288         char *mod; /* :A:C:S modifiers - optional */
289         struct mouse_state *ms = NULL;
290
291         clock_gettime(CLOCK_MONOTONIC, &now);
292
293         if (!ci->str)
294                 return Enoarg;
295
296         call("window:notify:Mouse-event-notify", ci->home,
297              ci->num, NULL, ci->str,
298              ci->num2);
299         log_add(im, "M", ci->str);
300
301         if (ci->num2 == 1) {
302                 /* Press */
303                 press = 1;
304                 b = ci->num - 1;
305         } else if (ci->num2 == 2) {
306                 /* Release */
307                 press = 0;
308                 b = ci->num - 1;
309         } else {
310                 /* 3 is Motion */
311                 press = 0;
312                 b = 100;
313         }
314         if (b < 3) {
315                 bool repeat = False;
316
317                 ms = &im->buttons[b];
318
319                 /* FIXME the max movement for a double-click should be
320                  * about a char width - maybe something based on scale
321                  */
322                 if (tspec_diff_ms(&now, &ms->last_action) <= 500 &&
323                     (abs(ci->x - ms->last_x) +
324                      abs(ci->y - ms->last_y)) <= 2)
325                         /* FIXME should I let 'release' know that
326                          * it was close to the 'press'??
327                          */
328                         repeat = True;
329                 else
330                         /* Too much time or space has elapsed.
331                          * This cannot be a click.
332                          */
333                         ms->click_on_up = 0;
334
335                 if (press) {
336                         if (!repeat)
337                                 ms->click_count = 1;
338                         else if (ms->click_count < 3)
339                                 ms->click_count += 1;
340                 }
341                 ms->last_action = now;
342                 ms->last_x = ci->x; ms->last_y = ci->y;
343         }
344
345         focus = ci->focus;
346         num = im->num;
347         ex = im->num2;
348         /* FIXME is there any point in this? */
349         xy = pane_mapxy(ci->focus, focus, ci->x, ci->y, False);
350
351         mode = strsave(ci->home, im->mode);
352         free((void*)im->mode);
353         im->mode = strdup("");
354         im->num = NO_NUMERIC;
355         im->num2 = 0;
356
357         if (im->grab && !press) {
358                 /* Release and Motion should go to the same
359                  * place as Press went.
360                  */
361                 focus = im->grab;
362                 xy = pane_mapxy(focus, ci->focus, 0, 0, False);
363                 xy.x = ci->x - xy.x;
364                 xy.y = ci->y - xy.y;
365         } else while (1) {
366                         struct pane *t, *chld = NULL;
367
368                         list_for_each_entry(t, &focus->children, siblings) {
369                                 if (t->z < 0)
370                                         continue;
371                                 if (xy.x < t->x || xy.x >= t->x + t->w)
372                                         continue;
373                                 if (xy.y < t->y || xy.y >= t->y + t->h)
374                                         continue;
375                                 if (chld == NULL || t->z > chld->z)
376                                         chld = t;
377                         }
378                         /* descend into chld */
379                         if (!chld)
380                                 break;
381                         xy.x -= chld->x;
382                         xy.y -= chld->y;
383                         focus = chld;
384                 }
385         if (im->grab != focus) {
386                 im->grab = focus;
387                 pane_add_notify(ci->home, focus, "Notify:Close");
388         }
389
390         if (!ms) {
391                 int ret;
392                 key = strconcat(ci->home, "M", mode, ci->str);
393                 time_starts(ci->home);
394                 ret = call(key, focus, num, NULL, NULL, ex, NULL, NULL,
395                             xy.x, xy.y);
396                 time_ends(ci->home);
397                 return ret;
398         }
399         if (press) {
400                 /* identify and save modifiers */
401                 free(ms->mod);
402                 if (ci->str2)
403                         mod = strdup(ci->str2);
404                 else {
405                         char *c = strrchr(ci->str, ':');
406                         if (c)
407                                 mod = strndup(ci->str, c - ci->str);
408                         else
409                                 mod = strdup("");
410                 }
411                 ms->mod = mod;
412         }
413
414         if (press)
415                 cmd = "Press-";
416         else if (ms->click_on_up)
417                 cmd = "Click-";
418         else
419                 cmd = "Release-";
420
421         /* Try :nPress :(n-1)Press ... (or :nRelease or :nClick)
422          * until something gets a result.
423          * 'n' is T (triple) or D(double) or ""(Single).
424          * If nothing got a result for a Press,, register for
425          * 'click' on release.
426          */
427         ms->click_on_up = 1;
428         for (r = ms->click_count; r >= 1 ; r--) {
429                 int ret;
430                 char *mult = "\0\0D\0T" + (r-1)*2;
431                 char n[2];
432
433                 n[0] = '1' + b;
434                 n[1] = 0;
435                 key = strconcat(ci->home, "M", mode, ms->mod, ":", mult,
436                                 cmd, n);
437                 time_starts(ci->home);
438                 ret = call(key, focus, num, NULL, NULL, ex,
439                            NULL, NULL, xy.x, xy.y);
440                 time_ends(ci->home);
441                 if (ret > 0) {
442                         /* If this is 'press', then don't want
443                          * click_on_up.  If this is release, it
444                          * don't matter what we set.
445                          */
446                         ms->click_on_up = 0;
447                         return ret;
448                 }
449         }
450         return Efalse;
451 }
452
453 DEF_CMD(mouse_grab)
454 {
455         struct input_mode *im = ci->home->data;
456
457         im->grab = ci->focus;
458         pane_add_notify(ci->home, ci->focus, "Notify:Close");
459         return 1;
460 }
461
462 DEF_CMD(input_cancel)
463 {
464         /* Other handlers fall-through.  We catch so that
465          * it doesn't appear to be ignored
466          */
467         return 1;
468 }
469
470 DEF_CMD(refocus)
471 {
472         struct input_mode *im = ci->home->data;
473
474         im->focus = NULL;
475         im->point = NULL;
476         im->source = NULL;
477         return Efallthrough;
478 }
479
480 DEF_CMD(close_focus)
481 {
482         struct input_mode *im = ci->home->data;
483
484         if (im->focus == ci->focus) {
485                 im->focus = NULL;
486                 im->point = NULL;
487                 im->source = NULL;
488         }
489
490         if (im->grab == ci->focus)
491                 im->grab = NULL;
492         return 1;
493 }
494
495 DEF_CMD_CLOSED(input_close)
496 {
497         struct input_mode *im = ci->home->data;
498         int i;
499
500         for (i = 0; i < 3; i++) {
501                 free(im->buttons[i].mod);
502                 im->buttons[i].mod = NULL;
503         }
504         free((void*)im->mode);
505         free((void*)im->context);
506         return 1;
507 }
508
509 static struct map *im_map;
510 static void register_map(void)
511 {
512         if (im_map)
513                 return;
514         im_map = key_alloc();
515         key_add(im_map, "Keystroke", &keystroke);
516         key_add(im_map, "Keystroke-sequence", &keystroke_sequence);
517         key_add(im_map, "Mouse-event", &mouse_event);
518         key_add(im_map, "Mouse-grab", &mouse_grab);
519         key_add(im_map, "Mode:set-mode", &set_mode);
520         key_add(im_map, "Mode:set-num", &set_num);
521         key_add(im_map, "Mode:set-num2", &set_num2);
522         key_add(im_map, "Mode:set-all", &set_all);
523         key_add(im_map, "pane:refocus", &refocus);
524         key_add(im_map, "Notify:Close", &close_focus);
525         key_add(im_map, "input:log", &log_input);
526         key_add(im_map, "Close", &input_close);
527
528         key_add(im_map, "Cancel", &input_cancel);
529         key_add(im_map, "Abort", &input_cancel);
530 }
531
532 DEF_LOOKUP_CMD(input_handle, im_map);
533 DEF_CMD(input_attach)
534 {
535         struct input_mode *im;
536         struct pane *p;
537
538         register_map();
539
540         p = pane_register(ci->focus, 0, &input_handle.c);
541         if (!p)
542                 return Efail;
543         im = p->data;
544         im->mode = strdup("");
545         im->context = strdup("");
546         im->num = NO_NUMERIC;
547         im->num2 = 0;
548
549         return comm_call(ci->comm2, "callback:attach", p);
550 }
551
552 void edlib_init(struct pane *ed safe)
553 {
554         call_comm("global-set-command", ed, &input_attach, 0, NULL, "attach-input");
555 }