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