aboutsummaryrefslogtreecommitdiff
path: root/ui-terminal-termbox2.c
blob: 2df68a172ea0702efdbd60a54ef924ff493e3df8 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
/* This file provides a termbox2-based backend for the Vis editor's terminal UI.
 * It is designed to be included from ui-terminal.c in place of ui-terminal-vt100.c
 * or ui-terminal-curses.c. Compile with termbox2 linked and define necessary
 * options like TB_OPT_ATTR_W=32 for truecolor support if desired.
 *
 * Focus is on portability, simplicity, and performance. We leverage termbox2's
 * double-buffering to eliminate flicker while keeping overhead low. Color
 * handling falls back gracefully from truecolor to 256 or 8 colors based on
 * terminal capabilities.
 *
 * Cells are inspected properly for content; empty data uses space. Colors map
 * from Vis CellColor (index or RGB) to termbox attrs, with approximation for
 * reduced palettes.
 */

#define TB_OPT_V1_COMPAT
#define TB_IMPL
#include "termbox2.h"
#define TB_OUTPUT_TRUECOLOR 1

#define CELL_COLOR_BLACK   { .index = 0 }
#define CELL_COLOR_RED     { .index = 1 }
#define CELL_COLOR_GREEN   { .index = 2 }
#define CELL_COLOR_YELLOW  { .index = 3 }
#define CELL_COLOR_BLUE    { .index = 4 }
#define CELL_COLOR_MAGENTA { .index = 5 }
#define CELL_COLOR_CYAN    { .index = 6 }
#define CELL_COLOR_WHITE   { .index = 7 }
#define CELL_COLOR_DEFAULT { .index = 9 }

#define CELL_ATTR_NORMAL    0
#define CELL_ATTR_UNDERLINE (1 << 0)
#define CELL_ATTR_REVERSE   (1 << 1)
#define CELL_ATTR_BLINK     (1 << 2)
#define CELL_ATTR_BOLD      (1 << 3)
#define CELL_ATTR_ITALIC    (1 << 4)
#define CELL_ATTR_DIM       (1 << 5)


#define UI_TERMKEY_FLAGS TERMKEY_FLAG_UTF8|TERMKEY_FLAG_NOTERMIOS

static int output_mode;

static const unsigned char color_256_to_16[256] = {
	 0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14, 15,
	 0,  4,  4,  4, 12, 12,  2,  6,  4,  4, 12, 12,  2,  2,  6,  4,
	12, 12,  2,  2,  2,  6, 12, 12, 10, 10, 10, 10, 14, 12, 10, 10,
	10, 10, 10, 14,  1,  5,  4,  4, 12, 12,  3,  8,  4,  4, 12, 12,
	 2,  2,  6,  4, 12, 12,  2,  2,  2,  6, 12, 12, 10, 10, 10, 10,
	14, 12, 10, 10, 10, 10, 10, 14,  1,  1,  5,  4, 12, 12,  1,  1,
	 5,  4, 12, 12,  3,  3,  8,  4, 12, 12,  2,  2,  2,  6, 12, 12,
	10, 10, 10, 10, 14, 12, 10, 10, 10, 10, 10, 14,  1,  1,  1,  5,
	12, 12,  1,  1,  1,  5, 12, 12,  1,  1,  1,  5, 12, 12,  3,  3,
	 3,  7, 12, 12, 10, 10, 10, 10, 14, 12, 10, 10, 10, 10, 10, 14,
	 9,  9,  9,  9, 13, 12,  9,  9,  9,  9, 13, 12,  9,  9,  9,  9,
	13, 12,  9,  9,  9,  9, 13, 12, 11, 11, 11, 11,  7, 12, 10, 10,
	10, 10, 10, 14,  9,  9,  9,  9,  9, 13,  9,  9,  9,  9,  9, 13,
	 9,  9,  9,  9,  9, 13,  9,  9,  9,  9,  9, 13,  9,  9,  9,  9,
	 9, 13, 11, 11, 11, 11, 11, 15,  0,  0,  0,  0,  0,  0,  8,  8,
	 8,  8,  8,  8,  7,  7,  7,  7,  7,  7, 15, 15, 15, 15, 15, 15
};

static void get_6cube_rgb(unsigned int n, int *r, int *g, int *b) {
	if (n < 16) {
		return;
	} else if (n < 232) {
		n -= 16;
		*r = (n / 36) ? (n / 36) * 40 + 55 : 0;
		*g = ((n / 6) % 6) ? ((n / 6) % 6) * 40 + 55 : 0;
		*b = (n % 6) ? (n % 6) * 40 + 55 : 0;
	} else if (n < 256) {
		n -= 232;
		*r = n * 10 + 8;
		*g = n * 10 + 8;
		*b = n * 10 + 8;
	}
}

static inline bool cell_color_equal(CellColor c1, CellColor c2) {
	if (c1.index != (uint8_t)-1 || c2.index != (uint8_t)-1)
		return c1.index == c2.index;
	return c1.r == c2.r && c1.g == c2.g && c1.b == c2.b;
}

static CellColor color_rgb(Ui *ui, uint8_t r, uint8_t g, uint8_t b) {
	return (CellColor){ .r = r, .g = g, .b = b, .index = (uint8_t)-1 };
}

