1 /* ncdc - NCurses Direct Connect client
2 
3   Copyright (c) 2011-2019 Yoran Heling
4 
5   Permission is hereby granted, free of charge, to any person obtaining
6   a copy of this software and associated documentation files (the
7   "Software"), to deal in the Software without restriction, including
8   without limitation the rights to use, copy, modify, merge, publish,
9   distribute, sublicense, and/or sell copies of the Software, and to
10   permit persons to whom the Software is furnished to do so, subject to
11   the following conditions:
12 
13   The above copyright notice and this permission notice shall be included
14   in all copies or substantial portions of the Software.
15 
16   THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17   EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18   MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19   IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
20   CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
21   TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
22   SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23 
24 */
25 
26 #include "ncdc.h"
27 #include "ui_textinput.h"
28 
29 
30 // We only have one command history, so the struct and its instance is local to
31 // this file, and the functions work with this instead of accepting an instance
32 // as argument. The ui_textinput functions also access the struct and static
33 // functions, but these don't need to be public - since ui_textinput is defined
34 // below.
35 
36 #define CMDHIST_BUF 511 // must be 2^x-1
37 #define CMDHIST_MAXCMD 2000
38 
39 
40 typedef struct ui_cmdhist_t {
41   char *buf[CMDHIST_BUF+1]; // circular buffer
42   char *fn;
43   int last;
44   gboolean ismod;
45 } ui_cmdhist_t;
46 
47 // we only have one command history, so this can be a global
48 static ui_cmdhist_t *cmdhist;
49 
50 
ui_cmdhist_add(const char * str)51 static void ui_cmdhist_add(const char *str) {
52   int cur = cmdhist->last & CMDHIST_BUF;
53   // ignore empty lines, or lines that are the same as the previous one
54   if(!str || !str[0] || (cmdhist->buf[cur] && 0 == strcmp(str, cmdhist->buf[cur])))
55     return;
56 
57   cmdhist->last++;
58   cur = cmdhist->last & CMDHIST_BUF;
59   if(cmdhist->buf[cur]) {
60     g_free(cmdhist->buf[cur]);
61     cmdhist->buf[cur] = NULL;
62   }
63 
64   // truncate the string if it is longer than CMDHIST_MAXCMD bytes, making sure
65   // to not truncate within a UTF-8 sequence
66   int len = 0;
67   while(len < CMDHIST_MAXCMD-10 && str[len])
68     len = g_utf8_next_char(str+len) - str;
69   cmdhist->buf[cur] = g_strndup(str, len);
70   cmdhist->ismod = TRUE;
71 }
72 
73 
ui_cmdhist_init(const char * file)74 void ui_cmdhist_init(const char *file) {
75   static char buf[CMDHIST_MAXCMD+2]; // + \n and \0
76   cmdhist = g_new0(ui_cmdhist_t, 1);
77 
78   cmdhist->fn = g_build_filename(db_dir, file, NULL);
79   FILE *f = fopen(cmdhist->fn, "r");
80   if(f) {
81     while(fgets(buf, CMDHIST_MAXCMD+2, f)) {
82       int len = strlen(buf);
83       if(len > 0 && buf[len-1] == '\n')
84         buf[len-1] = 0;
85 
86       if(g_utf8_validate(buf, -1, NULL))
87         ui_cmdhist_add(buf);
88     }
89     fclose(f);
90   }
91 }
92 
93 
94 // searches the history either backward or forward for the string q. The line 'start' is also counted.
ui_cmdhist_search(gboolean backward,const char * q,int start)95 static int ui_cmdhist_search(gboolean backward, const char *q, int start) {
96   int i;
97   for(i=start; cmdhist->buf[i&CMDHIST_BUF] && (backward ? (i>=MAX(1, cmdhist->last-CMDHIST_BUF)) : (i<=cmdhist->last)); backward ? i-- : i++) {
98     if(g_str_has_prefix(cmdhist->buf[i & CMDHIST_BUF], q))
99       return i;
100   }
101   return -1;
102 }
103 
104 
ui_cmdhist_save()105 static void ui_cmdhist_save() {
106   if(!cmdhist->ismod)
107     return;
108   cmdhist->ismod = FALSE;
109 
110   FILE *f = fopen(cmdhist->fn, "w");
111   if(!f) {
112     g_warning("Unable to open history file '%s' for writing: %s", cmdhist->fn, g_strerror(errno));
113     return;
114   }
115 
116   int i;
117   for(i=0; i<=CMDHIST_BUF; i++) {
118     char *l = cmdhist->buf[(cmdhist->last+1+i)&CMDHIST_BUF];
119     if(l) {
120       if(fputs(l, f) < 0 || fputc('\n', f) < 0)
121         g_warning("Error writing to history file '%s': %s", cmdhist->fn, strerror(errno));
122     }
123   }
124   if(fclose(f) < 0)
125     g_warning("Error writing to history file '%s': %s", cmdhist->fn, strerror(errno));
126 }
127 
128 
ui_cmdhist_close()129 void ui_cmdhist_close() {
130   int i;
131   ui_cmdhist_save();
132   for(i=0; i<=CMDHIST_BUF; i++)
133     if(cmdhist->buf[i])
134       g_free(cmdhist->buf[i]);
135   g_free(cmdhist->fn);
136   g_free(cmdhist);
137 }
138 
139 
140 
141 
142 
143 #if INTERFACE
144 
145 struct ui_textinput_t {
146   int pos; // position of the cursor, in number of characters
147   GString *str;
148   gboolean usehist;
149   int s_pos;
150   char *s_q;
151   gboolean s_top;
152   void (*complete)(char *, char **);
153   char *c_q, *c_last, **c_sug;
154   int c_cur;
155   gboolean bracketed_paste;
156 };
157 
158 #endif
159 
160 
ui_textinput_create(gboolean usehist,void (* complete)(char *,char **))161 ui_textinput_t *ui_textinput_create(gboolean usehist, void (*complete)(char *, char **)) {
162   ui_textinput_t *ti = g_new0(ui_textinput_t, 1);
163   ti->str = g_string_new("");
164   ti->usehist = usehist;
165   ti->s_pos = -1;
166   ti->complete = complete;
167   ti->bracketed_paste = FALSE;
168   return ti;
169 }
170 
171 
ui_textinput_complete_reset(ui_textinput_t * ti)172 static void ui_textinput_complete_reset(ui_textinput_t *ti) {
173   if(ti->complete) {
174     g_free(ti->c_q);
175     g_free(ti->c_last);
176     g_strfreev(ti->c_sug);
177     ti->c_q = ti->c_last = NULL;
178     ti->c_sug = NULL;
179   }
180 }
181 
182 
ui_textinput_complete(ui_textinput_t * ti)183 static void ui_textinput_complete(ui_textinput_t *ti) {
184   if(!ti->complete)
185     return;
186   if(!ti->c_q) {
187     ti->c_q = ui_textinput_get(ti);
188     char *sep = g_utf8_offset_to_pointer(ti->c_q, ti->pos);
189     ti->c_last = g_strdup(sep);
190     *(sep) = 0;
191     ti->c_cur = -1;
192     ti->c_sug = g_new0(char *, 25);
193     ti->complete(ti->c_q, ti->c_sug);
194   }
195   if(!ti->c_sug[++ti->c_cur])
196     ti->c_cur = -1;
197   char *first = ti->c_cur < 0 ? ti->c_q : ti->c_sug[ti->c_cur];
198   char *str = g_strconcat(first, ti->c_last, NULL);
199   ui_textinput_set(ti, str);
200   ti->pos = g_utf8_strlen(first, -1);
201   g_free(str);
202   if(!g_strv_length(ti->c_sug))
203     ui_beep = TRUE;
204   // If there is only one suggestion: finalize this auto-completion and reset
205   // state. This may be slightly counter-intuitive, but makes auto-completing
206   // paths a lot less annoying.
207   if(g_strv_length(ti->c_sug) <= 1)
208     ui_textinput_complete_reset(ti);
209 }
210 
211 
ui_textinput_free(ui_textinput_t * ti)212 void ui_textinput_free(ui_textinput_t *ti) {
213   ui_textinput_complete_reset(ti);
214   g_string_free(ti->str, TRUE);
215   if(ti->s_q)
216     g_free(ti->s_q);
217   g_free(ti);
218 }
219 
220 
ui_textinput_set(ui_textinput_t * ti,const char * str)221 void ui_textinput_set(ui_textinput_t *ti, const char *str) {
222   g_string_assign(ti->str, str);
223   ti->pos = g_utf8_strlen(ti->str->str, -1);
224 }
225 
226 
ui_textinput_get(ui_textinput_t * ti)227 char *ui_textinput_get(ui_textinput_t *ti) {
228   return g_strdup(ti->str->str);
229 }
230 
231 
232 
ui_textinput_reset(ui_textinput_t * ti)233 char *ui_textinput_reset(ui_textinput_t *ti) {
234   char *str = ui_textinput_get(ti);
235   ui_textinput_set(ti, "");
236   if(ti->usehist) {
237     // as a special case, don't allow /password to be logged. /hset password is
238     // okay, since it will be stored anyway.
239     if(!strstr(str, "/password "))
240       ui_cmdhist_add(str);
241     if(ti->s_q)
242       g_free(ti->s_q);
243     ti->s_q = NULL;
244     ti->s_pos = -1;
245   }
246   return str;
247 }
248 
249 
250 // must be drawn last, to keep the cursor position correct
251 // also not the most efficient function ever, but probably fast enough.
ui_textinput_draw(ui_textinput_t * ti,int y,int x,int col,ui_cursor_t * cur)252 void ui_textinput_draw(ui_textinput_t *ti, int y, int x, int col, ui_cursor_t *cur) {
253   //       |              |
254   // "Some random string etc etc"
255   //       f         #    l
256   // f = function(#, strwidth(upto_#), wincols)
257   // if(strwidth(upto_#) < wincols*0.85)
258   //   f = 0
259   // else
260   //   f = strwidth(upto_#) - wincols*0.85
261   int i;
262 
263   // calculate f (in number of columns)
264   int width = 0;
265   char *str = ti->str->str;
266   for(i=0; i<=ti->pos && *str; i++) {
267     width += gunichar_width(g_utf8_get_char(str));
268     str = g_utf8_next_char(str);
269   }
270   int f = width - (col*85)/100;
271   if(f < 0)
272     f = 0;
273 
274   // now print it on the screen, starting from column f in the string and
275   // stopping when we're out of screen columns
276   mvhline(y, x, ' ', col);
277   move(y, x);
278   int pos = 0;
279   str = ti->str->str;
280   i = 0;
281   while(*str) {
282     char *ostr = str;
283     str = g_utf8_next_char(str);
284     int l = gunichar_width(g_utf8_get_char(ostr));
285     f -= l;
286     if(f <= -col)
287       break;
288     if(f < 0) {
289       // Don't display control characters
290       if((unsigned char)*ostr >= 32)
291         addnstr(ostr, str-ostr);
292       if(i < ti->pos)
293         pos += l;
294     }
295     i++;
296   }
297   x += pos;
298   move(y, x);
299   curs_set(1);
300   if(cur) {
301     cur->x = x;
302     cur->y = y;
303   }
304 }
305 
306 
ui_textinput_search(ui_textinput_t * ti,gboolean backwards)307 static void ui_textinput_search(ui_textinput_t *ti, gboolean backwards) {
308   int start;
309   if(ti->s_pos < 0) {
310     if(!backwards) {
311       ui_beep = TRUE;
312       return;
313     }
314     ti->s_q = ui_textinput_get(ti);
315     start = cmdhist->last;
316   } else
317     start = ti->s_pos+(backwards ? -1 : 1);
318   int pos = ui_cmdhist_search(backwards, ti->s_q, start);
319   if(pos >= 0) {
320     ti->s_pos = pos;
321     ti->s_top = FALSE;
322     ui_textinput_set(ti, cmdhist->buf[pos & CMDHIST_BUF]);
323   } else if(backwards)
324     ui_beep = TRUE;
325   else {
326     ti->s_pos = -1;
327     ui_textinput_set(ti, ti->s_q);
328     g_free(ti->s_q);
329     ti->s_q = NULL;
330   }
331 }
332 
333 
334 #define iswordchar(x) (!(\
335       ((x) >= ' ' && (x) <= '/') ||\
336       ((x) >= ':' && (x) <= '@') ||\
337       ((x) >= '[' && (x) <= '`') ||\
338       ((x) >= '{' && (x) <= '~')\
339     ))
340 
341 
ui_textinput_key(ui_textinput_t * ti,guint64 key,char ** str)342 gboolean ui_textinput_key(ui_textinput_t *ti, guint64 key, char **str) {
343   int chars = g_utf8_strlen(ti->str->str, -1);
344   gboolean completereset = TRUE;
345   switch(key) {
346   case INPT_KEY(KEY_LEFT): // left  - cursor one character left
347     if(ti->pos > 0) ti->pos--;
348     break;
349   case INPT_KEY(KEY_RIGHT):// right - cursor one character right
350     if(ti->pos < chars) ti->pos++;
351     break;
352   case INPT_KEY(KEY_END):  // end
353   case INPT_CTRL('e'):     // C-e   - cursor to end
354     ti->pos = chars;
355     break;
356   case INPT_KEY(KEY_HOME): // home
357   case INPT_CTRL('a'):     // C-a   - cursor to begin
358     ti->pos = 0;
359     break;
360   case INPT_ALT('b'):      // Alt+b - cursor one word backward
361     if(ti->pos > 0) {
362       char *pos = g_utf8_offset_to_pointer(ti->str->str, ti->pos-1);
363       while(pos > ti->str->str && !iswordchar(*pos))
364         pos--;
365       while(pos > ti->str->str && iswordchar(*(pos-1)))
366         pos--;
367       ti->pos = g_utf8_strlen(ti->str->str, pos-ti->str->str);
368     }
369     break;
370   case INPT_ALT('f'):      // Alt+f - cursor one word forward
371     if(ti->pos < chars) {
372       char *pos = g_utf8_offset_to_pointer(ti->str->str, ti->pos);
373       while(!iswordchar(*pos))
374         pos++;
375       while(*pos && iswordchar(*pos))
376         pos++;
377       ti->pos = g_utf8_strlen(ti->str->str, pos-ti->str->str);
378     }
379     break;
380   case INPT_KEY(KEY_BACKSPACE): // backspace - delete character before cursor
381     if(ti->pos > 0) {
382       char *pos = g_utf8_offset_to_pointer(ti->str->str, ti->pos-1);
383       g_string_erase(ti->str, pos-ti->str->str, g_utf8_next_char(pos)-pos);
384       ti->pos--;
385     }
386     break;
387   case INPT_KEY(KEY_DC):   // del   - delete character under cursor
388     if(ti->pos < chars) {
389       char *pos = g_utf8_offset_to_pointer(ti->str->str, ti->pos);
390       g_string_erase(ti->str, pos-ti->str->str, g_utf8_next_char(pos)-pos);
391     }
392     break;
393   case INPT_CTRL('w'):     // C-w   - delete to previous space
394   case INPT_ALT(127):      // Alt+backspace
395     if(ti->pos > 0) {
396       char *end = g_utf8_offset_to_pointer(ti->str->str, ti->pos-1);
397       char *begin = end;
398       while(begin > ti->str->str && !iswordchar(*begin))
399         begin--;
400       while(begin > ti->str->str && iswordchar(*(begin-1)))
401         begin--;
402       ti->pos -= g_utf8_strlen(begin, g_utf8_next_char(end)-begin);
403       g_string_erase(ti->str, begin-ti->str->str, g_utf8_next_char(end)-begin);
404     }
405     break;
406   case INPT_ALT('d'):      // Alt+d - delete to next space
407     if(ti->pos < chars) {
408       char *begin = g_utf8_offset_to_pointer(ti->str->str, ti->pos);
409       char *end = begin;
410       while(*end == ' ')
411         end++;
412       while(*end && *(end+1) && *(end+1) != ' ')
413         end++;
414       g_string_erase(ti->str, begin-ti->str->str, g_utf8_next_char(end)-begin);
415     }
416     break;
417   case INPT_CTRL('k'):     // C-k   - delete everything after cursor
418     if(ti->pos < chars)
419       g_string_erase(ti->str, g_utf8_offset_to_pointer(ti->str->str, ti->pos)-ti->str->str, -1);
420     break;
421   case INPT_CTRL('u'):     // C-u   - delete entire line
422     g_string_erase(ti->str, 0, -1);
423     ti->pos = 0;
424     break;
425   case INPT_KEY(KEY_UP):   // up    - history search back
426   case INPT_KEY(KEY_DOWN): // down  - history search forward
427     if(ti->usehist)
428       ui_textinput_search(ti, key == INPT_KEY(KEY_UP));
429     else
430       return FALSE;
431     break;
432   case INPT_CTRL('i'):     // tab   - autocomplete
433     if(ti->bracketed_paste) {
434       g_string_insert_unichar(ti->str, g_utf8_offset_to_pointer(ti->str->str, ti->pos)-ti->str->str, ' ');
435       ti->pos++;
436       return FALSE;
437     } else {
438       ui_textinput_complete(ti);
439       completereset = FALSE;
440     }
441     break;
442   case INPT_CTRL('j'):     // newline - accept & clear
443     if(ti->bracketed_paste) {
444       g_string_insert_unichar(ti->str, g_utf8_offset_to_pointer(ti->str->str, ti->pos)-ti->str->str, '\n');
445       ti->pos++;
446       return FALSE;
447     }
448 
449     // if not responded to, input simply keeps buffering; avoids modality
450     // reappearing after each (non-bracketed) newline avoids user confusion
451     // UTF-8: <32 always 1 byte from trusted input
452     {
453       int num_lines = 1;
454       char *c;
455       for(c=ti->str->str; *c; c++)
456         num_lines += *c == '\n';
457       if(num_lines > 1) {
458         ui_mf(NULL, UIM_NOLOG, "Press Ctrl-y to accept %d-line paste", num_lines);
459         break;
460       }
461     }
462 
463     *str = ui_textinput_reset(ti);
464     break;
465   case INPT_CTRL('y'):     // C-y   - accept bracketed paste
466     *str = ui_textinput_reset(ti);
467     break;
468   case KEY_BRACKETED_PASTE_START:
469     ti->bracketed_paste = TRUE;
470     break;
471   case KEY_BRACKETED_PASTE_END:
472     ti->bracketed_paste = FALSE;
473     break;
474   default:
475     if(INPT_TYPE(key) == 1) { // char
476       g_string_insert_unichar(ti->str, g_utf8_offset_to_pointer(ti->str->str, ti->pos)-ti->str->str, INPT_CODE(key));
477       ti->pos++;
478     } else
479       return FALSE;
480   }
481   if(completereset)
482     ui_textinput_complete_reset(ti);
483   return TRUE;
484 }
485