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