diff options
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); +} + |
