You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

780 lines
21 KiB

#include "term_util.h"
#include "oso.h"
#include <ctype.h>
#include <form.h>
void term_util_init_colors()
{
if (has_colors()) {
// Enable color
start_color();
use_default_colors();
for (int ifg = 0; ifg < Colors_count; ++ifg) {
for (int ibg = 0; ibg < Colors_count; ++ibg) {
int res = init_pair(
(short int)(1 + ifg * Colors_count + ibg),
(short int)(ifg - 1),
(short int)(ibg - 1));
(void)res;
// Might fail on Linux virtual console/terminal for a couple of colors.
// Just ignore.
#if 0
if (res == ERR) {
endwin();
fprintf(stderr, "Error initializing color pair: %d %d\n", ifg - 1,
ibg - 1);
exit(1);
}
#endif
}
}
}
}
#define ORCA_CONTAINER_OF(ptr, type, member) \
((type *)((char *)(1 ? (ptr) : &((type *)0)->member) - offsetof(type, member)))
struct Qmsg {
Qblock qblock;
Qmsg_dismiss_mode dismiss_mode;
};
typedef struct Qmenu_item {
char const *text;
int id;
U8 owns_string : 1, is_spacer : 1;
} Qmenu_item;
struct Qmenu {
Qblock qblock;
Qmenu_item *items;
Usz items_count, items_cap;
int current_item, id;
U8 needs_reprint : 1, is_frontmost : 1;
};
struct Qform {
Qblock qblock;
FORM *ncurses_form;
FIELD *ncurses_fields[32];
Usz fields_count;
int id;
};
static void qmenu_free(Qmenu *qm);
static void qform_free(Qform *qf);
ORCA_NOINLINE static void qmenu_reprint(Qmenu *qm);
Qnav_stack qnav_stack;
void qnav_init()
{
qnav_stack = (Qnav_stack){ 0 };
}
void qnav_deinit()
{
while (qnav_stack.top)
qnav_stack_pop();
}
// Set new y and x coordinates for the top and left of a Qblock based on the
// position of the Qblock "below" it in the stack. (Below meaning its order in
// the stack, not vertical position on a Y axis.) The target Qblock should
// already be inserted into the stack somewhere, so don't call this before
// you've finished doing the rest of the setup on the Qblock. The y and x
// fields can be junk, though, since this function writes to them without
// reading them.
static ORCA_NOINLINE void qnav_reposition_block(Qblock *qb)
{
int top = 0, left = 0;
Qblock *prev = qb->down;
if (!prev)
goto done;
int total_h, total_w;
getmaxyx(qb->outer_window, total_h, total_w);
WINDOW *w = prev->outer_window;
int prev_y = prev->y, prev_x = prev->x, prev_h, prev_w;
getmaxyx(w, prev_h, prev_w);
// Start by trying to position the item to the right of the previous item.
left = prev_x + prev_w + 0;
int term_h, term_w;
getmaxyx(stdscr, term_h, term_w);
// Check if we'll run out of room if we position the new item to the right
// of the existing item (with the same Y position.)
if (left + total_w > term_w) {
// If we have enough room if we position just below the previous item in
// the stack, do that instead of positioning to the right of it.
if (prev_x + total_w <= term_w && total_h < term_h - (prev_y + prev_h)) {
top = prev_y + prev_h;
left = prev_x;
}
// If the item doesn't fit there, but it's less wide than the terminal,
// right-align it to the edge of the terminal.
else if (total_w < term_w) {
left = term_w - total_w;
}
// Otherwise, just start the layout over at Y=0,X=0
else {
left = 0;
}
}
done:
qb->y = top;
qb->x = left;
}
static ORCA_NOINLINE void qnav_stack_push(Qblock *qb, int height, int width)
{
#ifndef NDEBUG
for (Qblock *i = qnav_stack.top; i; i = i->down) {
assert(i != qb);
}
#endif
int total_h = height + 2, total_w = width + 2;
if (qnav_stack.top)
qnav_stack.top->up = qb;
else
qnav_stack.bottom = qb;
qb->down = qnav_stack.top;
qnav_stack.top = qb;
qb->outer_window = newpad(total_h, total_w);
qb->content_window = subpad(qb->outer_window, height, width, 1, 1);
qnav_reposition_block(qb);
qnav_stack.occlusion_dirty = true;
}
Qblock *qnav_top_block()
{
return qnav_stack.top;
}
void qblock_init(Qblock *qb, Qblock_type_tag tag)
{
*qb = (Qblock){ 0 };
qb->tag = tag;
}
void qnav_free_block(Qblock *qb)
{
switch (qb->tag) {
case Qblock_type_qmsg: {
Qmsg *qm = qmsg_of(qb);
free(qm);
break;
}
case Qblock_type_qmenu:
qmenu_free(qmenu_of(qb));
break;
case Qblock_type_qform:
qform_free(qform_of(qb));
break;
}
}
void qnav_stack_pop(void)
{
assert(qnav_stack.top);
if (!qnav_stack.top)
return;
Qblock *qb = qnav_stack.top;
qnav_stack.top = qb->down;
if (qnav_stack.top)
qnav_stack.top->up = NULL;
else
qnav_stack.bottom = NULL;
qnav_stack.occlusion_dirty = true;
WINDOW *content_window = qb->content_window;
WINDOW *outer_window = qb->outer_window;
// erase any stuff underneath where this window is, in case it's outside of
// the grid in an area that isn't actively redraw
werase(outer_window);
wnoutrefresh(outer_window);
qnav_free_block(qb);
delwin(content_window);
delwin(outer_window);
}
bool qnav_draw(void)
{
bool drew_any = false;
if (!qnav_stack.bottom)
goto done;
int term_h, term_w;
getmaxyx(stdscr, term_h, term_w);
for (Qblock *qb = qnav_stack.bottom; qb; qb = qb->up) {
bool is_frontmost = qb == qnav_stack.top;
if (qnav_stack.occlusion_dirty)
qblock_print_frame(qb, is_frontmost);
switch (qb->tag) {
case Qblock_type_qmsg:
break;
case Qblock_type_qmenu: {
Qmenu *qm = qmenu_of(qb);
if (qm->is_frontmost != is_frontmost) {
qm->is_frontmost = is_frontmost;
qm->needs_reprint = 1;
}
if (qm->needs_reprint) {
qmenu_reprint(qm);
qm->needs_reprint = 0;
}
break;
}
case Qblock_type_qform:
break;
}
touchwin(qb->outer_window); // here? or after continue?
if (term_h < 1 || term_w < 1)
continue;
int qbwin_h, qbwin_w;
getmaxyx(qb->outer_window, qbwin_h, qbwin_w);
int qbwin_endy = qb->y + qbwin_h;
int qbwin_endx = qb->x + qbwin_w;
if (qbwin_endy >= term_h)
qbwin_endy = term_h - 1;
if (qbwin_endx >= term_w)
qbwin_endx = term_w - 1;
if (qb->y >= qbwin_endy || qb->x >= qbwin_endx)
continue;
pnoutrefresh(qb->outer_window, 0, 0, qb->y, qb->x, qbwin_endy, qbwin_endx);
drew_any = true;
}
done:
qnav_stack.occlusion_dirty = false;
return drew_any;
}
void qnav_adjust_term_size(void)
{
if (!qnav_stack.bottom)
return;
for (Qblock *qb = qnav_stack.bottom; qb; qb = qb->up)
qnav_reposition_block(qb);
qnav_stack.occlusion_dirty = true;
}
void qblock_print_border(Qblock *qb, unsigned int attr)
{
wborder(
qb->outer_window,
ACS_VLINE | attr,
ACS_VLINE | attr,
ACS_HLINE | attr,
ACS_HLINE | attr,
ACS_ULCORNER | attr,
ACS_URCORNER | attr,
ACS_LLCORNER | attr,
ACS_LRCORNER | attr);
}
void qblock_print_title(Qblock *qb, char const *title, int attr)
{
wmove(qb->outer_window, 0, 1);
attr_t attrs = A_NORMAL;
short pair = 0;
wattr_get(qb->outer_window, &attrs, &pair, NULL);
wattrset(qb->outer_window, attr);
waddch(qb->outer_window, ' ');
waddstr(qb->outer_window, title);
waddch(qb->outer_window, ' ');
wattr_set(qb->outer_window, attrs, pair, NULL);
}
void qblock_set_title(Qblock *qb, char const *title)
{
qb->title = title;
}
void qblock_print_frame(Qblock *qb, bool active)
{
qblock_print_border(qb, active ? A_NORMAL : A_DIM);
if (qb->title) {
qblock_print_title(qb, qb->title, active ? A_NORMAL : A_DIM);
}
if (qb->tag == Qblock_type_qform) {
Qform *qf = qform_of(qb);
if (qf->ncurses_form) {
pos_form_cursor(qf->ncurses_form);
}
}
}
WINDOW *qmsg_window(Qmsg *qm)
{
return qm->qblock.content_window;
}
void qmsg_set_title(Qmsg *qm, char const *title)
{
qblock_set_title(&qm->qblock, title);
}
void qmsg_set_dismiss_mode(Qmsg *qm, Qmsg_dismiss_mode mode)
{
if (qm->dismiss_mode == mode)
return;
qm->dismiss_mode = mode;
}
Qmsg *qmsg_push(int height, int width)
{
Qmsg *qm = malloc(sizeof(Qmsg));
qblock_init(&qm->qblock, Qblock_type_qmsg);
qm->dismiss_mode = Qmsg_dismiss_mode_explicitly;
qnav_stack_push(&qm->qblock, height, width);
return qm;
}
Qmsg *qmsg_printf_push(char const *title, char const *fmt, ...)
{
int titlewidth = title ? (int)strlen(title) : 0;
va_list ap;
va_start(ap, fmt);
int msgbytes = vsnprintf(NULL, 0, fmt, ap);
va_end(ap);
char *buffer = malloc((Usz)msgbytes + 1);
if (!buffer)
exit(1);
va_start(ap, fmt);
int printedbytes = vsnprintf(buffer, (Usz)msgbytes + 1, fmt, ap);
va_end(ap);
if (printedbytes != msgbytes)
exit(1); // todo better handling?
int lines = 1;
int curlinewidth = 0;
int maxlinewidth = 0;
for (int i = 0; i < msgbytes; i++) {
if (buffer[i] == '\n') {
buffer[i] = '\0'; // This is terrifying :)
lines++;
if (curlinewidth > maxlinewidth)
maxlinewidth = curlinewidth;
curlinewidth = 0;
} else {
curlinewidth++;
}
}
if (curlinewidth > maxlinewidth)
maxlinewidth = curlinewidth;
int width = titlewidth > maxlinewidth ? titlewidth : maxlinewidth;
width += 2; // 1 padding on left and right each
Qmsg *msg = qmsg_push(lines, width); // no wrapping yet, no real wcwidth, etc
WINDOW *msgw = qmsg_window(msg);
int i = 0;
int offset = 0;
for (;;) {
if (offset == msgbytes + 1)
break;
int numbytes = (int)strlen(buffer + offset);
wmove(msgw, i, 1);
waddstr(msgw, buffer + offset);
offset += numbytes + 1;
i++;
}
free(buffer);
if (title)
qmsg_set_title(msg, title);
return msg;
}
bool qmsg_drive(Qmsg *qm, int key, Qmsg_action *out_action)
{
*out_action = (Qmsg_action){ 0 };
Qmsg_dismiss_mode dm = qm->dismiss_mode;
switch (dm) {
case Qmsg_dismiss_mode_explicitly:
break;
case Qmsg_dismiss_mode_easily:
out_action->dismiss = true;
return true;
case Qmsg_dismiss_mode_passthrough:
out_action->dismiss = true;
out_action->passthrough = true;
return true;
}
switch (key) {
case ' ':
case 27:
case '\r':
case KEY_ENTER:
out_action->dismiss = true;
return true;
}
return false;
}
Qmsg *qmsg_of(Qblock *qb)
{
return ORCA_CONTAINER_OF(qb, Qmsg, qblock);
}
Qmenu *qmenu_create(int id)
{
Qmenu *qm = (Qmenu *)malloc(sizeof(Qmenu));
qblock_init(&qm->qblock, Qblock_type_qmenu);
qm->items = NULL;
qm->items_count = 0;
qm->items_cap = 0;
qm->current_item = 0;
qm->id = id;
qm->needs_reprint = 1;
qm->is_frontmost = 0;
return qm;
}
void qmenu_destroy(Qmenu *qm)
{
qmenu_free(qm);
}
int qmenu_id(Qmenu const *qm)
{
return qm->id;
}
static ORCA_NOINLINE Qmenu_item *qmenu_allocitems(Qmenu *qm, Usz count)
{
Usz old_count = qm->items_count;
if (old_count > SIZE_MAX - count) // overflow
exit(1);
Usz new_count = old_count + count;
Usz items_cap = qm->items_cap;
Qmenu_item *items = qm->items;
if (new_count > items_cap) {
// todo overflow check, realloc fail check
Usz new_cap = new_count < 32 ? 32 : orca_round_up_power2(new_count);
Usz new_size = new_cap * sizeof(Qmenu_item);
Qmenu_item *new_items = (Qmenu_item *)realloc(items, new_size);
if (!new_items)
exit(1);
items = new_items;
items_cap = new_cap;
qm->items = new_items;
qm->items_cap = new_cap;
}
qm->items_count = new_count;
return items + old_count;
}
ORCA_NOINLINE static void qmenu_reprint(Qmenu *qm)
{
WINDOW *win = qm->qblock.content_window;
Qmenu_item *items = qm->items;
bool isfront = qm->is_frontmost;
werase(win);
for (Usz i = 0, n = qm->items_count; i < n; ++i) {
bool iscur = items[i].id == qm->current_item;
wattrset(win, isfront ? iscur ? A_BOLD : A_NORMAL : A_DIM);
wmove(win, (int)i, iscur ? 1 : 3);
if (iscur)
waddstr(win, "> ");
waddstr(win, items[i].text);
}
}
void qmenu_set_title(Qmenu *qm, char const *title)
{
qblock_set_title(&qm->qblock, title);
}
void qmenu_add_choice(Qmenu *qm, int id, char const *text)
{
assert(id != 0);
Qmenu_item *item = qmenu_allocitems(qm, 1);
item->text = text;
item->id = id;
item->owns_string = false;
item->is_spacer = false;
if (!qm->current_item)
qm->current_item = id;
}
void qmenu_add_printf(Qmenu *qm, int id, char const *fmt, ...)
{
va_list ap;
va_start(ap, fmt);
int textsize = vsnprintf(NULL, 0, fmt, ap);
va_end(ap);
char *buffer = malloc((Usz)textsize + 1);
if (!buffer)
exit(1);
va_start(ap, fmt);
int printedsize = vsnprintf(buffer, (Usz)textsize + 1, fmt, ap);
va_end(ap);
if (printedsize != textsize)
exit(1); // todo better handling?
Qmenu_item *item = qmenu_allocitems(qm, 1);
item->text = buffer;
item->id = id;
item->owns_string = true;
item->is_spacer = false;
if (!qm->current_item)
qm->current_item = id;
}
void qmenu_add_spacer(Qmenu *qm)
{
Qmenu_item *item = qmenu_allocitems(qm, 1);
item->text = " ";
item->id = 0;
item->owns_string = false;
item->is_spacer = true;
}
void qmenu_set_current_item(Qmenu *qm, int id)
{
if (qm->current_item == id)
return;
qm->current_item = id;
qm->needs_reprint = 1;
}
int qmenu_current_item(Qmenu *qm)
{
return qm->current_item;
}
void qmenu_push_to_nav(Qmenu *qm)
{
// Probably a programming error if there are no items. Make the menu visible
// so the programmer knows something went wrong.
if (qm->items_count == 0)
qmenu_add_spacer(qm);
Usz n = qm->items_count;
Qmenu_item *items = qm->items;
int menu_min_h = (int)n, menu_min_w = 0;
for (Usz i = 0; i < n; ++i) {
int item_w = (int)strlen(items[i].text);
if (item_w > menu_min_w)
menu_min_w = item_w;
}
menu_min_w += 3 + 1; // left " > " plus 1 empty space on right
if (qm->qblock.title) {
// Stupid lack of wcswidth() means we can't know how wide this string is
// actually displayed. Just fake it for now, until we have Unicode strings
// in the UI. Then we get sad.
int title_w = (int)strlen(qm->qblock.title) + 2;
if (title_w > menu_min_w)
menu_min_w = title_w;
}
qnav_stack_push(&qm->qblock, menu_min_h, menu_min_w);
}
static void qmenu_free(Qmenu *qm)
{
Qmenu_item *items = qm->items;
for (Usz i = 0, n = qm->items_count; i < n; ++i) {
if (items[i].owns_string)
free((void *)items[i].text);
}
free(qm->items);
free(qm);
}
ORCA_NOINLINE static void qmenu_drive_upordown(Qmenu *qm, bool downwards)
{
Qmenu_item *items = qm->items;
Usz n = qm->items_count;
if (n <= 1)
return;
int cur_id = qm->current_item;
Usz starting = 0;
for (; starting < n; ++starting) {
if (items[starting].id == cur_id)
goto found;
}
return;
found:;
Usz current = starting;
for (;;) {
if (downwards && current < n - 1)
current++;
else if (!downwards && current > 0)
current--;
if (current == starting)
break;
if (!items[current].is_spacer)
break;
}
if (current != starting) {
qm->current_item = items[current].id;
qm->needs_reprint = 1;
}
}
bool qmenu_drive(Qmenu *qm, int key, Qmenu_action *out_action)
{
switch (key) {
case 27: {
out_action->any.type = Qmenu_action_type_canceled;
return true;
}
case ' ':
case '\r':
case KEY_ENTER:
out_action->picked.type = Qmenu_action_type_picked;
out_action->picked.id = qm->current_item;
return true;
case KEY_UP:
qmenu_drive_upordown(qm, false);
return false;
case KEY_DOWN:
qmenu_drive_upordown(qm, true);
return false;
}
return false;
}
Qmenu *qmenu_of(Qblock *qb)
{
return ORCA_CONTAINER_OF(qb, Qmenu, qblock);
}
bool qmenu_top_is_menu(int id)
{
Qblock *qb = qnav_top_block();
if (!qb)
return false;
if (qb->tag != Qblock_type_qmenu)
return false;
Qmenu *qm = qmenu_of(qb);
return qm->id == id;
}
Qform *qform_create(int id)
{
Qform *qf = (Qform *)malloc(sizeof(Qform));
qblock_init(&qf->qblock, Qblock_type_qform);
qf->ncurses_form = NULL;
qf->ncurses_fields[0] = NULL;
qf->fields_count = 0;
qf->id = id;
return qf;
}
static void qform_free(Qform *qf)
{
curs_set(0);
unpost_form(qf->ncurses_form);
free_form(qf->ncurses_form);
for (Usz i = 0; i < qf->fields_count; ++i) {
free_field(qf->ncurses_fields[i]);
}
free(qf);
}
int qform_id(Qform const *qf)
{
return qf->id;
}
Qform *qform_of(Qblock *qb)
{
return ORCA_CONTAINER_OF(qb, Qform, qblock);
}
void qform_set_title(Qform *qf, char const *title)
{
qblock_set_title(&qf->qblock, title);
}
void qform_add_line_input(Qform *qf, int id, char const *initial)
{
FIELD *f = new_field(1, 30, 0, 0, 0, 0);
if (initial)
set_field_buffer(f, 0, initial);
set_field_userptr(f, (void *)(intptr_t)(id));
field_opts_off(f, O_WRAP | O_BLANK | O_STATIC);
qf->ncurses_fields[qf->fields_count] = f;
++qf->fields_count;
qf->ncurses_fields[qf->fields_count] = NULL;
}
void qform_push_to_nav(Qform *qf)
{
qf->ncurses_form = new_form(qf->ncurses_fields);
int form_min_h, form_min_w;
scale_form(qf->ncurses_form, &form_min_h, &form_min_w);
qnav_stack_push(&qf->qblock, form_min_h, form_min_w);
set_form_win(qf->ncurses_form, qf->qblock.outer_window);
set_form_sub(qf->ncurses_form, qf->qblock.content_window);
post_form(qf->ncurses_form);
// quick'n'dirty cursor change for now
curs_set(1);
form_driver(qf->ncurses_form, REQ_END_LINE);
}
void qform_single_line_input(int id, char const *title, char const *initial)
{
Qform *qf = qform_create(id);
qform_set_title(qf, title);
qform_add_line_input(qf, 1, initial);
qform_push_to_nav(qf);
}
bool qform_drive(Qform *qf, int key, Qform_action *out_action)
{
switch (key) {
case 27:
out_action->any.type = Qform_action_type_canceled;
return true;
case CTRL_PLUS('a'):
form_driver(qf->ncurses_form, REQ_BEG_LINE);
return false;
case CTRL_PLUS('e'):
form_driver(qf->ncurses_form, REQ_END_LINE);
return false;
case CTRL_PLUS('b'):
form_driver(qf->ncurses_form, REQ_PREV_CHAR);
return false;
case CTRL_PLUS('f'):
form_driver(qf->ncurses_form, REQ_NEXT_CHAR);
return false;
case CTRL_PLUS('k'):
form_driver(qf->ncurses_form, REQ_CLR_EOL);
return false;
case KEY_RIGHT:
form_driver(qf->ncurses_form, REQ_RIGHT_CHAR);
return false;
case KEY_LEFT:
form_driver(qf->ncurses_form, REQ_LEFT_CHAR);
return false;
case 127: // backspace in terminal.app, apparently
case KEY_BACKSPACE:
case CTRL_PLUS('h'):
form_driver(qf->ncurses_form, REQ_DEL_PREV);
return false;
case '\r':
case KEY_ENTER:
out_action->any.type = Qform_action_type_submitted;
return true;
}
form_driver(qf->ncurses_form, key);
return false;
}
static Usz size_without_trailing_spaces(char const *str)
{
Usz size = strlen(str);
for (;;) {
if (size == 0)
break;
if (!isspace(str[size - 1]))
break;
--size;
}
return size;
}
static FIELD *qform_find_field(Qform const *qf, int id)
{
Usz count = qf->fields_count;
for (Usz i = 0; i < count; ++i) {
FIELD *f = qf->ncurses_fields[i];
if ((int)(intptr_t)field_userptr(f) == id)
return f;
}
return NULL;
}
bool qform_get_text_line(Qform const *qf, int id, oso **out)
{
FIELD *f = qform_find_field(qf, id);
if (!f)
return false;
form_driver(qf->ncurses_form, REQ_VALIDATION);
char *buf = field_buffer(f, 0);
if (!buf)
return false;
Usz trimmed = size_without_trailing_spaces(buf);
osoputlen(out, buf, trimmed);
return true;
}
bool qform_get_single_text_line(Qform const *qf, struct oso **out)
{
return qform_get_text_line(qf, 1, out);
}
oso *qform_get_nonempty_single_line_input(Qform *qf)
{
oso *s = NULL;
if (qform_get_text_line(qf, 1, &s) && osolen(s) > 0)
return s;
osofree(s);
return NULL;
}