History

Change TUI menu code to not use 'menu' library

This commit implements the Qnav/Qmenu system directly in ncurses, replacing the old implementation which relied on the 'menu' library. The new code is shorter, easier to read, and doesn't need the 'menu' library to be linked. For users, the behavior of the menus should be the same. We still rely on the 'form' library for text field input in the menus.

cancel committed 6 years ago

Commit fbcf24f1 parent 774d7863 files · +124 −198Browse files

Unified · Split

3 files
  1. term_util.c +123 −195
  2. term_util.h +0 −2
  3. tool +1 −1
term_util.c+123−195
@@ -2,7 +2,8 @@ · 1 unmodified line
2#include "oso.h"2#include "oso.h"
3#include <ctype.h>3#include <ctype.h>
4#include <form.h>4#include <form.h>
5#include <menu.h>5
6ORCA_NOINLINE static void qmenu_reprint(Qmenu *qm);
6 7
7void term_util_init_colors() {8void term_util_init_colors() {
8 if (has_colors()) {9 if (has_colors()) {
@@ -38,20 +39,18 @@ struct Qmsg { · 29 unmodified lines
38 Qmsg_dismiss_mode dismiss_mode;39 Qmsg_dismiss_mode dismiss_mode;
39};40};
40 41
41struct Qmenu_item_extra {42typedef struct Qmenu_item {
42 int user_id;43 char const *text;
44 int id;
43 U8 owns_string : 1, is_spacer : 1;45 U8 owns_string : 1, is_spacer : 1;
44};46} Qmenu_item;
45 47
46struct Qmenu {48struct Qmenu {
47 Qblock qblock;49 Qblock qblock;
48 MENU *ncurses_menu;50 Qmenu_item *items;
49 ITEM **ncurses_items;
50 Usz items_count, items_cap;51 Usz items_count, items_cap;
51 ITEM *initial_item;52 int current_item, id;
52 int id;53 U8 needs_reprint : 1, is_frontmost : 1;
53 // Flag for right-padding hack. Temp until we do our own menus
54 U8 has_submenu_item : 1;
55};54};
56 55
57struct Qform {56struct Qform {
@@ -175,18 +174,26 @@ bool qnav_draw(void) { · 117 unmodified lines
175 getmaxyx(stdscr, term_h, term_w);174 getmaxyx(stdscr, term_h, term_w);
176 for (Usz i = 0; i < qnav_stack.count; ++i) {175 for (Usz i = 0; i < qnav_stack.count; ++i) {
177 Qblock *qb = qnav_stack.blocks[i];176 Qblock *qb = qnav_stack.blocks[i];
178 if (qnav_stack.occlusion_dirty) {177 bool is_frontmost = i == qnav_stack.count - 1;
179 bool is_frontmost = i == qnav_stack.count - 1;178 if (qnav_stack.occlusion_dirty)
180 qblock_print_frame(qb, is_frontmost);179 qblock_print_frame(qb, is_frontmost);
181 switch (qb->tag) {180 switch (qb->tag) {
182 case Qblock_type_qmsg:181 case Qblock_type_qmsg:
183 break;182 break;
184 case Qblock_type_qmenu:183 case Qblock_type_qmenu: {
185 qmenu_set_displayed_active(qmenu_of(qb), is_frontmost);184 Qmenu *qm = qmenu_of(qb);
186 break;185 if (qm->is_frontmost != is_frontmost) {
187 case Qblock_type_qform:186 qm->is_frontmost = is_frontmost;
188 break;187 qm->needs_reprint = 1;
188 }
189 if (qm->needs_reprint) {
190 qmenu_reprint(qm);
191 qm->needs_reprint = 0;
189 }192 }
193 break;
194 }
195 case Qblock_type_qform:
196 break;
190 }197 }
191 touchwin(qb->outer_window); // here? or after continue?198 touchwin(qb->outer_window); // here? or after continue?
192 if (term_h < 1 || term_w < 1)199 if (term_h < 1 || term_w < 1)
@@ -222,7 +229,7 @@ void qblock_print_title(Qblock *qb, char const *title, int attr) { · 29 unmodified lines
222 wattr_get(qb->outer_window, &attrs, &pair, NULL);229 wattr_get(qb->outer_window, &attrs, &pair, NULL);
223 wattrset(qb->outer_window, attr);230 wattrset(qb->outer_window, attr);
224 waddch(qb->outer_window, ' ');231 waddch(qb->outer_window, ' ');
225 wprintw(qb->outer_window, title);232 waddstr(qb->outer_window, title);
226 waddch(qb->outer_window, ' ');233 waddch(qb->outer_window, ' ');
227 wattr_set(qb->outer_window, attrs, pair, NULL);234 wattr_set(qb->outer_window, attrs, pair, NULL);
228}235}
@@ -343,91 +350,65 @@ Qmsg *qmsg_of(Qblock *qb) { return ORCA_CONTAINER_OF(qb, Qmsg, qblock); } · 114 unmodified lines
343Qmenu *qmenu_create(int id) {350Qmenu *qmenu_create(int id) {
344 Qmenu *qm = (Qmenu *)malloc(sizeof(Qmenu));351 Qmenu *qm = (Qmenu *)malloc(sizeof(Qmenu));
345 qblock_init(&qm->qblock, Qblock_type_qmenu);352 qblock_init(&qm->qblock, Qblock_type_qmenu);
346 qm->ncurses_menu = NULL;353 qm->items = NULL;
347 qm->ncurses_items = NULL;
348 qm->items_count = 0;354 qm->items_count = 0;
349 qm->items_cap = 0;355 qm->items_cap = 0;
350 qm->initial_item = NULL;356 qm->current_item = 0;
351 qm->id = id;357 qm->id = id;
352 qm->has_submenu_item = 0;358 qm->needs_reprint = 1;
359 qm->is_frontmost = 0;
353 return qm;360 return qm;
354}361}
355void qmenu_destroy(Qmenu *qm) { qmenu_free(qm); }362void qmenu_destroy(Qmenu *qm) { qmenu_free(qm); }
356int qmenu_id(Qmenu const *qm) { return qm->id; }363int qmenu_id(Qmenu const *qm) { return qm->id; }
357static ORCA_NOINLINE void364static ORCA_NOINLINE Qmenu_item *qmenu_allocitems(Qmenu *qm, Usz count) {
358qmenu_allocitems(Qmenu *qm, Usz count, Usz *out_idx, ITEM ***out_items,
359 struct Qmenu_item_extra **out_extras) {
360 Usz old_count = qm->items_count;365 Usz old_count = qm->items_count;
361 // Add 1 for the extra null terminator guy366 if (old_count > SIZE_MAX - count) // overflow
362 Usz new_count = old_count + count + 1;367 exit(1);
368 Usz new_count = old_count + count;
363 Usz items_cap = qm->items_cap;369 Usz items_cap = qm->items_cap;
364 ITEM **items = qm->ncurses_items;370 Qmenu_item *items = qm->items;
365 if (new_count > items_cap) {371 if (new_count > items_cap) {
366 // todo overflow check, realloc fail check372 // todo overflow check, realloc fail check
367 Usz old_cap = items_cap;
368 Usz new_cap = new_count < 32 ? 32 : orca_round_up_power2(new_count);373 Usz new_cap = new_count < 32 ? 32 : orca_round_up_power2(new_count);
369 Usz new_size = new_cap * (sizeof(ITEM *) + sizeof(struct Qmenu_item_extra));374 Usz new_size = new_cap * sizeof(Qmenu_item);
370 ITEM **new_items = (ITEM **)realloc(items, new_size);375 Qmenu_item *new_items = (Qmenu_item *)realloc(items, new_size);
371 if (!new_items)376 if (!new_items)
372 exit(1);377 exit(1);
373 items = new_items;378 items = new_items;
374 items_cap = new_cap;379 items_cap = new_cap;
375 // Move old extras data to new position380 qm->items = new_items;
376 Usz old_extras_offset = sizeof(ITEM *) * old_cap;
377 Usz new_extras_offset = sizeof(ITEM *) * new_cap;
378 Usz old_extras_size = sizeof(struct Qmenu_item_extra) * old_count;
379 memmove((char *)items + new_extras_offset,
380 (char *)items + old_extras_offset, old_extras_size);
381 qm->ncurses_items = new_items;
382 qm->items_cap = new_cap;381 qm->items_cap = new_cap;
383 }382 }
384 // Not using new_count here in order to leave an extra 1 for the null383 qm->items_count = new_count;
385 // terminator as required by ncurses.384 return items + old_count;
386 qm->items_count = old_count + count;385}
387 Usz extras_offset = sizeof(ITEM *) * items_cap;386ORCA_NOINLINE static void qmenu_reprint(Qmenu *qm) {
388 *out_idx = old_count;387 WINDOW *win = qm->qblock.content_window;
389 *out_items = items + old_count;388 Qmenu_item *items = qm->items;
390 *out_extras =389 bool isfront = qm->is_frontmost;
391 (struct Qmenu_item_extra *)((char *)items + extras_offset) + old_count;390 werase(win);
392}391 for (Usz i = 0, n = qm->items_count; i < n; ++i) {
393ORCA_FORCEINLINE static struct Qmenu_item_extra *392 bool iscur = items[i].id == qm->current_item;
394qmenu_item_extras_ptr(Qmenu *qm) {393 wattrset(win, isfront ? iscur ? A_BOLD : A_NORMAL : A_DIM);
395 Usz offset = sizeof(ITEM *) * qm->items_cap;394 wmove(win, (int)i, iscur ? 1 : 3);
396 return (struct Qmenu_item_extra *)((char *)qm->ncurses_items + offset);395 if (iscur)
397}396 waddstr(win, "> ");
398// Get the curses menu item user pointer out, turn it to an int, and use it as397 waddstr(win, items[i].text);
399// an index into the 'extras' arrays.398 }
400ORCA_FORCEINLINE static struct Qmenu_item_extra *
401qmenu_itemextra(struct Qmenu_item_extra *extras, ITEM *item) {
402 return extras + (int)(intptr_t)(item_userptr(item));
403}399}
404void qmenu_set_title(Qmenu *qm, char const *title) {400void qmenu_set_title(Qmenu *qm, char const *title) {
405 qblock_set_title(&qm->qblock, title);401 qblock_set_title(&qm->qblock, title);
406}402}
407void qmenu_add_choice(Qmenu *qm, int id, char const *text) {403void qmenu_add_choice(Qmenu *qm, int id, char const *text) {
408 assert(id != 0);404 assert(id != 0);
409 Usz idx;405 Qmenu_item *item = qmenu_allocitems(qm, 1);
410 ITEM **items;406 item->text = text;
411 struct Qmenu_item_extra *extras;407 item->id = id;
412 qmenu_allocitems(qm, 1, &idx, &items, &extras);408 item->owns_string = false;
413 items[0] = new_item(text, NULL);409 item->is_spacer = false;
414 set_item_userptr(items[0], (void *)(uintptr_t)idx);410 if (!qm->current_item)
415 extras[0].user_id = id;411 qm->current_item = id;
416 extras[0].owns_string = false;
417 extras[0].is_spacer = false;
418}
419void qmenu_add_submenu(Qmenu *qm, int id, char const *text) {
420 assert(id != 0);
421 qm->has_submenu_item = true; // don't add +1 right padding to subwindow
422 Usz idx;
423 ITEM **items;
424 struct Qmenu_item_extra *extras;
425 qmenu_allocitems(qm, 1, &idx, &items, &extras);
426 items[0] = new_item(text, ">");
427 set_item_userptr(items[0], (void *)(uintptr_t)idx);
428 extras[0].user_id = id;
429 extras[0].owns_string = false;
430 extras[0].is_spacer = false;
431}412}
432void qmenu_add_printf(Qmenu *qm, int id, char const *fmt, ...) {413void qmenu_add_printf(Qmenu *qm, int id, char const *fmt, ...) {
433 va_list ap;414 va_list ap;
@@ -442,85 +423,42 @@ void qmenu_add_printf(Qmenu *qm, int id, char const *fmt, ...) { · 8 unmodified lines
442 va_end(ap);423 va_end(ap);
443 if (printedsize != textsize)424 if (printedsize != textsize)
444 exit(1); // todo better handling?425 exit(1); // todo better handling?
445 Usz idx;426 Qmenu_item *item = qmenu_allocitems(qm, 1);
446 ITEM **items;427 item->text = buffer;
447 struct Qmenu_item_extra *extras;428 item->id = id;
448 qmenu_allocitems(qm, 1, &idx, &items, &extras);429 item->owns_string = true;
449 items[0] = new_item(buffer, NULL);430 item->is_spacer = false;
450 set_item_userptr(items[0], (void *)(uintptr_t)idx);431 if (!qm->current_item)
451 extras[0].user_id = id;432 qm->current_item = id;
452 extras[0].owns_string = true;
453 extras[0].is_spacer = false;
454}433}
455void qmenu_add_spacer(Qmenu *qm) {434void qmenu_add_spacer(Qmenu *qm) {
456 Usz idx;435 Qmenu_item *item = qmenu_allocitems(qm, 1);
457 ITEM **items;436 item->text = " ";
458 struct Qmenu_item_extra *extras;437 item->id = 0;
459 qmenu_allocitems(qm, 1, &idx, &items, &extras);438 item->owns_string = false;
460 items[0] = new_item(" ", NULL);439 item->is_spacer = true;
461 item_opts_off(items[0], O_SELECTABLE);
462 set_item_userptr(items[0], (void *)(uintptr_t)idx);
463 extras[0].user_id = 0;
464 extras[0].owns_string = false;
465 extras[0].is_spacer = true;
466}440}
467void qmenu_set_current_item(Qmenu *qm, int id) {441void qmenu_set_current_item(Qmenu *qm, int id) {
468 ITEM **items = qm->ncurses_items;442 if (qm->current_item == id)
469 struct Qmenu_item_extra *extras = qmenu_item_extras_ptr(qm);
470 ITEM *found = NULL;
471 for (Usz i = 0, n = qm->items_count; i < n; i++) {
472 ITEM *item = items[i];
473 if (qmenu_itemextra(extras, item)->user_id == id) {
474 found = item;
475 break;
476 }
477 }
478 if (!found)
479 return;443 return;
480 if (qm->ncurses_menu) {444 qm->current_item = id;
481 set_current_item(qm->ncurses_menu, found);445 qm->needs_reprint = 1;
482 } else {
483 qm->initial_item = found;
484 }
485}
486int qmenu_current_item(Qmenu *qm) {
487 ITEM *item = NULL;
488 if (qm->ncurses_menu)
489 item = current_item(qm->ncurses_menu);
490 if (!item)
491 item = qm->initial_item;
492 if (!item)
493 return 0;
494 struct Qmenu_item_extra *extras = qmenu_item_extras_ptr(qm);
495 return qmenu_itemextra(extras, item)->user_id;
496}
497void qmenu_set_displayed_active(Qmenu *qm, bool active) {
498 // Could add a flag in the Qmenu to avoid redundantly changing this stuff.
499 set_menu_fore(qm->ncurses_menu, active ? A_BOLD : A_DIM);
500 set_menu_back(qm->ncurses_menu, active ? A_NORMAL : A_DIM);
501 set_menu_grey(qm->ncurses_menu, active ? A_DIM : A_DIM);
502}446}
447int qmenu_current_item(Qmenu *qm) { return qm->current_item; }
503void qmenu_push_to_nav(Qmenu *qm) {448void qmenu_push_to_nav(Qmenu *qm) {
504 // new_menu() will get angry if there are no items in the menu. We'll get a449 // Probably a programming error if there are no items. Make the menu visible
505 // null pointer back, and our code will get angry. Instead, just add an empty450 // so the programmer knows something went wrong.
506 // spacer item. This will probably only ever occur as a programming error,
507 // but we should try to avoid having to deal with qmenu_push_to_nav()
508 // returning a non-ignorable error for now.
509 if (qm->items_count == 0)451 if (qm->items_count == 0)
510 qmenu_add_spacer(qm);452 qmenu_add_spacer(qm);
511 // Allocating items always leaves an extra available item at the end. This is453 Usz n = qm->items_count;
512 // so we can assign a NULL to it here, since ncurses requires the array to be454 Qmenu_item *items = qm->items;
513 // null terminated instead of using a count.455 int menu_min_h = (int)n, menu_min_w = 0;
514 qm->ncurses_items[qm->items_count] = NULL;456 for (Usz i = 0; i < n; ++i) {
515 qm->ncurses_menu = new_menu(qm->ncurses_items);457 int item_w = (int)strlen(items[i].text);
516 set_menu_mark(qm->ncurses_menu, " > ");458 if (item_w > menu_min_w)
517 set_menu_fore(qm->ncurses_menu, A_BOLD);459 menu_min_w = item_w;
518 set_menu_grey(qm->ncurses_menu, A_DIM);460 }
519 set_menu_format(qm->ncurses_menu, 30, 1); // temp to allow large Y461 menu_min_w += 3 + 1; // left " > " plus 1 empty space on right
520 int menu_min_h, menu_min_w;
521 scale_menu(qm->ncurses_menu, &menu_min_h, &menu_min_w);
522 if (!qm->has_submenu_item)
523 menu_min_w += 1; // temp hack
524 if (qm->qblock.title) {462 if (qm->qblock.title) {
525 // Stupid lack of wcswidth() means we can't know how wide this string is463 // Stupid lack of wcswidth() means we can't know how wide this string is
526 // actually displayed. Just fake it for now, until we have Unicode strings464 // actually displayed. Just fake it for now, until we have Unicode strings
@@ -529,58 +467,50 @@ void qmenu_push_to_nav(Qmenu *qm) { · 2 unmodified lines
529 if (title_w > menu_min_w)467 if (title_w > menu_min_w)
530 menu_min_w = title_w;468 menu_min_w = title_w;
531 }469 }
532 if (qm->initial_item)
533 set_current_item(qm->ncurses_menu, qm->initial_item);
534 qnav_stack_push(&qm->qblock, menu_min_h, menu_min_w);470 qnav_stack_push(&qm->qblock, menu_min_h, menu_min_w);
535 set_menu_win(qm->ncurses_menu, qm->qblock.outer_window);
536 set_menu_sub(qm->ncurses_menu, qm->qblock.content_window);
537 // TODO use this to set how "big" the menu is, visually, for scrolling.
538 // (ncurses can't figure that out on its own, aparently...)
539 // We'll need to split apart some work chunks so that we calculate the size
540 // beforehand.
541 // set_menu_format(qm->ncurses_menu, 5, 1);
542 post_menu(qm->ncurses_menu);
543}471}
544 472
545void qmenu_free(Qmenu *qm) {473void qmenu_free(Qmenu *qm) {
546 unpost_menu(qm->ncurses_menu);474 Qmenu_item *items = qm->items;
547 free_menu(qm->ncurses_menu);475 for (Usz i = 0, n = qm->items_count; i < n; ++i) {
548 struct Qmenu_item_extra *extras = qmenu_item_extras_ptr(qm);476 if (items[i].owns_string)
549 for (Usz i = 0; i < qm->items_count; ++i) {477 free((void *)items[i].text);
550 ITEM *item = qm->ncurses_items[i];
551 struct Qmenu_item_extra *extra = qmenu_itemextra(extras, item);
552 char const *freed_str = NULL;
553 if (extra->owns_string)
554 freed_str = item_name(item);
555 free_item(qm->ncurses_items[i]);
556 if (freed_str)
557 free((void *)freed_str);
558 }478 }
559 free(qm->ncurses_items);479 free(qm->items);
560 free(qm);480 free(qm);
561}481}
562 482
563ORCA_NOINLINE483ORCA_NOINLINE static void qmenu_drive_upordown(Qmenu *qm, bool downwards) {
564static void qmenu_drive_upordown(Qmenu *qm, int req_up_or_down) {484 Qmenu_item *items = qm->items;
565 struct Qmenu_item_extra *extras = qmenu_item_extras_ptr(qm);485 Usz n = qm->items_count;
566 ITEM *starting = current_item(qm->ncurses_menu);486 if (n <= 1)
567 menu_driver(qm->ncurses_menu, req_up_or_down);487 return;
568 ITEM *cur = current_item(qm->ncurses_menu);488 int cur_id = qm->current_item;
489 Usz starting = 0;
490 for (; starting < n; ++starting) {
491 if (items[starting].id == cur_id)
492 goto found;
493 }
494 return;
495found:;
496 Usz current = starting;
569 for (;;) {497 for (;;) {
570 if (!cur || cur == starting)498 if (downwards && current < n - 1)
571 break;499 current++;
572 if (!qmenu_itemextra(extras, cur)->is_spacer)500 else if (!downwards && current > 0)
501 current--;
502 if (current == starting)
573 break;503 break;
574 ITEM *prev = cur;504 if (!items[current].is_spacer)
575 menu_driver(qm->ncurses_menu, req_up_or_down);
576 cur = current_item(qm->ncurses_menu);
577 if (cur == prev)
578 break;505 break;
579 }506 }
507 if (current != starting) {
508 qm->current_item = items[current].id;
509 qm->needs_reprint = 1;
510 }
580}511}
581 512
582bool qmenu_drive(Qmenu *qm, int key, Qmenu_action *out_action) {513bool qmenu_drive(Qmenu *qm, int key, Qmenu_action *out_action) {
583 struct Qmenu_item_extra *extras = qmenu_item_extras_ptr(qm);
584 switch (key) {514 switch (key) {
585 case 27: {515 case 27: {
586 out_action->any.type = Qmenu_action_type_canceled;516 out_action->any.type = Qmenu_action_type_canceled;
@@ -588,17 +518,15 @@ bool qmenu_drive(Qmenu *qm, int key, Qmenu_action *out_action) { · 1 unmodified line
588 }518 }
589 case ' ':519 case ' ':
590 case '\r':520 case '\r':
591 case KEY_ENTER: {521 case KEY_ENTER:
592 ITEM *cur = current_item(qm->ncurses_menu);
593 out_action->picked.type = Qmenu_action_type_picked;522 out_action->picked.type = Qmenu_action_type_picked;
594 out_action->picked.id = cur ? qmenu_itemextra(extras, cur)->user_id : 0;523 out_action->picked.id = qm->current_item;
595 return true;524 return true;
596 }
597 case KEY_UP:525 case KEY_UP:
598 qmenu_drive_upordown(qm, REQ_UP_ITEM);526 qmenu_drive_upordown(qm, false);
599 return false;527 return false;
600 case KEY_DOWN:528 case KEY_DOWN:
601 qmenu_drive_upordown(qm, REQ_DOWN_ITEM);529 qmenu_drive_upordown(qm, true);
602 return false;530 return false;
603 }531 }
604 return false;532 return false;
term_util.h+0−2
@@ -141,12 +141,10 @@ void qmenu_destroy(Qmenu *qm); · 140 unmodified lines
141int qmenu_id(Qmenu const *qm);141int qmenu_id(Qmenu const *qm);
142void qmenu_set_title(Qmenu *qm, char const *title);142void qmenu_set_title(Qmenu *qm, char const *title);
143void qmenu_add_choice(Qmenu *qm, int id, char const *text);143void qmenu_add_choice(Qmenu *qm, int id, char const *text);
144void qmenu_add_submenu(Qmenu *qm, int id, char const *text);
145void qmenu_add_printf(Qmenu *qm, int id, char const *fmt, ...)144void qmenu_add_printf(Qmenu *qm, int id, char const *fmt, ...)
146 ORCA_TERM_UTIL_PRINTF(3, 4);145 ORCA_TERM_UTIL_PRINTF(3, 4);
147void qmenu_add_spacer(Qmenu *qm);146void qmenu_add_spacer(Qmenu *qm);
148void qmenu_set_current_item(Qmenu *qm, int id);147void qmenu_set_current_item(Qmenu *qm, int id);
149void qmenu_set_displayed_active(Qmenu *qm, bool active);
150void qmenu_push_to_nav(Qmenu *qm);148void qmenu_push_to_nav(Qmenu *qm);
151int qmenu_current_item(Qmenu *qm);149int qmenu_current_item(Qmenu *qm);
152bool qmenu_drive(Qmenu *qm, int key, Qmenu_action *out_action);150bool qmenu_drive(Qmenu *qm, int key, Qmenu_action *out_action);
tool+1−1
@@ -409,7 +409,7 @@ build_target() { · 408 unmodified lines
409 add cc_flags -D_POSIX_C_SOURCE=200809L409 add cc_flags -D_POSIX_C_SOURCE=200809L
410 ;;410 ;;
411 esac411 esac
412 add libraries -lmenuw -lformw -lncursesw412 add libraries -lformw -lncursesw
413 if [[ $portmidi_enabled = 1 ]]; then413 if [[ $portmidi_enabled = 1 ]]; then
414 add libraries -lportmidi414 add libraries -lportmidi
415 add cc_flags -DFEAT_PORTMIDI415 add cc_flags -DFEAT_PORTMIDI

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t