/* Bim - A Text Editor
 *
 * Copyright (C) 2012-2021 K. Lange
 *
 * Permission to use, copy, modify, and/or distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */
#include "bim.h"
#include <kuroko/kuroko.h>
#include <kuroko/vm.h>
#include <kuroko/debug.h>
#include <kuroko/util.h>
#include <kuroko/scanner.h>

global_config_t global_config = {
	/* State */
	.term_width = 0,
	.term_height = 0,
	.bottom_size = 2,
	.yanks = NULL,
	.yank_count = 0,
	.yank_is_full_lines = 0,
	.tty_in = STDIN_FILENO,
	.bimrc_path = "~/.bim3rc",
	.syntax_fallback = NULL, /* Syntax to fall back to if no other match applies */
	.search = NULL,
	.overlay_mode = OVERLAY_MODE_NONE,
	.command_buffer = NULL,
	.command_offset = 0,
	.command_col_no = 0,
	.history_point = -1,
	/* Bitset starts here */
	.highlight_on_open = 1,
	.initial_file_is_read_only = 0,
	.go_to_line = 1,
	.break_from_selection = 1,
	/* Terminal capabilities */
	.can_scroll = 1,
	.can_hideshow = 1,
	.can_altscreen = 1,
	.can_mouse = 1,
	.can_unicode = 1,
	.can_bright = 1,
	.can_title = 1,
	.can_bce = 1,
	.can_24bit = 1, /* can use 24-bit color */
	.can_256color = 1, /* can use 265 colors */
	.can_italic = 1, /* can use italics (without inverting) */
	.can_insert = 0, /* ^[[L */
	.can_bracketedpaste = 0, /* puts escapes before and after pasted stuff */
	.can_sgrmouse = 0, /* Whether SGR mouse mode is availabe (large coordinates) */
	/* Configuration options */
	.history_enabled = 1,
	.highlight_parens = 1, /* highlight parens/braces when cursor moves */
	.smart_case = 1, /* smart case-sensitivity while searching */
	.highlight_current_line = 1,
	.shift_scrolling = 1, /* shift rather than moving cursor*/
	.check_git = 0,
	.color_gutter = 1, /* shows modified lines */
	.relative_lines = 0,
	.numbers = 1,
	.horizontal_shift_scrolling = 0, /* Whether to shift the whole screen when scrolling horizontally */
	.hide_statusbar = 0,
	.tabs_visible = 1,
	.autohide_tabs = 0,
	.smart_complete = 0,
	.has_terminal = 0,
	.search_wraps = 1,
	.had_error = 0,
	.use_biminfo = 1,
	/* Integer config values */
	.cursor_padding = 4,
	.split_percent = 50,
	.scroll_amount = 5,
	.tab_offset = 0,
	.background_task = NULL,
	.tail_task = NULL,
};

struct key_name_map KeyNames[] = {
	{KEY_TIMEOUT, "[timeout]"},
	{KEY_BACKSPACE, "<backspace>"},
	{KEY_ENTER, "<enter>"},
	{KEY_ESCAPE, "<escape>"},
	{KEY_TAB, "<tab>"},
	{' ', "<space>"},
	/* These are mostly here for markdown output. */
	{'`', "<backtick>"},
	{'|', "<pipe>"},
	{KEY_DELETE, "<del>"},
	{KEY_MOUSE, "<mouse>"},
	{KEY_MOUSE_SGR, "<mouse-sgr>"},
	{KEY_F1, "<f1>"},{KEY_F2, "<f2>"},{KEY_F3, "<f3>"},{KEY_F4, "<f4>"},
	{KEY_F5, "<f5>"},{KEY_F6, "<f6>"},{KEY_F7, "<f7>"},{KEY_F8, "<f8>"},
	{KEY_F9, "<f9>"},{KEY_F10, "<f10>"},{KEY_F11, "<f11>"},{KEY_F12, "<f12>"},
	{KEY_HOME,"<home>"},{KEY_END,"<end>"},{KEY_PAGE_UP,"<page-up>"},{KEY_PAGE_DOWN,"<page-down>"},
	{KEY_UP, "<up>"},{KEY_DOWN, "<down>"},{KEY_RIGHT, "<right>"},{KEY_LEFT, "<left>"},
	{KEY_SHIFT_UP, "<shift-up>"},{KEY_SHIFT_DOWN, "<shift-down>"},{KEY_SHIFT_RIGHT, "<shift-right>"},{KEY_SHIFT_LEFT, "<shift-left>"},
	{KEY_CTRL_UP, "<ctrl-up>"},{KEY_CTRL_DOWN, "<ctrl-down>"},{KEY_CTRL_RIGHT, "<ctrl-right>"},{KEY_CTRL_LEFT, "<ctrl-left>"},
	{KEY_ALT_UP, "<alt-up>"},{KEY_ALT_DOWN, "<alt-down>"},{KEY_ALT_RIGHT, "<alt-right>"},{KEY_ALT_LEFT, "<alt-left>"},
	{KEY_ALT_SHIFT_UP, "<alt-shift-up>"},{KEY_ALT_SHIFT_DOWN, "<alt-shift-down>"},{KEY_ALT_SHIFT_RIGHT, "<alt-shift-right>"},{KEY_ALT_SHIFT_LEFT, "<alt-shift-left>"},
	{KEY_SHIFT_TAB,"<shift-tab>"},
	{KEY_PASTE_BEGIN,"<paste-begin>"},{KEY_PASTE_END,"<paste-end>"},
};

char * name_from_key(enum Key keycode) {
	for (unsigned int i = 0;  i < sizeof(KeyNames)/sizeof(KeyNames[0]); ++i) {
		if (KeyNames[i].keycode == keycode) return KeyNames[i].name;
	}
	static char keyNameTmp[8] = {0};
	if (keycode <= KEY_CTRL_UNDERSCORE) {
		keyNameTmp[0] = '^';
		keyNameTmp[1] = '@' + keycode;
		keyNameTmp[2] = 0;
		return keyNameTmp;
	}
	to_eight(keycode, keyNameTmp);
	return keyNameTmp;
}

#define S(c) (krk_copyString(c,sizeof(c)-1))

static KrkClass * syntaxStateClass = NULL;

struct SyntaxState {
	KrkInstance inst;
	struct syntax_state state;
};

static void schedule_complete_recalc(void);

/**
 * Theming data
 *
 * This default set is pretty simple "default foreground on default background"
 * except for search and selections which are black-on-white specifically.
 *
 * The theme colors get set by separate configurable theme scripts.
 */
const char * COLOR_FG        = "@9";
const char * COLOR_BG        = "@9";
const char * COLOR_ALT_FG    = "@9";
const char * COLOR_ALT_BG    = "@9";
const char * COLOR_NUMBER_FG = "@9";
const char * COLOR_NUMBER_BG = "@9";
const char * COLOR_STATUS_FG = "@9";
const char * COLOR_STATUS_BG = "@9";
const char * COLOR_STATUS_ALT= "@9";
const char * COLOR_TABBAR_BG = "@9";
const char * COLOR_TAB_BG    = "@9";
const char * COLOR_ERROR_FG  = "@9";
const char * COLOR_ERROR_BG  = "@9";
const char * COLOR_SEARCH_FG = "@0";
const char * COLOR_SEARCH_BG = "@17";
const char * COLOR_KEYWORD   = "@9";
const char * COLOR_STRING    = "@9";
const char * COLOR_COMMENT   = "@9";
const char * COLOR_TYPE      = "@9";
const char * COLOR_PRAGMA    = "@9";
const char * COLOR_NUMERAL   = "@9";
const char * COLOR_SELECTFG  = "@0";
const char * COLOR_SELECTBG  = "@17";
const char * COLOR_RED       = "@1";
const char * COLOR_GREEN     = "@2";
const char * COLOR_BOLD      = "@9";
const char * COLOR_LINK      = "@9";
const char * COLOR_ESCAPE    = "@9";
const char * current_theme = "none";

struct ColorName color_names[] = {
	{"text-fg", &COLOR_FG},
	{"text-bg", &COLOR_BG},
	{"alternate-fg", &COLOR_ALT_FG},
	{"alternate-bg", &COLOR_ALT_BG},
	{"number-fg", &COLOR_NUMBER_FG},
	{"number-bg", &COLOR_NUMBER_BG},
	{"status-fg", &COLOR_STATUS_FG},
	{"status-bg", &COLOR_STATUS_BG},
	{"status-alt", &COLOR_STATUS_ALT},
	{"tabbar-bg", &COLOR_TABBAR_BG},
	{"tab-bg", &COLOR_TAB_BG},
	{"error-fg", &COLOR_ERROR_FG},
	{"error-bg", &COLOR_ERROR_BG},
	{"search-fg", &COLOR_SEARCH_FG},
	{"search-bg", &COLOR_SEARCH_BG},
	{"keyword", &COLOR_KEYWORD},
	{"string", &COLOR_STRING},
	{"comment", &COLOR_COMMENT},
	{"type", &COLOR_TYPE},
	{"pragma", &COLOR_PRAGMA},
	{"numeral", &COLOR_NUMERAL},
	{"select-fg", &COLOR_SELECTFG},
	{"select-bg", &COLOR_SELECTBG},
	{"red", &COLOR_RED},
	{"green", &COLOR_GREEN},
	{"bold", &COLOR_BOLD},
	{"link", &COLOR_LINK},
	{"escape", &COLOR_ESCAPE},
	{NULL,NULL},
};


