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