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