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