1 #include "internal.h"
2 
3 static void
ncreader_destroy_internal(ncreader * n)4 ncreader_destroy_internal(ncreader* n){
5   if(n){
6     if(n->manage_cursor){
7       notcurses_cursor_disable(ncplane_notcurses(n->ncp));
8     }
9     if(ncplane_set_widget(n->ncp, NULL, NULL) == 0){
10       ncplane_destroy(n->ncp);
11     }
12     ncplane_destroy(n->textarea);
13     free(n);
14   }
15 }
16 
ncreader_destroy(ncreader * n,char ** contents)17 void ncreader_destroy(ncreader* n, char** contents){
18   if(n){
19     if(contents){
20       *contents = ncreader_contents(n);
21     }
22     ncreader_destroy_internal(n);
23   }
24 }
25 
ncreader_create(ncplane * n,const ncreader_options * opts)26 ncreader* ncreader_create(ncplane* n, const ncreader_options* opts){
27   ncreader_options zeroed = {};
28   if(!opts){
29     opts = &zeroed;
30   }
31   if(opts->flags > NCREADER_OPTION_CURSOR){
32     logwarn("Provided unsupported flags %016" PRIx64 "\n", opts->flags);
33   }
34   ncreader* nr = malloc(sizeof(*nr));
35   if(nr == NULL){
36     ncplane_destroy(n);
37     return NULL;
38   }
39   nr->ncp = n;
40   // do *not* bind it to the visible plane; we always want it offscreen,
41   // to the upper left of the true origin
42   struct ncplane_options nopts = {
43     .y = -ncplane_dim_y(n),
44     .x = -ncplane_dim_x(n),
45     .rows = ncplane_dim_y(n),
46     .cols = ncplane_dim_x(n),
47     .name = "text",
48   };
49   if((nr->textarea = ncplane_create(notcurses_stdplane(ncplane_notcurses(n)), &nopts)) == NULL){
50     ncplane_destroy(nr->ncp);
51     free(nr);
52     return NULL;
53   }
54 
55   nr->horscroll = opts->flags & NCREADER_OPTION_HORSCROLL;
56   nr->xproject = 0;
57   nr->tchannels = opts->tchannels;
58   nr->tattrs = opts->tattrword;
59   nr->no_cmd_keys = opts->flags & NCREADER_OPTION_NOCMDKEYS;
60   nr->manage_cursor = opts->flags & NCREADER_OPTION_CURSOR;
61   ncplane_set_channels(nr->ncp, opts->tchannels);
62   ncplane_set_styles(nr->ncp, opts->tattrword);
63   if(ncplane_set_widget(n, nr, (void(*)(void*))ncreader_destroy_internal)){
64     ncplane_destroy(nr->textarea);
65     ncplane_destroy(nr->ncp);
66     free(nr);
67     return NULL;
68   }
69   return nr;
70 }
71 
72 // empty both planes of all input, and home the cursors.
ncreader_clear(ncreader * n)73 int ncreader_clear(ncreader* n){
74   ncplane_erase(n->ncp);
75   ncplane_erase(n->textarea);
76   n->xproject = 0;
77   return 0;
78 }
79 
ncreader_plane(ncreader * n)80 ncplane* ncreader_plane(ncreader* n){
81   return n->ncp;
82 }
83 
84 // copy the viewed area down from the textarea
85 static int
ncreader_redraw(ncreader * n)86 ncreader_redraw(ncreader* n){
87   int ret = 0;
88 //fprintf(stderr, "redraw: xproj %d\n", n->xproject);
89 //notcurses_debug(n->ncp->nc, stderr);
90   assert(n->xproject >= 0);
91   assert(n->textarea->lenx >= n->ncp->lenx);
92   assert(n->textarea->leny >= n->ncp->leny);
93   for(unsigned y = 0 ; y < n->ncp->leny ; ++y){
94     const unsigned texty = y;
95     for(unsigned x = 0 ; x < n->ncp->lenx ; ++x){
96       const unsigned textx = x + n->xproject;
97       const nccell* src = &n->textarea->fb[nfbcellidx(n->textarea, texty, textx)];
98       nccell* dst = &n->ncp->fb[nfbcellidx(n->ncp, y, x)];
99 //fprintf(stderr, "projecting %d/%d [%s] to %d/%d [%s]\n", texty, textx, cell_extended_gcluster(n->textarea, src), y, x, cell_extended_gcluster(n->ncp, dst));
100       if(cellcmp_and_dupfar(&n->ncp->pool, dst, n->textarea, src) < 0){
101         ret = -1;
102       }
103     }
104   }
105   if(notcurses_cursor_enable(ncplane_notcurses(n->ncp), n->ncp->absy + n->ncp->y, n->ncp->absx + n->ncp->x)){
106     ret = -1;
107   }
108   return ret;
109 }
110 
111 // try to move left. does not move past the start of the textarea, but will
112 // try to move up and to the end of the previous row if not on the top row.
113 // if on the left side of the viewarea, but not the left side of the textarea,
114 // scrolls left. returns 0 if a move was made.
ncreader_move_left(ncreader * n)115 int ncreader_move_left(ncreader* n){
116   int viewx = n->ncp->x;
117   int textx = n->textarea->x;
118   int y = n->ncp->y;
119 //fprintf(stderr, "moving left: tcurs: %dx%d vcurs: %dx%d xproj: %d\n", y, textx, y, viewx, n->xproject);
120   if(textx == 0){
121     // are we on the first column of the textarea? if so, we must also be on
122     // the first column of the viewarea. try to move up.
123     if(y == 0){
124       return -1; // no move possible
125     }
126     viewx = n->ncp->lenx - 1; // FIXME find end of particular row
127     --y;
128     textx = n->textarea->lenx - 1;
129     n->xproject = n->textarea->x - n->ncp->x;
130   }else{
131     // if we're on the first column of the viewarea, but not the first column
132     // of the textarea, we must be able to scroll to the left. do so.
133     // if we're not on the last column anywhere, move cursor right everywhere.
134     if(viewx == 0){
135       --n->xproject;
136     }else{
137       --viewx;
138     }
139     --textx;
140   }
141   ncplane_cursor_move_yx(n->textarea, y, textx);
142   ncplane_cursor_move_yx(n->ncp, y, viewx);
143 //fprintf(stderr, "moved left: tcurs: %dx%d vcurs: %dx%d xproj: %d\n", y, textx, y, viewx, n->xproject);
144   ncreader_redraw(n);
145   return 0;
146 }
147 
148 // try to move right. does not move past the end of the textarea, but will
149 // try to move down and to the start of the previous row if not on the bottom
150 // row. if on the right side of the viewarea, but not the right side of the
151 // textarea, pans right. returns 0 if a move was made.
ncreader_move_right(ncreader * n)152 int ncreader_move_right(ncreader* n){
153   unsigned textx = n->textarea->x;
154   unsigned y = n->ncp->y;
155   unsigned viewx = n->ncp->x;
156 //fprintf(stderr, "moving right: tcurs: %dx%d vcurs: %dx%d xproj: %d\n", y, textx, y, viewx, n->xproject);
157   if(textx >= n->textarea->lenx - 1){
158     // are we on the last column of the textarea? if so, we must also be on
159     // the first column of the viewarea. try to move down.
160     if(y >= n->textarea->leny - 1){
161       return -1; // no move possible
162     }
163     viewx = 0;
164     ++y;
165     textx = viewx;
166     n->xproject = 0;
167   }else{
168     // if we're on the first column of the viewarea, but not the first column
169     // of the textarea, we must be able to scroll to the left. do so.
170     // if we're not on the last column anywhere, move cursor right everywhere.
171     if(viewx >= n->ncp->lenx - 1){
172       ++n->xproject;
173     }else{
174       ++viewx;
175     }
176     ++textx;
177   }
178   ncplane_cursor_move_yx(n->textarea, y, textx);
179   ncplane_cursor_move_yx(n->ncp, y, viewx);
180 //fprintf(stderr, "moved right: tcurs: %dx%d vcurs: %dx%d xproj: %d\n", y, textx, y, viewx, n->xproject);
181   ncreader_redraw(n);
182   return 0;
183 }
184 
185 // try to move up. does not move past the top of the textarea.
186 // returns 0 if a move was made.
ncreader_move_up(ncreader * n)187 int ncreader_move_up(ncreader* n){
188   int y = n->ncp->y;
189   if(y == 0){
190     // are we on the last row of the textarea? if so, we can't move.
191     return -1;
192   }
193   --y;
194   ncplane_cursor_move_yx(n->textarea, y, -1);
195   ncplane_cursor_move_yx(n->ncp, y, -1);
196   ncreader_redraw(n);
197   return 0;
198 }
199 
200 // try to move down. does not move past the bottom of the textarea.
201 // returns 0 if a move was made.
ncreader_move_down(ncreader * n)202 int ncreader_move_down(ncreader* n){
203   unsigned y = n->ncp->y;
204   if(y >= n->textarea->leny - 1){
205     // are we on the last row of the textarea? if so, we can't move.
206     return -1;
207   }
208   ++y;
209   ncplane_cursor_move_yx(n->textarea, y, -1);
210   ncplane_cursor_move_yx(n->ncp, y, -1);
211   ncreader_redraw(n);
212   return 0;
213 }
214 
215 // only writing can enlarge the textarea. movement can pan, but not enlarge.
ncreader_write_egc(ncreader * n,const char * egc)216 int ncreader_write_egc(ncreader* n, const char* egc){
217   const int cols = ncstrwidth(egc, NULL, NULL);
218   if(cols < 0){
219     logerror("Fed illegal UTF-8 [%s]\n", egc);
220     return -1;
221   }
222   if(n->textarea->x >= n->textarea->lenx - cols){
223     if(n->horscroll){
224       if(ncplane_resize_simple(n->textarea, n->textarea->leny, n->textarea->lenx + cols)){
225         return -1;
226       }
227       ++n->xproject;
228     }
229   }else if(n->ncp->x >= n->ncp->lenx){
230     ++n->xproject;
231   }
232   // use ncplane_putegc on both planes because it'll get cursor movement right
233   if(ncplane_putegc(n->textarea, egc, NULL) < 0){
234     return -1;
235   }
236   if(ncplane_putegc(n->ncp, egc, NULL) < 0){
237     return -1;
238   }
239   if(n->textarea->x >= n->textarea->lenx - cols){
240     if(!n->horscroll){
241       n->textarea->x = n->textarea->lenx - cols;
242     }
243   }
244   if(n->ncp->x >= n->ncp->lenx - cols){
245     n->ncp->x = n->ncp->lenx - cols;
246   }
247   ncreader_redraw(n);
248   return 0;
249 }
250 
251 static bool
do_backspace(ncreader * n)252 do_backspace(ncreader* n){
253   int x = n->textarea->x;
254   int y = n->textarea->y;
255   if(n->textarea->x == 0){
256     if(n->textarea->y){
257       y = n->textarea->y - 1;
258       x = n->textarea->lenx - 1;
259     }
260   }else{
261     --x;
262   }
263   ncplane_putegc_yx(n->textarea, y, x, "", NULL);
264   ncplane_cursor_move_yx(n->textarea, y, x);
265   ncplane_cursor_move_yx(n->ncp, n->ncp->y, n->ncp->x - 1);
266   ncreader_redraw(n);
267   return true;
268 }
269 
270 static bool
is_egc_wordbreak(ncplane * textarea)271 is_egc_wordbreak(ncplane* textarea){
272   char* egc = ncplane_at_yx(textarea, textarea->y, textarea->x, NULL, NULL);
273   if(egc == NULL){
274     return true;
275   }
276   wchar_t w;
277   mbstate_t mbstate;
278   memset(&mbstate, 0, sizeof(mbstate));
279   size_t s = mbrtowc(&w, egc, MB_CUR_MAX, &mbstate);
280   free(egc);
281   if(s == (size_t)-1 || s == (size_t)-2){
282     return true;
283   }
284   if(iswordbreak(w)){
285     return true;
286   }
287   return false;
288 }
289 
290 static bool
ncreader_ctrl_input(ncreader * n,const ncinput * ni)291 ncreader_ctrl_input(ncreader* n, const ncinput* ni){
292   switch(ni->id){
293     case 'B':
294       ncreader_move_left(n);
295       break;
296     case 'F':
297       ncreader_move_right(n);
298       break;
299     case 'A': // cursor to beginning of line
300       while(n->textarea->x){
301         if(ncreader_move_left(n)){
302           break;
303         }
304       }
305       break;
306     case 'E': // cursor to end of line
307       while(n->textarea->x < ncplane_dim_x(n->textarea) - 1){
308         if(ncreader_move_right(n)){
309           break;
310         }
311       }
312       break;
313     case 'U': // clear line before cursor
314       while(n->textarea->x){
315         do_backspace(n);
316       }
317       break;
318     case 'W': // clear word before cursor
319       while(n->textarea->x){
320         if(ncreader_move_left(n)){
321           break;
322         }
323         if(is_egc_wordbreak(n->textarea)){
324           break;
325         }
326         if(ncreader_move_right(n)){
327           break;
328         }
329         do_backspace(n);
330       }
331       break;
332     default:
333       return false; // pass on all other ctrls
334   }
335   return true;
336 }
337 
338 static bool
ncreader_alt_input(ncreader * n,const ncinput * ni)339 ncreader_alt_input(ncreader* n, const ncinput* ni){
340   switch(ni->id){
341     case 'b': // back one word (to first cell), but not to previous line
342       while(n->textarea->x){
343         if(ncreader_move_left(n)){
344           break;
345         }
346         if(is_egc_wordbreak(n->textarea)){
347           break;
348         }
349       }
350       break;
351     case 'f': // forward one word (past end cell)
352       while(n->textarea->x < ncplane_dim_x(n->textarea) - 1){
353         if(ncreader_move_right(n)){
354           break;
355         }
356         if(is_egc_wordbreak(n->textarea)){
357           break;
358         }
359       }
360       break;
361     default:
362       return false;
363   }
364   return true;
365 }
366 
367 // we pass along:
368 //  * anything with Alt
369 //  * anything with Ctrl, except 'U' (which clears all input)
370 //  * anything synthesized, save arrow keys and backspace
ncreader_offer_input(ncreader * n,const ncinput * ni)371 bool ncreader_offer_input(ncreader* n, const ncinput* ni){
372   if(ni->evtype == NCTYPE_RELEASE){
373     return false;
374   }
375   if(ni->ctrl && !n->no_cmd_keys){
376     return ncreader_ctrl_input(n, ni);
377   }else if(ni->alt && !n->no_cmd_keys){
378     return ncreader_alt_input(n, ni);
379   }
380   if(ni->alt || ni->ctrl){ // pass on all alts/ctrls if no_cmd_keys is set
381     return false;
382   }
383   if(ni->id == NCKEY_BACKSPACE){
384     return do_backspace(n);
385   }
386   // FIXME deal with multicolumn EGCs -- probably extract these and make them
387   // general ncplane_cursor_{left, right, up, down}()
388   if(ni->id == NCKEY_LEFT){
389     ncreader_move_left(n);
390     return true;
391   }else if(ni->id == NCKEY_RIGHT){
392     ncreader_move_right(n);
393     return true;
394   }else if(ni->id == NCKEY_UP){
395     ncreader_move_up(n);
396     return true;
397   }else if(ni->id == NCKEY_DOWN){
398     ncreader_move_down(n);
399     return true;
400   }else if(nckey_synthesized_p(ni->id)){
401     return false;
402   }
403   // FIXME need to collect full EGCs
404   char wbuf[WCHAR_MAX_UTF8BYTES + 1];
405   // FIXME breaks for wint_t < 32bits
406   if(snprintf(wbuf, sizeof(wbuf), "%lc", (wint_t)ni->id) < (int)sizeof(wbuf)){
407     ncreader_write_egc(n, wbuf);
408   }
409   return true;
410 }
411 
ncreader_contents(const ncreader * n)412 char* ncreader_contents(const ncreader* n){
413   return ncplane_contents(n->ncp, 0, 0, 0, 0);
414 }
415