so://phie

so-term

An easy-to-use terminal library
git clone git://soophie.de/so-term

blob: e990643c12659ada2ab9b00d606c138503f40ff3 (14019 bytes)[doc][blame][raw]

1#ifndef _LIB_TERM_H

2#define _LIB_TERM_H

3

4#include <stddef.h>

5#include <stdbool.h>

6

7typedef enum {

8 TERM_CODE_RAWMODE_ENABLE,

9 TERM_CODE_RAWMODE_DISABLE,

10 TERM_CODE_CURSOR_READ,

11 TERM_CODE_WINDOW_READ,

12 TERM_CODE_KEY_POLL,

13 TERM_CODE_KEY_READ,

14 TERM_CODE_WRITE,

15 TERM_CODE_FLUSH,

16} term_code_t;

17

18typedef enum {

19 KEY_ENTER = 13,

20 KEY_ESC = 27,

21 KEY_BACKSPACE = 127,

22 KEY_ARROW_LEFT = 1000,

23 KEY_ARROW_RIGHT,

24 KEY_ARROW_UP,

25 KEY_ARROW_DOWN,

26 KEY_DEL,

27 KEY_HOME,

28 KEY_END,

29 KEY_PAGE_UP,

30 KEY_PAGE_DOWN,

31} term_key_t;

32

33typedef struct term term_t;

34

35typedef void (*term_dispatch_fn_t)(term_t *self, term_code_t code, void *data, size_t len);

36typedef void *(*term_fetch_fn_t)(term_t *self, term_code_t code, void *data, size_t len, size_t ret_len);

37

38struct term {

39 char *buffer;

40 size_t len;

41 term_dispatch_fn_t dispatch;

42 term_fetch_fn_t fetch;

43 int fd;

44};

45

46term_t term_init(void);

47void term_cleanup(term_t *self);

48void term_rawmode_enable(term_t *self);

49void term_rawmode_disable(term_t *self);

50void term_write(term_t *self, char *str);

51void term_writef(term_t *self, const char *format, ...);

52void term_flush(term_t *self);

53int term_read_cursor(term_t *self, size_t *rows, size_t *cols);

54int term_read_window(term_t *self, size_t *rows, size_t *cols);

55int term_read_key(term_t *self);

56int term_poll_key(term_t *self, int timeout_ms);

57

58#ifdef LIB_TERM_IMPL

59

60#include <stdio.h>

61#include <stdlib.h>

62#include <string.h>

63#include <unistd.h>

64#include <errno.h>

65#include <stdarg.h>

66

67void _term_panic(term_t *self, const char *str) {

68 write(self->fd, "\x1b[2J", 4);

69 write(self->fd, "\x1b[H", 3);

70 write(self->fd, "\x1b[?25h", 6);

71 perror(str);

72 exit(EXIT_FAILURE);

73}

74

75#ifdef LIB_TERM_POSIX

76

77#include <termios.h>

78#include <sys/ioctl.h>

79#include <poll.h>

80

81static struct termios _posix_termios;

82

83static void posix_rawmode_enable(term_t *self) {

84 if (tcgetattr(STDIN_FILENO, &_posix_termios) == -1) {

85 _term_panic(self, "tcgetattr");

86 }

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);

91 raw.c_lflag &= ~(ECHO | ICANON | IEXTEN | ISIG);

92 raw.c_cc[VMIN] = 0;

93 raw.c_cc[VTIME] = 1;

94 if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw) == -1) {

95 _term_panic(self, "tcsetattr");

96 }

97}

98

99static void posix_rawmode_disable(term_t *self) {

100 if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &_posix_termios) == -1) {

101 _term_panic(self, "tcsetattr");

102 }

103}

104

105static void posix_write(term_t *self, void *data, size_t len) {

106 (void) self;

107 (void) data;

108 (void) len;

109}

110

111static void posix_flush(term_t *self, void *data, size_t len) {

112 (void) data;

113 (void) len;

114 if (self->len > 0) {

115 write(self->fd, self->buffer, self->len);

116 free(self->buffer);

117 self->buffer = NULL;

118 self->len = 0;

119 }

120}

121

122static 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) {

128 return NULL;

129 }

