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