Skip to content

Instantly share code, notes, and snippets.

@tarruda
Last active August 29, 2015 14:10
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save tarruda/58e04f33406ac1198792 to your computer and use it in GitHub Desktop.
Save tarruda/58e04f33406ac1198792 to your computer and use it in GitHub Desktop.
#include <assert.h>
#include <stdbool.h>
#include <stdio.h>
#include <unistd.h>
#include <termios.h>
#include <termkey.h>
#include <tickit.h>
#include "nvim/vim.h"
#include "nvim/mbyte.h"
#include "nvim/ui.h"
#include "nvim/os/rstream_defs.h"
#include "nvim/os/rstream.h"
#include "nvim/os/input.h"
#include "nvim/api/vim.h"
#include "nvim/os/event.h"
#define READ_BUFFER_SIZE 0xfff
typedef struct {
char data[8];
} Cell;
static int in_fd, out_fd;
struct termios normal, raw;
static UI self;
static RBuffer *read_buffer;
static RStream *read_stream;
static uv_signal_t swinch;
static TermKey *tk;
static TickitTerm *tt;
static TickitRect scrollrect;
static int width, height, row, col;
static Cell **screen = NULL;
static void clear_cells(Cell *ptr, int count)
{
while (count--) {
ptr->data[0] = ' ';
ptr->data[1] = 0;
ptr++;
}
}
static void print_cells(Cell *ptr, int count)
{
while (count--) {
tickit_term_print(tt, (ptr++)->data);
col++;
}
}
static void clear_block(int top, int lines, int left, int cols)
{
int bot = top + lines;
for (int i = top; i < bot; i++) {
tickit_term_goto(tt, i, left);
clear_cells(screen[i] + left, cols);
print_cells(screen[i] + left, cols);
}
}
static void clear_scroll_region(void)
{
clear_block(scrollrect.top, scrollrect.lines, scrollrect.left,
scrollrect.cols);
}
static void redraw_scroll_region(void)
{
int top = scrollrect.top;
int bot = tickit_rect_bottom(&scrollrect) - 1;
for (int i = top; i <= bot; i++) {
tickit_term_goto(tt, i, scrollrect.left);
print_cells(screen[i] + scrollrect.left, scrollrect.cols);
}
}
static void shift_scroll_region(int count) {
int top = scrollrect.top;
int bot = tickit_rect_bottom(&scrollrect) - 1;
int left = scrollrect.left;
int start, stop, step;
if (count > 0) {
start = top;
stop = bot - count + 1;
step = 1;
} else {
start = bot;
stop = top - count - 1;
step = -1;
}
int i;
// Shift row sections
for (i = start; i != stop; i += step) {
Cell *target_row = screen[i] + left;
Cell *source_row = screen[i + count] + left;
memcpy(target_row, source_row, sizeof(Cell) * (size_t)scrollrect.cols);
}
// Clear invalid row sections
for (stop += count; i != stop; i += step) {
clear_cells(screen[i] + left, scrollrect.cols);
}
}
static void scroll(int count)
{
// Update internal screen
shift_scroll_region(count);
// Update the terminal, first try with tickit_term_scrollrect which will
// try to do it in the most efficient way for the terminal
if (!tickit_term_scrollrect(tt, scrollrect.top, scrollrect.left,
scrollrect.lines, scrollrect.cols, count, 0)) {
// Failing that, redraw the whole scrolled region
redraw_scroll_region();
}
// Restore the cursor position
tickit_term_goto(tt, row, col);
}
static void forward_simple_utf8(TermKeyKey *key)
{
input_enqueue((String) {.data = key->utf8, .size = strlen(key->utf8)});
}
static void forward_modified_utf8(TermKeyKey *key)
{
size_t size;
char name[128];
#define SET(str) size = sizeof(str); memcpy(name, str, size)
switch (key->code.sym) {
// Handle names that differ from vim's internal representation
case TERMKEY_SYM_BACKSPACE:
SET("<Bs>");
break;
case TERMKEY_SYM_TAB:
SET("<Tab>");
break;
case TERMKEY_SYM_ENTER:
SET("<Cr>");
break;
case TERMKEY_SYM_ESCAPE:
SET("<Esc>");
break;
case TERMKEY_SYM_SPACE:
SET("<Space>");
break;
case TERMKEY_SYM_DEL:
SET("<Del>");
break;
default:
size = termkey_strfkey(tk, name, sizeof(name), key,
TERMKEY_FORMAT_VIM) + 1;
break;
}
input_enqueue((String){.data = name, .size = size - 1});
}
static void forward_mouse_event(TermKeyKey *key)
{
int button, line, column;
TermKeyMouseEvent ev;
termkey_interpret_mouse(tk, key, &ev, &button, &line, &column);
// TODO
}
static void read_cb(RStream *rstream, void *data, bool at_eof)
{
if (at_eof) {
input_done();
return;
}
char *bytes = rbuffer_read_ptr(read_buffer);
size_t len = rbuffer_pending(read_buffer);
termkey_push_bytes(tk, bytes, len);
rbuffer_consumed(read_buffer, len);
TermKeyKey key;
if (termkey_getkey_force(tk, &key) == TERMKEY_RES_KEY) {
if (key.type == TERMKEY_TYPE_MOUSE) {
forward_mouse_event(&key);
} else if (key.type == TERMKEY_TYPE_UNICODE && !key.modifiers) {
forward_simple_utf8(&key);
} else if (key.type == TERMKEY_TYPE_UNICODE ||
key.type == TERMKEY_TYPE_FUNCTION ||
key.type == TERMKEY_TYPE_KEYSYM) {
forward_modified_utf8(&key);
}
}
}
static void terminal_resized(Event ev)
{
int new_width, new_height;
tickit_term_refresh_size(tt);
tickit_term_get_size(tt, &new_height, &new_width);
vim_resize(new_width, new_height);
}
static void sigwinch_cb(uv_signal_t *handle, int signum)
{
// Queue the event because resizing can result in recursive event_poll calls
event_push((Event) { .handler = terminal_resized }, false);
}
static bool termios_setup(void)
{
if (tcgetattr(in_fd, &normal)) {
return false;
}
// save the current state
raw = normal;
raw.c_iflag &= (unsigned)(~(IGNBRK | BRKINT | PARMRK | ISTRIP | INLCR |
IGNCR | ICRNL | IXON));
raw.c_oflag &= (unsigned)(~OPOST);
raw.c_lflag &= (unsigned)(~(ECHO | ECHONL | ICANON | ISIG | IEXTEN));
raw.c_cflag &= (unsigned)(~(CSIZE | PARENB));
raw.c_cflag |= CS8;
tcsetattr(in_fd, TCSANOW, &raw);
return true;
}
static void termios_teardown(void)
{
tcsetattr(in_fd, TCSANOW, &normal);
}
static void tui_resize(int new_width, int new_height)
{
if (screen) {
for (int i = 0; i < height; i++) {
free(screen[i]);
}
free(screen);
}
screen = malloc((size_t)new_height * sizeof(Cell *));
for (int i = 0; i < new_height; i++) {
// utf-8 characters can occupy up to 6 bytes, so allocate for the worst
// possible scenario
screen[i] = malloc((size_t)new_width * sizeof(Cell));
clear_cells(screen[i], new_width);
}
tickit_term_set_size(tt, new_height, new_width);
tickit_rect_init_sized(&scrollrect, 0, 0, new_height, new_width);
row = col = 0;
// TODO If the new dimensions are bigger than the terminal, setup some
// kind of scrolling. If smaller, fill the extra area with dots(like tmux)
height = new_height;
width = new_width;
}
static void tui_clear(void)
{
// Update internal screen
for (int i = 0; i < height; i++) {
clear_cells(screen[i], width);
}
// Update terminal
tickit_term_clear(tt);
}
static void tui_eol_clear(void)
{
clear_block(row, 1, col, width - col);
}
static void tui_cursor_goto(int new_row, int new_col)
{
row = new_row;
col = new_col;
tickit_term_goto(tt, row, col);
}
static void tui_cursor_on(void)
{
tickit_term_setctl_int(tt, TICKIT_TERMCTL_CURSORVIS, true);
}
static void tui_cursor_off(void)
{
tickit_term_setctl_int(tt, TICKIT_TERMCTL_CURSORVIS, false);
}
static void tui_mouse_on(void)
{
tickit_term_setctl_int(tt, TICKIT_TERMCTL_MOUSE, TICKIT_TERM_MOUSEMODE_CLICK);
}
static void tui_mouse_off(void)
{
tickit_term_setctl_int(tt, TICKIT_TERMCTL_MOUSE, TICKIT_TERM_MOUSEMODE_OFF);
}
static void tui_insert_mode(void)
{
if (!tickit_term_setctl_int(tt, TICKIT_TERMCTL_CURSORSHAPE,
TICKIT_TERM_CURSORSHAPE_LEFT_BAR)) {
tickit_term_setctl_int(tt, TICKIT_TERMCTL_CURSORSHAPE,
TICKIT_TERM_CURSORSHAPE_UNDER);
}
}
static void tui_normal_mode(void)
{
tickit_term_setctl_int(tt, TICKIT_TERMCTL_CURSORSHAPE,
TICKIT_TERM_CURSORSHAPE_BLOCK);
}
static void tui_set_scroll_region(int top, int bot, int left, int right)
{
scrollrect.top = top;
scrollrect.lines = bot - top + 1;
scrollrect.left = left;
scrollrect.cols = right - left + 1;
}
static void tui_scroll_down(int count)
{
if (count > scrollrect.lines) {
// Scrolled out of the region, clear it
clear_scroll_region();
} else {
scroll(count);
}
}
static void tui_scroll_up(int count)
{
if (count > scrollrect.lines) {
// Scrolled out of the region, clear it
clear_scroll_region();
} else {
scroll(-count);
}
}
static void tui_highlight_set(HlFlags flags, int fg, int bg)
{
}
static void tui_print(uint8_t *data)
{
do {
size_t len = (size_t)utf_byte2len(*data);
Cell *cell = screen[row] + col;
memcpy(cell->data, data, len);
cell->data[len] = 0;
print_cells(cell, 1);
if (mb_string2cells((const char_u*)cell->data) == 2) {
col++;
(screen[row] + col)->data[0] = 0;
}
data += len;
} while (*data);
}
static void tui_bell(void)
{
}
static void tui_visual_bell(void)
{
}
static void tui_flush(void)
{
tickit_term_flush(tt);
}
static void tui_suspend(void)
{
}
static void tui_set_title(char *title)
{
}
static void tui_set_icon(char *icon)
{
}
static bool tui_setup(void)
{
// read input from stderr if stdin is not a tty
in_fd = isatty(0) ? 0 : (isatty(2) ? 2 : -1);
// write output to stderr if stdout is not a tty
out_fd = isatty(1) ? 1 : (isatty(2) ? 2 : -1);
if (in_fd == -1 || out_fd == -1) {
fprintf(stderr, "Stdio is not connected to a tty\n");
return false;
}
// set tty to raw mode
if (!termios_setup()) {
return false;
}
// libtickit/libtermkey setup
tk = termkey_new(-1, 0);
tt = tickit_term_new();
if (!tk || !tt) {
fprintf(stderr, "Failed to initialize tty libraries: %s\n", strerror(errno));
return false;
}
// initial setup of libtickit
tickit_term_set_output_fd(tt, out_fd);
tickit_term_await_started(tt, &(const struct timeval){
.tv_sec = 0, .tv_usec = 50000 });
tickit_term_setctl_int(tt, TICKIT_TERMCTL_ALTSCREEN, 1);
Event dummy;
terminal_resized(dummy);
tickit_term_clear(tt);
// listen input fd
read_buffer = rbuffer_new(READ_BUFFER_SIZE);
read_stream = rstream_new(read_cb, read_buffer, NULL);
rstream_set_file(read_stream, in_fd);
rstream_start(read_stream);
// listen for SIGWINCH
uv_signal_init(uv_default_loop(), &swinch);
uv_signal_start(&swinch, sigwinch_cb, SIGWINCH);
return true;
}
static void tui_teardown(void)
{
rstream_stop(read_stream);
rstream_free(read_stream);
uv_signal_stop(&swinch);
uv_close((uv_handle_t *)&swinch, NULL);
tickit_term_destroy(tt);
termios_teardown();
}
UI *ui_create(void)
{
self.resize = tui_resize;
self.clear = tui_clear;
self.eol_clear = tui_eol_clear;
self.cursor_goto = tui_cursor_goto;
self.ui_cursor_on = tui_cursor_on;
self.ui_cursor_off = tui_cursor_off;
self.mouse_on = tui_mouse_on;
self.mouse_off = tui_mouse_off;
self.insert_mode = tui_insert_mode;
self.normal_mode = tui_normal_mode;
self.set_scroll_region = tui_set_scroll_region;
self.scroll_down = tui_scroll_down;
self.scroll_up = tui_scroll_up;
self.highlight_set = tui_highlight_set;
self.print = tui_print;
self.bell = tui_bell;
self.visual_bell = tui_visual_bell;
self.flush = tui_flush;
self.suspend = tui_suspend;
self.set_title = tui_set_title;
self.set_icon = tui_set_icon;
self.setup = tui_setup;
self.teardown = tui_teardown;
return &self;
}
struct ui_t {
void (*resize)(int rows, int columns);
void (*clear)(void);
void (*eol_clear)(void);
void (*cursor_goto)(int row, int col);
void (*cursor_on)(void);
void (*cursor_off)(void);
void (*mouse_on)(void);
void (*mouse_off)(void);
void (*insert_mode)(void);
void (*normal_mode)(void);
void (*set_scroll_region)(int top, int bot, int left, int right);
void (*scroll_down)(int count);
void (*scroll_up)(int count);
void (*highlight_set)(HlFlags flags, int fg, int bg);
void (*puts)(char *data, size_t size);
void (*bell)(void);
void (*visual_bell)(void);
void (*flush)(void);
void (*suspend)(void);
void (*set_title)(char *title);
void (*set_icon)(char *icon);
bool (*setup)(void);
void (*teardown)(void);
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment