1 #include "internal.h"
2 
3 // ncmenu_item and ncmenu_section have internal and (minimal) external forms
4 typedef struct ncmenu_int_item {
5   char* desc;           // utf-8 menu item, NULL for horizontal separator
6   ncinput shortcut;     // shortcut, all should be distinct
7   int shortcut_offset;  // column offset with desc of shortcut EGC
8   char* shortdesc;      // description of shortcut, can be NULL
9   int shortdesccols;    // columns occupied by shortcut description
10   bool disabled;        // disabled?
11 } ncmenu_int_item;
12 
13 typedef struct ncmenu_int_section {
14   char* name;             // utf-8 c string
15   unsigned itemcount;
16   ncmenu_int_item* items; // items, NULL iff itemcount == 0
17   ncinput shortcut;       // shortcut, will be underlined if present in name
18   int xoff;               // column offset from beginning of menu bar
19   int bodycols;           // column width of longest item
20   int itemselected;       // current item selected, -1 for no selection
21   int shortcut_offset;    // column offset within name of shortcut EGC
22   int enabled_item_count; // number of enabled items: section is disabled iff 0
23 } ncmenu_int_section;
24 
25 typedef struct ncmenu {
26   ncplane* ncp;
27   int sectioncount;         // must be positive
28   ncmenu_int_section* sections; // NULL iff sectioncount == 0
29   int unrolledsection;      // currently unrolled section, -1 if none
30   int headerwidth;          // minimum space necessary to display all sections
31   uint64_t headerchannels;  // styling for header
32   uint64_t dissectchannels; // styling for disabled section headers
33   uint64_t sectionchannels; // styling for sections
34   uint64_t disablechannels; // styling for disabled entries
35   bool bottom;              // are we on the bottom (vs top)?
36 } ncmenu;
37 
38 // Search the provided multibyte (UTF8) string 's' for the provided unicode
39 // codepoint 'cp'. If found, return the column offset of the EGC in which the
40 // codepoint appears in 'col', and the byte offset as the return value. If not
41 // found, -1 is returned, and 'col' is meaningless.
42 static int
mbstr_find_codepoint(const char * s,uint32_t cp,int * col)43 mbstr_find_codepoint(const char* s, uint32_t cp, int* col){
44   mbstate_t ps;
45   memset(&ps, 0, sizeof(ps));
46   size_t bytes = 0;
47   size_t r;
48   wchar_t w;
49   *col = 0;
50   while((r = mbrtowc(&w, s + bytes, MB_CUR_MAX, &ps)) != (size_t)-1 && r != (size_t)-2){
51     if(r == 0){
52       break;
53     }
54     if(towlower(cp) == towlower(w)){
55       return bytes;
56     }
57     *col += wcwidth(w);
58     bytes += r;
59   }
60   return -1;
61 }
62 
63 static void
free_menu_section(ncmenu_int_section * ms)64 free_menu_section(ncmenu_int_section* ms){
65   for(unsigned i = 0 ; i < ms->itemcount ; ++i){
66     free(ms->items[i].desc);
67     free(ms->items[i].shortdesc);
68   }
69   free(ms->items);
70   free(ms->name);
71 }
72 
73 static void
free_menu_sections(ncmenu * ncm)74 free_menu_sections(ncmenu* ncm){
75   for(int i = 0 ; i < ncm->sectioncount ; ++i){
76     free_menu_section(&ncm->sections[i]);
77   }
78   free(ncm->sections);
79 }
80 
81 static int
dup_menu_item(ncmenu_int_item * dst,const struct ncmenu_item * src)82 dup_menu_item(ncmenu_int_item* dst, const struct ncmenu_item* src){
83 #define ALTMOD "Alt+"
84 #define CTLMOD "Ctrl+"
85   dst->disabled = false;
86   if((dst->desc = strdup(src->desc)) == NULL){
87     return -1;
88   }
89   if(!src->shortcut.id){
90     dst->shortdesccols = 0;
91     dst->shortdesc = NULL;
92     return 0;
93   }
94   size_t bytes = 1; // NUL terminator
95   if(src->shortcut.alt){
96     bytes += strlen(ALTMOD);
97   }
98   if(src->shortcut.ctrl){
99     bytes += strlen(CTLMOD);
100   }
101   mbstate_t ps;
102   memset(&ps, 0, sizeof(ps));
103   size_t shortsize = wcrtomb(NULL, src->shortcut.id, &ps);
104   if(shortsize == (size_t)-1){
105     free(dst->desc);
106     return -1;
107   }
108   bytes += shortsize + 1;
109   char* sdup = malloc(bytes);
110   int n = snprintf(sdup, bytes, "%s%s", src->shortcut.alt ? ALTMOD : "",
111                    src->shortcut.ctrl ? CTLMOD : "");
112   if(n < 0 || (size_t)n >= bytes){
113     free(sdup);
114     free(dst->desc);
115     return -1;
116   }
117   memset(&ps, 0, sizeof(ps));
118   size_t mbbytes = wcrtomb(sdup + n, src->shortcut.id, &ps);
119   if(mbbytes == (size_t)-1){ // shouldn't happen
120     free(sdup);
121     free(dst->desc);
122     return -1;
123   }
124   sdup[n + mbbytes] = '\0';
125   dst->shortdesc = sdup;
126   dst->shortdesccols = ncstrwidth(dst->shortdesc, NULL, NULL);
127   return 0;
128 #undef CTLMOD
129 #undef ALTMOD
130 }
131 
132 static int
dup_menu_section(ncmenu_int_section * dst,const struct ncmenu_section * src)133 dup_menu_section(ncmenu_int_section* dst, const struct ncmenu_section* src){
134   // we must reject any empty section
135   if(src->itemcount == 0 || src->items == NULL){
136     return -1;
137   }
138   dst->bodycols = 0;
139   dst->itemselected = 0;
140   dst->items = NULL;
141   // we must reject any section which is entirely separators
142   bool gotitem = false;
143   dst->itemcount = 0;
144   dst->enabled_item_count = 0;
145   dst->items = malloc(sizeof(*dst->items) * src->itemcount);
146   if(dst->items == NULL){
147     return -1;
148   }
149   for(int i = 0 ; i < src->itemcount ; ++i){
150     if(src->items[i].desc){
151       if(dup_menu_item(&dst->items[i], &src->items[i])){
152         while(i--){
153           free(dst->items[i].desc);
154         }
155         free(dst->items);
156         return -1;
157       }
158       gotitem = true;
159       int cols = ncstrwidth(dst->items[i].desc, NULL, NULL);
160       if(dst->items[i].shortdesc){
161         cols += 2 + dst->items[i].shortdesccols; // two spaces minimum
162       }
163       if(cols > dst->bodycols){
164         dst->bodycols = cols;
165       }
166       memcpy(&dst->items[i].shortcut, &src->items[i].shortcut, sizeof(dst->items[i].shortcut));
167       if(mbstr_find_codepoint(dst->items[i].desc,
168                               dst->items[i].shortcut.id,
169                               &dst->items[i].shortcut_offset) < 0){
170         dst->items[i].shortcut_offset = -1;
171       }
172     }else{
173       dst->items[i].desc = NULL;
174       dst->items[i].shortdesc = NULL;
175     }
176     ++dst->itemcount;
177   }
178   dst->enabled_item_count = dst->itemcount;
179   if(!gotitem){
180     while(dst->itemcount){
181       free(dst->items[--dst->itemcount].desc);
182     }
183     free(dst->items);
184     return -1;
185   }
186   return 0;
187 }
188 
189 // Duplicates all menu sections in opts, adding their length to '*totalwidth'.
190 static int
dup_menu_sections(ncmenu * ncm,const ncmenu_options * opts,unsigned * totalwidth,unsigned * totalheight)191 dup_menu_sections(ncmenu* ncm, const ncmenu_options* opts, unsigned* totalwidth, unsigned* totalheight){
192   if(opts->sectioncount == 0){
193     return -1;
194   }
195   ncm->sections = malloc(sizeof(*ncm->sections) * opts->sectioncount);
196   if(ncm->sections == NULL){
197     return -1;
198   }
199   bool rightaligned = false; // can only right-align once. twice is error.
200   unsigned maxheight = 0;
201   unsigned maxwidth = *totalwidth;
202   unsigned xoff = 2;
203   int i;
204   for(i = 0 ; i < opts->sectioncount ; ++i){
205     if(opts->sections[i].name){
206       int cols = ncstrwidth(opts->sections[i].name, NULL, NULL);
207       if(rightaligned){ // FIXME handle more than one right-aligned section
208         ncm->sections[i].xoff = -(cols + 2);
209       }else{
210         ncm->sections[i].xoff = xoff;
211       }
212       if(cols < 0 || (ncm->sections[i].name = strdup(opts->sections[i].name)) == NULL){
213         goto err;
214       }
215       if(dup_menu_section(&ncm->sections[i], &opts->sections[i])){
216         free(ncm->sections[i].name);
217         goto err;
218       }
219       if(ncm->sections[i].itemcount > maxheight){
220         maxheight = ncm->sections[i].itemcount;
221       }
222       if(*totalwidth + cols + 2 > maxwidth){
223         maxwidth = *totalwidth + cols + 2;
224       }
225       if(*totalwidth + ncm->sections[i].bodycols + 2 > maxwidth){
226         maxwidth = *totalwidth + ncm->sections[i].bodycols + 2;
227       }
228       *totalwidth += cols + 2;
229       memcpy(&ncm->sections[i].shortcut, &opts->sections[i].shortcut, sizeof(ncm->sections[i].shortcut));
230       if(mbstr_find_codepoint(ncm->sections[i].name,
231                               ncm->sections[i].shortcut.id,
232                               &ncm->sections[i].shortcut_offset) < 0){
233         ncm->sections[i].shortcut_offset = -1;
234       }
235       xoff += cols + 2;
236     }else{ // divider; remaining sections are right-aligned
237       if(rightaligned){
238         goto err;
239       }
240       rightaligned = true;
241       ncm->sections[i].name = NULL;
242       ncm->sections[i].items = NULL;
243       ncm->sections[i].itemcount = 0;
244       ncm->sections[i].xoff = -1;
245       ncm->sections[i].bodycols = 0;
246       ncm->sections[i].itemselected = -1;
247       ncm->sections[i].shortcut_offset = -1;
248       ncm->sections[i].enabled_item_count = 0;
249     }
250   }
251   if(ncm->sectioncount == 1 && rightaligned){
252     goto err;
253   }
254   *totalwidth = maxwidth;
255   *totalheight += maxheight + 2; // two rows of border
256   return 0;
257 
258 err:
259   while(i--){
260     free_menu_section(&ncm->sections[i]);
261   }
262   free(ncm->sections);
263   return -1;
264 }
265 
266 // what section header, if any, is living at the provided x coordinate? solves
267 // by replaying the write_header() algorithm. returns -1 if no such section.
268 static int
section_x(const ncmenu * ncm,int x)269 section_x(const ncmenu* ncm, int x){
270   int dimx = ncplane_dim_x(ncm->ncp);
271   for(int i = 0 ; i < ncm->sectioncount ; ++i){
272     if(!ncm->sections[i].name){
273       continue;
274     }
275     if(ncm->sections[i].xoff < 0){ // right-aligned
276       int pos = dimx + ncm->sections[i].xoff;
277       if(x < pos){
278         break;
279       }
280       if(x < pos + ncstrwidth(ncm->sections[i].name, NULL, NULL)){
281         return i;
282       }
283     }else{
284       if(x < ncm->sections[i].xoff){
285         break;
286       }
287       if(x < ncm->sections[i].xoff + ncstrwidth(ncm->sections[i].name, NULL, NULL)){
288         return i;
289       }
290     }
291   }
292   return -1;
293 }
294 
295 static int
write_header(ncmenu * ncm)296 write_header(ncmenu* ncm){
297   ncplane_set_channels(ncm->ncp, ncm->headerchannels);
298   unsigned dimy, dimx;
299   ncplane_dim_yx(ncm->ncp, &dimy, &dimx);
300   unsigned xoff = 0; // 2-column margin on left
301   int ypos = ncm->bottom ? dimy - 1 : 0;
302   if(ncplane_cursor_move_yx(ncm->ncp, ypos, 0)){
303     return -1;
304   }
305   nccell c = NCCELL_INITIALIZER(' ', 0, ncm->headerchannels);
306   ncplane_set_styles(ncm->ncp, 0);
307   if(ncplane_putc(ncm->ncp, &c) < 0){
308     return -1;
309   }
310   if(ncplane_putc(ncm->ncp, &c) < 0){
311     return -1;
312   }
313   for(int i = 0 ; i < ncm->sectioncount ; ++i){
314     if(ncm->sections[i].name){
315       ncplane_cursor_move_yx(ncm->ncp, ypos, xoff);
316       int spaces = ncm->sections[i].xoff - xoff;
317       if(ncm->sections[i].xoff < 0){ // right-aligned
318         spaces = dimx + ncm->sections[i].xoff - xoff;
319         if(spaces < 0){
320           spaces = 0;
321         }
322       }
323       xoff += spaces;
324       while(spaces--){
325         if(ncplane_putc(ncm->ncp, &c) < 0){
326           return -1;
327         }
328       }
329       if(ncm->sections[i].enabled_item_count <= 0){
330         ncplane_set_channels(ncm->ncp, ncm->dissectchannels);
331       }else{
332         ncplane_set_channels(ncm->ncp, ncm->headerchannels);
333       }
334       if(ncplane_putstr_yx(ncm->ncp, ypos, xoff, ncm->sections[i].name) < 0){
335         return -1;
336       }
337       if(ncm->sections[i].shortcut_offset >= 0){
338         nccell cl = NCCELL_TRIVIAL_INITIALIZER;
339         if(ncplane_at_yx_cell(ncm->ncp, ypos, xoff + ncm->sections[i].shortcut_offset, &cl) < 0){
340           return -1;
341         }
342         nccell_on_styles(&cl, NCSTYLE_UNDERLINE|NCSTYLE_BOLD);
343         if(ncplane_putc_yx(ncm->ncp, ypos, xoff + ncm->sections[i].shortcut_offset, &cl) < 0){
344           return -1;
345         }
346         nccell_release(ncm->ncp, &cl);
347       }
348       xoff += ncstrwidth(ncm->sections[i].name, NULL, NULL);
349     }
350   }
351   while(xoff < dimx){
352     if(ncplane_putc_yx(ncm->ncp, ypos, xoff, &c) < 0){
353       return -1;
354     }
355     ++xoff;
356   }
357   return 0;
358 }
359 
360 static int
resize_menu(ncplane * n)361 resize_menu(ncplane* n){
362   const ncplane* parent = ncplane_parent_const(n);
363   int dimx = ncplane_dim_x(parent);
364   int dimy = ncplane_dim_y(n);
365   if(ncplane_resize_simple(n, dimy, dimx)){
366     return -1;
367   }
368   ncmenu* menu = ncplane_userptr(n);
369   int unrolled = menu->unrolledsection;
370   if(unrolled < 0){
371     return write_header(menu);
372   }
373   ncplane_erase(n); // "rolls up" section without resetting unrolledsection
374   return ncmenu_unroll(menu, unrolled);
375 }
376 
ncmenu_create(ncplane * n,const ncmenu_options * opts)377 ncmenu* ncmenu_create(ncplane* n, const ncmenu_options* opts){
378   ncmenu_options zeroed = {};
379   if(!opts){
380     opts = &zeroed;
381   }
382   if(opts->sectioncount <= 0 || !opts->sections){
383     logerror("Invalid %d-ary section information\n", opts->sectioncount);
384     return NULL;
385   }
386   if(opts->flags >= (NCMENU_OPTION_HIDING << 1u)){
387     logwarn("Provided unsupported flags %016" PRIx64 "\n", opts->flags);
388   }
389   unsigned totalheight = 1;
390   unsigned totalwidth = 2; // start with two-character margin on the left
391   ncmenu* ret = malloc(sizeof(*ret));
392   ret->sectioncount = opts->sectioncount;
393   ret->sections = NULL;
394   unsigned dimy, dimx;
395   ncplane_dim_yx(n, &dimy, &dimx);
396   if(ret){
397     ret->bottom = !!(opts->flags & NCMENU_OPTION_BOTTOM);
398     if(dup_menu_sections(ret, opts, &totalwidth, &totalheight) == 0){
399       ret->headerwidth = totalwidth;
400       if(totalwidth < dimx){
401         totalwidth = dimx;
402       }
403       struct ncplane_options nopts = {
404         .y = ret->bottom ? dimy - totalheight : 0,
405         .x = 0,
406         .rows = totalheight,
407         .cols = totalwidth,
408         .userptr = ret,
409         .name = "menu",
410         .resizecb = resize_menu,
411         .flags = NCPLANE_OPTION_FIXED,
412       };
413       ret->ncp = ncplane_create(n, &nopts);
414       if(ret->ncp){
415         if(ncplane_set_widget(ret->ncp, ret, (void(*)(void*))ncmenu_destroy) == 0){
416           ret->unrolledsection = -1;
417           ret->headerchannels = opts->headerchannels;
418           ret->dissectchannels = opts->headerchannels;
419           ncchannels_set_fg_rgb(&ret->dissectchannels, 0xdddddd);
420           ret->sectionchannels = opts->sectionchannels;
421           ret->disablechannels = ret->sectionchannels;
422           ncchannels_set_fg_rgb(&ret->disablechannels, 0xdddddd);
423           nccell c = NCCELL_TRIVIAL_INITIALIZER;
424           nccell_set_fg_alpha(&c, NCALPHA_TRANSPARENT);
425           nccell_set_bg_alpha(&c, NCALPHA_TRANSPARENT);
426           ncplane_set_base_cell(ret->ncp, &c);
427           nccell_release(ret->ncp, &c);
428           if(write_header(ret) == 0){
429             return ret;
430           }
431         }
432         ncplane_destroy(ret->ncp);
433       }
434       free_menu_sections(ret);
435     }
436     free(ret);
437   }
438   logerror("Error creating ncmenu\n");
439   return NULL;
440 }
441 
442 static inline int
section_height(const ncmenu * n,int sectionidx)443 section_height(const ncmenu* n, int sectionidx){
444   return n->sections[sectionidx].itemcount + 2;
445 }
446 
447 static inline int
section_width(const ncmenu * n,int sectionidx)448 section_width(const ncmenu* n, int sectionidx){
449   return n->sections[sectionidx].bodycols + 2;
450 }
451 
ncmenu_unroll(ncmenu * n,int sectionidx)452 int ncmenu_unroll(ncmenu* n, int sectionidx){
453   if(ncmenu_rollup(n)){ // roll up any unrolled section
454     return -1;
455   }
456   if(sectionidx < 0 || sectionidx >= n->sectioncount){
457     logerror("Unrolled invalid sectionidx %d\n", sectionidx);
458     return -1;
459   }
460   if(n->sections[sectionidx].enabled_item_count <= 0){
461     return 0;
462   }
463   if(n->sections[sectionidx].name == NULL){
464     return -1;
465   }
466   n->unrolledsection = sectionidx;
467   unsigned dimy, dimx;
468   ncplane_dim_yx(n->ncp, &dimy, &dimx);
469   const int height = section_height(n, sectionidx);
470   const int width = section_width(n, sectionidx);
471   int xpos = n->sections[sectionidx].xoff < 0 ?
472     (int)dimx + (n->sections[sectionidx].xoff - 2) : n->sections[sectionidx].xoff;
473   if(xpos + width >= (int)dimx){
474     xpos = dimx - (width + 2);
475   }
476   int ypos = n->bottom ? dimy - height - 1 : 1;
477   if(ncplane_cursor_move_yx(n->ncp, ypos, xpos)){
478     return -1;
479   }
480   if(ncplane_rounded_box_sized(n->ncp, 0, n->headerchannels, height, width, 0)){
481     return -1;
482   }
483   const ncmenu_int_section* sec = &n->sections[sectionidx];
484   for(unsigned i = 0 ; i < sec->itemcount ; ++i){
485     ++ypos;
486     if(sec->items[i].desc){
487       // FIXME the user ought be able to configure the disabled channel
488       if(!sec->items[i].disabled){
489         ncplane_set_channels(n->ncp, n->sectionchannels);
490       }else{
491         ncplane_set_channels(n->ncp, n->disablechannels);
492       }
493       if(sec->itemselected >= 0){
494         if(i == (unsigned)sec->itemselected){
495           ncplane_set_channels(n->ncp, ncchannels_reverse(ncplane_channels(n->ncp)));
496         }
497       }
498       ncplane_set_styles(n->ncp, 0);
499       int cols = ncplane_putstr_yx(n->ncp, ypos, xpos + 1, sec->items[i].desc);
500       if(cols < 0){
501         return -1;
502       }
503       // we need pad out the remaining columns of this line with spaces. if
504       // there's a shortcut description, we align it to the right, printing
505       // spaces only through the start of the aligned description.
506       int thiswidth = width;
507       if(sec->items[i].shortdesc){
508         thiswidth -= sec->items[i].shortdesccols;
509       }
510       // print any necessary padding spaces
511       for(int j = cols + 1 ; j < thiswidth - 1 ; ++j){
512         if(ncplane_putchar(n->ncp, ' ') < 0){
513           return -1;
514         }
515       }
516       if(sec->items[i].shortdesc){
517         if(ncplane_putstr(n->ncp, sec->items[i].shortdesc) < 0){
518           return -1;
519         }
520       }
521       if(sec->items[i].shortcut_offset >= 0){
522         nccell cl = NCCELL_TRIVIAL_INITIALIZER;
523         if(ncplane_at_yx_cell(n->ncp, ypos, xpos + 1 + sec->items[i].shortcut_offset, &cl) < 0){
524           return -1;
525         }
526         nccell_on_styles(&cl, NCSTYLE_UNDERLINE|NCSTYLE_BOLD);
527         if(ncplane_putc_yx(n->ncp, ypos, xpos + 1 + sec->items[i].shortcut_offset, &cl) < 0){
528           return -1;
529         }
530         nccell_release(n->ncp, &cl);
531       }
532     }else{
533       n->ncp->channels = n->headerchannels;
534       ncplane_set_styles(n->ncp, 0);
535       if(ncplane_putegc_yx(n->ncp, ypos, xpos, "├", NULL) < 0){
536         return -1;
537       }
538       for(int j = 1 ; j < width - 1 ; ++j){
539         if(ncplane_putegc(n->ncp, "─", NULL) < 0){
540           return -1;
541         }
542       }
543       if(ncplane_putegc(n->ncp, "┤", NULL) < 0){
544         return -1;
545       }
546     }
547   }
548   return 0;
549 }
550 
ncmenu_rollup(ncmenu * n)551 int ncmenu_rollup(ncmenu* n){
552   if(n->unrolledsection < 0){
553     return 0;
554   }
555   n->unrolledsection = -1;
556   ncplane_erase(n->ncp);
557   return write_header(n);
558 }
559 
ncmenu_nextsection(ncmenu * n)560 int ncmenu_nextsection(ncmenu* n){
561   int nextsection = n->unrolledsection;
562   // FIXME probably best to detect cycles
563   do{
564     if(++nextsection == n->sectioncount){
565       nextsection = 0;
566     }
567   }while(n->sections[nextsection].name == NULL ||
568          n->sections[nextsection].enabled_item_count == 0);
569   return ncmenu_unroll(n, nextsection);
570 }
571 
ncmenu_prevsection(ncmenu * n)572 int ncmenu_prevsection(ncmenu* n){
573   int prevsection = n->unrolledsection;
574   // FIXME probably best to detect cycles
575   do{
576     if(--prevsection < 0){
577       prevsection = n->sectioncount - 1;
578     }
579   }while(n->sections[prevsection].name == NULL ||
580          n->sections[prevsection].enabled_item_count == 0);
581   return ncmenu_unroll(n, prevsection);
582 }
583 
ncmenu_nextitem(ncmenu * n)584 int ncmenu_nextitem(ncmenu* n){
585   if(n->unrolledsection == -1){
586     if(ncmenu_unroll(n, 0)){
587       return -1;
588     }
589   }
590   ncmenu_int_section* sec = &n->sections[n->unrolledsection];
591   // FIXME probably best to detect cycles
592   do{
593     if((unsigned)++sec->itemselected == sec->itemcount){
594       sec->itemselected = 0;
595     }
596   }while(!sec->items[sec->itemselected].desc || sec->items[sec->itemselected].disabled);
597   return ncmenu_unroll(n, n->unrolledsection);
598 }
599 
ncmenu_previtem(ncmenu * n)600 int ncmenu_previtem(ncmenu* n){
601   if(n->unrolledsection == -1){
602     if(ncmenu_unroll(n, 0)){
603       return -1;
604     }
605   }
606   ncmenu_int_section* sec = &n->sections[n->unrolledsection];
607   // FIXME probably best to detect cycles
608   do{
609     if(sec->itemselected-- == 0){
610       sec->itemselected = sec->itemcount - 1;
611     }
612   }while(!sec->items[sec->itemselected].desc || sec->items[sec->itemselected].disabled);
613   return ncmenu_unroll(n, n->unrolledsection);
614 }
615 
ncmenu_selected(const ncmenu * n,ncinput * ni)616 const char* ncmenu_selected(const ncmenu* n, ncinput* ni){
617   if(n->unrolledsection < 0){
618     return NULL;
619   }
620   const struct ncmenu_int_section* sec = &n->sections[n->unrolledsection];
621   const int itemidx = sec->itemselected;
622   if(ni){
623     memcpy(ni, &sec->items[itemidx].shortcut, sizeof(*ni));
624   }
625   return sec->items[itemidx].desc;
626 }
627 
ncmenu_mouse_selected(const ncmenu * n,const ncinput * click,ncinput * ni)628 const char* ncmenu_mouse_selected(const ncmenu* n, const ncinput* click,
629                                   ncinput* ni){
630   if(click->id != NCKEY_BUTTON1){
631     return NULL;
632   }
633   if(click->evtype != NCTYPE_RELEASE){
634     return NULL;
635   }
636   struct ncplane* nc = n->ncp;
637   int y = click->y;
638   int x = click->x;
639   unsigned dimy, dimx;
640   ncplane_dim_yx(nc, &dimy, &dimx);
641   if(!ncplane_translate_abs(nc, &y, &x)){
642     return NULL;
643   }
644   // FIXME section_x() works only off the section header lengths, meaning that
645   // if we click an item outside of those columns covered by the header, it will
646   // read as a -1 from section_x(). we want to instead get the unrolled section,
647   // find its boundaries, and verify that we are within them.
648   int i = section_x(n, x);
649   if(i < 0 || i != n->unrolledsection){
650     return NULL;
651   }
652   const struct ncmenu_int_section* sec = &n->sections[n->unrolledsection];
653   if(y < 2 || (unsigned)y - 2 >= sec->itemcount){
654     return NULL;
655   }
656   const int itemidx = y - 2;
657   if(ni){
658     memcpy(ni, &sec->items[itemidx].shortcut, sizeof(*ni));
659   }
660   return sec->items[itemidx].desc;
661 }
662 
ncmenu_offer_input(ncmenu * n,const ncinput * nc)663 bool ncmenu_offer_input(ncmenu* n, const ncinput* nc){
664   // we can't actually select menu items in this function, since we need to
665   // invoke an arbitrary function as a result.
666   if(nc->id == NCKEY_BUTTON1 && nc->evtype == NCTYPE_RELEASE){
667     int y = nc->y;
668     int x = nc->x;
669     unsigned dimy, dimx;
670     ncplane_dim_yx(n->ncp, &dimy, &dimx);
671     if(!ncplane_translate_abs(n->ncp, &y, &x)){
672       return false;
673     }
674     if(y != (n->bottom ? (int)dimy - 1 : 0)){
675       return false;
676     }
677     int i = section_x(n, x);
678     if(i < 0 || i == n->unrolledsection){
679       ncmenu_rollup(n);
680     }else{
681       ncmenu_unroll(n, i);
682     }
683     return true;
684   }else if(nc->evtype == NCTYPE_RELEASE){
685     return false;
686   }
687   for(int si = 0 ; si < n->sectioncount ; ++si){
688     const ncmenu_int_section* sec = &n->sections[si];
689     if(sec->enabled_item_count == 0){
690       continue;
691     }
692     if(!ncinput_equal_p(&sec->shortcut, nc)){
693       continue;
694     }
695     ncmenu_unroll(n, si);
696     return true;
697   }
698   if(n->unrolledsection < 0){ // all following need an unrolled section
699     return false;
700   }
701   if(nc->id == NCKEY_LEFT){
702     if(ncmenu_prevsection(n)){
703       return false;
704     }
705     return true;
706   }else if(nc->id == NCKEY_RIGHT){
707     if(ncmenu_nextsection(n)){
708       return false;
709     }
710     return true;
711   }else if(nc->id == NCKEY_UP || nc->id == NCKEY_SCROLL_UP){
712     if(ncmenu_previtem(n)){
713       return false;
714     }
715     return true;
716   }else if(nc->id == NCKEY_DOWN || nc->id == NCKEY_SCROLL_DOWN){
717     if(ncmenu_nextitem(n)){
718       return false;
719     }
720     return true;
721   }else if(nc->id == NCKEY_ESC){
722     ncmenu_rollup(n);
723     return true;
724   }
725   return false;
726 }
727 
728 // FIXME we probably ought implement this with a trie or something
ncmenu_item_set_status(ncmenu * n,const char * section,const char * item,bool enabled)729 int ncmenu_item_set_status(ncmenu* n, const char* section, const char* item,
730                            bool enabled){
731   for(int si = 0 ; si < n->sectioncount ; ++si){
732     struct ncmenu_int_section* sec = &n->sections[si];
733     if(strcmp(sec->name, section) == 0){
734       for(unsigned ii = 0 ; ii < sec->itemcount ; ++ii){
735         struct ncmenu_int_item* i = &sec->items[ii];
736         if(strcmp(i->desc, item) == 0){
737           const bool changed = i->disabled == enabled;
738           i->disabled = !enabled;
739           if(changed){
740             if(i->disabled){
741               if(--sec->enabled_item_count == 0){
742                 write_header(n);
743               }
744             }else{
745               if(++sec->enabled_item_count == 1){
746                 write_header(n);
747               }
748             }
749             if(n->unrolledsection == si){
750               if(sec->enabled_item_count == 0){
751                 ncmenu_rollup(n);
752               }else{
753                 ncmenu_unroll(n, n->unrolledsection);
754               }
755             }
756           }
757           return 0;
758         }
759       }
760       break;
761     }
762   }
763   return -1;
764 }
765 
ncmenu_plane(ncmenu * menu)766 ncplane* ncmenu_plane(ncmenu* menu){
767   return menu->ncp;
768 }
769 
ncmenu_destroy(ncmenu * n)770 void ncmenu_destroy(ncmenu* n){
771   if(n){
772     free_menu_sections(n);
773     if(ncplane_set_widget(n->ncp, NULL, NULL) == 0){
774       ncplane_destroy(n->ncp);
775     }
776     free(n);
777   }
778 }
779