main uint/cterm / source / cterm.c
  1#include "maus_input.h"
  2#include <errno.h>
  3#include <fcntl.h>
  4#include <locale.h>
  5#include <poll.h>
  6#include <stdbool.h>
  7#include <stdint.h>
  8#include <stdlib.h>
  9#include <string.h>
 10#include <sys/ioctl.h>
 11#include <sys/wait.h>
 12#include <time.h>
 13#ifdef __linux__
 14#include <pty.h>
 15#else
 16#include <util.h>
 17#endif
 18#include <unistd.h>
 19
 20#include <maus.h>
 21
 22#include "../config.h"
 23#include "draw.h"
 24#include "font.h"
 25#include "term.h"
 26#include "utils.h"
 27
 28static void cleanup(void);
 29static bool handle_key(const MausEvent* ev);
 30static void init(void);
 31static bool ptyread(void);
 32static void ptywrite(const char* s, size_t n);
 33static bool reload_font(void);
 34static void resize_pty(void);
 35static int resize_window(int w, int h);
 36static void resize_terminal(int w, int h);
 37static void run(void);
 38static bool shell_exited(void);
 39static void wait_events(void);
 40
 41static Fontface font;
 42static Caret car;
 43static Maus* win = NULL;
 44static int winw;
 45static int winh;
 46
 47static Term term = {
 48	.runes = NULL,
 49	.cols = 0,
 50	.rows = 0,
 51	.ptyfd = -1,
 52	.ptypid = -1,
 53	.fg = default_fg,
 54	.bg = default_bg,
 55	.cursor_visible = true,
 56};
 57
 58/* clean up resources */
 59static void cleanup(void)
 60{
 61	free(term.runes);
 62	free(term.alt);
 63	font_free(&font);
 64
 65	if (win) {
 66		maus_close(win);
 67		free(win);
 68	}
 69}
 70
 71/* dispatch correct keycode handling non ascii characters */
 72static bool handle_key(const MausEvent* ev)
 73{
 74	char text;
 75
 76	if (ev->type != MAUS_EV_KEY || !ev->key.pressed)
 77		return false;
 78
 79	if (BIND_RELOAD_FONT)
 80		return reload_font();
 81
 82	if (BIND_PASTE) {
 83		char* text = maus_clipboard_get_text(win);
 84
 85		if (text && text[0] != '\0') {
 86			/* bracketed paste mode on */
 87			if (term.bracketed_paste)
 88				ptywrite("\033[200~", 6);
 89
 90			for (char* p = text; *p; p++) {
 91				/* replace newlines with carriage returns */
 92				char ch = (*p == '\n') ? '\r' : *p;
 93				ptywrite(&ch, 1);
 94			}
 95
 96			/* bracketed paste mode off */
 97			if (term.bracketed_paste)
 98				ptywrite("\033[201~", 6);
 99		}
100
101		return true;
102	}
103
104	switch(ev->key.key) {
105	case MAUS_KEY_ENTER:
106	case MAUS_KEY_KP_ENTER: ptywrite("\r", 1); break;
107	case MAUS_KEY_BACKSPACE: ptywrite("\x7F", 1); break;
108	case MAUS_KEY_TAB:       ptywrite("\t",  1); break;
109	case MAUS_KEY_ESCAPE:    ptywrite("\x1B", 1); break;
110	case MAUS_KEY_UP:        ptywrite("\x1B[A", 3); break;
111	case MAUS_KEY_DOWN:      ptywrite("\x1B[B", 3); break;
112	case MAUS_KEY_RIGHT:     ptywrite("\x1B[C", 3); break;
113	case MAUS_KEY_LEFT:      ptywrite("\x1B[D", 3); break;
114	case MAUS_KEY_HOME:      ptywrite("\x1B[H", 3); break;
115	case MAUS_KEY_END:       ptywrite("\x1B[F", 3); break;
116	case MAUS_KEY_INSERT:    ptywrite("\x1B[2~", 4); break;
117	case MAUS_KEY_DELETE:    ptywrite("\x1B[3~", 4); break;
118	case MAUS_KEY_PAGE_UP:   ptywrite("\x1B[5~", 4); break;
119	case MAUS_KEY_PAGE_DOWN: ptywrite("\x1B[6~", 4); break;
120	default:
121		text = ev->key.text;
122		if (text != '\0')
123			ptywrite(&text, 1);
124		break;
125	}
126
127	return false;
128}
129
130/* initialise font, window, pty */
131static void init(void)
132{
133	setlocale(LC_CTYPE, "");
134
135	/* font */
136	if (font_load(&font, font_path) < 0)
137		die(1, "failed to load font");
138
139	winw = win_width*font.cellw;
140	winh = win_height*font.cellh;
141
142	if (term_resize(&term, &car, win_width, win_height) < 0)
143		die(1, "failed to resize terminal");
144
145	/* window */
146	win = maus_init(win_title, 0, 0, winw, winh);
147	if (!win)
148		die(1, "failed to init window");
149	if (!maus_create_window(win))
150		die(1, "failed to create window");
151
152	/* pty */
153	struct winsize ws = {
154		.ws_col = term.cols,
155		.ws_row = term.rows,
156		.ws_xpixel = winw,
157		.ws_ypixel = winh
158	};
159	term.ptypid = forkpty(&term.ptyfd, NULL, NULL, &ws);
160	if (term.ptypid < 0)
161		die(EXIT_FAILURE, "failed to fork pty");
162	if (term.ptypid == 0) {
163		setenv("TERM", term_name, 1);
164
165		/* shell */
166		const char* sh = shell;
167		if (sh == NULL)
168			sh = getenv("SHELL");
169
170		execl(sh, sh, "-i", (char *)NULL);
171
172		_exit(127);
173	}
174
175	int flags = fcntl(term.ptyfd, F_GETFL, 0);
176	if (flags >= 0) /* append to existing flags */
177		fcntl(term.ptyfd, F_SETFL, flags|O_NONBLOCK);
178
179	term_clear(&term, &car);
180}
181
182/* read from pty master */
183static bool ptyread(void)
184{
185	char buf[4096];
186	bool damaged = false;
187
188	for (;;) {
189		ssize_t n = read(term.ptyfd, buf, sizeof(buf));
190		if (n < 0 && errno == EINTR)
191			continue;
192		if (n < 0 && (errno == EAGAIN || errno == EWOULDBLOCK))
193			break;
194		if (n > 0) {
195			if (term_write(&term, &car, buf, n))
196				damaged = true;
197			continue;
198		}
199
200		/* EOF or other error */
201		break;
202	}
203
204	return damaged;
205}
206
207/* write to pty master */
208static void ptywrite(const char* s, size_t n)
209{
210	while (n > 0) {
211		ssize_t ret = write(term.ptyfd, s, n);
212		if (ret < 0 && (errno == EINTR))
213			continue;
214		if (ret > 0) {
215			s+= ret;
216			n -= ret;
217			continue;
218		}
219
220		break;
221	}
222}
223
224/* reload the font. true on success, false on failure.
225   in event of failure, current font will still be in use  */
226static bool reload_font(void)
227{
228	Fontface next;
229	if (font_load(&next, font_path) < 0)
230		return false;
231
232	font_free(&font);
233	font = next;
234	resize_terminal(winw, winh);
235	term_damage_all(&term);
236
237	return true;
238}
239
240/* resize pty to reflect terminal values */
241static void resize_pty(void)
242{
243	struct winsize ws = {
244		.ws_col = term.cols,
245		.ws_row = term.rows,
246		.ws_xpixel = winw,
247		.ws_ypixel = winh,
248	};
249
250	if (term.ptyfd >= 0)
251		ioctl(term.ptyfd, TIOCSWINSZ, &ws);
252}
253
254/* resize window framebuffer to reflect resized size */
255static int resize_window(int w, int h)
256{
257	if (w < 1)
258		w = 1;
259	if (h < 1)
260		h = 1;
261
262	if (w == winw && h == winh)
263		return 0;
264
265	if (!maus_resize(win, w, h))
266		return -1;
267	winw = w;
268	winh = h;
269
270	return 0;
271}
272
273/* TODO */
274static void resize_terminal(int w, int h)
275{
276	int cols = w / font.cellw;
277	int rows = h / font.cellh;
278
279	if (cols < 1)
280		cols = 1;
281	if (rows < 1)
282		rows = 1;
283
284	if (cols == term.cols && rows == term.rows)
285		return;
286
287	if (term_resize(&term, &car, cols, rows) < 0)
288		die(1, "failed to resize terminal");
289
290	resize_pty();
291}
292
293/* event loop (TODO) */
294static void run(void)
295{
296	MausEvent ev;
297	Caret ocar = { -1, -1 };
298	bool running = true;
299	bool dirty = true;
300	bool redraw_all = true;
301
302	while (running) {
303		while (maus_event_poll(win, &ev)) {
304			if (ev.type == MAUS_EV_CLOSE || shell_exited()) {
305				running = false;
306				break;
307			}
308
309			if (ev.type == MAUS_EV_RESIZE) {
310				if (resize_window(ev.resize.width, ev.resize.height) < 0)
311					die(EXIT_FAILURE, "failed to resize window");
312				resize_terminal(ev.resize.width, ev.resize.height);
313				term_damage_all(&term);
314				dirty = true;
315				redraw_all = true;
316				continue;
317			}
318
319			if (ev.type == MAUS_EV_REDRAW) {
320				dirty = true;
321				continue;
322			}
323
324			if (handle_key(&ev)) {
325				dirty = true;
326				redraw_all = true;
327			}
328		}
329
330		if (!running)
331			break;
332
333		if (ptyread())
334			dirty = true;
335
336		if (ocar.x != car.x || ocar.y != car.y) {
337			term_damage_rune(&term, ocar.x, ocar.y);
338			term_damage_rune(&term, car.x, car.y);
339			dirty = true;
340		}
341
342		if (!dirty) {
343			wait_events();
344			continue;
345		}
346
347		if (redraw_all) {
348			draw_clear(win->bfb, win->stride, winh, rgba(0, 0, 0, 255));
349			term_damage_all(&term);
350		}
351
352		for (int y = 0; y < term.rows; y++) {
353			for (int x = 0; x < term.cols; x++) {
354				Rune* r = &RUNE(&term, x, y);
355				bool cursor = term.cursor_visible &&
356				              x == car.x && y == car.y;
357				bool reverse = (r->attr & TERM_ATTR_REVERSE) != 0;
358				uint32_t fg = reverse ? r->bg : r->fg;
359				uint32_t bg = reverse ? r->fg : r->bg;
360
361				if (cursor) {
362					fg = cursor_fg;
363					bg = cursor_bg;
364				}
365
366				if (!r->dmg)
367					continue;
368
369				draw_rune(
370					win->bfb, win->stride, winh, x, y,
371					font.cellw, font.cellh, bg
372				);
373
374				if (r->cp != ' ') {
375					draw_codepoint(
376						&font, win->bfb, win->stride, winh,
377						x * font.cellw,
378						y * font.cellh + (int)font.asc,
379						r->cp,
380						fg
381					);
382				}
383
384				r->dmg = false;
385			}
386		}
387
388		maus_target_fps(win, win_fps);
389		maus_present(win);
390
391		ocar = car;
392		dirty = false;
393		redraw_all = false;
394	}
395}
396
397/* check if the shell has exited */
398static bool shell_exited(void)
399{
400	int status;
401
402	if (term.ptypid <= 0)
403		return true;
404	return waitpid(term.ptypid, &status, WNOHANG) == term.ptypid;
405}
406
407/* wait for activity while allowing window events */
408static void wait_events(void)
409{
410	struct pollfd pfd = {
411		.fd = term.ptyfd,
412		.events = POLLIN,
413	};
414
415	if (term.ptyfd < 0) {
416		struct timespec ts = {
417			.tv_sec = 0,
418			.tv_nsec = 16 * 1000 * 1000, /* TODO? libmaus */
419		};
420		nanosleep(&ts, NULL);
421		return;
422	}
423
424	for (;;) {
425		int ret = poll(&pfd, 1, 16);
426		if (ret < 0 && errno == EINTR)
427			continue;
428		break;
429	}
430}
431
432int main(int argc, char* argv[])
433{
434	/* TODO: pledges */
435	init();
436	run();
437	cleanup();
438
439	return EXIT_SUCCESS;
440	(void) argc, (void) argv;
441}
442