#define FLEXIBLE_ARRAY(name, add_name, type, zero) \
	int flex_ ## name ## _count = 0; \
	int flex_ ## name ## _space = 0; \
	type * name = NULL; \
	void add_name (type input) { \
		if (flex_ ## name ## _space == 0) { \
			flex_ ## name ## _space = 4; \
			name = calloc(sizeof(type), flex_ ## name ## _space); \
		} else if (flex_ ## name ## _count + 1 == flex_ ## name ## _space) { \
			flex_ ## name ## _space *= 2; \
			name = realloc(name, sizeof(type) * flex_ ## name ## _space); \
			for (int i = flex_ ## name ## _count; i < flex_ ## name ## _space; ++i) name[i] = zero; \
		} \
		name[flex_ ## name ## _count] = input; \
		flex_ ## name ## _count ++; \
	}

FLEXIBLE_ARRAY(mappable_actions, add_action, struct action_def, ((struct action_def){NULL,NULL,0,NULL}))
FLEXIBLE_ARRAY(regular_commands, add_command, struct command_def, ((struct command_def){NULL,NULL,NULL}))
FLEXIBLE_ARRAY(prefix_commands, add_prefix_command, struct command_def, ((struct command_def){NULL,NULL,NULL}))
FLEXIBLE_ARRAY(themes, add_colorscheme, struct theme_def, ((struct theme_def){NULL,NULL}))

/**
 * Special implementation of getch with a timeout
 */
int _bim_unget = -1;

void bim_unget(int c) {
	_bim_unget = c;
}

void redraw_statusbar(void);
int bim_getch_timeout(int timeout) {
	fflush(stdout);
	if (_bim_unget != -1) {
		int out = _bim_unget;
		_bim_unget = -1;
		return out;
	}
	struct pollfd fds[1];
	fds[0].fd = global_config.tty_in;
	fds[0].events = POLLIN;
	int ret = poll(fds,1,timeout);
	if (ret > 0 && fds[0].revents & POLLIN) {
		unsigned char buf[1];
		read(global_config.tty_in, buf, 1);
		return buf[0];
	} else {
		background_task_t * task = global_config.background_task;
		if (task) {
			global_config.background_task = task->next;
			task->func(task);
			free(task);
			if (!global_config.background_task) {
				global_config.tail_task = NULL;
				redraw_statusbar();
			}
		}
		return -1;
	}
}

/**
 * UTF-8 parser state
 */
static uint32_t codepoint_r;
static uint32_t state = 0;

#define UTF8_ACCEPT 0
#define UTF8_REJECT 1

static inline uint32_t decode(uint32_t* state, uint32_t* codep, uint32_t byte) {
	static int state_table[32] = {
		0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, /* 0xxxxxxx */
		1,1,1,1,1,1,1,1,                 /* 10xxxxxx */
		2,2,2,2,                         /* 110xxxxx */
		3,3,                             /* 1110xxxx */
		4,                               /* 11110xxx */
		1                                /* 11111xxx */
	};

	static int mask_bytes[32] = {
		0x7F,0x7F,0x7F,0x7F,0x7F,0x7F,0x7F,0x7F,
		0x7F,0x7F,0x7F,0x7F,0x7F,0x7F,0x7F,0x7F,
		0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
		0x1F,0x1F,0x1F,0x1F,
		0x0F,0x0F,
		0x07,
		0x00
	};

	static int next[5] = {
		0,
		1,
		0,
		2,
		3
	};

	if (*state == UTF8_ACCEPT) {
		*codep = byte & mask_bytes[byte >> 3];
		*state = state_table[byte >> 3];
	} else if (*state > 0) {
		*codep = (byte & 0x3F) | (*codep << 6);
		*state = next[*state];
	}
	return *state;
}

#define shift_key(i) _shift_key((i), this_buf, &timeout);
int _shift_key(int i, int this_buf[20], int *timeout) {
	int thing = this_buf[*timeout-1];
	(*timeout) = 0;
	switch (thing) {
		/* There are other combinations we can handle... */
		case '2': return i + 4;
		case '5': return i + 8;
		case '3': return i + 12;
		case '4': return i + 16;
		default: return i;
	}
}

int bim_getkey(int read_timeout) {

	int timeout = 0;
	int this_buf[20];

	int cin;
	uint32_t c;
	uint32_t istate = 0;

	while ((cin = bim_getch_timeout((timeout == 1) ? 50 : read_timeout))) {
		if (cin == -1) {
			if (timeout && this_buf[timeout-1] == '\033')
				return KEY_ESCAPE;
			return KEY_TIMEOUT;
		}

		if (!decode(&istate, &c, cin)) {
			if (timeout == 0) {
				switch (c) {
					case '\033':
						if (timeout == 0) {
							this_buf[timeout] = c;
							timeout++;
						}
						continue;
					case KEY_LINEFEED: return KEY_ENTER;
					case KEY_DELETE: return KEY_BACKSPACE;
				}
				return c;
			} else {
				if (timeout >= 1 && this_buf[timeout-1] == '\033' && c == '\033') {
					bim_unget(c);
					timeout = 0;
					return KEY_ESCAPE;
				}
				if (timeout >= 1 && this_buf[0] == '\033' && c == 'O') {
					this_buf[timeout] = c;
					timeout++;
					continue;
				}
				if (timeout >= 2 && this_buf[0] == '\033' && this_buf[1] == 'O') {
					switch (c) {
						case 'P': return KEY_F1;
						case 'Q': return KEY_F2;
						case 'R': return KEY_F3;
						case 'S': return KEY_F4;
					}
					timeout = 0;
					continue;
				}
				if (timeout >= 1 && this_buf[timeout-1] == '\033' && c != '[') {
					timeout = 0;
					bim_unget(c);
					return KEY_ESCAPE;
				}
				if (timeout >= 1 && this_buf[timeout-1] == '\033' && c == '[') {
					timeout = 1;
					this_buf[timeout] = c;
					timeout++;
					continue;
				}
				if (timeout >= 2 && this_buf[0] == '\033' && this_buf[1] == '[' &&
				    (isdigit(c) || (c == ';'))) {
					this_buf[timeout] = c;
					timeout++;
					continue;
				}
				if (timeout >= 2 && this_buf[0] == '\033' && this_buf[1] == '[') {
					switch (c) {
						case 'M': return KEY_MOUSE;
						case '<': return KEY_MOUSE_SGR;
						case 'A': return shift_key(KEY_UP);
						case 'B': return shift_key(KEY_DOWN);
						case 'C': return shift_key(KEY_RIGHT);
						case 'D': return shift_key(KEY_LEFT);
						case 'H': return KEY_HOME;
						case 'F': return KEY_END;
						case 'I': return KEY_PAGE_UP;
						case 'G': return KEY_PAGE_DOWN;
						case 'Z': return KEY_SHIFT_TAB;
						case '~':
							if (timeout == 3) {
								switch (this_buf[2]) {
									case '1': return KEY_HOME;
									case '3': return KEY_DELETE;
									case '4': return KEY_END;
									case '5': return KEY_PAGE_UP;
									case '6': return KEY_PAGE_DOWN;
								}
							} else if (timeout == 5) {
								if (this_buf[2] == '2' && this_buf[3] == '0' && this_buf[4] == '0') {
									return KEY_PASTE_BEGIN;
								} else if (this_buf[2] == '2' && this_buf[3] == '0' && this_buf[4] == '1') {
									return KEY_PASTE_END;
								}
							} else if (this_buf[2] == '1') {
								switch (this_buf[3]) {
									case '5': return KEY_F5;
									case '7': return KEY_F6;
									case '8': return KEY_F7;
									case '9': return KEY_F8;
								}
							} else if (this_buf[2] == '2') {
								switch (this_buf[3]) {
									case '0': return KEY_F9;
									case '1': return KEY_F10;
									case '3': return KEY_F11;
									case '4': return KEY_F12;
								}
							}
							break;
					}
				}
				timeout = 0;
				continue;
			}
		} else if (istate == UTF8_REJECT) {
			istate = 0;
		}
	}

	return KEY_TIMEOUT;
}

enum Key key_from_name(char * name) {
	for (unsigned int i = 0;  i < sizeof(KeyNames)/sizeof(KeyNames[0]); ++i) {
		if (!strcmp(KeyNames[i].name, name)) return KeyNames[i].keycode;
	}
	if (name[0] == '^' && name[1] && !name[2]) {
		return name[1] - '@';
	}
	if (!name[1]) return name[0];
	/* Try decoding */
	uint32_t c, state = 0;
	int candidate = -1;
	while (*name) {
		if (!decode(&state, &c, (unsigned char)*name)) {
			if (candidate == -1) candidate = c;
			else return -1; /* Reject `name` if it is multiple codepoints */
		} else if (state == UTF8_REJECT) {
			return -1;
		}
	}
	return candidate;
}


/**
 * Pointer to current active buffer
 */
buffer_t * env = NULL;

buffer_t * left_buffer = NULL;
buffer_t * right_buffer = NULL;

/**
 * A buffer for holding a number (line, repetition count)
 */
#define NAV_BUFFER_MAX 10
char nav_buf[NAV_BUFFER_MAX+1];
int nav_buffer = 0;

/**
 * Available buffers
 */
int    buffers_len = 0;
int    buffers_avail = 0;
buffer_t ** buffers = NULL;

/**
 * Create a new buffer
 */
buffer_t * buffer_new(void) {
	if (buffers_len == buffers_avail) {
		/* If we are out of buffer space, expand the buffers vector */
		buffers_avail *= 2;
		buffers = realloc(buffers, sizeof(buffer_t *) * buffers_avail);
	}

	/* TODO: Clean up split support and support multiple splits... */
	if (left_buffer) {
		left_buffer->left = 0;
		left_buffer->width = global_config.term_width;
		right_buffer->left = 0;
		right_buffer->width = global_config.term_width;
		left_buffer = NULL;
		right_buffer = NULL;
	}

	/* Allocate a new buffer */
	buffers[buffers_len] = malloc(sizeof(buffer_t));
	memset(buffers[buffers_len], 0x00, sizeof(buffer_t));
	buffers[buffers_len]->left = 0;
	buffers[buffers_len]->width = global_config.term_width;
	buffers[buffers_len]->highlighting_paren = -1;
	buffers[buffers_len]->numbers = global_config.numbers;
	buffers[buffers_len]->gutter = 1;
	buffers_len++;
	global_config.tabs_visible = (!global_config.autohide_tabs) || (buffers_len > 1);

	return buffers[buffers_len-1];
}

/**
 * Open the biminfo file.
 */
FILE * open_biminfo(void) {
	if (!global_config.use_biminfo) return NULL;

	char * home = getenv("HOME");
	if (!home) {
		/* ... but since it's not, we need $HOME, so fail if it isn't set. */
		return NULL;
	}

	/* biminfo lives at ~/.biminfo */
	char biminfo_path[PATH_MAX+1] = {0};
	sprintf(biminfo_path,"%s/.biminfo",home);

	/* Try to open normally first... */
	FILE * biminfo = fopen(biminfo_path,"r+");
	if (!biminfo) {
		/* Otherwise, try to create it. */
		biminfo = fopen(biminfo_path,"w+");
	}
	return biminfo;
}

/**
 * Check if a file is open by examining the biminfo file
 */
int file_is_open(char * file_name) {
	/* Get the absolute path of the file to normalize for lookup */
	char tmp_path[PATH_MAX+2];
	if (!realpath(file_name, tmp_path)) {
		return 0; /* Assume not */
	}
	strcat(tmp_path," ");

	FILE * biminfo = open_biminfo();
	if (!biminfo) return 0; /* Assume not */

	/* Scan */
	char line[PATH_MAX+64];

	while (!feof(biminfo)) {
		fpos_t start_of_line;
		fgetpos(biminfo, &start_of_line);
		fgets(line, PATH_MAX+63, biminfo);
		if (line[0] != '%') {
			continue;
		}

		if (!strncmp(&line[1],tmp_path, strlen(tmp_path))) {
			/* File is currently open */
			int pid = -1;
			sscanf(line+1+strlen(tmp_path)+1,"%d",&pid);
			if (pid != -1 && pid != getpid()) {
				if (!kill(pid, 0)) {
					int key = 0;
					render_error("biminfo indicates another instance may already be editing this file");
					render_commandline_message("\n");
					render_commandline_message("file path = %s\n", tmp_path);
					render_commandline_message("pid = %d (still running)\n", pid);
					render_commandline_message("Open file anyway? (y/N)");
					while ((key = bim_getkey(DEFAULT_KEY_WAIT)) == KEY_TIMEOUT);
					if (key != 'y') {
						fclose(biminfo);
						return 1;
					}
				}
			}
			fclose(biminfo);
			return 0;
		}
	}
	fclose(biminfo);
	return 0;
}

/**
 * Fetch the cursor position from a biminfo file
 */
int fetch_from_biminfo(buffer_t * buf) {
	/* Can't fetch if we don't have a filename */
	if (!buf->file_name) return 1;

	/* Get the absolute path of the file to normalize for lookup */
	char tmp_path[PATH_MAX+2];
	if (!realpath(buf->file_name, tmp_path)) {
		return 1;
	}
	strcat(tmp_path," ");

	FILE * biminfo = open_biminfo();
	if (!biminfo) return 1;

	/* Scan */
	char line[PATH_MAX+64];

	while (!feof(biminfo)) {
		fpos_t start_of_line;
		fgetpos(biminfo, &start_of_line);
		fgets(line, PATH_MAX+63, biminfo);
		if (line[0] != '>') {
			continue;
		}

		if (!strncmp(&line[1],tmp_path, strlen(tmp_path))) {
			/* Read */
			sscanf(line+1+strlen(tmp_path)+1,"%d",&buf->line_no);
			sscanf(line+1+strlen(tmp_path)+21,"%d",&buf->col_no);

			if (buf->line_no > buf->line_count) buf->line_no = buf->line_count;
			if (buf->col_no > buf->lines[buf->line_no-1]->actual) buf->col_no = buf->lines[buf->line_no-1]->actual;
			try_to_center();

			fclose(biminfo);
			return 0;
		}
	}

	fclose(biminfo);
	return 0;
}

/**
 * Write a file containing the last cursor position of a buffer.
 */
int update_biminfo(buffer_t * buf, int is_open) {
	if (!buf->file_name) return 1;

	/* Get the absolute path of the file to normalize for lookup */
	char tmp_path[PATH_MAX+1];
	if (!realpath(buf->file_name, tmp_path)) {
		return 1;
	}
	strcat(tmp_path," ");

	FILE * biminfo = open_biminfo();
	if (!biminfo) return 1;

	/* Scan */
	char line[PATH_MAX+64];

	while (!feof(biminfo)) {
		fpos_t start_of_line;
		fgetpos(biminfo, &start_of_line);
		fgets(line, PATH_MAX+63, biminfo);
		if (line[0] != '>' && line[0] != '%') {
			continue;
		}

		if (!strncmp(&line[1],tmp_path, strlen(tmp_path))) {
			/* Update */
			fsetpos(biminfo, &start_of_line);
			fprintf(biminfo,"%c%s %20d %20d\n", is_open ? '%' : '>', tmp_path,
				is_open ? getpid() : buf->line_no, buf->col_no);
			goto _done;
		}
	}

	if (ftell(biminfo) == 0) {
		/* New biminfo */
		fprintf(biminfo, "# This is a biminfo file.\n");
		fprintf(biminfo, "# It was generated by bim. Do not edit it by hand!\n");
		fprintf(biminfo, "# Cursor positions and other state are stored here.\n");
	}

	/* If we reach this point, we didn't find a record for this file
	 * and the write cursor should be at the end, so just add a new line */
	fprintf(biminfo,"%c%s %20d %20d\n", is_open ? '%' : '>', tmp_path,
		is_open ? getpid() : buf->line_no, buf->col_no);

_done:
	fclose(biminfo);
	return 0;
}

void cancel_background_tasks(buffer_t * buf) {
	background_task_t * t = global_config.background_task;
	background_task_t * last = NULL;
	while (t) {
		if (t->env == buf) {
			if (last) {
				last->next = t->next;
			} else {
				global_config.background_task = t->next;
			}
			if (!t->next) {
				global_config.tail_task = last;
			}
			background_task_t * tmp = t->next;
			free(t);
			t = tmp;
		} else {
			last = t;
			t = t->next;
		}
	}
}

/**
 * Close a buffer
 */
buffer_t * buffer_close(buffer_t * buf) {
	int i;

	/* Locate the buffer in the buffer list */
	for (i = 0; i < buffers_len; i++) {
		if (buf == buffers[i])
			break;
	}

	/* This buffer doesn't exist? */
	if (i == buffers_len) {
		return NULL;
	}

	/* Cancel any background tasks for this env */
	cancel_background_tasks(buf);

	update_biminfo(buf, 0);

	/* Clean up lines used by old buffer */
	for (int i = 0; i < buf->line_count; ++i) {
		free(buf->lines[i]);
	}

	free(buf->lines);

	if (buf->file_name) {
		free(buf->file_name);
	}

	history_t * h = buf->history;
	while (h->next) {
		h = h->next;
	}
	while (h) {
		history_t * x = h->previous;
		free(h);
		h = x;
	}

	/* Clean up the old buffer */
	free(buf);

	/* Remove the buffer from the vector, moving others up */
	if (i != buffers_len - 1) {
		memmove(&buffers[i], &buffers[i+1], sizeof(*buffers) * (buffers_len - i - 1));
	}

	/* There is one less buffer */
	buffers_len--;
	if (buffers_len && global_config.tab_offset >= buffers_len) global_config.tab_offset--;
	global_config.tabs_visible = (!global_config.autohide_tabs) || (buffers_len > 1);
	if (!buffers_len) { 
		/* There are no more buffers. */
		return NULL;
	}

	/* If this was the last buffer in the list, return the previous last buffer */
	if (i == buffers_len) {
		return buffers[buffers_len-1];
	}

	/* Otherwise return the buffer in the same location */
	return buffers[i];
}

/**
 * Convert syntax highlighting flag to color code
 */
const char * flag_to_color(int _flag) {
	int flag = _flag & FLAG_MASK_COLORS;
	switch (flag) {
		case FLAG_KEYWORD:
			return COLOR_KEYWORD;
		case FLAG_STRING:
			return COLOR_STRING;
		case FLAG_COMMENT:
			return COLOR_COMMENT;
		case FLAG_TYPE:
			return COLOR_TYPE;
		case FLAG_NUMERAL:
			return COLOR_NUMERAL;
		case FLAG_PRAGMA:
			return COLOR_PRAGMA;
		case FLAG_DIFFPLUS:
			return COLOR_GREEN;
		case FLAG_DIFFMINUS:
			return COLOR_RED;
		case FLAG_SELECT:
			return COLOR_FG;
		case FLAG_BOLD:
			return COLOR_BOLD;
		case FLAG_LINK_COLOR:
			return COLOR_LINK;
		case FLAG_ESCAPE:
			return COLOR_ESCAPE;
		default:
			return COLOR_FG;
	}
}

/**
 * Match and paint a single keyword. Returns 1 if the keyword was matched and 0 otherwise,
 * so it can be used for prefix checking for things that need further special handling.
 */
int match_and_paint(struct syntax_state * state, const char * keyword, int flag, int (*keyword_qualifier)(int c)) {
	if (keyword_qualifier(lastchar())) return 0;
	if (!keyword_qualifier(charat())) return 0;
	int i = state->i;
	int slen = 0;
	while (i < state->line->actual || *keyword == '\0') {
		if (*keyword == '\0' && (i >= state->line->actual || !keyword_qualifier(state->line->text[i].codepoint))) {
			for (int j = 0; j < slen; ++j) {
				paint(1, flag);
			}
			return 1;
		}
		if (*keyword != state->line->text[i].codepoint) return 0;

		i++;
		keyword++;
		slen++;
	}
	return 0;
}

/**
 * This is a basic character matcher for "keyword" characters.
 */
int simple_keyword_qualifier(int c) {
	return isalnum(c) || (c == '_');
}

/**
 * These words can appear in comments and should be highlighted.
 * Since there are a lot of comment highlighters, this is provided
 * as a common function that can be used by multiple highlighters.
 */
int common_comment_buzzwords(struct syntax_state * state) {
	if (match_and_paint(state, "TODO", FLAG_NOTICE, simple_keyword_qualifier)) { return 1; }
	else if (match_and_paint(state, "XXX", FLAG_NOTICE, simple_keyword_qualifier)) { return 1; }
	else if (match_and_paint(state, "FIXME", FLAG_ERROR, simple_keyword_qualifier)) { return 1; }
	return 0;
}

/**
 * Paint a comment until end of line; assumes this comment can not continue.
 * (Some languages have comments that can continue with a \ - don't use this!)
 * Assumes you've already painted your comment start characters.
 */
int paint_comment(struct syntax_state * state) {
	while (charat() != -1) {
		if (common_comment_buzzwords(state)) continue;
		else { paint(1, FLAG_COMMENT); }
	}
	return -1;
}

/**
 * Find and return a highlighter by name, or return NULL if none was found.
 */
struct syntax_definition * find_syntax_calculator(const char * name) {
	for (struct syntax_definition * s = syntaxes; syntaxes && s->name; ++s) {
		if (!strcmp(s->name, name)) {
			return s;
		}
	}
	return NULL;
}

int syntax_count = 0;
int syntax_space = 0;
struct syntax_definition * syntaxes = NULL;

void add_syntax(struct syntax_definition def) {
	/* See if a name match already exists for this def. */
	for (struct syntax_definition * s = syntaxes; syntaxes && s->name; ++s) {
		if (!strcmp(def.name,s->name)) {
			*s = def;
			return;
		}
	}

	if (syntax_space == 0) {
		syntax_space = 4;
		syntaxes = calloc(sizeof(struct syntax_definition), syntax_space);
	} else if (syntax_count +1 == syntax_space) {
		syntax_space *= 2;
		syntaxes = realloc(syntaxes, sizeof(struct syntax_definition) * syntax_space);
		for (int i = syntax_count; i < syntax_space; ++i) syntaxes[i].name = NULL;
	}
	syntaxes[syntax_count] = def;
	syntax_count++;
}

void redraw_all(void);

/**
 * Calculate syntax highlighting for the given line, and lines after
 * if their initial syntax state has changed by this recalculation.
 *
 * If `line_no` is -1, this line is taken to be a special line and not
 * part of a buffer; search highlighting will not be processed and syntax
 * highlighting will halt after the line is finished.
 *
 * If `env->slowop` is currently enabled, recalculation is skipped.
 */
void recalculate_syntax(line_t * line, int line_no) {
	if (env->slowop) return;
	/* Clear syntax for this line first */
	int is_original = 1;
	while (1) {
		for (int i = 0; i < line->actual; ++i) {
			line->text[i].flags = line->text[i].flags & (3 << 5);
		}

		if (!env->syntax) {
			if (line_no != -1) rehighlight_search(line);
			return;
		}

		/* Start from the line's stored in initial state */
		struct SyntaxState * s = (void*)krk_newInstance(env->syntax->krkClass);
		s->state.env = env;
		s->state.line = line;
		s->state.line_no = line_no;
		s->state.state = line->istate;
		s->state.i = 0;

		while (1) {
			ptrdiff_t before = krk_currentThread.stackTop - krk_currentThread.stack;
			krk_push(OBJECT_VAL(env->syntax->krkFunc));
			krk_push(OBJECT_VAL(s));
			KrkValue result = krk_callStack(1);
			krk_currentThread.stackTop = krk_currentThread.stack + before;
			if (IS_NONE(result) && (krk_currentThread.flags & KRK_THREAD_HAS_EXCEPTION)) {
				render_error("Exception occurred in plugin: %s", AS_INSTANCE(krk_currentThread.currentException)->_class->name->chars);
				render_commandline_message("\n");
				krk_dumpTraceback();
				goto _syntaxError;
			} else if (!IS_NONE(result) && !IS_INTEGER(result)) {
				render_error("Instead of an integer, got %s", krk_typeName(result));
				render_commandline_message("\n");
				goto _syntaxError;
			}
			s->state.state = IS_NONE(result) ? -1 : AS_INTEGER(result);

			if (s->state.state != 0) {
				if (line_no == -1) return;
				rehighlight_search(line);
				if (!is_original) {
					redraw_line(line_no);
				}
				if (line_no + 1 < env->line_count && env->lines[line_no+1]->istate != s->state.state) {
					line_no++;
					line = env->lines[line_no];
					line->istate = s->state.state;
					if (env->loading) return;
					is_original = 0;
					goto _next;
				}
				return;
			}
		}
_next:
		(void)0;
	}

_syntaxError:
	krk_resetStack();
	fprintf(stderr,"This syntax highlighter will be disabled in this environment.");
	env->syntax = NULL;
	cancel_background_tasks(env);
	pause_for_key();
	redraw_all();
}

/**
 * Recalculate tab widths.
 */
void recalculate_tabs(line_t * line) {
	if (env->loading) return;

	int j = 0;
	for (int i = 0; i < line->actual; ++i) {
		if (line->text[i].codepoint == '\t') {
			line->text[i].display_width = env->tabstop - (j % env->tabstop);
		}
		j += line->text[i].display_width;
	}
}

/**
 * The next section contains the basic editing primitives. All other
 * actions are built out of these primitives, and they are the basic
 * instructions that get stored in history to be undone (or redone).
 *
 * Primitives may recalculate syntax or redraw lines, if needed, but only
 * when conditions for redrawing are met (such as not being in `slowop`,
 * or loading the file; also replaying history, or when loading files).
 *
 * At the moment, primitives and most other functions do not take the current
 * buffer (environment) as an argument and instead rely on a global variable;
 * this should definitely be fixed at some point...
 */

/**
 * When a new action that produces history happens and there is forward
 * history that can be redone, we need to erase it as our tree has branched.
 * If we wanted, we could actually story things in a tree structure so that
 * the new branch and the old branch can both stick around, and that should
 * probably be explored in the future...
 */
void history_free(history_t * root) {
	if (!root->next) return;

	/* Find the last entry so we can free backwards */
	history_t * n = root->next;
	while (n->next) {
		n = n->next;
	}

	/* Free everything after the root, including stored content. */
	while (n != root) {
		history_t * p = n->previous;
		switch (n->type) {
			case HISTORY_REPLACE_LINE:
				free(n->contents.remove_replace_line.contents);
				/* fall-through */
			case HISTORY_REMOVE_LINE:
				free(n->contents.remove_replace_line.old_contents);
				break;
			default:
				/* Nothing extra to free */
				break;
		}
		free(n);
		n = p;
	}

	root->next = NULL;
}

/**
 * This macro is called by primitives to insert history elements for each
 * primitive action when performing in edit modes.
 */
#define HIST_APPEND(e) do { \
		e->col = env->col_no; \
		e->line = env->line_no; \
		if (env->history) { \
			e->previous = env->history; \
			history_free(env->history); \
			env->history->next = e; \
			e->next = NULL; \
		} \
		env->history = e; \
	} while (0)

/**
 * Mark a point where a complete set of actions has ended.
 * Individual history entries include things like "insert one character"
 * but a user action that should be undone is "insert several characters";
 * breaks should be inserted after a series of primitives when there is
 * a clear "end", such as when switching out of insert mode after typing.
 */
void set_history_break(void) {
	if (!global_config.history_enabled) return;

	/* Do not produce duplicate breaks, or add breaks if we are at a sentinel. */
	if (env->history->type != HISTORY_BREAK && env->history->type != HISTORY_SENTINEL) {
		history_t * e = malloc(sizeof(history_t));
		e->type = HISTORY_BREAK;
		HIST_APPEND(e);
	}
}

/**
 * (Primitive) Insert a character into an existing line.
 *
 * This is the most basic primitive action: Take a line, and insert a codepoint
 * into it at a given offset. If `lineno` is not -1, the line is assumed to
 * be part of the active buffer. If inserting a character means the line needs
 * to grow, then it will be reallocated, so the return value of the new line
 * must ALWAYS be used. This primitive will NOT automatically update the
 * buffer with the new pointer, so if you are calling insert on a buffer you
 * MUST update env->lines[lineno-1] yourself.
 */
__attribute__((warn_unused_result)) line_t * line_insert(line_t * line, char_t c, int offset, int lineno) {

	if (!env->loading && global_config.history_enabled && lineno != -1) {
		history_t * e = malloc(sizeof(history_t));
		e->type = HISTORY_INSERT;
		e->contents.insert_delete_replace.lineno = lineno;
		e->contents.insert_delete_replace.offset = offset;
		e->contents.insert_delete_replace.codepoint = c.codepoint;
		HIST_APPEND(e);
	}

	/* If there is not enough space... */
	if (line->actual == line->available) {
		/* Expand the line buffer */
		if (line->available == 0) {
			line->available = 8;
		} else {
			line->available *= 2;
		}
		line = realloc(line, sizeof(line_t) + sizeof(char_t) * line->available);
	}

	/* If this was not the last character, then shift remaining characters forward. */
	if (offset < line->actual) {
		memmove(&line->text[offset+1], &line->text[offset], sizeof(char_t) * (line->actual - offset));
	}

	/* Insert the new character */
	line->text[offset] = c;

	/* There is one new character in the line */
	line->actual += 1;

	if (!env->loading) {
		line->rev_status = 2; /* Modified */
		recalculate_tabs(line);
		recalculate_syntax(line, lineno);
	}

	return line;
}

/**
 * (Primitive) Delete a character from a line.
 *
 * Remove the character at the given offset. We never shrink lines, so this
 * does not have a return value, and delete should never be called during
 * a loading operation (though it may be called during a history replay).
 */
void line_delete(line_t * line, int offset, int lineno) {

	/* Can't delete character before start of line. */
	if (offset == 0) return;
	/* Can't delete past end of line either */
	if (offset > line->actual) return;

	if (!env->loading && global_config.history_enabled && lineno != -1) {
		history_t * e = malloc(sizeof(history_t));
		e->type = HISTORY_DELETE;
		e->contents.insert_delete_replace.lineno = lineno;
		e->contents.insert_delete_replace.offset = offset;
		e->contents.insert_delete_replace.old_codepoint = line->text[offset-1].codepoint;
		HIST_APPEND(e);
	}

	/* If this isn't the last character, we need to move all subsequent characters backwards */
	if (offset < line->actual) {
		memmove(&line->text[offset-1], &line->text[offset], sizeof(char_t) * (line->actual - offset));
	}

	/* The line is one character shorter */
	line->actual -= 1;
	line->rev_status = 2;

	recalculate_tabs(line);
	recalculate_syntax(line, lineno);
}

/**
 * (Primitive) Replace a character in a line.
 *
 * Replaces the codepoint at the given offset with a new character. Since this
 * does not involve any size changes, it does not have a return value.
 * Since a replacement character may be a tab, we do still need to recalculate
 * character widths for tabs as they may change.
 */
void line_replace(line_t * line, char_t _c, int offset, int lineno) {

	if (!env->loading && global_config.history_enabled && lineno != -1) {
		history_t * e = malloc(sizeof(history_t));
		e->type = HISTORY_REPLACE;
		e->contents.insert_delete_replace.lineno = lineno;
		e->contents.insert_delete_replace.offset = offset;
		e->contents.insert_delete_replace.codepoint = _c.codepoint;
		e->contents.insert_delete_replace.old_codepoint = line->text[offset].codepoint;
		HIST_APPEND(e);
	}

	line->text[offset] = _c;

	if (!env->loading) {
		line->rev_status = 2; /* Modified */
		recalculate_tabs(line);
		recalculate_syntax(line, lineno);
	}
}

/**
 * (Primitive) Remove a line from the active buffer
 *
 * This primitive is only valid for a buffer. Delete a line, or if this is the
 * only line in the buffer, clear it but keep the line around with no
 * characters. We use the `line_delete` primitive to clear that line,
 * otherwise we are our own primitive and produce history entries.
 *
 * While we do not shrink the `lines` array, it is returned here anyway.
 */
line_t ** remove_line(line_t ** lines, int offset) {

	/* If there is only one line, clear it instead of removing it. */
	if (env->line_count == 1) {
		while (lines[offset]->actual > 0) {
			line_delete(lines[offset], lines[offset]->actual, offset);
		}
		return lines;
	}

	/* When a line is removed, we need to keep its contents so we
	 * can un-remove it on redo... */
	if (!env->loading && global_config.history_enabled) {
		history_t * e = malloc(sizeof(history_t));
		e->type = HISTORY_REMOVE_LINE;
		e->contents.remove_replace_line.lineno = offset;
		e->contents.remove_replace_line.old_contents = malloc(sizeof(line_t) + sizeof(char_t) * lines[offset]->available);
		memcpy(e->contents.remove_replace_line.old_contents, lines[offset], sizeof(line_t) + sizeof(char_t) * lines[offset]->available);
		HIST_APPEND(e);
	}

	/* Otherwise, free the data used by the line */
	free(lines[offset]);

	/* Move other lines up */
	if (offset < env->line_count-1) {
		memmove(&lines[offset], &lines[offset+1], sizeof(line_t *) * (env->line_count - (offset - 1)));
		lines[env->line_count-1] = NULL;
	}

	/* There is one less line */
	env->line_count -= 1;
	return lines;
}

/**
 * (Primitive) Add a new line to the active buffer.
 *
 * Inserts a new line into a buffer at the given line offset.
 * Since this grows the buffer, it will return the new line array
 * after reallocation if needed.
 */
line_t ** add_line(line_t ** lines, int offset) {

	/* Invalid offset? */
	if (offset > env->line_count) return lines;

	if (!env->loading && global_config.history_enabled) {
		history_t * e = malloc(sizeof(history_t));
		e->type = HISTORY_ADD_LINE;
		e->contents.add_merge_split_lines.lineno = offset;
		HIST_APPEND(e);
	}

	/* Not enough space */
	if (env->line_count == env->line_avail) {
		/* Allocate more space */
		env->line_avail *= 2;
		lines = realloc(lines, sizeof(line_t *) * env->line_avail);
	}

	/* If this isn't the last line, move other lines down */
	if (offset < env->line_count) {
		memmove(&lines[offset+1], &lines[offset], sizeof(line_t *) * (env->line_count - offset));
	}

	/* Allocate the new line */
	lines[offset] = calloc(sizeof(line_t) + sizeof(char_t) * 32, 1);
	lines[offset]->available = 32;

	/* There is one new line */
	env->line_count += 1;
	env->lines = lines;

	if (!env->loading) {
		lines[offset]->rev_status = 2; /* Modified */
	}

	if (offset > 0 && !env->loading) {
		recalculate_syntax(lines[offset-1],offset-1);
	}
	return lines;
}

/**
 * (Primitive) Replace a line with data from another line.
 *
 * This is only called when pasting yanks after calling `add_line`,
 * but it allows us to have simpler history for that action.
 */
void replace_line(line_t ** lines, int offset, line_t * replacement) {

	if (!env->loading && global_config.history_enabled) {
		history_t * e = malloc(sizeof(history_t));
		e->type = HISTORY_REPLACE_LINE;
		e->contents.remove_replace_line.lineno = offset;
		e->contents.remove_replace_line.old_contents = malloc(sizeof(line_t) + sizeof(char_t) * lines[offset]->available);
		memcpy(e->contents.remove_replace_line.old_contents, lines[offset], sizeof(line_t) + sizeof(char_t) * lines[offset]->available);
		e->contents.remove_replace_line.contents = malloc(sizeof(line_t) + sizeof(char_t) * replacement->available);
		memcpy(e->contents.remove_replace_line.contents, replacement, sizeof(line_t) + sizeof(char_t) * replacement->available);
		HIST_APPEND(e);
	}

	if (lines[offset]->available < replacement->actual) {
		lines[offset] = realloc(lines[offset], sizeof(line_t) + sizeof(char_t) * replacement->available);
		lines[offset]->available = replacement->available;
	}
	lines[offset]->actual = replacement->actual;
	memcpy(&lines[offset]->text, &replacement->text, sizeof(char_t) * replacement->actual);

	if (!env->loading) {
		lines[offset]->rev_status = 2;
		recalculate_syntax(lines[offset],offset);
	}
}

/**
 * (Primitive) Merge two consecutive lines.
 *
 * Take two lines in a buffer and turn them into one line.
 * `lineb` is the offset of the second line... or the
 * line number of the first line, depending on which indexing
 * system you prefer to think about. This won't grow `lines`,
 * but it will likely modify it and can reallocate individual
 * lines as well.
 */
line_t ** merge_lines(line_t ** lines, int lineb) {

	/* linea is the line immediately before lineb */
	int linea = lineb - 1;

	if (!env->loading && global_config.history_enabled) {
		history_t * e = malloc(sizeof(history_t));
		e->type = HISTORY_MERGE_LINES;
		e->contents.add_merge_split_lines.lineno = lineb;
		e->contents.add_merge_split_lines.split = env->lines[linea]->actual;
		HIST_APPEND(e);
	}

	/* If there isn't enough space in linea hold both... */
	if (lines[linea]->available < lines[linea]->actual + lines[lineb]->actual) {
		while (lines[linea]->available < lines[linea]->actual + lines[lineb]->actual) {
			/* ... allocate more space until it fits */
			if (lines[linea]->available == 0) {
				lines[linea]->available = 8;
			} else {
				lines[linea]->available *= 2;
			}
		}
		lines[linea] = realloc(lines[linea], sizeof(line_t) + sizeof(char_t) * lines[linea]->available);
	}

	/* Copy the second line into the first line */
	memcpy(&lines[linea]->text[lines[linea]->actual], &lines[lineb]->text, sizeof(char_t) * lines[lineb]->actual);

	/* The first line is now longer */
	lines[linea]->actual = lines[linea]->actual + lines[lineb]->actual;

	if (!env->loading) {
		lines[linea]->rev_status = 2;
		recalculate_tabs(lines[linea]);
		recalculate_syntax(lines[linea], linea);
	}

	/* Remove the second line */
	free(lines[lineb]);

	/* Move other lines up */
	if (lineb < env->line_count) {
		memmove(&lines[lineb], &lines[lineb+1], sizeof(line_t *) * (env->line_count - (lineb - 1)));
		lines[env->line_count-1] = NULL;
	}

	/* There is one less line */
	env->line_count -= 1;
	return lines;
}

/**
 * (Primitive) Split a line into two lines at the given column.
 *
 * Takes one line and makes it two lines. There are some optimizations
 * if you are trying to "split" at the first or last column, which
 * are both just treated as add_line.
 */
line_t ** split_line(line_t ** lines, int line, int split) {

	/* If we're trying to split from the start, just add a new blank line before */
	if (split == 0) {
		return add_line(lines, line);
	}

	if (!env->loading && global_config.history_enabled) {
		history_t * e = malloc(sizeof(history_t));
		e->type = HISTORY_SPLIT_LINE;
		e->contents.add_merge_split_lines.lineno = line;
		e->contents.add_merge_split_lines.split  = split;
		HIST_APPEND(e);
	}

	if (!env->loading) {
		unhighlight_matching_paren();
	}

	/* Allocate more space as needed */
	if (env->line_count == env->line_avail) {
		env->line_avail *= 2;
		lines = realloc(lines, sizeof(line_t *) * env->line_avail);
	}

	/* Shift later lines down */
	if (line < env->line_count) {
		memmove(&lines[line+2], &lines[line+1], sizeof(line_t *) * (env->line_count - line));
	}

	int remaining = lines[line]->actual - split;

	/* This is some wacky math to get a good power-of-two */
	int v = remaining;
	v--;
	v |= v >> 1;
	v |= v >> 2;
	v |= v >> 4;
	v |= v >> 8;
	v |= v >> 16;
	v++;

	/* Allocate space for the new line */
	lines[line+1] = calloc(sizeof(line_t) + sizeof(char_t) * v, 1);
	lines[line+1]->available = v;
	lines[line+1]->actual = remaining;

	/* Move the data from the old line into the new line */
	memmove(lines[line+1]->text, &lines[line]->text[split], sizeof(char_t) * remaining);
	lines[line]->actual = split;

	if (!env->loading) {
		lines[line]->rev_status = 2;
		lines[line+1]->rev_status = 2;
		recalculate_tabs(lines[line]);
		recalculate_tabs(lines[line+1]);
		recalculate_syntax(lines[line], line);
		recalculate_syntax(lines[line+1], line+1);
	}

	/* There is one new line */
	env->line_count += 1;

	/* We may have reallocated lines */
	return lines;
}

/**
 * Primitives end here. Everything after this point that wants to
 * perform modifications must be built on these primitives and
 * should never directly modify env->lines or the contents thereof,
 * outside of syntax highlighting flag changes.
 */

/**
 * The following section is where we implement "smart" automatic
 * indentation. A lot of this is hacked-together nonsense and "smart"
 * is a bit of an overstatement.
 */

/**
 * Understand spaces and comments and check if the previous line
 * ended with a brace or a colon.
 */
int line_ends_with_brace(line_t * line) {
	int i = line->actual-1;
	while (i >= 0) {
		if ((line->text[i].flags & 0x1F) == FLAG_COMMENT || line->text[i].codepoint == ' ') {
			i--;
		} else {
			break;
		}
	}
	if (i < 0) return 0;
	return (line->text[i].codepoint == '{' || line->text[i].codepoint == ':') ? i+1 : 0;
}

/**
 * Determine if a given line is a comment by checking its initial
 * syntax highlighting state value. This is used by automatic indentation
 * to continue block comments in languages like C.
 *
 * TODO: Why isn't this a syntax-> method?
 */
int line_is_comment(line_t * line) {
	if (!env->syntax) return 0;

	if (!strcmp(env->syntax->name,"c")) {
		if (line->istate == 1) return 1;
	} else if (!strcmp(env->syntax->name,"java")) {
		if (line->istate == 1) return 1;
	} else if (!strcmp(env->syntax->name,"kotlin")) {
		if (line->istate == 1) return 1;
	} else if (!strcmp(env->syntax->name,"rust")) {
		if (line->istate > 0) return 1;
	}

	return 0;
}

/**
 * Figure out where the indentation for a brace belongs by finding
 * where the start of the line is after whitespace. This is called
 * by default when we insert } and tries to align it with the indentation
 * of the matching opening {.
 */
int find_brace_line_start(int line, int col) {
	int ncol = col - 1;
	while (ncol > 0) {
		if (env->lines[line-1]->text[ncol-1].codepoint == ')') {
			int t_line_no = env->line_no;
			int t_col_no = env->col_no;
			env->line_no = line;
			env->col_no = ncol;
			int paren_match_line = -1, paren_match_col = -1;
			find_matching_paren(&paren_match_line, &paren_match_col, 1);

			if (paren_match_line != -1) {
				line = paren_match_line;
			}

			env->line_no = t_line_no;
			env->col_no = t_col_no;
			break;
		} else if (env->lines[line-1]->text[ncol-1].codepoint == ' ') {
			ncol--;
		} else {
			break;
		}
	}
	return line;
}

/**
 * Add indentation from the previous (temporally) line.
 *
 * By "temporally", we mean not necessarily the line above, but
 * potentially the line below if we are inserting a line above
 * the cursor.
 */
void add_indent(int new_line, int old_line, int ignore_brace) {
	if (env->indent) {
		int changed = 0;
		if (old_line < new_line && line_is_comment(env->lines[new_line])) {
			for (int i = 0; i < env->lines[old_line]->actual; ++i) {
				if (env->lines[old_line]->text[i].codepoint == '/') {
					if (env->lines[old_line]->text[i+1].codepoint == '*') {
						/* Insert ' * ' */
						char_t space = {1,FLAG_COMMENT,' '};
						char_t asterisk = {1,FLAG_COMMENT,'*'};
						env->lines[new_line] = line_insert(env->lines[new_line],space,i,new_line);
						env->lines[new_line] = line_insert(env->lines[new_line],asterisk,i+1,new_line);
						env->lines[new_line] = line_insert(env->lines[new_line],space,i+2,new_line);
						env->col_no += 3;
					}
					break;
				} else if (env->lines[old_line]->text[i].codepoint == ' ' && env->lines[old_line]->text[i+1].codepoint == '*') {
					/* Insert ' * ' */
					char_t space = {1,FLAG_COMMENT,' '};
					char_t asterisk = {1,FLAG_COMMENT,'*'};
					env->lines[new_line] = line_insert(env->lines[new_line],space,i,new_line);
					env->lines[new_line] = line_insert(env->lines[new_line],asterisk,i+1,new_line);
					env->lines[new_line] = line_insert(env->lines[new_line],space,i+2,new_line);
					env->col_no += 3;
					break;
				} else if (env->lines[old_line]->text[i].codepoint == ' ' ||
					env->lines[old_line]->text[i].codepoint == '\t' ||
					env->lines[old_line]->text[i].codepoint == '*') {
					env->lines[new_line] = line_insert(env->lines[new_line],env->lines[old_line]->text[i],i,new_line);
					env->col_no++;
					changed = 1;
				} else {
					break;
				}
			}
		} else {
			int line_to_copy_from = old_line;
			int col;
			if (old_line < new_line &&
				!ignore_brace &&
				(col = line_ends_with_brace(env->lines[old_line])) &&
				env->lines[old_line]->text[col-1].codepoint == '{') {
				line_to_copy_from = find_brace_line_start(old_line+1, col)-1;
			}
			for (int i = 0; i < env->lines[line_to_copy_from]->actual; ++i) {
				if (line_to_copy_from < new_line && i == env->lines[line_to_copy_from]->actual - 3 &&
					env->lines[line_to_copy_from]->text[i].codepoint == ' ' &&
					env->lines[line_to_copy_from]->text[i+1].codepoint == '*' &&
					env->lines[line_to_copy_from]->text[i+2].codepoint == '/') {
					break;
				} else if (env->lines[line_to_copy_from]->text[i].codepoint == ' ' ||
					env->lines[line_to_copy_from]->text[i].codepoint == '\t') {
					env->lines[new_line] = line_insert(env->lines[new_line],env->lines[line_to_copy_from]->text[i],i,new_line);
					env->col_no++;
					changed = 1;
				} else {
					break;
				}
			}
		}
		if (old_line < new_line && !ignore_brace && line_ends_with_brace(env->lines[old_line])) {
			if (env->tabs) {
				char_t c = {0};
				c.codepoint = '\t';
				c.display_width = env->tabstop;
				env->lines[new_line] = line_insert(env->lines[new_line], c, env->col_no-1, new_line);
				env->col_no++;
				changed = 1;
			} else {
				for (int j = 0; j < env->tabstop; ++j) {
					char_t c = {0};
					c.codepoint = ' ';
					c.display_width = 1;
					env->lines[new_line] = line_insert(env->lines[new_line], c, env->col_no-1, new_line);
					env->col_no++;
				}
				changed = 1;
			}
		}
		int was_whitespace = 1;
		for (int i = 0; i < env->lines[old_line]->actual; ++i) {
			if (env->lines[old_line]->text[i].codepoint != ' ' &&
				env->lines[old_line]->text[i].codepoint != '\t') {
				was_whitespace = 0;
				break;
			}
		}
		if (was_whitespace) {
			while (env->lines[old_line]->actual) {
				line_delete(env->lines[old_line], env->lines[old_line]->actual, old_line);
			}
		}
		if (changed) {
			recalculate_syntax(env->lines[new_line],new_line);
		}
	}
}

/**
 * Initialize a buffer with default values.
 *
 * Should be called after creating a buffer.
 */
void setup_buffer(buffer_t * env) {
	/* If this buffer was already initialized, clear out its line data */
	if (env->lines) {
		for (int i = 0; i < env->line_count; ++i) {
			free(env->lines[i]);
		}
		free(env->lines);
	}

	/* Default state parameters */
	env->line_no     = 1; /* Default cursor position */
	env->col_no      = 1;
	env->line_count  = 1; /* Buffers always have at least one line */
	env->modified    = 0;
	env->readonly    = 0;
	env->offset      = 0;
	env->line_avail  = 8; /* Default line buffer capacity */
	env->tabs        = 1; /* Tabs by default */
	env->tabstop     = 4; /* Tab stop width */
	env->indent      = 1; /* Auto-indent by default */
	env->history     = malloc(sizeof(struct history));
	memset(env->history, 0, sizeof(struct history));
	env->last_save_history = env->history;

	/* Allocate line buffer */
	env->lines = malloc(sizeof(line_t *) * env->line_avail);

	/* Initialize the first line */
	env->lines[0] = calloc(sizeof(line_t) + sizeof(char_t) * 32, 1);
	env->lines[0]->available = 32;
}

/**
 * Toggle buffered / unbuffered modes
 */
struct termios old;
void get_initial_termios(void) {
	tcgetattr(STDOUT_FILENO, &old);
}

void set_unbuffered(void) {
	struct termios new = old;
	new.c_iflag &= (~ICRNL) & (~IXON);
	new.c_lflag &= (~ICANON) & (~ECHO) & (~ISIG);
#ifdef VLNEXT
	new.c_cc[VLNEXT] = 0;
#endif
	tcsetattr(STDOUT_FILENO, TCSAFLUSH, &new);
}

void set_buffered(void) {
	tcsetattr(STDOUT_FILENO, TCSAFLUSH, &old);
}

/**
 * Convert codepoint to utf-8 string
 */
int to_eight(uint32_t codepoint, char * out) {
	memset(out, 0x00, 7);

	if (codepoint < 0x0080) {
		out[0] = (char)codepoint;
	} else if (codepoint < 0x0800) {
		out[0] = 0xC0 | (codepoint >> 6);
		out[1] = 0x80 | (codepoint & 0x3F);
	} else if (codepoint < 0x10000) {
		out[0] = 0xE0 | (codepoint >> 12);
		out[1] = 0x80 | ((codepoint >> 6) & 0x3F);
		out[2] = 0x80 | (codepoint & 0x3F);
	} else if (codepoint < 0x200000) {
		out[0] = 0xF0 | (codepoint >> 18);
		out[1] = 0x80 | ((codepoint >> 12) & 0x3F);
		out[2] = 0x80 | ((codepoint >> 6) & 0x3F);
		out[3] = 0x80 | ((codepoint) & 0x3F);
	} else if (codepoint < 0x4000000) {
		out[0] = 0xF8 | (codepoint >> 24);
		out[1] = 0x80 | (codepoint >> 18);
		out[2] = 0x80 | ((codepoint >> 12) & 0x3F);
		out[3] = 0x80 | ((codepoint >> 6) & 0x3F);
		out[4] = 0x80 | ((codepoint) & 0x3F);
	} else {
		out[0] = 0xF8 | (codepoint >> 30);
		out[1] = 0x80 | ((codepoint >> 24) & 0x3F);
		out[2] = 0x80 | ((codepoint >> 18) & 0x3F);
		out[3] = 0x80 | ((codepoint >> 12) & 0x3F);
		out[4] = 0x80 | ((codepoint >> 6) & 0x3F);
		out[5] = 0x80 | ((codepoint) & 0x3F);
	}

	return strlen(out);
}

/**
 * Get the presentation width of a codepoint
 */
int codepoint_width(wchar_t codepoint) {
	if (codepoint == '\t') {
		return 1; /* Recalculate later */
	}
	if (codepoint < 32) {
		/* We render these as ^@ */
		return 2;
	}
	if (codepoint == 0x7F) {
		/* Renders as ^? */
		return 2;
	}
	if (codepoint > 0x7f && codepoint < 0xa0) {
		/* Upper control bytes <xx> */
		return 4;
	}
	if (codepoint == 0xa0) {
		/* Non-breaking space _ */
		return 1;
	}
	/* Skip wcwidth for anything under 256 */
	if (codepoint > 127) {
		if (global_config.can_unicode) {
			/* Higher codepoints may be wider (eg. Japanese) */
			int out = wcwidth(codepoint);
			if (out >= 1) return out;
		}
		/* Invalid character, render as [U+ABCD] or [U+ABCDEF] */
		return (codepoint < 0x10000) ? 8 : 10;
	}
	return 1;
}

/**
 * The following section contains methods for crafting terminal escapes
 * for rendering the display. We do not use curses or any similar
 * library, so we have to generate these sequences ourself based on
 * some assumptions about the target terminal.
 */

/**
 * Move the terminal cursor
 */
void place_cursor(int x, int y) {
	printf("\033[%d;%dH", y, x);
}

/**
 * Given two color codes from a theme, convert them to an escape
 * sequence to set the foreground and background color. This allows
 * us to support basic 16, xterm 256, and true color in themes.
 */
char * color_string(const char * fg, const char * bg) {
	static char output[100];
	char * t = output;
	t += sprintf(t,"\033[22;23;");
	if (*bg == '@') {
		int _bg = atoi(bg+1);
		if (_bg < 10) {
			t += sprintf(t, "4%d;", _bg);
		} else {
			t += sprintf(t, "10%d;", _bg-10);
		}
	} else {
		t += sprintf(t, "48;%s;", bg);
	}
	if (*fg == '@') {
		int _fg = atoi(fg+1);
		if (_fg < 10) {
			t += sprintf(t, "3%dm", _fg);
		} else {
			t += sprintf(t, "9%dm", _fg-10);
		}
	} else {
		t += sprintf(t, "38;%sm", fg);
	}
	return output;
}

/**
 * Set text colors
 *
 * Normally, text colors are just strings, but if they
 * start with @ they will be parsed as integers
 * representing one of the 16 standard colors, suitable
 * for terminals without support for the 256- or 24-bit
 * color modes.
 */
void set_colors(const char * fg, const char * bg) {
	printf("%s", color_string(fg, bg));
}

/**
 * Set just the foreground color
 *
 * (See set_colors above)
 */
void set_fg_color(const char * fg) {
	printf("\033[22;23;");
	if (*fg == '@') {
		int _fg = atoi(fg+1);
		if (_fg < 10) {
			printf("3%dm", _fg);
		} else {
			printf("9%dm", _fg-10);
		}
	} else {
		printf("38;%sm", fg);
	}
}

/**
 * Clear the rest of this line
 */
void clear_to_end(void) {
	if (global_config.can_bce) {
		printf("\033[K");
	}
}

/**
 * For terminals without bce, prepaint the whole line, so we don't have to track
 * where the cursor is for everything. Inefficient, but effective.
 */
void paint_line(const char * bg) {
	if (!global_config.can_bce) {
		set_colors(COLOR_FG, bg);
		for (int i = 0; i < global_config.term_width; ++i) {
			printf(" ");
		}
		printf("\r");
	}
}

/**
 * Enable bold text display
 */
void set_bold(void) {
	printf("\033[1m");
}

/**
 * Disable bold
 */
void unset_bold(void) {
	printf("\033[22m");
}

/**
 * Enable underlined text display
 */
void set_underline(void) {
	printf("\033[4m");
}

/**
 * Disable underlined text display
 */
void unset_underline(void) {
	printf("\033[24m");
}

/**
 * Reset text display attributes
 */
void reset(void) {
	printf("\033[0m");
}

/**
 * Clear the entire screen
 */
void clear_screen(void) {
	printf("\033[H\033[2J");
}

/**
 * Hide the cursor
 */
void hide_cursor(void) {
	if (global_config.can_hideshow) {
		printf("\033[?25l");
	}
}

/**
 * Show the cursor
 */
void show_cursor(void) {
	if (global_config.can_hideshow) {
		printf("\033[?25h");
	}
}

/**
 * Store the cursor position
 */
void store_cursor(void) {
	printf("\0337");
}

/**
 * Restore the cursor position.
 */
void restore_cursor(void) {
	printf("\0338");
}

/**
 * Request mouse events
 */
void mouse_enable(void) {
	if (global_config.can_mouse) {
		printf("\033[?1000h");
		if (global_config.can_sgrmouse) {
			printf("\033[?1006h");
		}
	}
}

/**
 * Stop mouse events
 */
void mouse_disable(void) {
	if (global_config.can_mouse) {
		if (global_config.can_sgrmouse) {
			printf("\033[?1006l");
		}
		printf("\033[?1000l");
	}
}

/**
 * Shift the screen up one line
 */
void shift_up(int amount) {
	printf("\033[%dS", amount);
}

/**
 * Shift the screen down one line.
 */
void shift_down(int amount) {
	printf("\033[%dT", amount);
}

void insert_lines_at(int line, int count) {
	place_cursor(1, line);
	printf("\033[%dL", count);
}

void delete_lines_at(int line, int count) {
	place_cursor(1, line);
	printf("\033[%dM", count);
}

/**
 * Switch to the alternate terminal screen.
 */
void set_alternate_screen(void) {
	if (global_config.can_altscreen) {
		printf("\033[?1049h");
	}
}

/**
 * Restore the standard terminal screen.
 */
void unset_alternate_screen(void) {
	if (global_config.can_altscreen) {
		printf("\033[?1049l");
	}
}

/**
 * Enable bracketed paste mode.
 */
void set_bracketed_paste(void) {
	if (global_config.can_bracketedpaste) {
		printf("\033[?2004h");
	}
}

/**
 * Disable bracketed paste mode.
 */
void unset_bracketed_paste(void) {
	if (global_config.can_bracketedpaste) {
		printf("\033[?2004l");
	}
}

/**
 * Get the name of just a file from a full path.
 * Returns a pointer within the original string.
 *
 * Called in a few different places where the name of a file
 * is needed from its full path, such as drawing tab names or
 * building HTML files.
 */
char * file_basename(char * file) {
	char * c = strrchr(file, '/');
	if (!c) return file;
	return (c+1);
}

/**
 * Print a tab name with fixed width and modifiers
 * into an output buffer and return the written width.
 */
int draw_tab_name(buffer_t * _env, char * out, int max_width, int * width) {
	uint32_t c, state = 0;
	char * t = _env->file_name ? file_basename(_env->file_name) : "[No Name]";

#define ADD(c) do { \
	*o = c; \
	o++; \
	*o = '\0'; \
	bytes++; \
} while (0)

	char * o = out;
	*o = '\0';

	int bytes = 0;

	if (max_width < 2) return 1;

	ADD(' ');
	(*width)++;

	if (_env->modified) {
		if (max_width < 4) return 1;
		ADD('+');
		(*width)++;
		ADD(' ');
		(*width)++;
	}

	while (*t) {
		/* File names can definitely by UTF-8, and we need to
		 * understand their display width... */
		if (!decode(&state, &c, (unsigned char)*t)) {

			/* But our displayed tab name is also just stored
			 * as UTF-8 again, so we essentially rebuild it... */
			char tmp[7];
			int size = to_eight(c, tmp);
			if (bytes + size > 62) break;
			if (*width + size >= max_width) return 1;

			for (int i = 0; i < size; ++i) {
				ADD(tmp[i]);
			}

			(*width) += codepoint_width(c);

		} else if (state == UTF8_REJECT) {
			state = 0;
		}
		t++;
	}

	if (max_width == *width + 1) return 1;

	ADD(' ');
	(*width)++;

#undef ADD
	return 0;
}

/**
 * Redraw the tabbar, with a tab for each buffer.
 *
 * The active buffer is highlighted.
 */
void redraw_tabbar(void) {
	if (!global_config.tabs_visible) return;
	/* Hide cursor while rendering UI */
	hide_cursor();

	/* Move to upper left */
	place_cursor(1,1);

	paint_line(COLOR_TABBAR_BG);

	/* For each buffer... */
	int offset = 0;

	if (global_config.tab_offset) {
		set_colors(COLOR_NUMBER_FG, COLOR_NUMBER_BG);
		printf("<");
		offset++;
	}

	for (int i = global_config.tab_offset; i < buffers_len; i++) {
		buffer_t * _env = buffers[i];

		if (_env == env) {
			/* If this is the active buffer, highlight it */
			reset();
			set_colors(COLOR_FG, COLOR_BG);
			set_bold();
		} else {
			/* Otherwise use default tab color */
			reset();
			set_colors(COLOR_FG, COLOR_TAB_BG);
			set_underline();
		}

		if (global_config.overlay_mode == OVERLAY_MODE_FILESEARCH) {
			if (global_config.command_buffer->actual) {
				char * f = _env->file_name ? file_basename(_env->file_name) : "";
				/* TODO: Support unicode input here; needs conversion */
				int i = 0;
				for (; i < global_config.command_buffer->actual &&
				      f[i] == global_config.command_buffer->text[i].codepoint; ++i);
				if (global_config.command_buffer->actual == i) {
					set_colors(COLOR_SEARCH_FG, COLOR_SEARCH_BG);
				}
			}
		}


		char title[64];
		int size = 0;
		int filled = draw_tab_name(_env, title, global_config.term_width - offset, &size);

		if (filled) {
			offset += size;
			printf("%s", title);
			set_colors(COLOR_NUMBER_FG, COLOR_NUMBER_BG);
			while (offset != global_config.term_width - 1) {
				printf(" ");
				offset++;
			}
			printf(">");
			break;
		}

		printf("%s", title);

		offset += size;
	}

	/* Reset bold/underline */
	reset();
	/* Fill the rest of the tab bar */
	set_colors(COLOR_FG, COLOR_TABBAR_BG);
	clear_to_end();
}

/**
 * Braindead log10 implementation for figuring out the width of the
 * line number column.
 */
int log_base_10(unsigned int v) {
	int r = (v >= 1000000000) ? 9 : (v >= 100000000) ? 8 : (v >= 10000000) ? 7 :
		(v >= 1000000) ? 6 : (v >= 100000) ? 5 : (v >= 10000) ? 4 :
		(v >= 1000) ? 3 : (v >= 100) ? 2 : (v >= 10) ? 1 : 0;
	return r;
}

/**
 * Render a line of text.
 *
 * This handles rendering the actual text content. A full line of text
 * also includes a line number and some padding, but those elements
 * are rendered elsewhere; this method can be used for lines that are
 * not attached to a buffer such as command input lines.
 *
 * width: width of the text display region (term width - line number width)
 * offset: how many cells into the line to start rendering at
 */
void render_line(line_t * line, int width, int offset, int line_no) {
	int i = 0; /* Offset in char_t line data entries */
	int j = 0; /* Offset in terminal cells */

	const char * last_color = NULL;
	int was_selecting = 0, was_searching = 0, was_underlining = 0;

	/* Set default text colors */
	set_colors(COLOR_FG, line->is_current ? COLOR_ALT_BG : COLOR_BG);

	/*
	 * When we are rendering in the middle of a wide character,
	 * we render -'s to fill the remaining amount of the character's width.
	 */
	int remainder = 0;

	int is_spaces = 1;

	/* For each character in the line ... */
	while (i < line->actual) {

		/* If there is remaining text... */
		if (remainder) {

			/* If we should be drawing by now... */
			if (j >= offset) {
				if (was_underlining) printf("\033[24m");
				/* Fill remainder with -'s */
				set_colors(COLOR_ALT_FG, COLOR_ALT_BG);
				printf("-");
				set_colors(COLOR_FG, line->is_current ? COLOR_ALT_BG : COLOR_BG);
			}

			/* One less remaining width cell to fill */
			remainder--;

			/* Terminal offset moves forward */
			j++;

			/*
			 * If this was the last remaining character, move to
			 * the next codepoint in the line
			 */
			if (remainder == 0) {
				i++;
			}

			continue;
		}

		/* Get the next character to draw */
		char_t c = line->text[i];

		if (c.codepoint != ' ') is_spaces = 0;

		/* If we should be drawing by now... */
		if (j >= offset) {

			/* If this character is going to fall off the edge of the screen... */
			if (j - offset + c.display_width >= width) {
				/* We draw this with special colors so it isn't ambiguous */
				set_colors(COLOR_ALT_FG, COLOR_ALT_BG);
				if (was_underlining) printf("\033[24m");

				/* If it's wide, draw ---> as needed */
				while (j - offset < width - 1) {
					printf("-");
					j++;
				}

				/* End the line with a > to show it overflows */
				printf(">");
				set_colors(COLOR_FG, COLOR_BG);
				return;
			}

			/* Syntax highlighting */
			const char * color = flag_to_color(c.flags);

			if (c.flags & FLAG_SELECT) {
				if ((c.flags & FLAG_MASK_COLORS) == FLAG_NONE) color = COLOR_SELECTFG;
				set_colors(color, COLOR_SELECTBG);
				was_selecting = 1;
			} else if ((c.flags & FLAG_SEARCH) || (c.flags == FLAG_NOTICE)) {
				set_colors(COLOR_SEARCH_FG, COLOR_SEARCH_BG);
				was_searching = 1;
			} else if (c.flags == FLAG_ERROR) {
				set_colors(COLOR_ERROR_FG, COLOR_ERROR_BG);
				was_searching = 1; /* co-opting this should work... */
			} else {
				if (was_selecting || was_searching) {
					was_selecting = 0;
					was_searching = 0;
					set_colors(color, line->is_current ? COLOR_ALT_BG : COLOR_BG);
					last_color = color;
				} else if (!last_color || strcmp(color, last_color)) {
					set_fg_color(color);
					last_color = color;
				}
			}

			if ((c.flags & FLAG_UNDERLINE) && !was_underlining) {
				printf("\033[4m");
				was_underlining = 1;
			} else if (!(c.flags & FLAG_UNDERLINE) && was_underlining) {
				printf("\033[24m");
				was_underlining = 0;
			}

			if ((env->mode == MODE_COL_SELECTION || env->mode == MODE_COL_INSERT) &&
				line_no >= ((env->start_line < env->line_no) ? env->start_line : env->line_no) &&
				line_no <= ((env->start_line < env->line_no) ? env->line_no : env->start_line) &&
				((j == env->sel_col) ||
				(j < env->sel_col && j + c.display_width > env->sel_col))) {
				set_colors(COLOR_SELECTFG, COLOR_SELECTBG);
				was_selecting = 1;
			}

#define _set_colors(fg,bg) \
	if (!(c.flags & FLAG_SELECT) && !(c.flags & FLAG_SEARCH) && !(was_selecting)) { \
		set_colors(fg,(line->is_current && bg == COLOR_BG) ? COLOR_ALT_BG : bg); \
	}

			/* Render special characters */
			if (c.codepoint == '\t') {
				_set_colors(COLOR_ALT_FG, COLOR_ALT_BG);
				printf("%s", global_config.tab_indicator);
				for (int i = 1; i < c.display_width; ++i) {
					printf("%s" ,global_config.space_indicator);
				}
				_set_colors(last_color ? last_color : COLOR_FG, COLOR_BG);
			} else if (c.codepoint < 32) {
				/* Codepoints under 32 to get converted to ^@ escapes */
				_set_colors(COLOR_ALT_FG, COLOR_ALT_BG);
				printf("^%c", '@' + c.codepoint);
				_set_colors(last_color ? last_color : COLOR_FG, COLOR_BG);
			} else if (c.codepoint == 0x7f) {
				_set_colors(COLOR_ALT_FG, COLOR_ALT_BG);
				printf("^?");
				_set_colors(last_color ? last_color : COLOR_FG, COLOR_BG);
			} else if (c.codepoint > 0x7f && c.codepoint < 0xa0) {
				_set_colors(COLOR_ALT_FG, COLOR_ALT_BG);
				printf("<%2x>", c.codepoint);
				_set_colors(last_color ? last_color : COLOR_FG, COLOR_BG);
			} else if (c.codepoint == 0xa0) {
				_set_colors(COLOR_ALT_FG, COLOR_ALT_BG);
				printf("_");
				_set_colors(last_color ? last_color : COLOR_FG, COLOR_BG);
			} else if (c.display_width == 8) {
				_set_colors(COLOR_ALT_FG, COLOR_ALT_BG);
				printf("[U+%04x]", c.codepoint);
				_set_colors(last_color ? last_color : COLOR_FG, COLOR_BG);
			} else if (c.display_width == 10) {
				_set_colors(COLOR_ALT_FG, COLOR_ALT_BG);
				printf("[U+%06x]", c.codepoint);
				_set_colors(last_color ? last_color : COLOR_FG, COLOR_BG);
			} else if (i > 0 && is_spaces && c.codepoint == ' ' && !(i % env->tabstop)) {
				_set_colors(COLOR_ALT_FG, COLOR_BG); /* Normal background so this is more subtle */
				if (global_config.can_unicode) {
					printf("▏");
				} else {
					printf("|");
				}
				_set_colors(last_color ? last_color : COLOR_FG, COLOR_BG);
			} else if (c.codepoint == ' ' && i == line->actual - 1) {
				/* Special case: space at end of line */
				_set_colors(COLOR_ALT_FG, COLOR_ALT_BG);
				printf("%s",global_config.space_indicator);
				_set_colors(COLOR_FG, COLOR_BG);
			} else {
				/* Normal characters get output */
				char tmp[7]; /* Max six bytes, use 7 to ensure last is always nil */
				to_eight(c.codepoint, tmp);
				printf("%s", tmp);
			}

			/* Advance the terminal cell offset by the render width of this character */
			j += c.display_width;

			/* Advance to the next character */
			i++;
		} else if (c.display_width > 1) {
			/*
			 * If this is a wide character but we aren't ready to render yet,
			 * we may need to draw some filler text for the remainder of its
			 * width to ensure we don't jump around when horizontally scrolling
			 * past wide characters.
			 */
			remainder = c.display_width - 1;
			j++;
		} else {
			/* Regular character, not ready to draw, advance without doing anything */
			j++;
			i++;
		}
	}

	if (was_underlining) printf("\033[24m");

	/**
	 * Determine what color the rest of the line should be.
	 */
	if (env->mode != MODE_LINE_SELECTION) {
		/* If we are not selecting, then use the normal background or highlight
		 * the current line if that feature is enabled. */
		if (line->is_current) {
			set_colors(COLOR_FG, COLOR_ALT_BG);
		} else {
			set_colors(COLOR_FG, COLOR_BG);
		}
	} else {
		/* If this line was empty but was part of the selection, we didn't
		 * set the selection color already, so we need to do that here. */
		if (!line->actual) {
			if (env->line_no == line_no ||
				(env->start_line > env->line_no && 
					(line_no >= env->line_no && line_no <= env->start_line)) ||
				(env->start_line < env->line_no &&
					(line_no >= env->start_line && line_no <= env->line_no))) {
				set_colors(COLOR_SELECTFG, COLOR_SELECTBG);
			}
		}
	}

	/**
	 * In column modes, we may need to draw a column select beyond the end
	 * of a given line, so we need to draw up to that point first.
	 */
	if ((env->mode == MODE_COL_SELECTION  || env->mode == MODE_COL_INSERT) &&
		line_no >= ((env->start_line < env->line_no) ? env->start_line : env->line_no) &&
		line_no <= ((env->start_line < env->line_no) ? env->line_no : env->start_line) &&
		j <= env->sel_col &&
		env->sel_col < width) {
		set_colors(COLOR_FG, COLOR_BG);
		while (j < env->sel_col) {
			printf(" ");
			j++;
		}
		set_colors(COLOR_SELECTFG, COLOR_SELECTBG);
		printf(" ");
		j++;
		set_colors(COLOR_FG, COLOR_BG);
	}

	/*
	 * `maxcolumn` renders the background outside of the requested line length
	 * in a different color, with a line at the border between the two.
	 */
	if (env->maxcolumn && line_no > -1 /* ensures we don't do this for command line */) {

		/* Fill out the normal background */
		if (j < offset) j = offset;
		for (; j < width + offset && j < env->maxcolumn; ++j) {
			printf(" ");
		}

		/* Draw the line */
		if (j < width + offset && j == env->maxcolumn) {
			j++;
			set_colors(COLOR_ALT_FG, COLOR_ALT_BG);
			if (global_config.can_unicode) {
				printf("▏"); /* Should this be configurable? */
			} else {
				printf("|");
			}
		}

		/* Fill the rest with the alternate background color */
		set_colors(COLOR_ALT_FG, COLOR_ALT_BG);
	}

	/**
	 * Clear out the rest of the line. If we are the only buffer or the right split,
	 * and our terminal supports `bce`, we can just bce; otherwise write spaces
	 * until we reach the right side of the screen.
	 */
	if (global_config.can_bce && (line_no == -1 || env->left + env->width == global_config.term_width)) {
		clear_to_end();
	} else {
		/* Paint the rest of the line */
		if (j < offset) j = offset;
		for (; j < width + offset; ++j) {
			printf(" ");
		}
	}
}

/**
 * Get the width of the line number region
 */
int num_width(void) {
	if (!env->numbers) return 0;
	int w = log_base_10(env->line_count) + 3;
	if (w < 4) return 4;
	return w;
}

/**
 * Display width of the revision status gutter.
 */
int gutter_width(void) {
	return env->gutter;
}

/**
 * Draw the gutter and line numbers.
 */
void draw_line_number(int x) {
	if (!env->numbers) return;
	/* Draw the line number */
	if (env->lines[x]->is_current) {
		set_colors(COLOR_NUMBER_BG, COLOR_NUMBER_FG);
	} else {
		set_colors(COLOR_NUMBER_FG, COLOR_NUMBER_BG);
	}
	if (global_config.relative_lines && x+1 != env->line_no) {
		x = x+1 - env->line_no;
		x = ((x < 0) ? -x : x)-1;
	}
	int num_size = num_width() - 2; /* Padding */
	for (int y = 0; y < num_size - log_base_10(x + 1); ++y) {
		printf(" ");
	}
	printf("%d%c", x + 1, ((x+1 == env->line_no || global_config.horizontal_shift_scrolling) && env->coffset > 0) ? '<' : ' ');
}

/**
 * Used to highlight the current line after moving the cursor.
 */
void recalculate_current_line(void) {
	int something_changed = 0;
	if (global_config.highlight_current_line) {
		for (int i = 0; i < env->line_count; ++i) {
			if (env->lines[i]->is_current && i != env->line_no-1) {
				env->lines[i]->is_current = 0;
				something_changed = 1;
				redraw_line(i);
			} else if (i == env->line_no-1 && !env->lines[i]->is_current) {
				env->lines[i]->is_current = 1;
				something_changed = 1;
				redraw_line(i);
			}
		}
	} else {
		something_changed = 1;
	}
	if (something_changed && global_config.relative_lines) {
		for (int i = env->offset; i < env->offset + global_config.term_height - global_config.bottom_size - 1 && i < env->line_count; ++i) {
			/* Place cursor for line number */
			place_cursor(1 + gutter_width() + env->left, (i)-env->offset + 1 + global_config.tabs_visible);
			draw_line_number(i);
		}
	}
}

/**
 * Redraw line.
 *
 * This draws the line number as well as the actual text.
 */
void redraw_line(int x) {
	if (env->loading) return;

	/* Determine if this line is visible. */
	if (x - env->offset < 0 || x - env->offset > global_config.term_height - global_config.bottom_size - 1 - global_config.tabs_visible) {
		return;
	}

	/* Calculate offset in screen */
	int j = x - env->offset;

	/* Hide cursor when drawing */
	hide_cursor();

	/* Move cursor to upper left most cell of this line */
	place_cursor(1 + env->left,1 + global_config.tabs_visible + j);

	/* Draw a gutter on the left. */
	if (env->gutter) {
		switch (env->lines[x]->rev_status) {
			case 1:
				set_colors(COLOR_NUMBER_FG, COLOR_GREEN);
				printf(" ");
				break;
			case 2:
				set_colors(COLOR_NUMBER_FG, global_config.color_gutter ? COLOR_SEARCH_BG : COLOR_ALT_FG);
				printf(" ");
				break;
			case 3:
				set_colors(COLOR_NUMBER_FG, COLOR_KEYWORD);
				printf(" ");
				break;
			case 4:
				set_colors(COLOR_ALT_FG, COLOR_RED);
				printf("▆");
				break;
			case 5:
				set_colors(COLOR_KEYWORD, COLOR_RED);
				printf("▆");
				break;
			default:
				set_colors(COLOR_NUMBER_FG, COLOR_ALT_FG);
				printf(" ");
				break;
		}
	}

	draw_line_number(x);

	int should_shift = x + 1 == env->line_no || global_config.horizontal_shift_scrolling || 
			((env->mode == MODE_COL_SELECTION || env->mode == MODE_COL_INSERT) &&
			x + 1 >= ((env->start_line < env->line_no) ? env->start_line : env->line_no) &&
			x + 1 <= ((env->start_line < env->line_no) ? env->line_no : env->start_line));
	

	/*
	 * Draw the line text 
	 * If this is the active line, the current character cell offset should be used.
	 * (Non-active lines are not shifted and always render from the start of the line)
	 */
	render_line(env->lines[x], env->width - gutter_width() - num_width(), should_shift ? env->coffset : 0, x+1);

}

/**
 * Draw a ~ line where there is no buffer text.
 */
void draw_excess_line(int j) {
	place_cursor(1+env->left,1 + global_config.tabs_visible + j);
	paint_line(COLOR_ALT_BG);
	set_colors(COLOR_ALT_FG, COLOR_ALT_BG);
	printf("~");
	if (env->left + env->width == global_config.term_width && global_config.can_bce) {
		clear_to_end();
	} else {
		/* Paint the rest of the line */
		for (int x = 1; x < env->width; ++x) {
			printf(" ");
		}
	}
}

/**
 * Redraw the entire text area
 */
void redraw_text(void) {
	if (!env) return;
	if (!global_config.has_terminal) return;

	/* Hide cursor while rendering */
	hide_cursor();

	/* Figure out the available size of the text region */
	int l = global_config.term_height - global_config.bottom_size - global_config.tabs_visible;
	int j = 0;

	/* Draw each line */
	for (int x = env->offset; j < l && x < env->line_count; x++) {
		redraw_line(x);
		j++;
	}

	/* Draw the rest of the text region as ~ lines */
	for (; j < l; ++j) {
		draw_excess_line(j);
	}
}

static int view_left_offset = 0;
static int view_right_offset = 0;

/**
 * When in split view, draw the other buffer.
 * Has special handling for when the split is
 * on a single buffer.
 */
void redraw_alt_buffer(buffer_t * buf) {
	if (left_buffer == right_buffer) {
		/* draw the opposite view */
		int left, width, offset;
		left = env->left;
		width = env->width;
		offset = env->offset;
		if (left == 0) {
			/* Draw the right side */

			env->left = width;
			env->width = global_config.term_width - width;
			env->offset = view_right_offset;
			view_left_offset = offset;
		} else {
			env->left = 0;
			env->width = global_config.term_width * global_config.split_percent / 100;
			env->offset = view_left_offset;
			view_right_offset = offset;
		}
		redraw_text();

		env->left = left;
		env->width = width;
		env->offset = offset;
	}
	/* Swap out active buffer */
	buffer_t * tmp = env;
	env = buf;
	/* Redraw text */
	redraw_text();
	/* Return original active buffer */
	env = tmp;
}

/**
 * Basically wcswidth() but implemented internally using our
 * own utf-8 decoder to ensure it works properly.
 */
int display_width_of_string(const char * str) {
	uint8_t * s = (uint8_t *)str;

	int out = 0;
	uint32_t c, state = 0;
	while (*s) {
		if (!decode(&state, &c, *s)) {
			out += codepoint_width(c);
		} else if (state == UTF8_REJECT) {
			state = 0;
		}
		s++;
	}

	return out;
}

int statusbar_append_status(int *remaining_width, size_t *filled, char * output, char * base, ...) {
	va_list args;
	va_start(args, base);
	char tmp[100] = {0}; /* should be big enough */
	vsnprintf(tmp, 100, base, args);
	va_end(args);

	int width = display_width_of_string(tmp) + 2;

	size_t totalWidth = strlen(tmp);
	totalWidth += strlen(color_string(COLOR_STATUS_ALT, COLOR_STATUS_BG));
	totalWidth += strlen(color_string(COLOR_STATUS_FG, COLOR_STATUS_BG));
	totalWidth += strlen(color_string(COLOR_STATUS_ALT, COLOR_STATUS_BG));
	totalWidth += 3;

	if (totalWidth + *filled >= 2047) {
		return 0;
	}

	if (width < *remaining_width) {
		strcat(output,color_string(COLOR_STATUS_ALT, COLOR_STATUS_BG));
		strcat(output,"[");
		strcat(output,color_string(COLOR_STATUS_FG, COLOR_STATUS_BG));
		strcat(output, tmp);
		strcat(output,color_string(COLOR_STATUS_ALT, COLOR_STATUS_BG));
		strcat(output,"]");
		(*remaining_width) -= width;
		(*filled) += totalWidth;
		return width;
	} else {
		return 0;
	}
}

int statusbar_build_right(char * right_hand) {
	char tmp[1024] = {0};
	sprintf(tmp, " Line %d/%d Col: %d ", env->line_no, env->line_count, env->col_no);
	int out = display_width_of_string(tmp);
	char * s = right_hand;

	s += sprintf(s, "%s", color_string(COLOR_STATUS_ALT, COLOR_STATUS_BG));
	s += sprintf(s, " Line ");
	s += sprintf(s, "%s", color_string(COLOR_STATUS_FG, COLOR_STATUS_BG));
	s += sprintf(s, "%d/%d ", env->line_no, env->line_count);
	s += sprintf(s, "%s", color_string(COLOR_STATUS_ALT, COLOR_STATUS_BG));
	s += sprintf(s, " Col: ");
	s += sprintf(s, "%s", color_string(COLOR_STATUS_FG, COLOR_STATUS_BG));
	s += sprintf(s, "%d ", env->col_no);

	return out;
}

/**
 * Draw the status bar
 *
 * The status bar shows the name of the file, whether it has modifications,
 * and (in the future) what syntax highlighting mode is enabled.
 *
 * The right side of the status bar shows the line number and column.
 */
void redraw_statusbar(void) {
	if (global_config.hide_statusbar) return;
	if (!global_config.has_terminal) return;
	if (!env) return;
	/* Hide cursor while rendering */
	hide_cursor();

	/* Move cursor to the status bar line (second from bottom */
	place_cursor(1, global_config.term_height - 1);

	/* Set background colors for status line */
	paint_line(COLOR_STATUS_BG);
	set_colors(COLOR_STATUS_FG, COLOR_STATUS_BG);


	/* Pre-render the right hand side of the status bar */
	char right_hand[1024] = {0};
	int right_width = statusbar_build_right(right_hand);

	char status_bits[2048] = {0}; /* Sane maximum */
	size_t filled = 0;
	int status_bits_width = 0;

	int remaining_width = global_config.term_width - right_width;

#define ADD(...) do { status_bits_width += statusbar_append_status(&remaining_width, &filled, status_bits, __VA_ARGS__); } while (0)
	if (env->syntax) {
		ADD("%s",env->syntax->name);
	}

	/* Print file status indicators */
	if (env->modified) {
		ADD("+");
	}

	if (env->readonly) {
		ADD("ro");
	}

	if (env->crnl) {
		ADD("crnl");
	}

	if (env->tabs) {
		ADD("tabs");
	} else {
		ADD("spaces=%d", env->tabstop);
	}

	if (global_config.yanks) {
		ADD("y:%ld", global_config.yank_count);
	}

	if (env->indent) {
		ADD("indent");
	}

	if (global_config.smart_complete) {
		ADD("complete");
	}

	if (global_config.background_task) {
		ADD("working");
	}

#undef ADD

	uint8_t * file_name = (uint8_t *)(env->file_name ? env->file_name : "[No Name]");
	int file_name_width = display_width_of_string((char*)file_name);

	if (remaining_width > 3) {
		int is_chopped = 0;
		while (remaining_width < file_name_width + 3) {
			is_chopped = 1;
			if ((*file_name & 0xc0) == 0xc0) { /* First byte of a multibyte character */
				file_name++;
				while ((*file_name & 0xc0) == 0x80) file_name++;
			} else {
				file_name++;
			}
			file_name_width = display_width_of_string((char*)file_name);
		}
		if (is_chopped) {
			set_colors(COLOR_ALT_FG, COLOR_STATUS_BG);
			printf("<");
		}
		set_colors(COLOR_STATUS_FG, COLOR_STATUS_BG);
		printf("%s ", file_name);
	}

	printf("%s", status_bits);

	/* Clear the rest of the status bar */
	clear_to_end();

	/* Move the cursor appropriately to draw it */
	place_cursor(global_config.term_width - right_width, global_config.term_height - 1);
	set_colors(COLOR_STATUS_FG, COLOR_STATUS_BG);
	printf("%s",right_hand);
}

/**
 * Redraw the navigation numbers on the right side of the command line
 */
void redraw_nav_buffer() {
	if (!global_config.has_terminal) return;
	if (nav_buffer) {
		store_cursor();
		place_cursor(global_config.term_width - nav_buffer - 2, global_config.term_height);
		printf("%s", nav_buf);
		clear_to_end();
		restore_cursor();
	}
}

/**
 * Draw the command line
 *
 * The command line either has input from the user (:quit, :!make, etc.)
 * or shows the INSERT (or VISUAL in the future) mode name.
 */
void redraw_commandline(void) {
	if (!global_config.has_terminal) return;
	if (!env) return;

	/* Hide cursor while rendering */
	hide_cursor();

	/* Move cursor to the last line */
	place_cursor(1, global_config.term_height);

	/* Set background color */
	paint_line(COLOR_BG);
	set_colors(COLOR_FG, COLOR_BG);

	/* If we are in an edit mode, note that. */
	if (env->mode == MODE_INSERT) {
		set_bold();
		printf("-- INSERT --");
		clear_to_end();
		unset_bold();
	} else if (env->mode == MODE_LINE_SELECTION) {
		set_bold();
		printf("-- LINE SELECTION -- (%d:%d)",
			(env->start_line < env->line_no) ? env->start_line : env->line_no,
			(env->start_line < env->line_no) ? env->line_no : env->start_line
		);
		clear_to_end();
		unset_bold();
	} else if (env->mode == MODE_COL_SELECTION) {
		set_bold();
		printf("-- COL SELECTION -- (%d:%d %d)",
			(env->start_line < env->line_no) ? env->start_line : env->line_no,
			(env->start_line < env->line_no) ? env->line_no : env->start_line,
			(env->sel_col)
		);
		clear_to_end();
		unset_bold();
	} else if (env->mode == MODE_COL_INSERT) {
		set_bold();
		printf("-- COL INSERT -- (%d:%d %d)",
			(env->start_line < env->line_no) ? env->start_line : env->line_no,
			(env->start_line < env->line_no) ? env->line_no : env->start_line,
			(env->sel_col)
		);
		clear_to_end();
		unset_bold();
	} else if (env->mode == MODE_REPLACE) {
		set_bold();
		printf("-- REPLACE --");
		clear_to_end();
		unset_bold();
	} else if (env->mode == MODE_CHAR_SELECTION) {
		set_bold();
		printf("-- CHAR SELECTION -- ");
		clear_to_end();
		unset_bold();
	} else if (env->mode == MODE_DIRECTORY_BROWSE) {
		set_bold();
		printf("-- DIRECTORY BROWSE --");
		clear_to_end();
		unset_bold();
	} else {
		clear_to_end();
	}

	redraw_nav_buffer();
}

/**
 * Draw a message on the command line.
 */
void render_commandline_message(char * message, ...) {
	/* varargs setup */
	va_list args;
	va_start(args, message);

	/* Hide cursor while rendering */
	hide_cursor();

	/* Move cursor to the last line */
	place_cursor(1, global_config.term_height);

	/* Set background color */
	paint_line(COLOR_BG);
	set_colors(COLOR_FG, COLOR_BG);

	vprintf(message, args);
	va_end(args);

	/* Clear the rest of the status bar */
	clear_to_end();

	redraw_nav_buffer();
}

BIM_ACTION(redraw_all, 0,
	"Repaint the screen."
)(void) {
	if (!env) return;
	redraw_tabbar();
	redraw_text();
	if (left_buffer) {
		redraw_alt_buffer(left_buffer == env ? right_buffer : left_buffer);
	}
	redraw_statusbar();
	redraw_commandline();
	if (global_config.overlay_mode == OVERLAY_MODE_COMMAND ||
	    global_config.overlay_mode == OVERLAY_MODE_SEARCH ||
	    global_config.overlay_mode == OVERLAY_MODE_FILESEARCH) {
		render_command_input_buffer();
	}
}

void pause_for_key(void) {
	int c;
	while ((c = bim_getch())== -1);
	bim_unget(c);
	redraw_all();
}

/**
 * Redraw all screen elements except the other split view.
 */
void redraw_most(void) {
	redraw_tabbar();
	redraw_text();
	redraw_statusbar();
	redraw_commandline();
}

/**
 * Disable screen splitting.
 */
void unsplit(void) {
	if (left_buffer) {
		left_buffer->left = 0;
		left_buffer->width = global_config.term_width;
	}
	if (right_buffer) {
		right_buffer->left = 0;
		right_buffer->width = global_config.term_width;
	}
	left_buffer = NULL;
	right_buffer = NULL;

	redraw_all();
}

/**
 * Update the terminal title bar
 */
void update_title(void) {
	if (!global_config.can_title) return;

	char cwd[1024] = {'/',0};
	getcwd(cwd, 1024);

	for (int i = 1; i < 3; ++i) {
		printf("\033]%d;%s%s (%s) - Bim\007", i, env->file_name ? env->file_name : "[No Name]", env->modified ? " +" : "", cwd);
	}
}

/**
 * Mark this buffer as modified and
 * redraw the status and tabbar if needed.
 */
void set_modified(void) {
	/* If it was already marked modified, no need to do anything */
	if (env->modified) return;

	/* Mark as modified */
	env->modified = 1;

	/* Redraw some things */
	update_title();
	redraw_tabbar();
	redraw_statusbar();
}

/**
 * Draw a message on the status line
 */
void render_status_message(char * message, ...) {
	if (!env) return; /* Don't print when there's no active environment; this usually means a bimrc command tried to print something */
	/* varargs setup */
	va_list args;
	va_start(args, message);

	/* Hide cursor while rendering */
	hide_cursor();

	/* Move cursor to the status bar line (second from bottom */
	place_cursor(1, global_config.term_height - 1);

	/* Set background colors for status line */
	paint_line(COLOR_STATUS_BG);
	set_colors(COLOR_STATUS_FG, COLOR_STATUS_BG);

	/* Process format string */
	vprintf(message, args);
	va_end(args);

	/* Clear the rest of the status bar */
	clear_to_end();
}

/**
 * Draw an error message to the command line.
 */
void render_error(char * message, ...) {
	/* varargs setup */
	va_list args;
	va_start(args, message);

	if (env) {
		/* Hide cursor while rendering */
		hide_cursor();

		/* Move cursor to the command line */
		place_cursor(1, global_config.term_height);

		/* Set appropriate error message colors */
		set_colors(COLOR_ERROR_FG, COLOR_ERROR_BG);

		/* Draw the message */
		vprintf(message, args);
		va_end(args);
		global_config.had_error = 1;
	} else {
		printf("bim: error during startup: ");
		vprintf(message, args);
		va_end(args);
		printf("\n");
	}

}

char * paren_pairs = "()[]{}<>";

int is_paren(int c) {
	char * p = paren_pairs;
	while (*p) {
		if (c == *p) return 1;
		p++;
	}
	return 0;
}

#define _rehighlight_parens() do { \
	if (i < 0 || i >= env->line_count) break; \
	for (int j = 0; j < env->lines[i]->actual; ++j) { \
		if (i == line-1 && j == col-1) { \
			env->lines[line-1]->text[col-1].flags |= FLAG_SELECT; \
			continue; \
		} else { \
			env->lines[i]->text[j].flags &= (~FLAG_SELECT); \
		} \
	} \
	redraw_line(i); \
} while (0)

/**
 * If the config option is enabled, find the matching
 * paren character and highlight it with the SELECT
 * colors, clearing out other SELECT values. As we
 * co-opt the SELECT flag, don't do this in selection
 * modes - only in normal and insert modes.
 */
void highlight_matching_paren(void) {
	if (env->mode == MODE_LINE_SELECTION || env->mode == MODE_CHAR_SELECTION) return;
	if (!global_config.highlight_parens) return;
	int line = -1, col = -1;
	if (env->line_no <= env->line_count && env->col_no <= env->lines[env->line_no-1]->actual &&
		is_paren(env->lines[env->line_no-1]->text[env->col_no-1].codepoint)) {
		find_matching_paren(&line, &col, 1);
	} else if (env->line_no <= env->line_count && env->col_no > 1 && is_paren(env->lines[env->line_no-1]->text[env->col_no-2].codepoint)) {
		find_matching_paren(&line, &col, 2);
	}
	if (env->highlighting_paren == -1 && line == -1) return;
	if (env->highlighting_paren > 0) {
		int i = env->highlighting_paren - 1;
		_rehighlight_parens();
	}
	if (env->highlighting_paren != line && line != -1) {
		int i = line - 1;
		_rehighlight_parens();
	}
	env->highlighting_paren = line;
}

/**
 * Recalculate syntax for the matched paren.
 * Useful when entering selection modes.
 */
void unhighlight_matching_paren(void) {
	if (env->highlighting_paren > 0 && env->highlighting_paren <= env->line_count) {
		for (int i = 0; i < env->line_count; i++) {
			for (int j = 0; j < env->lines[i]->actual; ++j) {
				env->lines[i]->text[j].flags &= ~(FLAG_SELECT);
			}
		}
		env->highlighting_paren = -1;
	}
}

/**
 * Move the cursor to the appropriate location based
 * on where it is in the text region.
 *
 * This does some additional math to set the text
 * region horizontal offset.
 */
void place_cursor_actual(void) {

	/* Invalid positions */
	if (env->line_no < 1) env->line_no = 1;
	if (env->col_no  < 1) env->col_no  = 1;

	/* Account for the left hand gutter */
	int num_size = num_width() + gutter_width();
	int x = num_size + 1 - env->coffset;

	/* Determine where the cursor is physically */
	for (int i = 0; i < env->col_no - 1; ++i) {
		char_t * c = &env->lines[env->line_no-1]->text[i];
		x += c->display_width;
	}

	/* y is a bit easier to calculate */
	int y = env->line_no - env->offset + 1;

	int needs_redraw = 0;

	while (y < 2 + global_config.cursor_padding && env->offset > 0) {
		y++;
		env->offset--;
		needs_redraw = 1;
	}

	while (y > 1 + global_config.term_height - global_config.bottom_size - global_config.cursor_padding - global_config.tabs_visible) {
		y--;
		env->offset++;
		needs_redraw = 1;
	}

	if (needs_redraw) {
		redraw_text();
		redraw_tabbar();
		redraw_statusbar();
		redraw_commandline();
	}

	/* If the cursor has gone off screen to the right... */
	if (x > env->width - 1) {
		/* Adjust the offset appropriately to scroll horizontally */
		int diff = x - (env->width - 1);
		env->coffset += diff;
		x -= diff;
		redraw_text();
	}

	/* Same for scrolling horizontally to the left */
	if (x < num_size + 1) {
		int diff = (num_size + 1) - x;
		env->coffset -= diff;
		x += diff;
		redraw_text();
	}

	highlight_matching_paren();
	recalculate_current_line();

	/* Move the actual terminal cursor */
	place_cursor(x+env->left,y - !global_config.tabs_visible);

	/* Show the cursor */
	show_cursor();
}

/**
 * If the screen is split, update the split sizes based
 * on the new terminal width and the user's split_percent setting.
 */
void update_split_size(void) {
	if (!left_buffer) return;
	if (left_buffer == right_buffer) {
		if (left_buffer->left == 0) {
			left_buffer->width = global_config.term_width * global_config.split_percent / 100;
		} else {
			right_buffer->left = global_config.term_width * global_config.split_percent / 100;
			right_buffer->width = global_config.term_width - right_buffer->left;
		}
		return;
	}
	left_buffer->left = 0;
	left_buffer->width = global_config.term_width * global_config.split_percent / 100;
	right_buffer->left = left_buffer->width;
	right_buffer->width = global_config.term_width - left_buffer->width;
}

/**
 * Update screen size
 */
void update_screen_size(void) {
	struct winsize w;
	ioctl(STDOUT_FILENO, TIOCGWINSZ, &w);
	global_config.term_width = w.ws_col;
	global_config.term_height = w.ws_row;
	if (env) {
		if (left_buffer) {
			update_split_size();
		} else if (env != left_buffer && env != right_buffer) {
			env->width = w.ws_col;
		}
	}
	for (int i = 0; i < buffers_len; ++i) {
		if (buffers[i] != left_buffer && buffers[i] != right_buffer) {
			buffers[i]->width = w.ws_col;
		}
	}
}

/**
 * Handle terminal size changes
 */
void SIGWINCH_handler(int sig) {
	(void)sig;
	update_screen_size();
	redraw_all();

	signal(SIGWINCH, SIGWINCH_handler);
}

/**
 * Handle suspend
 */
void SIGTSTP_handler(int sig) {
	(void)sig;
	mouse_disable();
	set_buffered();
	reset();
	clear_screen();
	show_cursor();
	unset_bracketed_paste();
	unset_alternate_screen();
	fflush(stdout);

	signal(SIGTSTP, SIG_DFL);
	raise(SIGTSTP);
}

void SIGCONT_handler(int sig) {
	(void)sig;
	set_alternate_screen();
	set_bracketed_paste();
	set_unbuffered();
	update_screen_size();
	mouse_enable();
	redraw_all();
	update_title();
	signal(SIGCONT, SIGCONT_handler);
	signal(SIGTSTP, SIGTSTP_handler);
}

void try_to_center() {
	int half_a_screen = (global_config.term_height - 3) / 2;
	if (half_a_screen < env->line_no) {
		env->offset = env->line_no - half_a_screen;
	} else {
		env->offset = 0;
	}
}

BIM_ACTION(suspend, 0,
	"Suspend bim and the rest of the job it was run in."
)(void) {
	kill(0, SIGTSTP);
}

/**
 * Move the cursor to a specific line.
 */
BIM_ACTION(goto_line, ARG_IS_CUSTOM,
	"Jump to the requested line."
)(int line) {

	if (line == -1) line = env->line_count;

	/* Respect file bounds */
	if (line < 1) line = 1;
	if (line > env->line_count) line = env->line_count;

	/* Move the cursor / text region offsets */
	env->coffset = 0;
	env->line_no = line;
	env->col_no  = 1;

	if (!env->loading) {
		if (line > env->offset && line < env->offset + global_config.term_height - global_config.bottom_size) {
			place_cursor_actual();
		} else {
			try_to_center();
		}
		redraw_most();
	} else {
		try_to_center();
	}
}


/**
 * Process (part of) a file and add it to a buffer.
 */
void add_buffer(uint8_t * buf, int size) {
	for (int i = 0; i < size; ++i) {
		if (!decode(&state, &codepoint_r, buf[i])) {
			uint32_t c = codepoint_r;
			if (c == '\n') {
				if (!env->crnl && env->lines[env->line_no-1]->actual && env->lines[env->line_no-1]->text[env->lines[env->line_no-1]->actual-1].codepoint == '\r') {
					env->lines[env->line_no-1]->actual--;
					env->crnl = 1;
				}
				env->lines = add_line(env->lines, env->line_no);
				env->col_no = 1;
				env->line_no += 1;
			} else if (env->crnl && c == '\r') {
				continue;
			} else {
				char_t _c;
				_c.codepoint = (uint32_t)c;
				_c.flags = 0;
				_c.display_width = codepoint_width((wchar_t)c);
				line_t * line  = env->lines[env->line_no - 1];
				line_t * nline = line_insert(line, _c, env->col_no - 1, env->line_no-1);
				if (line != nline) {
					env->lines[env->line_no - 1] = nline;
				}
				env->col_no += 1;
			}
		} else if (state == UTF8_REJECT) {
			state = 0;
		}
	}
}

/**
 * Add a raw string to a buffer. Convenience wrapper
 * for add_buffer for nil-terminated strings.
 */
void add_string(char * string) {
	add_buffer((uint8_t*)string,strlen(string));
}

int str_ends_with(const char * haystack, const char * needle) {
	int i = strlen(haystack);
	int j = strlen(needle);

	do {
		if (haystack[i] != needle[j]) return 0;
		if (j == 0) return 1;
		if (i == 0) return 0;
		i--;
		j--;
	} while (1);
}

/**
 * Find a syntax highlighter for the given filename.
 */
struct syntax_definition * match_syntax(char * file) {
	for (struct syntax_definition * s = syntaxes; syntaxes && s->name; ++s) {
		for (char ** ext = s->ext; *ext; ++ext) {
			if (str_ends_with(file, *ext)) return s;
		}
	}

	return NULL;
}

/**
 * Set the syntax configuration by the name of the syntax highlighter.
 */
void set_syntax_by_name(const char * name) {
	if (!strcmp(name,"none")) {
		for (int i = 0; i < env->line_count; ++i) {
			env->lines[i]->istate = -1;
			for (int j = 0; j < env->lines[i]->actual; ++j) {
				env->lines[i]->text[j].flags &= (3 << 5);
			}
		}
		env->syntax = NULL;
		redraw_all();
		return;
	}
	for (struct syntax_definition * s = syntaxes; syntaxes && s->name; ++s) {
		if (!strcmp(name,s->name)) {
			env->syntax = s;
			for (int i = 0; i < env->line_count; ++i) {
				env->lines[i]->istate = -1;
			}
			schedule_complete_recalc();
			redraw_all();
			return;
		}
	}
	render_error("unrecognized syntax type");
}


/**
 * Check if a string is all numbers.
 */
int is_all_numbers(const char * c) {
	while (*c) {
		if (!isdigit(*c)) return 0;
		c++;
	}
	return 1;
}

struct file_listing {
	int type;
	char * filename;
};

int sort_files(const void * a, const void * b) {
	struct file_listing * _a = (struct file_listing *)a;
	struct file_listing * _b = (struct file_listing *)b;

	if (_a->type == _b->type) {
		return strcmp(_a->filename, _b->filename);
	} else {
		return _a->type - _b->type;
	}
}

void read_directory_into_buffer(char * file) {
	DIR * dirp = opendir(file);
	if (!dirp) {
		env->loading = 0;
		return;
	}

	add_string("# Directory listing for `");
	add_string(file);
	add_string("`\n");

	/* Flexible array to hold directory contents */
	int available = 32;
	int count = 0;
	struct file_listing * files = calloc(sizeof(struct file_listing), available);

	/* Read directory */
	struct dirent * ent = readdir(dirp);
	while (ent) {
		struct stat statbuf;
		char * tmp = malloc(strlen(file) + 1 + strlen(ent->d_name) + 1);
		snprintf(tmp, strlen(file) + 1 + strlen(ent->d_name) + 1, "%s/%s", file, ent->d_name);
		stat(tmp, &statbuf);
		int type = (S_ISDIR(statbuf.st_mode)) ? 'd' : 'f';
		if (count + 1 == available) {
			available *= 2; \
			files = realloc(files, sizeof(struct file_listing) * available); \
		} \
		files[count].type = type;
		files[count].filename = strdup(ent->d_name);
		count++;
		ent = readdir(dirp);
		free(tmp);
	}
	closedir(dirp);

	/* Sort directory entries */
	qsort(files, count, sizeof(struct file_listing), sort_files);

	for (int i = 0; i < count; ++i) {
		add_string(files[i].type == 'd' ? "d" : "f");
		add_string(" ");
		add_string(files[i].filename);
		add_string("\n");

		free(files[i].filename);
	}

	free(files);

	env->file_name = strdup(file);
	env->syntax = find_syntax_calculator("dirent");
	schedule_complete_recalc();
	env->readonly = 1;
	env->loading = 0;
	env->mode = MODE_DIRECTORY_BROWSE;
	env->line_no = 1;
	redraw_all();
}

BIM_ACTION(open_file_from_line, 0,
	"When browsing a directory, open the file under the cursor."
)(void) {
	if (env->lines[env->line_no-1]->actual < 1) return;
	if (env->lines[env->line_no-1]->text[0].codepoint != 'd' &&
	    env->lines[env->line_no-1]->text[0].codepoint != 'f') return;
	/* Collect file name */
	char * tmp = malloc(strlen(env->file_name) + 1 + env->lines[env->line_no-1]->actual * 7); /* Should be enough */
	memset(tmp, 0, strlen(env->file_name) + 1 + env->lines[env->line_no-1]->actual * 7);
	char * t = tmp;
	/* Start by copying the filename */
	t += sprintf(t, "%s/", env->file_name);
	/* Start from character 2 to skip d/f and space */
	for (int i = 2; i < env->lines[env->line_no-1]->actual; ++i) {
		t += to_eight(env->lines[env->line_no-1]->text[i].codepoint, t);
	}
	*t = '\0';
	/* Normalize */
	char tmp_path[PATH_MAX+1];
	if (!realpath(tmp, tmp_path)) {
		free(tmp);
		return;
	}
	free(tmp);
	/* Open file */
	buffer_t * old_buffer = env;
	open_file(tmp_path);
	buffer_close(old_buffer);
	update_title();
	redraw_all();
}

int line_matches(line_t * line, char * string) {
	uint32_t c = 0, state = 0;
	int i = 0;
	while (*string) {
		if (!decode(&state, &c, *string)) {
			if (i >= line->actual) return 0;
			if (line->text[i].codepoint != c) return 0;
			string++;
			i++;
		} else if (state == UTF8_REJECT) {
			state = 0;
		}
	}
	return 1;
}

void run_onload(buffer_t * env) {
	/* TODO */
	KrkValue onLoad;
	if (krk_tableGet_fast(&krk_currentThread.module->fields, S("onload"), &onLoad)) {
		krk_push(onLoad);
		krk_push(krk_dict_of(0,NULL,0));

		if (env->file_name) {
			krk_attachNamedObject(AS_DICT(krk_peek(0)), "filename",
				(KrkObj*)krk_copyString(env->file_name,strlen(env->file_name)));
		}

		if (env->syntax && env->syntax->krkClass) {
			krk_attachNamedObject(AS_DICT(krk_peek(0)), "highlighter",
				(KrkObj*)env->syntax->krkClass);
		}

		krk_callStack(1);
		krk_resetStack();
	}
}

static void render_syntax_async(background_task_t * task) {
	buffer_t * old_env = env;
	env = task->env;
	int line_no = task->_private_i;

	if (env->line_count && line_no < env->line_count) {
		int tmp = env->loading;
		env->loading = 1;
		recalculate_syntax(env->lines[line_no], line_no);
		env->loading = tmp;
		if (env == old_env) {
			redraw_line(line_no);
		}
	}
	env = old_env;
}

static void schedule_complete_recalc(void) {
	if (env->line_count < 1000) {
		for (int i = 0; i < env->line_count; ++i) {
			recalculate_syntax(env->lines[i], i);
		}
		return;
	}

	/* TODO see if there's already a redraw scheduled */
	for (int i = 0; i < env->line_count; ++i) {
		background_task_t * task = malloc(sizeof(background_task_t));
		task->env  = env;
		task->_private_i = i;
		task->func = render_syntax_async;
		task->next = NULL;
		if (global_config.tail_task) {
			global_config.tail_task->next = task;
		}
		global_config.tail_task = task;
		if (!global_config.background_task) {
			global_config.background_task = task;
		}
	}
	redraw_statusbar();
}

/**
 * Create a new buffer from a file.
 */
void open_file(char * file) {
	env = buffer_new();
	env->width = global_config.term_width;
	env->left = 0;
	env->loading = 1;

	setup_buffer(env);

	FILE * f;
	int init_line = -1;

	if (!strcmp(file,"-")) {
		/**
		 * Read file from stdin. stderr provides terminal input.
		 */
		if (isatty(STDIN_FILENO)) {
			if (buffers_len == 1) {
				quit("stdin is a terminal and you tried to open -; not letting you do that");
			}
			close_buffer();
			render_error("stdin is a terminal and you tried to open -; not letting you do that");
			return;
		}
		f = stdin;
		env->modified = 1;
	} else {
		char * l = strrchr(file, ':');
		if (l && is_all_numbers(l+1)) {
			*l = '\0';
			l++;
			init_line = atoi(l);
		}

		char * _file = file;

		if (file[0] == '~') {
			char * home = getenv("HOME");
			if (home) {
				_file = malloc(strlen(file) + strlen(home) + 4); /* Paranoia */
				sprintf(_file, "%s%s", home, file+1);
			}
		}

		if (file_is_open(_file)) {
			if (file != _file) free(_file);
			close_buffer();
			return;
		}

		struct stat statbuf;
		if (!stat(_file, &statbuf) && S_ISDIR(statbuf.st_mode)) {
			read_directory_into_buffer(_file);
			if (file != _file) free(_file);
			return;
		}
		f = fopen(_file, "r");
		if (file != _file) free(_file);
		if (!f && errno != ENOENT) {
			render_error("%s: %s", file, strerror(errno));
			pause_for_key();
			close_buffer();
			return;
		}
		env->file_name = strdup(file);
	}

	if (!f) {
		if (global_config.highlight_on_open) {
			env->syntax = match_syntax(file);
		}
		env->loading = 0;
		if (global_config.go_to_line) {
			goto_line(1);
		}
		if (env->syntax && env->syntax->prefers_spaces) {
			env->tabs = 0;
		}
		update_biminfo(env, 1);
		run_onload(env);
		return;
	}

	uint8_t buf[BLOCK_SIZE];

	state = 0;

	while (!feof(f) && !ferror(f)) {
		size_t r = fread(buf, 1, BLOCK_SIZE, f);
		add_buffer(buf, r);
	}

	if (ferror(f)) {
		env->loading = 0;
		return;
	}

	if (env->line_no && env->lines[env->line_no-1] && env->lines[env->line_no-1]->actual == 0) {
		/* Remove blank line from end */
		env->lines = remove_line(env->lines, env->line_no-1);
	}

	if (global_config.highlight_on_open) {
		env->syntax = match_syntax(file);
		if (!env->syntax) {
			if (line_matches(env->lines[0], "<?xml")) set_syntax_by_name("xml");
			else if (line_matches(env->lines[0], "<!doctype")) set_syntax_by_name("xml");
			else if (line_matches(env->lines[0], "#!/usr/bin/env bash")) set_syntax_by_name("bash");
			else if (line_matches(env->lines[0], "#!/bin/bash")) set_syntax_by_name("bash");
			else if (line_matches(env->lines[0], "#!/bin/sh")) set_syntax_by_name("bash");
			else if (line_matches(env->lines[0], "#!/usr/bin/env python")) set_syntax_by_name("py");
			else if (line_matches(env->lines[0], "#!/usr/bin/env groovy")) set_syntax_by_name("groovy");
		}
		if (!env->syntax && global_config.syntax_fallback) {
			set_syntax_by_name(global_config.syntax_fallback);
		}
		schedule_complete_recalc();
	}

	/* Try to automatically figure out tabs vs. spaces */
	int tabs = 0, spaces = 0;
	for (int i = 0; i < env->line_count; ++i) {
		if (env->lines[i]->actual > 1) { /* Make sure line has at least some text on it */
			if (env->lines[i]->text[0].codepoint == '\t') tabs++;
			if (env->lines[i]->text[0].codepoint == ' ' &&
				env->lines[i]->text[1].codepoint == ' ') /* Ignore spaces at the start of asterisky C comments */
				spaces++;
		}
	}
	if (spaces > tabs) {
		env->tabs = 0;
	} else if (spaces == tabs && env->syntax) {
		env->tabs = env->syntax->prefers_spaces;
	}

	if (spaces > tabs) {
		int one = 0, two = 0, three = 0, four = 0; /* If you use more than that, I don't like you. */
		int lastCount = 0;
		for (int i = 0; i < env->line_count; ++i) {
			if (env->lines[i]->actual > 1 && !line_is_comment(env->lines[i])) {
				/* Count spaces at beginning */
				int c = 0, diff = 0;
				while (c < env->lines[i]->actual && env->lines[i]->text[c].codepoint == ' ') c++;
				if (c > lastCount) {
					diff = c - lastCount;
				} else if (c < lastCount) {
					diff = lastCount - c;
				}
				if (diff == 1) one++;
				if (diff == 2) two++;
				if (diff == 3) three++;
				if (diff == 4) four++;
				lastCount = c;
			}
		}
		if (four > three && four > two && four > one) {
			env->tabstop = 4;
		} else if (three > two && three > one) {
			env->tabstop = 3;
		} else if (two > one) {
			env->tabstop = 2;
		} else {
			env->tabstop = 1;
		}
	}

	env->loading = 0;

	if (global_config.check_git) {
		env->checkgitstatusonwrite = 1;
		git_examine(file);
	}

	for (int i = 0; i < env->line_count; ++i) {
		recalculate_tabs(env->lines[i]);
	}

	if (global_config.go_to_line) {
		if (init_line != -1) {
			goto_line(init_line);
		} else {
			env->line_no = 1;
			env->col_no = 1;
			fetch_from_biminfo(env);
			place_cursor_actual();
			redraw_all();
			set_preferred_column();
		}
	}

	update_biminfo(env, 1);

	fclose(f);

	run_onload(env);
}

/**
 * Clean up the terminal and exit the editor.
 */
void quit(const char * message) {
	mouse_disable();
	set_buffered();
	reset();
	clear_screen();
	show_cursor();
	unset_bracketed_paste();
	unset_alternate_screen();
	krk_freeVM();
	if (message) {
		fprintf(stdout, "%s\n", message);
	}
	exit(0);
}

/**
 * Try to quit, but don't continue if there are
 * modified buffers open.
 */
void try_quit(void) {
	for (int i = 0; i < buffers_len; i++ ) {
		buffer_t * _env = buffers[i];
		if (_env->modified) {
			if (_env->file_name) {
				render_error("Modifications made to file `%s` in tab %d. Aborting.", _env->file_name, i+1);
			} else {
				render_error("Unsaved new file in tab %d. Aborting.", i+1);
			}
			return;
		}
	}
	/* Close all buffers */
	while (buffers_len) {
		buffer_close(buffers[0]);
	}
	quit(NULL);
}

/**
 * Switch to the previous buffer
 */
BIM_ACTION(previous_tab, 0,
	"Switch the previous tab"
)(void) {
	buffer_t * last = NULL;
	for (int i = 0; i < buffers_len; i++) {
		buffer_t * _env = buffers[i];
		if (_env == env) {
			if (last) {
				/* Wrap around */
				env = last;
				if (left_buffer && (left_buffer != env && right_buffer != env)) unsplit();
				redraw_all();
				update_title();
				return;
			} else {
				env = buffers[buffers_len-1];
				if (left_buffer && (left_buffer != env && right_buffer != env)) unsplit();
				redraw_all();
				update_title();
				return;
			}
		}
		last = _env;
	}
}

/**
 * Switch to the next buffer
 */
BIM_ACTION(next_tab, 0,
	"Switch to the next tab"
)(void) {
	for (int i = 0; i < buffers_len; i++) {
		buffer_t * _env = buffers[i];
		if (_env == env) {
			if (i != buffers_len - 1) {
				env = buffers[i+1];
				if (left_buffer && (left_buffer != env && right_buffer != env)) unsplit();
				redraw_all();
				update_title();
				return;
			} else {
				/* Wrap around */
				env = buffers[0];
				if (left_buffer && (left_buffer != env && right_buffer != env)) unsplit();
				redraw_all();
				update_title();
				return;
			}
		}
	}
}

/**
 * Check for modified lines in a file by examining `git diff` output.
 * This can be enabled globally in bimrc or per environment with the 'git' option.
 */
int git_examine(char * filename) {
	if (env->modified) return 1;
	int fds[2];
	pipe(fds);
	int child = fork();
	if (child == 0) {
		FILE * dev_null = fopen("/dev/null","w");
		close(fds[0]);
		dup2(fds[1], STDOUT_FILENO);
		dup2(fileno(dev_null), STDERR_FILENO);
		char * args[] = {"git","--no-pager","diff","-U0","--no-color","--",filename,NULL};
		exit(execvp("git",args));
	} else if (child < 0) {
		return 1;
	}

	close(fds[1]);
	FILE * f = fdopen(fds[0],"r");

	int line_offset = 0;
	while (!feof(f)) {
		int c = fgetc(f);
		if (c < 0) break;

		if (c == '@' && line_offset == 0) {
			/* Read line offset, count */
			if (fgetc(f) == '@' && fgetc(f) == ' ' && fgetc(f) == '-') {
				/* This algorithm is borrowed from Kakoune and only requires us to parse the @@ line */
				int from_line = 0;
				int from_count = 0;
				int to_line = 0;
				int to_count = 0;
				fscanf(f,"%d",&from_line);
				if (fgetc(f) == ',') {
					fscanf(f,"%d",&from_count);
				} else {
					from_count = 1;
				}
				fscanf(f,"%d",&to_line);
				if (fgetc(f) == ',') {
					fscanf(f,"%d",&to_count);
				} else {
					to_count = 1;
				}

				if (to_line > env->line_count) continue;

				if (from_count == 0 && to_count > 0) {
					/* No -, all + means all of to_count is green */
					for (int i = 0; i < to_count; ++i) {
						env->lines[to_line+i-1]->rev_status = 1; /* Green */
					}
				} else if (from_count > 0 && to_count == 0) {
					/*
					 * No +, all - means we have a deletion. We mark the next line such that it has a red bar at the top
					 * Note that to_line is one lower than the affected line, so we don't need to mess with indexes.
					 */
					if (to_line >= env->line_count) continue;
					env->lines[to_line]->rev_status = 4; /* Red */
				} else if (from_count > 0 && from_count == to_count) {
					/* from = to, all modified */
					for (int i = 0; i < to_count; ++i) {
						env->lines[to_line+i-1]->rev_status = 3; /* Blue */
					}
				} else if (from_count > 0 && from_count < to_count) {
					/* from < to, some modified, some added */
					for (int i = 0; i < from_count; ++i) {
						env->lines[to_line+i-1]->rev_status = 3; /* Blue */
					}
					for (int i = from_count; i < to_count; ++i) {
						env->lines[to_line+i-1]->rev_status = 1; /* Green */
					}
				} else if (to_count > 0 && from_count > to_count) {
					/* from > to, we deleted but also modified some lines */
					env->lines[to_line-1]->rev_status = 5; /* Red + Blue */
					for (int i = 1; i < to_count; ++i) {
						env->lines[to_line+i-1]->rev_status = 3; /* Blue */
					}
				}
			}
		}

		if (c == '\n') {
			line_offset = 0;
			continue;
		}

		line_offset++;
	}

	fclose(f);
	return 0;
}

/**
 * Write file contents to FILE
 */
void output_file(buffer_t * env, FILE * f) {
	int i, j;
	for (i = 0; i < env->line_count; ++i) {
		line_t * line = env->lines[i];
		line->rev_status = 0;
		for (j = 0; j < line->actual; j++) {
			char_t c = line->text[j];
			if (c.codepoint == 0) {
				char buf[1] = {0};
				fwrite(buf, 1, 1, f);
			} else {
				char tmp[8] = {0};
				int i = to_eight(c.codepoint, tmp);
				fwrite(tmp, i, 1, f);
			}
		}
		if (env->crnl) fputc('\r', f);
		fputc('\n', f);
	}
}

/**
 * Write active buffer to file
 */
void write_file(char * file) {
	if (!file) {
		render_error("Need a file to write to.");
		return;
	}

	char * _file = file;

	if (file[0] == '~') {
		char * home = getenv("HOME");
		if (home) {
			_file = malloc(strlen(file) + strlen(home) + 4); /* Paranoia */
			sprintf(_file, "%s%s", home, file+1);
		}
	}


	FILE * f = fopen(_file, "w+");
	if (file != _file) free(_file);

	if (!f) {
		render_error("Failed to open file for writing.");
		return;
	}

	/* Go through each line and convert it back to UTF-8 */
	output_file(env, f);

	fclose(f);

	/* Mark it no longer modified */
	env->modified = 0;
	env->last_save_history = env->history;

	/* If there was no file name set, set one */
	if (!env->file_name) {
		env->file_name = malloc(strlen(file) + 1);
		memcpy(env->file_name, file, strlen(file) + 1);
	}

	if (env->checkgitstatusonwrite) {
		git_examine(file);
	}

	update_title();
	redraw_all();
}

/**
 * Close the active buffer
 */
void close_buffer(void) {
	buffer_t * previous_env = env;
	buffer_t * new_env = buffer_close(env);
	if (new_env == previous_env) {
		/* ?? Failed to close buffer */
		render_error("lolwat");
	}
	if (left_buffer && env == left_buffer) {
		left_buffer = NULL;
		right_buffer->left = 0;
		right_buffer->width = global_config.term_width;
		right_buffer = NULL;
	} else if (left_buffer && env == right_buffer) {
		right_buffer = NULL;
		left_buffer->left = 0;
		left_buffer->width = global_config.term_width;
		left_buffer = NULL;
	}
	/* No more buffers, exit */
	if (!new_env) {
		quit(NULL);
	}

	/* Set the new active buffer */
	env = new_env;

	/* Redraw the screen */
	redraw_all();
	update_title();
}

/**
 * Set the visual column the cursor should attempt to keep
 * when moving up and down based on where the cursor currently is.
 * This should happen whenever the user intentionally changes
 * the cursor's horizontal positioning, such as with left/right
 * arrow keys, word-move, search, mouse, etc.
 */
void set_preferred_column(void) {
	int c = 0;
	for (int i = 0; i < env->lines[env->line_no-1]->actual && i < env->col_no-1; ++i) {
		c += env->lines[env->line_no-1]->text[i].display_width;
	}
	env->preferred_column = c;
}

BIM_ACTION(cursor_down, 0,
	"Move the cursor one line down."
)(void) {
	/* If this isn't already the last line... */
	if (env->line_no < env->line_count) {

		/* Move the cursor down */
		env->line_no += 1;

		/* Try to place the cursor horizontally at the preferred column */
		int _x = 0;
		for (int i = 0; i < env->lines[env->line_no-1]->actual; ++i) {
			char_t * c = &env->lines[env->line_no-1]->text[i];
			_x += c->display_width;
			env->col_no = i+1;
			if (_x > env->preferred_column) {
				break;
			}
		}

		if (env->mode == MODE_INSERT && _x <= env->preferred_column) {
			env->col_no = env->lines[env->line_no-1]->actual + 1;
		}

		/*
		 * If the horizontal cursor position exceeds the width the new line,
		 * then move the cursor left to the extent of the new line.
		 *
		 * If we're in insert mode, we can go one cell beyond the end of the line
		 */
		if (env->col_no > env->lines[env->line_no-1]->actual + (env->mode == MODE_INSERT)) {
			env->col_no = env->lines[env->line_no-1]->actual + (env->mode == MODE_INSERT);
			if (env->col_no == 0) env->col_no = 1;
		}

		if (env->loading) return;

		/*
		 * If the screen was scrolled horizontally, unscroll it;
		 * if it will be scrolled on this line as well, that will
		 * be handled by place_cursor_actual
		 */
		int redraw = 0;
		if (env->coffset != 0) {
			env->coffset = 0;
			redraw = 1;
		}

		/* If we've scrolled past the bottom of the screen, scroll the screen */
		if (env->line_no > env->offset + global_config.term_height - global_config.bottom_size - global_config.tabs_visible - global_config.cursor_padding) {
			env->offset += 1;

			/* Tell terminal to scroll */
			if (global_config.can_scroll && !left_buffer) {
				if (!global_config.can_insert) {
					shift_up(1);
					redraw_tabbar();
				} else {
					delete_lines_at(global_config.tabs_visible ? 2 : 1, 1);
				}

				/* A new line appears on screen at the bottom, draw it */
				int l = global_config.term_height - global_config.bottom_size - global_config.tabs_visible;
				if (env->offset + l < env->line_count + 1) {
					redraw_line(env->offset + l-1);
				} else {
					draw_excess_line(l - 1);
				}
			} else {
				redraw_text();
			}
			redraw_statusbar();
			redraw_commandline();
			place_cursor_actual();
			return;
		} else if (redraw) {
			/* Otherwise, if we need to redraw because of coffset change, do that */
			redraw_text();
		}

		set_history_break();

		/* Update the status bar */
		redraw_statusbar();

		/* Place the terminal cursor again */
		place_cursor_actual();
	}
}

BIM_ACTION(cursor_up, 0,
	"Move the cursor up one line."
)(void) {
	/* If this isn't the first line already */
	if (env->line_no > 1) {

		/* Move the cursor down */
		env->line_no -= 1;

		/* Try to place the cursor horizontally at the preferred column */
		int _x = 0;
		for (int i = 0; i < env->lines[env->line_no-1]->actual; ++i) {
			char_t * c = &env->lines[env->line_no-1]->text[i];
			_x += c->display_width;
			env->col_no = i+1;
			if (_x > env->preferred_column) {
				break;
			}
		}

		if (env->mode == MODE_INSERT && _x <= env->preferred_column) {
			env->col_no = env->lines[env->line_no-1]->actual + 1;
		}

		/*
		 * If the horizontal cursor position exceeds the width the new line,
		 * then move the cursor left to the extent of the new line.
		 *
		 * If we're in insert mode, we can go one cell beyond the end of the line
		 */
		if (env->col_no > env->lines[env->line_no-1]->actual + (env->mode == MODE_INSERT)) {
			env->col_no = env->lines[env->line_no-1]->actual + (env->mode == MODE_INSERT);
			if (env->col_no == 0) env->col_no = 1;
		}

		if (env->loading) return;

		/*
		 * If the screen was scrolled horizontally, unscroll it;
		 * if it will be scrolled on this line as well, that will
		 * be handled by place_cursor_actual
		 */
		int redraw = 0;
		if (env->coffset != 0) {
			env->coffset = 0;
			redraw = 1;
		}

		int e = (env->offset == 0) ? env->offset : env->offset + global_config.cursor_padding;
		if (env->line_no <= e) {
			env->offset -= 1;

			/* Tell terminal to scroll */
			if (global_config.can_scroll && !left_buffer) {
				if (!global_config.can_insert) {
					shift_down(1);
					redraw_tabbar();
				} else {
					insert_lines_at(global_config.tabs_visible ? 2 : 1, 1);
				}

				/*
				 * The line at the top of the screen should always be real
				 * so we can just call redraw_line here
				 */
				redraw_line(env->offset);
			} else {
				redraw_tabbar();
				redraw_text();
			}
			redraw_statusbar();
			redraw_commandline();
			place_cursor_actual();
			return;
		} else if (redraw) {
			/* Otherwise, if we need to redraw because of coffset change, do that */
			redraw_text();
		}

		set_history_break();

		/* Update the status bar */
		redraw_statusbar();

		/* Place the terminal cursor again */
		place_cursor_actual();
	}
}

BIM_ACTION(cursor_left, 0,
	"Move the cursor one character to the left."
)(void) {
	if (env->col_no > 1) {
		env->col_no -= 1;

		/* Update the status bar */
		redraw_statusbar();

		/* Place the terminal cursor again */
		place_cursor_actual();
	}
	set_history_break();
	set_preferred_column();
}

BIM_ACTION(cursor_right, 0,
	"Move the cursor one character to the right."
)(void) {

	/* If this isn't already the rightmost column we can reach on this line in this mode... */
	if (env->col_no < env->lines[env->line_no-1]->actual + !!(env->mode == MODE_INSERT)) {
		env->col_no += 1;

		/* Update the status bar */
		redraw_statusbar();

		/* Place the terminal cursor again */
		place_cursor_actual();
	}
	set_history_break();
	set_preferred_column();
}

BIM_ACTION(cursor_home, 0,
	"Move the cursor to the beginning of the line."
)(void) {
	env->col_no = 1;
	set_history_break();
	set_preferred_column();

	/* Update the status bar */
	redraw_statusbar();

	/* Place the terminal cursor again */
	place_cursor_actual();
}

BIM_ACTION(cursor_end, 0,
	"Move the cursor to the end of the line, or past the end in insert mode."
)(void) {
	env->col_no = env->lines[env->line_no-1]->actual+!!(env->mode == MODE_INSERT);
	set_history_break();
	set_preferred_column();

	/* Update the status bar */
	redraw_statusbar();

	/* Place the terminal cursor again */
	place_cursor_actual();
}

BIM_ACTION(leave_insert, 0,
	"Leave insert modes and return to normal mode."
)(void) {
	if (env->col_no > env->lines[env->line_no-1]->actual) {
		env->col_no = env->lines[env->line_no-1]->actual;
		if (env->col_no == 0) env->col_no = 1;
		set_preferred_column();
	}
		set_history_break();
		env->mode = MODE_NORMAL;
		redraw_commandline();
}

/**
 * Helper for handling smart case sensitivity.
 */
int search_matches(uint32_t a, uint32_t b, int mode) {
	if (mode == 0) {
		return a == b;
	} else if (mode == 1) {
		return tolower(a) == tolower(b);
	}
	return 0;
}

int subsearch_matches(line_t * line, int j, uint32_t * needle, int ignorecase, int *len) {
	int k = j;
	uint32_t * match = needle;
	if (*match == '^') {
		if (j != 0) return 0;
		match++;
	}
	while (k < line->actual + 1) {
		if (*match == '\0') {
			if (len) *len = k - j;
			return 1;
		}
		if (*match == '$') {
			if (k != line->actual) return 0;
			match++;
			continue;
		}
		if (*match == '.') {
			if (match[1] == '*') {
				int greedy = !(match[2] == '?');
				/* Short-circuit chained .*'s */
				if (match[greedy ? 2 : 3] == '.' && match[greedy ? 3 : 4] == '*') {
					int _len;
					if (subsearch_matches(line, k, &match[greedy ? 2 : 3], ignorecase, &_len)) {
						if (len) *len = _len + k - j;
						return 1;
					}
					return 0;
				}
				int _j = greedy ? line->actual : k;
				int _break = -1;
				int _len = -1;
				if (!match[greedy ? 2 : 3]) {
					_len = greedy ? (line->actual - _j) : 0;
					_break = _j;
				} else {
					while (_j < line->actual + 1 && _j >= k) {
						int len;
						if (subsearch_matches(line, _j, &match[greedy ? 2 : 3], ignorecase, &len)) {
							_break = _j;
							_len = len;
							break;
						}
						_j += (greedy ? -1 : 1);
					}
				}
				if (_break != -1) {
					if (len) *len = (_break - j) + _len;
					return 1;
				}
				return 0;
			} else {
				if (k >= line->actual) return 0;
				match++;
				k++;
				continue;
			}
		}
		if (*match == '\\' && (match[1] == '$' || match[1] == '^' || match[1] == '/' || match[1] == '\\' || match[1] == '.')) {
			match++;
		} else if (*match == '\\' && match[1] == 't') {
			if (line->text[k].codepoint != '\t') break;
			match += 2;
			k++;
			continue;
		}
		if (k == line->actual) break;
		if (!search_matches(*match, line->text[k].codepoint, ignorecase)) break;
		match++;
		k++;
	}
	return 0;
}

/**
 * Replace text on a given line with other text.
 */
void perform_replacement(int line_no, uint32_t * needle, uint32_t * replacement, int col, int ignorecase, int *out_col) {
	line_t * line = env->lines[line_no-1];
	int j = col;
	while (j < line->actual + 1) {
		int match_len;
		if (subsearch_matches(line,j,needle,ignorecase,&match_len)) {
			/* Perform replacement */
			for (int i = 0; i < match_len; ++i) {
				line_delete(line, j+1, line_no-1);
			}
			int t = 0;
			for (uint32_t * r = replacement; *r; ++r) {
				char_t _c;
				_c.codepoint = *r;
				_c.flags = 0;
				_c.display_width = codepoint_width(*r);
				line_t * nline = line_insert(line, _c, j + t, line_no -1);
				if (line != nline) {
					env->lines[line_no-1] = nline;
					line = nline;
				}
				t++;
			}
			*out_col = j + t;
			set_modified();
			return;
		}
		j++;
	}
	*out_col = -1;
}

#define COMMAND_HISTORY_MAX 255
unsigned char * command_history[COMMAND_HISTORY_MAX] = {NULL};

/**
 * Add a command to the history. If that command was
 * already in history, it is moved to the front of the list;
 * otherwise, the whole list is shifted backwards and
 * overflow is freed up.
 */
void insert_command_history(char * cmd) {
	/* See if this is already in the history. */
	size_t amount_to_shift = COMMAND_HISTORY_MAX - 1;
	for (int i = 0; i < COMMAND_HISTORY_MAX && command_history[i]; ++i) {
		if (!strcmp((char*)command_history[i], cmd)) {
			free(command_history[i]);
			amount_to_shift = i;
			break;
		}
	}

	/* Remove last entry that will roll off the stack */
	if (amount_to_shift == COMMAND_HISTORY_MAX - 1) {
		if (command_history[COMMAND_HISTORY_MAX-1]) free(command_history[COMMAND_HISTORY_MAX-1]);
	}

	/* Roll the history */
	memmove(&command_history[1], &command_history[0], sizeof(char *) * (amount_to_shift));

	command_history[0] = (unsigned char*)strdup(cmd);
}

static uint32_t term_colors[] = {
 0x000000, 0xcc0000, 0x3e9a06, 0xc4a000, 0x3465a4, 0x75507b, 0x06989a, 0xeeeeec, 0x555753, 0xef2929, 0x8ae234, 0xfce94f, 0x729fcf, 0xad7fa8, 0x34e2e2,
 0xFFFFFF, 0x000000, 0x00005f, 0x000087, 0x0000af, 0x0000d7, 0x0000ff, 0x005f00, 0x005f5f, 0x005f87, 0x005faf, 0x005fd7, 0x005fff, 0x008700, 0x00875f,
 0x008787, 0x0087af, 0x0087d7, 0x0087ff, 0x00af00, 0x00af5f, 0x00af87, 0x00afaf, 0x00afd7, 0x00afff, 0x00d700, 0x00d75f, 0x00d787, 0x00d7af, 0x00d7d7,
 0x00d7ff, 0x00ff00, 0x00ff5f, 0x00ff87, 0x00ffaf, 0x00ffd7, 0x00ffff, 0x5f0000, 0x5f005f, 0x5f0087, 0x5f00af, 0x5f00d7, 0x5f00ff, 0x5f5f00, 0x5f5f5f,
 0x5f5f87, 0x5f5faf, 0x5f5fd7, 0x5f5fff, 0x5f8700, 0x5f875f, 0x5f8787, 0x5f87af, 0x5f87d7, 0x5f87ff, 0x5faf00, 0x5faf5f, 0x5faf87, 0x5fafaf, 0x5fafd7,
 0x5fafff, 0x5fd700, 0x5fd75f, 0x5fd787, 0x5fd7af, 0x5fd7d7, 0x5fd7ff, 0x5fff00, 0x5fff5f, 0x5fff87, 0x5fffaf, 0x5fffd7, 0x5fffff, 0x870000, 0x87005f,
 0x870087, 0x8700af, 0x8700d7, 0x8700ff, 0x875f00, 0x875f5f, 0x875f87, 0x875faf, 0x875fd7, 0x875fff, 0x878700, 0x87875f, 0x878787, 0x8787af, 0x8787d7,
 0x8787ff, 0x87af00, 0x87af5f, 0x87af87, 0x87afaf, 0x87afd7, 0x87afff, 0x87d700, 0x87d75f, 0x87d787, 0x87d7af, 0x87d7d7, 0x87d7ff, 0x87ff00, 0x87ff5f,
 0x87ff87, 0x87ffaf, 0x87ffd7, 0x87ffff, 0xaf0000, 0xaf005f, 0xaf0087, 0xaf00af, 0xaf00d7, 0xaf00ff, 0xaf5f00, 0xaf5f5f, 0xaf5f87, 0xaf5faf, 0xaf5fd7,
 0xaf5fff, 0xaf8700, 0xaf875f, 0xaf8787, 0xaf87af, 0xaf87d7, 0xaf87ff, 0xafaf00, 0xafaf5f, 0xafaf87, 0xafafaf, 0xafafd7, 0xafafff, 0xafd700, 0xafd75f,
 0xafd787, 0xafd7af, 0xafd7d7, 0xafd7ff, 0xafff00, 0xafff5f, 0xafff87, 0xafffaf, 0xafffd7, 0xafffff, 0xd70000, 0xd7005f, 0xd70087, 0xd700af, 0xd700d7,
 0xd700ff, 0xd75f00, 0xd75f5f, 0xd75f87, 0xd75faf, 0xd75fd7, 0xd75fff, 0xd78700, 0xd7875f, 0xd78787, 0xd787af, 0xd787d7, 0xd787ff, 0xd7af00, 0xd7af5f,
 0xd7af87, 0xd7afaf, 0xd7afd7, 0xd7afff, 0xd7d700, 0xd7d75f, 0xd7d787, 0xd7d7af, 0xd7d7d7, 0xd7d7ff, 0xd7ff00, 0xd7ff5f, 0xd7ff87, 0xd7ffaf, 0xd7ffd7,
 0xd7ffff, 0xff0000, 0xff005f, 0xff0087, 0xff00af, 0xff00d7, 0xff00ff, 0xff5f00, 0xff5f5f, 0xff5f87, 0xff5faf, 0xff5fd7, 0xff5fff, 0xff8700, 0xff875f,
 0xff8787, 0xff87af, 0xff87d7, 0xff87ff, 0xffaf00, 0xffaf5f, 0xffaf87, 0xffafaf, 0xffafd7, 0xffafff, 0xffd700, 0xffd75f, 0xffd787, 0xffd7af, 0xffd7d7,
 0xffd7ff, 0xffff00, 0xffff5f, 0xffff87, 0xffffaf, 0xffffd7, 0xffffff, 0x080808, 0x121212, 0x1c1c1c, 0x262626, 0x303030, 0x3a3a3a, 0x444444, 0x4e4e4e,
 0x585858, 0x626262, 0x6c6c6c, 0x767676, 0x808080, 0x8a8a8a, 0x949494, 0x9e9e9e, 0xa8a8a8, 0xb2b2b2, 0xbcbcbc, 0xc6c6c6, 0xd0d0d0, 0xdadada, 0xe4e4e4,
 0xeeeeee,
};

/**
 * Convert a color setting from terminal format
 * to a hexadecimal color code and add it to the current
 * buffer. This is used for HTML conversion, but could
 * possibly be used for other purposes.
 */
static void html_convert_color(const char * color_string) {
	char tmp[100];
	if (!strncmp(color_string,"2;",2)) {
		/* 24-bit color */
		int red, green, blue;
		sscanf(color_string+2,"%d;%d;%d",&red,&green,&blue);
		sprintf(tmp, "#%02x%02x%02x;", red,green,blue);
	} else if (!strncmp(color_string,"5;",2)) {
		/* 256 colors; needs lookup table */
		int index;
		sscanf(color_string+2,"%d",&index);
		sprintf(tmp,"#%06x;", (unsigned int)term_colors[(index >= 0 && index <= 255) ? index : 0]);
	} else {
		/* 16 colors; needs lookup table */
		int index;
		uint32_t color;
		sscanf(color_string+1,"%d",&index);
		if (index >= 10) {
			index -= 10;
			color = term_colors[index+8];
		} else if (index == 9) {
			color = term_colors[0];
		} else {
			color = term_colors[index];
		}
		sprintf(tmp,"#%06x;", (unsigned int)color);
	}
	add_string(tmp);
	char * italic = strstr(color_string,";3");
	if (italic && italic[2] == '\0') {
		add_string(" font-style: oblique;");
	}
	char * bold = strstr(color_string,";1");
	if (bold && bold[2] == '\0') {
		add_string(" font-weight: bold;");
	}
	char * underline = strstr(color_string,";4");
	if (underline && underline[2] == '\0') {
		add_string(" font-decoration: underline;");
	}
}

int convert_to_html(void) {
	buffer_t * old = env;
	env = buffer_new();
	setup_buffer(env);
	env->loading = 1;

	add_string("<!doctype html>\n");
	add_string("<html>\n");
	add_string("	<head>\n");
	add_string("		<meta charset=\"UTF-8\">\n");
	if (old->file_name) {
		add_string("		<title>");
		add_string(file_basename(old->file_name));
		add_string("</title>\n");
	}
	add_string("		<style>\n");
	add_string("			body {\n");
	add_string("				margin: 0;\n");
	add_string("				-webkit-text-size-adjust: none;\n");
	add_string("				counter-reset: line-no;\n");
	add_string("				background-color: ");
	/* Convert color */
	html_convert_color(COLOR_BG);
	add_string("\n");
	add_string("			}\n");
	add_string("			.ul { text-decoration: underline; }\n");
	for (int i = 0; i < 15; ++i) {
		/* For each of the relevant flags... */
		char tmp[20];
		sprintf(tmp,"			.s%d { color: ", i);
		add_string(tmp);
		/* These are special */
		if (i == FLAG_NOTICE) {
			html_convert_color(COLOR_SEARCH_FG);
			add_string(" background-color: ");
			html_convert_color(COLOR_SEARCH_BG);
		} else if (i == FLAG_ERROR) {
			html_convert_color(COLOR_ERROR_FG);
			add_string(" background-color: ");
			html_convert_color(COLOR_ERROR_BG);
		} else {
			html_convert_color(flag_to_color(i));
		}
		add_string("}\n");
	}
	add_string("			pre {\n");
	add_string("				margin: 0;\n");
	add_string("				white-space: pre-wrap;\n");
	add_string("				font-family: \"DejaVu Sans Mono\", Courier, monospace;\n");
	add_string("				font-size: 10pt;\n");
	add_string("			}\n");
	add_string("			pre>span {\n");
	add_string("				display: inline-block;\n");
	add_string("				width: 100%;\n");
	add_string("			}\n");
	add_string("			pre>span>a::before {\n");
	add_string("				counter-increment: line-no;\n");
	add_string("				content: counter(line-no);\n");
	add_string("				padding-right: 1em;\n");
	add_string("				width: 3em;\n");
	add_string("				display: inline-block;\n");
	add_string("				text-align: right;\n");
	add_string("				background-color: ");
	html_convert_color(COLOR_NUMBER_BG);
	add_string("\n");
	add_string("				color: ");
	html_convert_color(COLOR_NUMBER_FG);
	add_string("\n");
	add_string("			}\n");
	add_string("			pre>span:target {\n");
	add_string("				background-color: ");
	html_convert_color(COLOR_ALT_BG);
	add_string("\n");
	add_string("			}\n");
	add_string("			pre>span:target>a::before {\n");
	add_string("				background-color: ");
	html_convert_color(COLOR_NUMBER_FG);
	add_string("\n");
	add_string("				color: ");
	html_convert_color(COLOR_NUMBER_BG);
	add_string("\n");
	add_string("			}\n");
	for (int i = 1; i <= env->tabstop; ++i) {
		char tmp[20];
		sprintf(tmp, ".tab%d", i);
		add_string("			");
		add_string(tmp);
		add_string(">span {\n");
		add_string("				display: inline-block;\n");
		add_string("				overflow: hidden;\n");
		add_string("				width: 0;\n");
		add_string("				height: 0;\n");
		add_string("			}\n");
		add_string("			");
		add_string(tmp);
		add_string("::after {\n");
		add_string("				content: '»");
		for (int j = 1; j < i; ++j) {
			add_string("·");
		}
		add_string("';\n");
		add_string("				background-color: ");
		html_convert_color(COLOR_ALT_BG);
		add_string("\n");
		add_string("				color: ");
		html_convert_color(COLOR_ALT_FG);
		add_string("\n");
		add_string("			}\n");
	}
	add_string("			.space {\n");
	add_string("				border-left: 1px solid ");
	html_convert_color(COLOR_ALT_FG);
	add_string("\n");
	add_string("				margin-left: -1px;\n");
	add_string("			}\n");
	add_string("		</style>\n");
	add_string("	</head>\n");
	add_string("	<body><pre>\n");

	for (int i = 0; i < old->line_count; ++i) {
		char tmp[100];
		sprintf(tmp, "<span id=\"L%d\"><a href=\"#L%d\"></a>", i+1, i+1);
		add_string(tmp);
		int last_flag = -1;
		int opened = 0;
		int all_spaces = 1;
		for (int j = 0; j < old->lines[i]->actual; ++j) {
			char_t c = old->lines[i]->text[j];

			if (c.codepoint != ' ') all_spaces = 0;

			if (last_flag == -1 || last_flag != (c.flags & 0x1F)) {
				if (opened) add_string("</span>");
				opened = 1;
				char tmp[100];
				sprintf(tmp, "<span class=\"s%d%s\">",
					c.flags & FLAG_MASK_COLORS,
					(c.flags & FLAG_UNDERLINE) ? " ul" : "");
				add_string(tmp);
				last_flag = (c.flags & 0x1F);
			}

			if (c.codepoint == '<') {
				add_string("&lt;");
			} else if (c.codepoint == '>') {
				add_string("&gt;");
			} else if (c.codepoint == '&') {
				add_string("&amp;");
			} else if (c.codepoint == '\t') {
				char tmp[100];
				sprintf(tmp, "<span class=\"tab%d\"><span>	</span></span>",c.display_width);
				add_string(tmp);
			} else if (j > 0 && c.codepoint == ' ' && all_spaces && !(j % old->tabstop)) {
				add_string("<span class=\"space\"> </span>");
			} else {
				char tmp[7] = {0}; /* Max six bytes, use 7 to ensure last is always nil */
				to_eight(c.codepoint, tmp);
				add_string(tmp);
			}
		}
		if (opened) {
			add_string("</span>");
		} else {
			add_string("<wbr>");
		}
		add_string("</span>\n");
	}

	add_string("</pre></body>\n");
	add_string("</html>\n");

	env->loading = 0;
	env->modified = 1;
	if (old->file_name) {
		char * base = file_basename(old->file_name);
		char * tmp = malloc(strlen(base) + 5);
		*tmp = '\0';
		strcat(tmp, base);
		strcat(tmp, ".htm");
		env->file_name = tmp;
	}
	for (int i = 0; i < env->line_count; ++i) {
		recalculate_tabs(env->lines[i]);
	}
	env->syntax = match_syntax(".htm");
	schedule_complete_recalc();

	return 0;
}

/**
 * Based on vim's :TOhtml
 * Convert syntax-highlighted buffer contents to HTML.
 */
BIM_COMMAND(tohtml,"tohtml","Convert the document to an HTML representation with syntax highlighting.") {
	convert_to_html();

	redraw_all();
	return 0;
}

BIM_ALIAS("TOhtml",tohtml,tohtml)

int _prefix_command_run_script(char * cmd) {
	if (env->mode == MODE_LINE_SELECTION) {
		int range_top, range_bot;
		range_top = env->start_line < env->line_no ? env->start_line : env->line_no;
		range_bot = env->start_line < env->line_no ? env->line_no : env->start_line;

		int in[2];
		pipe(in);
		int out[2];
		pipe(out);
		int child = fork();

		/* Open child process and set up pipes */
		if (child == 0) {
			FILE * dev_null = fopen("/dev/null","w"); /* for stderr */
			close(out[0]);
			close(in[1]);
			dup2(out[1], STDOUT_FILENO);
			dup2(in[0], STDIN_FILENO);
			dup2(fileno(dev_null), STDERR_FILENO);
			system(&cmd[1]); /* Yes we can just do this */
			exit(1);
		} else if (child < 0) {
			render_error("Failed to fork");
			return 1;
		}
		close(out[1]);
		close(in[0]);

		/* Write lines to child process */
		FILE * f = fdopen(in[1],"w");
		for (int i = range_top; i <= range_bot; ++i) {
			line_t * line = env->lines[i-1];
			for (int j = 0; j < line->actual; j++) {
				char_t c = line->text[j];
				if (c.codepoint == 0) {
					char buf[1] = {0};
					fwrite(buf, 1, 1, f);
				} else {
					char tmp[8] = {0};
					int i = to_eight(c.codepoint, tmp);
					fwrite(tmp, i, 1, f);
				}
			}
			fputc('\n', f);
		}
		fclose(f);
		close(in[1]);

		/* Read results from child process into a new buffer */
		FILE * result = fdopen(out[0],"r");
		buffer_t * old = env;
		env = buffer_new();
		setup_buffer(env);
		env->loading = 1;
		uint8_t buf[BLOCK_SIZE];
		state = 0;
		while (!feof(result) && !ferror(result)) {
			size_t r = fread(buf, 1, BLOCK_SIZE, result);
			add_buffer(buf, r);
		}
		if (env->line_no && env->lines[env->line_no-1] && env->lines[env->line_no-1]->actual == 0) {
			env->lines = remove_line(env->lines, env->line_no-1);
		}
		fclose(result);
		env->loading = 0;

		/* Return to the original buffer and replace the selected lines with the output */
		buffer_t * new = env;
		env = old;
		for (int i = range_top; i <= range_bot; ++i) {
			/* Remove the existing lines */
			env->lines = remove_line(env->lines, range_top-1);
		}
		for (int i = 0; i < new->line_count; ++i) {
			/* Add the new lines */
			env->lines = add_line(env->lines, range_top + i - 1);
			replace_line(env->lines, range_top + i - 1, new->lines[i]);
			recalculate_tabs(env->lines[range_top+i-1]);
		}

		env->modified = 1;

		/* Close the temporary buffer */
		buffer_close(new);
	} else {
		/* Reset and draw some line feeds */
		reset();
		printf("\n\n");

		/* Set buffered for shell application */
		set_buffered();

		/* Call the shell and wait for completion */
		system(&cmd[1]);

		/* Return to the editor, wait for user to press enter. */
		set_unbuffered();
		printf("\n\nPress ENTER to continue.");
		int c;
		while ((c = bim_getch(), c != ENTER_KEY && c != LINE_FEED));

		/* Redraw the screen */
		redraw_all();
	}

	/* Done processing command */
	return 0;
}

int replace_text(int range_top, int range_bot, char divider, char * needle) {
	char * c = needle;
	char * replacement = NULL;
	char * options = "";

	while (*c) {
		if (*c == divider) {
			*c = '\0';
			replacement = c + 1;
			break;
		}
		c++;
	}

	if (!replacement) {
		render_error("nothing to replace with");
		return 1;
	}

	c = replacement;
	while (*c) {
		if (*c == divider) {
			*c = '\0';
			options = c + 1;
			break;
		}
		c++;
	}

	int global = 0;
	int case_insensitive = 0;

	/* Parse options */
	while (*options) {
		switch (*options) {
			case 'g':
				global = 1;
				break;
			case 'i':
				case_insensitive = 1;
				break;
		}
		options++;
	}

	uint32_t * needle_c = malloc(sizeof(uint32_t) * (strlen(needle) + 1));
	uint32_t * replacement_c = malloc(sizeof(uint32_t) * (strlen(replacement) + 1));

	{
		int i = 0;
		uint32_t c, state = 0;
		for (char * cin = needle; *cin; cin++) {
			if (!decode(&state, &c, *cin)) {
				needle_c[i] = c;
				i++;
			} else if (state == UTF8_REJECT) {
				state = 0;
			}
		}
		needle_c[i] = 0;
		i = 0;
		c = 0;
		state = 0;
		for (char * cin = replacement; *cin; cin++) {
			if (!decode(&state, &c, *cin)) {
				replacement_c[i] = c;
				i++;
			} else if (state == UTF8_REJECT) {
				state = 0;
			}
		}
		replacement_c[i] = 0;
	}

	int replacements = 0;
	for (int line = range_top; line <= range_bot; ++line) {
		int col = 0;
		while (col != -1) {
			perform_replacement(line, needle_c, replacement_c, col, case_insensitive, &col);
			if (col != -1) replacements++;
			if (!global) break;
		}
	}
	free(needle_c);
	free(replacement_c);
	if (replacements) {
		render_status_message("replaced %d instance%s of %s", replacements, replacements == 1 ? "" : "s", needle);
		set_history_break();
		redraw_text();
	} else {
		render_error("Pattern not found: %s", needle);
	}

	return 0;
}

BIM_PREFIX_COMMAND(repsome,"s","Perform a replacement over selected lines") {
	int range_top, range_bot;
	if (env->mode == MODE_LINE_SELECTION) {
		range_top = env->start_line < env->line_no ? env->start_line : env->line_no;
		range_bot = env->start_line < env->line_no ? env->line_no : env->start_line;
	} else {
		range_top = env->line_no;
		range_bot = env->line_no;
	}
	return replace_text(range_top, range_bot, cmd[1], &cmd[2]);
}

BIM_PREFIX_COMMAND(repall,"%s","Perform a replacement over the entire file.") {
	return replace_text(1, env->line_count, cmd[2], &cmd[3]);
}

BIM_COMMAND(e,"e","Open a file") {
	if (argc > 1) {
		/* This actually opens a new tab */
		open_file(argv[1]);
		update_title();
	} else {
		if (env->modified) {
			render_error("File is modified, can not reload.");
			return 1;
		}

		buffer_t * old_env = env;
		open_file(env->file_name);
		buffer_t * new_env = env;
		env = old_env;

#define SWAP(T,a,b) do { T x = a; a = b; b = x; } while (0)
		SWAP(line_t **, env->lines, new_env->lines);
		SWAP(int, env->line_count, new_env->line_count);
		SWAP(int, env->line_avail, new_env->line_avail);
		SWAP(history_t *, env->history, new_env->history);

		buffer_close(new_env); /* Should probably also free, this needs editing. */
		schedule_complete_recalc();
		redraw_all();
	}
	return 0;
}

BIM_COMMAND(tabnew,"tabnew","Open a new tab") {
	if (argc > 1) {
		open_file(argv[1]);
		update_title();
	} else {
		env = buffer_new();
		setup_buffer(env);
		redraw_all();
		update_title();
	}
	return 0;
}

BIM_COMMAND(w,"w","Write a file") {
	/* w: write file */
	if (argc > 1) {
		write_file(argv[1]);
	} else {
		write_file(env->file_name);
	}
	return 0;
}

BIM_COMMAND(wq,"wq","Write and close buffer") {
	write_file(env->file_name);
	close_buffer();
	return 0;
}

BIM_COMMAND(history,"history","Display command history") {
	render_commandline_message(""); /* To clear command line */
	for (int i = COMMAND_HISTORY_MAX; i > 1; --i) {
		if (command_history[i-1]) render_commandline_message("%d:%s\n", i-1, command_history[i-1]);
	}
	render_commandline_message("\n");
	redraw_tabbar();
	redraw_commandline();
	pause_for_key();
	return 0;
}

BIM_COMMAND(q,"q","Close buffer") {
	if (left_buffer && left_buffer == right_buffer) {
		unsplit();
		return 0;
	}
	if (env->modified) {
		render_error("No write since last change. Use :q! to force exit.");
	} else {
		close_buffer();
	}
	update_title();
	return 0;
}

BIM_COMMAND(qbang,"q!","Force close buffer") {
	close_buffer();
	update_title();
	return 0;
}

BIM_COMMAND(qa,"qa","Try to close all buffers") {
	try_quit();
	return 0;
}

BIM_ALIAS("qall",qall,qa)

BIM_COMMAND(qabang,"qa!","Force exit") {
	/* Forcefully exit editor */
	while (buffers_len) {
		buffer_close(buffers[0]);
	}
	quit(NULL);
	return 1; /* doesn't return */
}

BIM_COMMAND(tabp,"tabp","Previous tab") {
	previous_tab();
	update_title();
	return 0;
}

BIM_COMMAND(tabn,"tabn","Next tab") {
	next_tab();
	update_title();
	return 0;
}

BIM_COMMAND(tabm,"tabm","Move the current tab to a new index") {
	/* Figure out the current index */
	int i = 0;
	for (; i < buffers_len; i++) {
		if (buffers[i] == env) break;
	}

	if (i == buffers_len) {
		render_status_message("(invalid state?)");