diff options
| author | Marc André Tanner <mat@brain-dump.org> | 2015-03-30 23:04:54 +0200 |
|---|---|---|
| committer | Marc André Tanner <mat@brain-dump.org> | 2015-04-03 23:52:06 +0200 |
| commit | 9915b9fd0b8e59bda9e334eb9485c473b06055c9 (patch) | |
| tree | 768f0a04df7e6cfc6d1379de8f19d1d4acdbf3a2 /ui-curses.c | |
| parent | b3b1abc76a832e53159b1191120bef56fe3a7041 (diff) | |
| download | vis-9915b9fd0b8e59bda9e334eb9485c473b06055c9.tar.gz vis-9915b9fd0b8e59bda9e334eb9485c473b06055c9.tar.xz | |
Preliminary user interface separation
In theory only ui-curses.[hc] should depend on curses, however in
practice keyboard input is still handled in vis.c. Furthermore the
syntax definitions as well as keyboard bindings and selection code
in window.c still depends on some curses constants.
There is also a slight regression in that the window status bar
does not show the current mode name. This and related global state
should be eliminated in the future.
Diffstat (limited to 'ui-curses.c')
| -rw-r--r-- | ui-curses.c | 568 |
1 files changed, 568 insertions, 0 deletions
diff --git a/ui-curses.c b/ui-curses.c new file mode 100644 index 0000000..4b351e5 --- /dev/null +++ b/ui-curses.c @@ -0,0 +1,568 @@ +#include <stdlib.h> +#include <string.h> +#include <signal.h> +#include <curses.h> +#include <locale.h> +#include <sys/ioctl.h> + +#include "ui.h" +#include "ui-curses.h" +#include "util.h" + +#ifdef NCURSES_VERSION +# ifndef NCURSES_EXT_COLORS +# define NCURSES_EXT_COLORS 0 +# endif +# if !NCURSES_EXT_COLORS +# define MAX_COLOR_PAIRS 256 +# endif +#endif +#ifndef MAX_COLOR_PAIRS +# define MAX_COLOR_PAIRS COLOR_PAIRS +#endif + +#if 0 +#define wresize(win, y, x) do { \ + if (wresize(win, y, x) == ERR) { \ + printf("ERROR resizing: %d x %d\n", x, y); \ + } else { \ + printf("OK resizing: %d x %d\n", x, y); \ + } \ + fflush(stdout); \ +} while (0); + +#define mvwin(win, y, x) do { \ + if (mvwin(win, y, x) == ERR) { \ + printf("ERROR moving: %d x %d\n", x, y); \ + } else { \ + printf("OK moving: %d x %d\n", x, y); \ + } \ + fflush(stdout); \ +} while (0); +#endif + +typedef struct UiCursesWin UiCursesWin; + +typedef struct { + Ui ui; /* generic ui interface, has to be the first struct member */ + Editor *ed; /* editor instance to which this ui belongs */ + UiCursesWin *windows; /* all windows managed by this ui */ + UiCursesWin *selwin; /* the currently selected layout */ + char prompt_title[255]; /* prompt_title[0] == '\0' if prompt isn't shown */ + UiCursesWin *prompt_win; /* like a normal window but without a status bar */ + char info[255]; /* info message displayed at the bottom of the screen */ + int width, height; /* terminal dimensions available for all windows */ + enum UiLayout layout; /* whether windows are displayed horizontally or vertically */ +} UiCurses; + +struct UiCursesWin { + UiWin uiwin; /* generic interface, has to be the first struct member */ + UiCurses *ui; /* ui which manages this window */ + Text *text; /* underlying text management */ + Win *view; /* current viewport */ + WINDOW *win; /* curses window for the text area */ + WINDOW *winstatus; /* curses window for the status bar */ + WINDOW *winside; /* curses window for the side bar (line numbers) */ + int width, height; /* window dimension including status bar */ + int x, y; /* window position */ + int sidebar_width; /* width of the sidebar showing line numbers etc. */ + UiCursesWin *next, *prev; /* pointers to neighbouring windows */ + enum UiOption options; /* display settings for this window */ +}; + +static unsigned int color_hash(short fg, short bg) { + if (fg == -1) + fg = COLORS; + if (bg == -1) + bg = COLORS + 1; + return fg * (COLORS + 2) + bg; +} + +static short color_get(short fg, short bg) { + static bool has_default_colors; + static short *color2palette, default_fg, default_bg; + static short color_pairs_max, color_pair_current; + + if (!color2palette) { + pair_content(0, &default_fg, &default_bg); + if (default_fg == -1) + default_fg = COLOR_WHITE; + if (default_bg == -1) + default_bg = COLOR_BLACK; + has_default_colors = (use_default_colors() == OK); + color_pairs_max = MIN(COLOR_PAIRS, MAX_COLOR_PAIRS); + if (COLORS) + color2palette = calloc((COLORS + 2) * (COLORS + 2), sizeof(short)); + } + + if (fg >= COLORS) + fg = default_fg; + if (bg >= COLORS) + bg = default_bg; + + if (!has_default_colors) { + if (fg == -1) + fg = default_fg; + if (bg == -1) + bg = default_bg; + } + + if (!color2palette || (fg == -1 && bg == -1)) + return 0; + + unsigned int index = color_hash(fg, bg); + if (color2palette[index] == 0) { + short oldfg, oldbg; + if (++color_pair_current >= color_pairs_max) + color_pair_current = 1; + pair_content(color_pair_current, &oldfg, &oldbg); + unsigned int old_index = color_hash(oldfg, oldbg); + if (init_pair(color_pair_current, fg, bg) == OK) { + color2palette[old_index] = 0; + color2palette[index] = color_pair_current; + } + } + + return color2palette[index]; +} + +static void ui_window_resize(UiCursesWin *win, int width, int height) { + win->width = width; + win->height = height; + if (win->winstatus) + wresize(win->winstatus, 1, width); + wresize(win->win, win->winstatus ? height - 1 : height, width - win->sidebar_width); + if (win->winside) + wresize(win->winside, height-1, win->sidebar_width); + window_resize(win->view, width - win->sidebar_width, win->winstatus ? height - 1 : height); +} + +static void ui_window_move(UiCursesWin *win, int x, int y) { + win->x = x; + win->y = y; + mvwin(win->win, y, x + win->sidebar_width); + if (win->winside) + mvwin(win->winside, y, x); + if (win->winstatus) + mvwin(win->winstatus, y + win->height - 1, x); +} + +static void ui_window_draw_status(UiWin *w) { + UiCursesWin *win = (UiCursesWin*)w; + if (!win->winstatus) + return; + UiCurses *uic = win->ui; + Editor *vis = uic->ed; + bool focused = uic->selwin == win; + const char *filename = text_filename_get(win->text); + CursorPos pos = window_cursor_getpos(win->view); + wattrset(win->winstatus, focused ? A_REVERSE|A_BOLD : A_REVERSE); + mvwhline(win->winstatus, 0, 0, ' ', win->width); + mvwprintw(win->winstatus, 0, 0, "%s %s %s %s", + "", // TODO mode->name && mode->name[0] == '-' ? mode->name : "", + filename ? filename : "[No Name]", + text_modified(win->text) ? "[+]" : "", + vis->recording ? "recording": ""); + char buf[win->width + 1]; + int len = snprintf(buf, win->width, "%zd, %zd", pos.line, pos.col); + if (len > 0) { + buf[len] = '\0'; + mvwaddstr(win->winstatus, 0, win->width - len - 1, buf); + } +} + +static void ui_window_draw(UiWin *w) { + UiCursesWin *win = (UiCursesWin*)w; + if (win->winstatus) + ui_window_draw_status((UiWin*)win); + window_draw(win->view); + window_cursor_to(win->view, window_cursor_get(win->view)); +} + +static void ui_window_reload(UiWin *w, Text *text) { + UiCursesWin *win = (UiCursesWin*)w; + win->text = text; + win->sidebar_width = 0; + ui_window_draw(w); +} + +static void ui_window_draw_sidebar(UiCursesWin *win, const Line *line) { + if (!win->winside) + return; + int sidebar_width = snprintf(NULL, 0, "%zd", line->lineno + win->height - 2) + 1; + if (win->sidebar_width != sidebar_width) { + win->sidebar_width = sidebar_width; + ui_window_resize(win, win->width, win->height); + ui_window_move(win, win->x, win->y); + } else { + int i = 0; + size_t prev_lineno = 0; + werase(win->winside); + for (const Line *l = line; l; l = l->next, i++) { + if (l->lineno != prev_lineno) + mvwprintw(win->winside, i, 0, "%*u", sidebar_width-1, l->lineno); + prev_lineno = l->lineno; + } + mvwvline(win->winside, 0, sidebar_width-1, ACS_VLINE, win->height-1); + } +} + +static void ui_window_update(UiCursesWin *win) { + if (win->winstatus) + wnoutrefresh(win->winstatus); + if (win->winside) + wnoutrefresh(win->winside); + wnoutrefresh(win->win); +} + +static void update(Ui *ui) { + UiCurses *uic = (UiCurses*)ui; + for (UiCursesWin *win = uic->windows; win; win = win->next) { + if (win != uic->selwin) + ui_window_update(win); + } + + if (uic->selwin) + ui_window_update(uic->selwin); + if (uic->prompt_title[0]) { + wnoutrefresh(uic->prompt_win->win); + ui_window_update(uic->prompt_win); + } + doupdate(); +} + +static void arrange(Ui *ui, enum UiLayout layout) { + UiCurses *uic = (UiCurses*)ui; + uic->layout = layout; + int n = 0, x = 0, y = 0; + for (UiCursesWin *win = uic->windows; win; win = win->next) + n++; + int max_height = uic->height - !!(uic->prompt_title[0] || uic->info[0]); + int width = (uic->width / MAX(1, n)) - 1; + int height = max_height / MAX(1, n); + for (UiCursesWin *win = uic->windows; win; win = win->next) { + if (layout == UI_LAYOUT_HORIZONTAL) { + ui_window_resize(win, uic->width, win->next ? height : max_height - y); + ui_window_move(win, x, y); + y += height; + } else { + ui_window_resize(win, win->next ? width : uic->width - x, max_height); + ui_window_move(win, x, y); + x += width; + if (win->next) + mvvline(0, x++, ACS_VLINE, max_height); + } + } +} + +static void draw(Ui *ui) { + UiCurses *uic = (UiCurses*)ui; + erase(); + arrange(ui, uic->layout); + + for (UiCursesWin *win = uic->windows; win; win = win->next) + ui_window_draw((UiWin*)win); + + if (uic->info[0]) { + attrset(A_BOLD); + mvaddstr(uic->height-1, 0, uic->info); + } + + if (uic->prompt_title[0]) { + attrset(A_NORMAL); + mvaddstr(uic->height-1, 0, uic->prompt_title); + ui_window_draw((UiWin*)uic->prompt_win); + } + + wnoutrefresh(stdscr); +} + +static void ui_resize_to(Ui *ui, int width, int height) { + UiCurses *uic = (UiCurses*)ui; + uic->width = width; + uic->height = height; + if (uic->prompt_title[0]) { + size_t title_width = strlen(uic->prompt_title); + ui_window_resize(uic->prompt_win, width - title_width, 1); + ui_window_move(uic->prompt_win, title_width, height-1); + } + draw(ui); +} + +static void ui_resize(Ui *ui) { + struct winsize ws; + int width, height; + + if (ioctl(0, TIOCGWINSZ, &ws) == -1) { + getmaxyx(stdscr, height, width); + } else { + width = ws.ws_col; + height = ws.ws_row; + } + + resizeterm(height, width); + wresize(stdscr, height, width); + ui_resize_to(ui, width, height); +} + +static void ui_window_free(UiWin *w) { + UiCursesWin *win = (UiCursesWin*)w; + if (!win) + return; + UiCurses *uic = win->ui; + if (win->prev) + win->prev->next = win->next; + if (win->next) + win->next->prev = win->prev; + if (uic->windows == win) + uic->windows = win->next; + if (uic->selwin == win) + uic->selwin = NULL; + win->next = win->prev = NULL; + if (win->winstatus) + delwin(win->winstatus); + if (win->winside) + delwin(win->winside); + if (win->win) + delwin(win->win); + window_free(win->view); + free(win); +} + +static Win *ui_window_view_get(UiWin *win) { + UiCursesWin *cwin = (UiCursesWin*)win; + return cwin->view; +} + +static void ui_window_cursor_to(UiWin *w, int x, int y) { + UiCursesWin *win = (UiCursesWin*)w; + wmove(win->win, y, x); + ui_window_draw_status(w); +} + +static void ui_window_draw_text(UiWin *w, const Line *line) { + UiCursesWin *win = (UiCursesWin*)w; + wmove(win->win, 0, 0); + attr_t attr = 0; + for (const Line *l = line; l; l = l->next) { + /* add a single space in an otherwise empty line to make + * the selection cohorent */ + if (l->width == 0) + waddch(win->win, ' '); + + for (int x = 0; x < l->width; x++) { + attr_t newattr = l->cells[x].attr; + if (newattr != attr) { + wattrset(win->win, newattr); + attr = newattr; + } + waddstr(win->win, l->cells[x].data); + } + wclrtoeol(win->win); + } + wclrtobot(win->win); + + ui_window_draw_sidebar(win, line); +} + +static void ui_window_focus(UiWin *w) { + UiCursesWin *win = (UiCursesWin*)w; + UiCursesWin *oldsel = win->ui->selwin; + win->ui->selwin = win; + if (oldsel) + ui_window_draw_status((UiWin*)oldsel); + ui_window_draw_status(w); +} + +static void ui_window_options(UiWin *w, enum UiOption options) { + UiCursesWin *win = (UiCursesWin*)w; + win->options = options; + switch (options) { + case UI_OPTION_LINE_NUMBERS_NONE: + if (win->winside) { + delwin(win->winside); + win->winside = NULL; + win->sidebar_width = 0; + } + break; + case UI_OPTION_LINE_NUMBERS_ABSOLUTE: + if (!win->winside) + win->winside = newwin(1, 1, 1, 1); + break; + } + ui_window_draw(w); +} + +static UiWin *ui_window_new(Ui *ui, Text *text) { + UiCurses *uic = (UiCurses*)ui; + UiCursesWin *win = calloc(1, sizeof(UiCursesWin)); + if (!win) + return NULL; + + win->uiwin = (UiWin) { + .draw = ui_window_draw, + .draw_status = ui_window_draw_status, + .draw_text = ui_window_draw_text, + .cursor_to = ui_window_cursor_to, + .view_get = ui_window_view_get, + .options = ui_window_options, + .reload = ui_window_reload, + }; + + if (!(win->view = window_new(text, &win->uiwin, uic->width, uic->height)) || + !(win->win = newwin(0, 0, 0, 0)) || + !(win->winstatus = newwin(1, 0, 0, 0))) { + ui_window_free((UiWin*)win); + return NULL; + } + + win->ui = uic; + win->text = text; + if (uic->windows) + uic->windows->prev = win; + win->next = uic->windows; + uic->windows = win; + + return &win->uiwin; +} + +static void info(Ui *ui, const char *msg, va_list ap) { + UiCurses *uic = (UiCurses*)ui; + vsnprintf(uic->info, sizeof(uic->info), msg, ap); + draw(ui); +} + +static void info_hide(Ui *ui) { + UiCurses *uic = (UiCurses*)ui; + if (uic->info[0]) { + uic->info[0] = '\0'; + draw(ui); + } +} + +static UiWin *prompt_new(Ui *ui, Text *text) { + UiCurses *uic = (UiCurses*)ui; + if (uic->prompt_win) + return (UiWin*)uic->prompt_win; + UiWin *uiwin = ui_window_new(ui, text); + UiCursesWin *win = (UiCursesWin*)uiwin; + if (!win) + return NULL; + uic->windows = win->next; + if (uic->windows) + uic->windows->prev = NULL; + if (win->winstatus) + delwin(win->winstatus); + if (win->winside) + delwin(win->winside); + win->winstatus = NULL; + win->winside = NULL; + uic->prompt_win = win; + return uiwin; +} + +static void prompt(Ui *ui, const char *title, const char *text) { + UiCurses *uic = (UiCurses*)ui; + if (uic->prompt_title[0]) + return; + size_t text_len = strlen(text); + strncpy(uic->prompt_title, title, sizeof(uic->prompt_title)-1); + text_insert(uic->prompt_win->text, 0, text, text_len); + window_cursor_to(uic->prompt_win->view, text_len); + ui_resize_to(ui, uic->width, uic->height); +} + +static char *prompt_input(Ui *ui) { + UiCurses *uic = (UiCurses*)ui; + if (!uic->prompt_win) + return NULL; + Text *text = uic->prompt_win->text; + char *buf = malloc(text_size(text) + 1); + if (!buf) + return NULL; + size_t len = text_bytes_get(text, 0, text_size(text), buf); + buf[len] = '\0'; + return buf; +} + +static void prompt_hide(Ui *ui) { + UiCurses *uic = (UiCurses*)ui; + uic->prompt_title[0] = '\0'; + if (uic->prompt_win) { + while (text_undo(uic->prompt_win->text) != EPOS); + window_cursor_to(uic->prompt_win->view, 0); + } + ui_resize_to(ui, uic->width, uic->height); +} + +static bool ui_init(Ui *ui, Editor *ed) { + UiCurses *uic = (UiCurses*)ui; + uic->ed = ed; + return true; +} + +static void ui_suspend(Ui *ui) { + endwin(); + raise(SIGSTOP); +} + +Ui *ui_curses_new(void) { + setlocale(LC_CTYPE, ""); + if (!getenv("ESCDELAY")) + set_escdelay(50); + char *term = getenv("TERM"); + if (!term) + term = "xterm"; + if (!newterm(term, stderr, stdin)) + return NULL; + start_color(); + raw(); + noecho(); + keypad(stdscr, TRUE); + meta(stdscr, TRUE); + /* needed because we use getch() which implicitly calls refresh() which + would clear the screen (overwrite it with an empty / unused stdscr */ + refresh(); + + UiCurses *uic = calloc(1, sizeof(UiCurses)); + Ui *ui = (Ui*)uic; + if (!uic) + return NULL; + + *ui = (Ui) { + .init = ui_init, + .free = ui_curses_free, + .suspend = ui_suspend, + .resume = ui_resize, + .resize = ui_resize, + .update = update, + .window_new = ui_window_new, + .window_free = ui_window_free, + .window_focus = ui_window_focus, + .prompt_new = prompt_new, + .prompt = prompt, + .prompt_input = prompt_input, + .prompt_hide = prompt_hide, + .draw = draw, + .arrange = arrange, + .info = info, + .info_hide = info_hide, + .color_get = color_get, + }; + + ui_resize(ui); + + return ui; +} + +void ui_curses_free(Ui *ui) { + UiCurses *uic = (UiCurses*)ui; + if (!uic) + return; + ui_window_free((UiWin*)uic->prompt_win); + while (uic->windows) + ui_window_free((UiWin*)uic->windows); + endwin(); + free(uic); +} + |
