An easy-to-use terminal library
git clone git://soophie.de/so-term
4
#include <stddef.h>5
#include <stdbool.h>6
#include <termios.h>7
typedef enum {8
TERM_CODE_RAWMODE_ENABLE,
28
KEY_END,29
KEY_PAGE_UP,30
KEY_PAGE_DOWN,32
} term_keycode_t;31
} term_key_t;33
typedef struct term term_t;34
typedef void (*term_dispatch_fn_t)(int fd, term_code_t code, void *data, size_t len);35
typedef void *(*term_fetch_fn_t)(int fd, term_code_t code, void *data, size_t len, size_t ret_len);35
typedef void (*term_dispatch_fn_t)(term_t *self, term_code_t code, void *data, size_t len);36
typedef void *(*term_fetch_fn_t)(term_t *self, term_code_t code, void *data, size_t len, size_t ret_len);37
typedef struct {38
struct termios termios;38
struct term {39
char *buffer;40
size_t len;41
bool teleterm_mode;41
term_dispatch_fn_t dispatch;42
term_fetch_fn_t fetch;43
int fd;45
} term_t;44
};46
term_t term_init(void);47
void term_cleanup(term_t *self);49
void term_fd_set(term_t *self, int fd);50
void term_teleterm_enable(term_t *self, term_dispatch_fn_t dispatch, term_fetch_fn_t fetch);48
void term_rawmode_enable(term_t *self);49
void term_rawmode_disable(term_t *self);50
void term_write(term_t *self, char *str);
61
#include <stdlib.h>62
#include <string.h>63
#include <unistd.h>67
#include <sys/ioctl.h>64
#include <errno.h>65
#include <stdarg.h>70
#include <poll.h>67
void _term_panic(term_t *self, const char *str) {68
write(self->fd, "\x1b[2J", 4);
72
exit(EXIT_FAILURE);73
}80
term_t term_init(void) {81
return (term_t) {82
.buffer = NULL,83
.len = 0,84
.teleterm_mode = false,85
.dispatch = NULL,86
.fetch = NULL,87
.fd = STDOUT_FILENO,88
};89
}75
#ifdef LIB_TERM_POSIX91
void term_cleanup(term_t *self) {92
if (self) {93
if (self->buffer) {94
free(self->buffer);95
self->buffer = NULL;96
self->len = 0;97
}98
}99
}77
#include <termios.h>78
#include <sys/ioctl.h>79
#include <poll.h>101
void term_fd_set(term_t *self, int fd) {102
self->fd = fd;103
}81
static struct termios _posix_termios;105
void term_teleterm_enable(term_t *self, term_dispatch_fn_t dispatch, term_fetch_fn_t fetch) {106
self->teleterm_mode = true;107
self->dispatch = dispatch;108
self->fetch = fetch;109
}111
void term_rawmode_enable(term_t *self) {112
if (self->teleterm_mode) {113
self->dispatch(self->fd, TERM_CODE_RAWMODE_ENABLE, NULL, 0);114
return;115
}116
if (tcgetattr(STDIN_FILENO, &self->termios) == -1) {83
static void posix_rawmode_enable(term_t *self) {84
if (tcgetattr(STDIN_FILENO, &_posix_termios) == -1) {85
_term_panic(self, "tcgetattr");86
}119
struct termios raw = self->termios;87
struct termios raw = _posix_termios;88
raw.c_iflag &= ~(BRKINT | ICRNL | INPCK | ISTRIP | IXON);89
raw.c_oflag &= ~(OPOST);90
raw.c_cflag |= (CS8);
96
}97
}131
void term_rawmode_disable(term_t *self) {132
if (self->teleterm_mode) {133
self->dispatch(self->fd, TERM_CODE_RAWMODE_DISABLE, NULL, 0);134
return;135
}136
if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &self->termios) == -1) {99
static void posix_rawmode_disable(term_t *self) {100
if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &_posix_termios) == -1) {101
_term_panic(self, "tcsetattr");102
}103
}141
void term_write(term_t *self, char *str) {142
if (self->teleterm_mode) {143
self->dispatch(self->fd, TERM_CODE_WRITE, NULL, 0);144
}145
size_t str_len = strlen(str);146
char *new_buffer = (char *) realloc(self->buffer, sizeof(char) * (self->len + str_len + 1));147
if (new_buffer == NULL) {148
fprintf(stderr, "Cannot realloc memory!\n");149
return;150
}151
self->buffer = new_buffer;152
memcpy(self->buffer + self->len, str, str_len);153
self->len += str_len;154
self->buffer[self->len] = '\0';155
}157
void term_writef(term_t *self, const char *format, ...) {158
va_list args;159
va_start(args, format);160
char str[1000];161
vsnprintf(str, sizeof(str), format, args);162
term_write(self, str);163
va_end(args);105
static void posix_write(term_t *self, void *data, size_t len) {106
(void) self;107
(void) data;108
(void) len;109
}166
void term_flush(term_t *self) {111
static void posix_flush(term_t *self, void *data, size_t len) {112
(void) data;113
(void) len;114
if (self->len > 0) {168
if (self->teleterm_mode) {169
self->dispatch(self->fd, TERM_CODE_FLUSH, self->buffer, self->len);170
}171
else {172
write(self->fd, self->buffer, self->len);173
}115
write(self->fd, self->buffer, self->len);116
free(self->buffer);117
self->buffer = NULL;118
self->len = 0;119
}120
}180
int term_read_cursor(term_t *self, size_t *rows, size_t *cols) {181
if (self->teleterm_mode) {182
void *data = self->fetch(self->fd, TERM_CODE_CURSOR_READ, NULL, 0, 2 * sizeof(size_t));183
if (data) {184
size_t *tuple = (size_t *) data;185
*rows = tuple[0];186
*cols = tuple[1];187
free(data);188
return 0;189
}190
return -1;191
}122
static void *posix_read_cursor(term_t *self, void *data, size_t len) {123
(void) data;124
(void) len;125
char buf[32];126
unsigned int i = 0;127
if (write(self->fd, "\x1b[6n", 4) != 4) {195
return -1;128
return NULL;129
}130
while (i < sizeof(buf) - 1) {131
if (read(STDIN_FILENO, &buf[i], 1) != 1) {
138
}139
buf[i] = '\0';140
if (buf[0] != '\x1b' || buf[1] != '[') {208
return -1;141
return NULL;142
}210
if (sscanf(&buf[2], "%zu;%zu", rows, cols) != 2) {211
return -1;143
size_t *tuple = malloc(2 * sizeof(size_t));144
if (sscanf(&buf[2], "%zu;%zu", &tuple[0], &tuple[1]) != 2) {145
free(tuple);146
return NULL;147
}213
return 0;148
return tuple;149
}216
int term_read_window(term_t *self, size_t *rows, size_t *cols) {217
if (self->teleterm_mode) {218
void *data = self->fetch(self->fd, TERM_CODE_WINDOW_READ, NULL, 0, 2 * sizeof(size_t));219
if (data) {220
size_t *tuple = (size_t *) data;221
*rows = tuple[0];222
*cols = tuple[1];223
free(data);224
return 0;225
}226
return -1;227
}151
static void *posix_read_window(term_t *self, void *data, size_t len) {152
struct winsize ws;153
if (ioctl(self->fd, TIOCGWINSZ, &ws) == -1 || ws.ws_col == 0) {230
if (write(self->fd, "\x1b[999C\x1b[999B", 12) != 12) {231
return -1;154
if (write(self->fd, "\x1b[999C\x1b[999B", 12) == 12) {155
return posix_read_cursor(self, data, len);156
}233
return term_read_cursor(self, rows, cols);157
}158
else {236
*cols = ws.ws_col;237
*rows = ws.ws_row;238
return 0;159
size_t *tuple = malloc(2 * sizeof(size_t));160
tuple[0] = ws.ws_row;161
tuple[1] = ws.ws_col;162
return tuple;163
}164
return NULL;165
}242
int term_read_key(term_t *self) {243
if (self->teleterm_mode) {244
void *data = self->fetch(self->fd, TERM_CODE_KEY_READ, NULL, 0, sizeof(int));245
if (data) {246
int *single = (int *) data;247
int key = *single;248
free(data);249
return key;250
}251
return -1;252
}167
static void *posix_read_key(term_t *self) {168
int len;169
char c;170
int key;171
while ((len = read(STDIN_FILENO, &c, 1)) != 1) {172
if (len == -1 && errno != EAGAIN) {173
_term_panic(self, "read");
176
if (c == '\x1b') {177
char seq[3];178
if (read(STDIN_FILENO, &seq[0], 1) != 1) {263
return '\x1b';179
key = '\x1b';180
}181
if (read(STDIN_FILENO, &seq[1], 1) != 1) {266
return '\x1b';182
key = '\x1b';183
}184
if (seq[0] == '[') {185
if (seq[1] >= '0' && seq[1] <= '9') {186
if (read(STDIN_FILENO, &seq[2], 1) != 1) {271
return '\x1b';187
key = '\x1b';188
}189
if (seq[2] == '~') {190
switch (seq[1]) {191
case '1':276
return KEY_HOME;192
key = KEY_HOME;193
break;194
case '3':278
return KEY_DEL;195
key = KEY_DEL;196
break;197
case '4':280
return KEY_END;198
key = KEY_END;199
break;200
case '5':282
return KEY_PAGE_UP;201
key = KEY_PAGE_UP;202
break;203
case '6':284
return KEY_PAGE_DOWN;204
key = KEY_PAGE_DOWN;205
break;206
case '7':286
return KEY_HOME;207
key = KEY_HOME;208
break;209
case '8':288
return KEY_END;210
key = KEY_END;211
break;212
default:213
break;214
}215
}216
}217
else {218
switch (seq[1]) {219
case 'A':295
return KEY_ARROW_UP;220
key = KEY_ARROW_UP;221
break;222
case 'B':297
return KEY_ARROW_DOWN;223
key = KEY_ARROW_DOWN;224
break;225
case 'C':299
return KEY_ARROW_RIGHT;226
key = KEY_ARROW_RIGHT;227
break;228
case 'D':301
return KEY_ARROW_LEFT;229
key = KEY_ARROW_LEFT;230
break;231
case 'H':303
return KEY_HOME;232
key = KEY_HOME;233
break;234
case 'F':305
return KEY_END;235
key = KEY_END;236
break;237
default:238
key = '\x1b';239
break;240
}307
return '\x1b';241
}242
}243
else if (seq[0] == 'O') {244
switch (seq[1]) {245
case 'H':313
return KEY_HOME;246
key = KEY_HOME;247
break;248
case 'F':315
return KEY_END;249
key = KEY_END;250
break;251
default:252
break;253
}254
}255
}319
return c;256
else {257
key = c;258
}259
int *ret = malloc(sizeof(int));260
*ret = key;261
return ret;262
}322
int term_poll_key(term_t *self, int timeout_ms) {323
if (self->teleterm_mode) {324
void *data = self->fetch(self->fd, TERM_CODE_KEY_POLL, &timeout_ms, sizeof(timeout_ms), sizeof(int));325
if (data) {326
int *single = (int *) data;327
int key = *single;328
free(data);329
return key;330
}331
return -1;332
}264
static void *posix_poll_key(term_t *self, void *data, size_t len) {265
(void) len;266
struct pollfd fds[1];267
fds[0].fd = STDIN_FILENO;268
fds[0].events = POLLIN | POLLPRI;269
int timeout_ms = *(int *) data;270
if (poll(fds, 1, timeout_ms)) {337
return term_read_key(self);271
return posix_read_key(self);272
}273
int *ret = malloc(sizeof(int));274
*ret = 0;275
return ret;276
}278
static void posix_dispatch(term_t *self, term_code_t code, void *data, size_t len) {279
switch (code) {280
case TERM_CODE_RAWMODE_ENABLE:281
posix_rawmode_enable(self);282
break;283
case TERM_CODE_RAWMODE_DISABLE:284
posix_rawmode_disable(self);285
break;286
case TERM_CODE_WRITE:287
posix_write(self, data, len);288
break;289
case TERM_CODE_FLUSH:290
posix_flush(self, data, len);291
break;292
default:293
break;294
}295
}297
static void *posix_fetch(term_t *self, term_code_t code, void *data, size_t len, size_t ret_len) {298
(void) ret_len;299
switch (code) {300
case TERM_CODE_CURSOR_READ:301
return posix_read_cursor(self, data, len);302
case TERM_CODE_WINDOW_READ:303
return posix_read_window(self, data, len);304
case TERM_CODE_KEY_READ:305
return posix_read_key(self);306
case TERM_CODE_KEY_POLL:307
return posix_poll_key(self, data, len);308
default:309
return NULL;310
}311
}313
#endif // LIB_TERM_POSIX315
#ifdef LIB_TERM_WINDOWS317
#include <windows.h>318
#include <conio.h>320
static DWORD _windows_console_mode;322
static void windows_rawmode_enable(term_t *self) {323
HANDLE handle = GetStdHandle(STD_INPUT_HANDLE);324
DWORD mode;325
if (!GetConsoleMode(handle, &mode)) {326
_term_panic(self, "GetConsoleMode");327
}328
_windows_console_mode = mode;329
mode &= ~(ENABLE_ECHO_INPUT | ENABLE_LINE_INPUT | ENABLE_PROCESSED_INPUT);330
if (!SetConsoleMode(handle, mode)) {331
_term_panic(self, "SetConsoleMode");332
}333
}335
static void windows_rawmode_disable(term_t *self) {336
HANDLE handle = GetStdHandle(STD_INPUT_HANDLE);337
if (!SetConsoleMode(handle, _windows_console_mode)) {338
_term_panic(self, "SetConsoleMode");339
}340
}342
static void windows_write(term_t *self, void *data, size_t len) {343
DWORD written;344
WriteConsole(GetStdHandle(self->fd), data, (DWORD) len, &written, NULL);345
}347
static void windows_flush(term_t *self, void *data, size_t len) {348
(void) data;349
(void) len;350
if (self->len > 0) {351
DWORD written;352
WriteConsole(GetStdHandle(self->fd), self->buffer, (DWORD) self->len, &written, NULL);353
free(self->buffer);354
self->buffer = NULL;355
self->len = 0;356
}357
}359
static void *windows_read_cursor(term_t *self, void *data, size_t len) {360
(void) data;361
(void) len;362
CONSOLE_SCREEN_BUFFER_INFO console_info;363
if (!GetConsoleScreenBufferInfo(GetStdHandle(self->fd), &console_info)) {364
return NULL;365
}366
size_t *tuple = malloc(2 * sizeof(size_t));367
tuple[0] = console_info.dwCursorPosition.Y + 1;368
tuple[1] = console_info.dwCursorPosition.X + 1;369
return tuple;370
}372
static void *windows_read_window(term_t *self, void *data, size_t len) {373
(void) data;374
(void) len;375
CONSOLE_SCREEN_BUFFER_INFO console_info;376
if (!GetConsoleScreenBufferInfo(GetStdHandle(self->fd), &console_info)) {377
return NULL;378
}379
size_t *tuple = malloc(2 * sizeof(size_t));380
tuple[0] = console_info.srWindow.Bottom - console_info.srWindow.Top + 1;381
tuple[1] = console_info.srWindow.Right - console_info.srWindow.Left + 1;382
return tuple;383
}385
static void *windows_read_key(term_t *self) {386
(void) self;387
int c = _getch();388
if (c == 0 || c == 224) {389
int ext = _getch();390
switch (ext) {391
case 72:392
c = KEY_ARROW_UP;393
break;394
case 80:395
c = KEY_ARROW_DOWN;396
break;397
case 75:398
c = KEY_ARROW_LEFT;399
break;400
case 77:401
c = KEY_ARROW_RIGHT;402
break;403
case 71:404
c = KEY_HOME;405
break;406
case 79:407
c = KEY_END;408
break;409
case 73:410
c = KEY_PAGE_UP;411
break;412
case 81:413
c = KEY_PAGE_DOWN;414
break;415
case 83:416
c = KEY_DEL;417
break;418
default:419
c = 0;420
break;421
}422
}423
int *ret = malloc(sizeof(int));424
*ret = c;425
return ret;426
}428
static void *windows_poll_key(term_t *self, void *data, size_t len) {429
(void) len;430
int timeout_ms = *(int *) data;431
DWORD start = GetTickCount();432
while (GetTickCount() - start < (DWORD) timeout_ms) {433
if (_kbhit()) {434
return windows_read_key(self);435
}436
Sleep(1);437
}438
int *ret = malloc(sizeof(int));439
*ret = 0;440
return ret;441
}443
static void windows_dispatch(term_t *self, term_code_t code, void *data, size_t len) {444
switch (code) {445
case TERM_CODE_RAWMODE_ENABLE:446
windows_rawmode_enable(self);447
break;448
case TERM_CODE_RAWMODE_DISABLE:449
windows_rawmode_disable(self);450
break;451
case TERM_CODE_WRITE:452
windows_write(self, data, len);453
break;454
case TERM_CODE_FLUSH:455
windows_flush(self, data, len);456
break;457
default:458
break;459
}460
}462
static void *windows_fetch(term_t *self, term_code_t code, void *data, size_t len, size_t ret_len) {463
(void) ret_len;464
switch (code) {465
case TERM_CODE_CURSOR_READ:466
return windows_read_cursor(self, data, len);467
case TERM_CODE_WINDOW_READ:468
return windows_read_window(self, data, len);469
case TERM_CODE_KEY_READ:470
return windows_read_key(self);471
case TERM_CODE_KEY_POLL:472
return windows_poll_key(self, data, len);473
default:474
return NULL;475
}476
}478
#endif // LIB_TERM_WINDOWS480
term_t term_init(void) {481
term_t self = {482
.buffer = NULL,483
.len = 0,484
.dispatch = NULL,485
.fetch = NULL,486
.fd = 0,487
};488
#ifdef LIB_TERM_POSIX489
self.dispatch = posix_dispatch;490
self.fetch = posix_fetch;491
self.fd = STDOUT_FILENO;492
#endif /* LIB_TERM_POSIX */493
#ifdef LIB_TERM_WINDOWS494
self.dispatch = windows_dispatch;495
self.fetch = windows_fetch;496
self.fd = STD_OUTPUT_HANDLE;497
#endif /* LIB_TERM_WINDOWS */498
return self;499
}501
void term_cleanup(term_t *self) {502
if (self) {503
if (self->buffer) {504
free(self->buffer);505
self->buffer = NULL;506
self->len = 0;507
}508
}509
}511
void term_rawmode_enable(term_t *self) {512
self->dispatch(self, TERM_CODE_RAWMODE_ENABLE, NULL, 0);513
}515
void term_rawmode_disable(term_t *self) {516
self->dispatch(self, TERM_CODE_RAWMODE_DISABLE, NULL, 0);517
}519
void term_write(term_t *self, char *str) {520
self->dispatch(self, TERM_CODE_WRITE, NULL, 0);521
size_t str_len = strlen(str);522
char *new_buffer = (char *) realloc(self->buffer, sizeof(char) * (self->len + str_len + 1));523
if (new_buffer == NULL) {524
fprintf(stderr, "Cannot realloc memory!\n");525
return;526
}527
self->buffer = new_buffer;528
memcpy(self->buffer + self->len, str, str_len);529
self->len += str_len;530
self->buffer[self->len] = '\0';531
}533
void term_writef(term_t *self, const char *format, ...) {534
va_list args;535
va_start(args, format);536
char str[1000];537
vsnprintf(str, sizeof(str), format, args);538
term_write(self, str);539
va_end(args);540
}542
void term_flush(term_t *self) {543
self->dispatch(self, TERM_CODE_FLUSH, NULL, 0);544
}546
int term_read_cursor(term_t *self, size_t *rows, size_t *cols) {547
void *data = self->fetch(self, TERM_CODE_CURSOR_READ, NULL, 0, 2 * sizeof(size_t));548
if (data) {549
size_t *tuple = (size_t *) data;550
*rows = tuple[0];551
*cols = tuple[1];552
free(data);553
return 0;554
}555
return -1;556
}558
int term_read_window(term_t *self, size_t *rows, size_t *cols) {559
void *data = self->fetch(self, TERM_CODE_WINDOW_READ, NULL, 0, 2 * sizeof(size_t));560
if (data) {561
size_t *tuple = (size_t *) data;562
*rows = tuple[0];563
*cols = tuple[1];564
free(data);565
return 0;566
}567
return -1;568
}570
int term_read_key(term_t *self) {571
void *data = self->fetch(self, TERM_CODE_KEY_READ, NULL, 0, sizeof(int));572
if (data) {573
int *single = (int *) data;574
int key = *single;575
free(data);576
return key;577
}578
return -1;579
}581
int term_poll_key(term_t *self, int timeout_ms) {582
void *data = self->fetch(self, TERM_CODE_KEY_POLL, &timeout_ms, sizeof(timeout_ms), sizeof(int));583
if (data) {584
int *single = (int *) data;585
int key = *single;586
free(data);587
return key;588
}339
return 0;589
return -1;590
}592
#endif // LIB_TERM_IMPL