static CellColor color_terminal(Ui *ui, uint8_t index) {
	return (CellColor){ .r = 0, .g = 0, .b = 0, .index = index };
}

static uintattr_t get_tb_attr(CellAttr a) {
	uintattr_t ret = 0;
	if (a & CELL_ATTR_BOLD) ret |= TB_BOLD;
	if (a & CELL_ATTR_UNDERLINE) ret |= TB_UNDERLINE;
	if (a & CELL_ATTR_REVERSE) ret |= TB_REVERSE;
	if (a & CELL_ATTR_BLINK) ret |= TB_BLINK;
	if (a & CELL_ATTR_ITALIC) ret |= TB_ITALIC;
	if (a & CELL_ATTR_DIM) ret |= TB_DIM;
	return ret;
}

static uintattr_t get_tb_color(CellColor c) {
	int mode = output_mode;
	int r, g, b;
	if (c.index != (uint8_t)-1) {
		if (c.index == 9) return TB_DEFAULT;
		static const int ansi_r[8] = {0, 128, 0, 128, 0, 128, 0, 192};
		static const int ansi_g[8] = {0, 0, 128, 128, 0, 0, 128, 192};
		static const int ansi_b[8] = {0, 0, 0, 0, 128, 128, 128, 192};
		r = ansi_r[c.index];
		g = ansi_g[c.index];
		b = ansi_b[c.index];
	} else {
		r = c.r; g = c.g; b = c.b;
	}

	if (mode == TB_OUTPUT_TRUECOLOR) {
		return (r << 16) | (g << 8) | b;
	}

	// Approximate to 256-color index
	int i = 0;
	if ((!r || (r - 55) % 40 == 0) &&
	    (!g || (g - 55) % 40 == 0) &&
	    (!b || (b - 55) % 40 == 0)) {
		i = 16;
		i += r ? ((r - 55) / 40) * 36 : 0;
		i += g ? ((g - 55) / 40) * 6 : 0;
		i += b ? ((b - 55) / 40) : 0;
	} else if (r == g && g == b && (r - 8) % 10 == 0 && r < 239) {
		i = 232 + ((r - 8) / 10);
	} else {
		unsigned lowest = UINT_MAX;
		for (int j = 16; j < 256; ++j) {
			int jr, jg, jb;
			get_6cube_rgb(j, &jr, &jg, &jb);
			int dr = jr - r, dg = jg - g, db = jb - b;
			unsigned distance = dr * dr + dg * dg + db * db;
			if (distance < lowest) {
				lowest = distance;
				i = j;
			}
		}
	}

	if (mode == TB_OUTPUT_256) {
		return i;
	} else { // TB_OUTPUT_NORMAL
		int idx16 = color_256_to_16[i];
		if (idx16 == 0) return TB_BLACK; // Treat as black for non-default
		if (idx16 < 8) return idx16 + 1;
		return (idx16 - 8 + 1) | TB_BRIGHT;
	}
}

static void ui_term_backend_blit(Ui *tui) {
	int w = tui->width, h = tui->height;
	Cell *cell = tui->cells;
	for (int y = 0; y < h; y++) {
		for (int x = 0; x < w; x++) {
			uint32_t ch = ' ';
			tb_utf8_char_to_unicode(&ch, cell->data);
			uintattr_t fg = get_tb_color(cell->style.fg) | get_tb_attr(cell->style.attr);
			uintattr_t bg = get_tb_color(cell->style.bg);
			tb_change_cell(x, y, ch, fg, bg);
			cell++;
		}
	}
	tb_present();
}

static void ui_term_backend_clear(Ui *tui) {
	tb_clear();
}

static bool ui_term_backend_resize(Ui *tui, int width, int height) {
	return true; // termbox2 handles resize internally
}

static void ui_term_backend_save(Ui *tui, bool fscr) {
	tb_shutdown();
}

static void ui_term_backend_restore(Ui *tui) {
	tb_init();
	tb_select_output_mode(output_mode);
	tb_hide_cursor();
	tb_clear();
}

int ui_terminal_colors(void) {
	switch (output_mode) {
	case TB_OUTPUT_TRUECOLOR: return 16777216;
	case TB_OUTPUT_256: return 256;
	default: return 16;
	}
}

static bool ui_term_backend_init(Ui *tui, char *term) {
	if (tb_init() < 0) return false;
	output_mode = tb_select_output_mode(TB_OUTPUT_TRUECOLOR);
	if (output_mode != TB_OUTPUT_TRUECOLOR)
		output_mode = tb_select_output_mode(TB_OUTPUT_256);
	if (output_mode != TB_OUTPUT_256)
		output_mode = TB_OUTPUT_NORMAL;
	tb_hide_cursor();
	return true;
}

static bool ui_backend_init(Ui *ui) {
	return true;
}

void ui_terminal_resume(Ui *term) { }

static void ui_term_backend_suspend(Ui *tui) { }

static void ui_term_backend_free(Ui *tui) {
	tb_shutdown();
}

static bool is_default_color(CellColor c) {
	return c.index == ((CellColor) CELL_COLOR_DEFAULT).index;
}