130 while (i < sizeof(buf) - 1) {

131 if (read(STDIN_FILENO, &buf[i], 1) != 1) {

132 break;

133 }

134 if (buf[i] == 'R') {

135 break;

136 }

137 i++;

138 }

139 buf[i] = '\0';

140 if (buf[0] != '\x1b' || buf[1] != '[') {

141 return NULL;

142 }

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 }

148 return tuple;

149}

150

151static 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) {

154 if (write(self->fd, "\x1b[999C\x1b[999B", 12) == 12) {

155 return posix_read_cursor(self, data, len);

156 }

157 }

158 else {

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}

166

167static 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");

174 }

175 }

176 if (c == '\x1b') {

177 char seq[3];

178 if (read(STDIN_FILENO, &seq[0], 1) != 1) {

179 key = '\x1b';

180 }

181 if (read(STDIN_FILENO, &seq[1], 1) != 1) {

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) {

187 key = '\x1b';

188 }

189 if (seq[2] == '~') {

190 switch (seq[1]) {

191 case '1':

192 key = KEY_HOME;

193 break;

194 case '3':

195 key = KEY_DEL;

196 break;

197 case '4':

198 key = KEY_END;

199 break;

200 case '5':

201 key = KEY_PAGE_UP;

202 break;

203 case '6':

204 key = KEY_PAGE_DOWN;

205 break;

206 case '7':

207 key = KEY_HOME;

208 break;

209 case '8':

210 key = KEY_END;

211 break;

212 default:

213 break;

214 }

215 }

216 }

217 else {

218 switch (seq[1]) {

219 case 'A':

220 key = KEY_ARROW_UP;

221 break;

222 case 'B':

223 key = KEY_ARROW_DOWN;

224 break;

225 case 'C':

226 key = KEY_ARROW_RIGHT;

227 break;

228 case 'D':

229 key = KEY_ARROW_LEFT;

230 break;

231 case 'H':

232 key = KEY_HOME;

233 break;

234 case 'F':

235 key = KEY_END;

236 break;

237 default:

238 key = '\x1b';

239 break;

240 }

241 }

242 }

243 else if (seq[0] == 'O') {

244 switch (seq[1]) {

245 case 'H':

246 key = KEY_HOME;

247 break;

248 case 'F':

249 key = KEY_END;

250 break;

251 default:

252 break;

253 }

254 }

255 }

256 else {

257 key = c;

258 }

259 int *ret = malloc(sizeof(int));

260 *ret = key;

261 return ret;

262}

263

264static 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)) {

271 return posix_read_key(self);

272 }

273 int *ret = malloc(sizeof(int));

274 *ret = 0;

275 return ret;

276}

277

278static 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}

296

297static 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}

312

313#endif // LIB_TERM_POSIX

314

315#ifdef LIB_TERM_WINDOWS

316

317#include <windows.h>

318#include <conio.h>

319

320static DWORD _windows_console_mode;

321

322static 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}

334

335static 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}

341

342static 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}

346

347static 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}

358

359static 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}

371

372static 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}

384

385static 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}

427

428static 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}

442

443static 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}

461

462static 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}

477

478#endif // LIB_TERM_WINDOWS

479

480term_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_POSIX

489 self.dispatch = posix_dispatch;

490 self.fetch = posix_fetch;

491 self.fd = STDOUT_FILENO;

492 #endif /* LIB_TERM_POSIX */

493 #ifdef LIB_TERM_WINDOWS

494 self.dispatch = windows_dispatch;

495 self.fetch = windows_fetch;

496 self.fd = STD_OUTPUT_HANDLE;

497 #endif /* LIB_TERM_WINDOWS */

498 return self;

499}

500

501void 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}

510

511void term_rawmode_enable(term_t *self) {

512 self->dispatch(self, TERM_CODE_RAWMODE_ENABLE, NULL, 0);

513}

514

515void term_rawmode_disable(term_t *self) {

516 self->dispatch(self, TERM_CODE_RAWMODE_DISABLE, NULL, 0);

517}

518

519void 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}

532

533void 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}

541

542void term_flush(term_t *self) {

543 self->dispatch(self, TERM_CODE_FLUSH, NULL, 0);

544}

545

546int 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}

557

558int 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}

569

570int 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}

580

581int 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 }

589 return -1;

590}

591

592#endif // LIB_TERM_IMPL

593#endif // _LIB_TERM_H