main
1/*** includes ***/
2
3#define _DEFAULT_SOURCE
4#define _BSD_SOURCE
5#define _GNU_SOURCE
6
7#include <ctype.h>
8#include <errno.h>
9#include <stdio.h>
10#include <stdarg.h>
11#include <stdlib.h>
12#include <string.h>
13#include <sys/ioctl.h>
14#include <sys/types.h>
15#include <termios.h>
16#include <time.h>
17#include <unistd.h>
18
19/*** defines ***/
20
21#define KILO_VERSION "0.0.1"
22#define KILO_TAB_STOP 8
23
24#define CTRL_KEY(k) ((k) & 0x1f)
25
26enum editorKey {
27 ARROW_LEFT = 1000,
28 ARROW_RIGHT,
29 ARROW_UP,
30 ARROW_DOWN,
31 DEL_KEY,
32 HOME_KEY,
33 END_KEY,
34 PAGE_UP,
35 PAGE_DOWN
36};
37
38/*** data ***/
39
40typedef struct erow {
41 int size;
42 int rsize;
43 char *chars;
44 char *render;
45} erow;
46
47struct editor_config {
48 int cx, cy;
49 int rx;
50 int rowoff;
51 int coloff;
52 int screenrows;
53 int screencols;
54 int numrows;
55 erow *row;
56 char *filename;
57 char statusmsg[80];
58 time_t statusmsg_time;
59 struct termios orig_termios;
60};
61
62struct editor_config E;
63
64/*** terminal ***/
65
66void die(const char *s) {
67 write(STDOUT_FILENO, "\x1b[2J", 4);
68 write(STDOUT_FILENO, "\x1b[H", 3);
69
70 perror(s);
71 exit(1);
72}
73
74void disable_raw_mode() {
75 if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &E.orig_termios) == -1)
76 die("tcsetattr");
77}
78
79void enable_raw_mode() {
80 if (tcgetattr(STDIN_FILENO, &E.orig_termios) == -1) die("tcgetattr");
81 atexit(disable_raw_mode);
82
83 struct termios raw = E.orig_termios;
84 raw.c_lflag &= ~(BRKINT | ICRNL | INPCK | ISTRIP | IXON);
85 raw.c_lflag &= ~(OPOST);
86 raw.c_lflag &= ~(CS8);
87 raw.c_lflag &= ~(ECHO | ICANON | IEXTEN | ISIG);
88 raw.c_cc[VMIN] = 0;
89 raw.c_cc[VTIME] = 1;
90
91 if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw) == -1) die("tcsetattr");
92}
93
94int editor_read_key() {
95 int nread;
96 char c;
97 while ((nread = read(STDIN_FILENO, &c, 1)) != 1) {
98 if (nread == -1 && errno != EAGAIN) die("read");
99 }
100
101 if (c == '\x1b') {
102 char seq[3];
103
104 if (read(STDIN_FILENO, &seq[0], 1) != 1) return '\x1b';
105 if (read(STDIN_FILENO, &seq[1], 1) != 1) return '\x1b';
106
107 if (seq[0] == '[') {
108 if (seq[1] >= '0' && seq[1] <= '9') {
109 if (read(STDIN_FILENO, &seq[2], 1) != 1) return '\x1b';
110 if (seq[2] == '~') {
111 switch (seq[1]) {
112 case '1': return HOME_KEY;
113 case '3': return DEL_KEY;
114 case '4': return END_KEY;
115 case '5': return PAGE_UP;
116 case '6': return PAGE_DOWN;
117 case '7': return HOME_KEY;
118 case '8': return END_KEY;
119 }
120 }
121 } else {
122 switch (seq[1]) {
123 case 'A': return ARROW_UP;
124 case 'B': return ARROW_DOWN;
125 case 'C': return ARROW_RIGHT;
126 case 'D': return ARROW_LEFT;
127 case 'H': return HOME_KEY;
128 case 'F': return END_KEY;
129 }
130 }
131 } else if (seq[0] == 'O') {
132 switch(seq[1]) {
133 case 'H': return HOME_KEY;
134 case 'F': return END_KEY;
135 }
136 }
137
138 return '\x1b';
139 } else {
140 return c;
141 }
142}
143
144int get_cursor_position(int *rows, int *cols) {
145 char buf[32];
146 unsigned int i = 0;
147
148 if (write(STDOUT_FILENO, "\x1b[6n", 4) != 4) return -1;
149
150 while (i < sizeof(buf) - 1) {
151 if (read(STDIN_FILENO, &buf[i], 1) != 1) break;
152 if (buf[i] == 'R') break;
153 i++;
154 }
155 buf[i] = '\0';
156
157 if (buf[0] != '\x1b' || buf[1] != '[') return -1;
158 if (sscanf(&buf[2], "%d;%d", rows, cols) != 2) return -1;
159
160 return 0;
161}
162
163int get_window_size(int *rows, int *cols) {
164 struct winsize ws;
165
166 if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == -1 || ws.ws_col == 0) {
167 if (write(STDOUT_FILENO, "\x1b[999C\x1b[999B", 12) != 12) return -1;
168 return get_cursor_position(rows, cols);
169 } else {
170 *cols = ws.ws_col;
171 *rows = ws.ws_row;
172 return 0;
173 }
174}
175
176/*** row operations ***/
177
178int editor_row_cx_to_rx(erow *row, int cx) {
179 int rx = 0;
180 int j;
181 for (j = 0; j < cx; j++) {
182 if (row->chars[j] == '\t')
183 rx += (KILO_TAB_STOP - 1) - (rx % KILO_TAB_STOP);
184 rx++;
185 }
186 return rx;
187}
188
189void editor_update_row(erow *row) {
190 int tabs = 0;
191 int j;
192 for (j = 0; j < row->size; j++)
193 if (row->chars[j] == '\t') tabs++;
194
195 free(row->render);
196 row->render = malloc(row->size + tabs*(KILO_TAB_STOP - 1) + 1);
197
198 int idx = 0;
199 for (j = 0; j < row->size; j++) {
200 if (row->chars[j] == '\t') {
201 row->render[idx++] = ' ';
202 while (idx % KILO_TAB_STOP != 0) row->render[idx++] = ' ';
203 } else {
204 row->render[idx++] = row->chars[j];
205 }
206 }
207 row->render[idx] = '\0';
208 row->rsize = idx;
209}
210
211void editor_append_row(char *s, size_t len) {
212 E.row = realloc(E.row, sizeof(erow) * (E.numrows + 1));
213
214 int at = E.numrows;
215 E.row[at].size = len;
216 E.row[at].chars = malloc(len + 1);
217 memcpy(E.row[at].chars, s, len);
218 E.row[at].chars[len] = '\0';
219
220 E.row[at].rsize = 0;
221 E.row[at].render = NULL;
222 editor_update_row(&E.row[at]);
223
224 E.numrows++;
225}
226
227/*** file i/o ***/
228
229void editor_open(char *filename) {
230 free(E.filename);
231 E.filename = strdup(filename);
232
233 FILE *fp = fopen(filename, "r");
234 if (!fp) die("fopen");
235
236 char *line = NULL;
237 size_t linecap = 0;
238 ssize_t linelen;
239 while ((linelen = getline(&line, &linecap, fp)) != -1) {
240 while (linelen > 0 && (line[linelen - 1] == '\n' || line[linelen - 1] == '\r'))
241 linelen--;
242 editor_append_row(line, linelen);
243 }
244 free(line);
245 fclose(fp);
246}
247
248/*** append buffer ***/
249
250struct abuf {
251 char *b;
252 int len;
253};
254
255#define ABUF_INIT {NULL, 0}
256
257void ab_append(struct abuf *ab, const char *s, int len) {
258 char *new = realloc(ab->b, ab->len + len);
259
260 if (new == NULL) return;
261 memcpy(&new[ab->len], s, len);
262 ab->b = new;
263 ab->len += len;
264}
265
266void ab_free(struct abuf *ab) {
267 free(ab->b);
268}
269
270/*** output ***/
271
272void editor_scroll() {
273 E.rx = 0;
274 if (E.cy < E.numrows) {
275 E.rx = editor_row_cx_to_rx(&E.row[E.cy], E.cx);
276 }
277
278 if (E.cy < E.rowoff) {
279 E.rowoff = E.cy;
280 }
281 if (E.cy >= E.rowoff + E.screenrows) {
282 E.rowoff = E.cy - E.screenrows + 1;
283 }
284 if (E.rx < E.coloff) {
285 E.coloff = E.rx;
286 }
287 if (E.rx >= E.coloff + E.screencols) {
288 E.coloff = E.rx - E.screencols + 1;
289 }
290}
291
292void editor_draw_rows(struct abuf *ab) {
293 for (int i = 0; i < E.screenrows; i++) {
294 int filerow = i + E.rowoff;
295 if (filerow >= E.numrows) {
296 if (E.numrows == 0 && i == E.screenrows / 3) {
297 char welcome[80];
298 int length = snprintf(welcome, sizeof(welcome),
299 "Kilo editor -- version %s", KILO_VERSION);
300 if (length > E.screencols) length = E.screencols;
301 int padding = (E.screencols - length) / 2;
302 if (padding) {
303 ab_append(ab, "~", 1);
304 padding--;
305 }
306 while (padding--) ab_append(ab, " ", 1);
307 ab_append(ab, welcome, length);
308 } else {
309 ab_append(ab, "~", 1);
310 }
311 } else {
312 int len = E.row[filerow].rsize - E.coloff;
313 if (len < 0) len = 0;
314 if (len > E.screencols) len = E.screencols;
315 ab_append(ab, &E.row[filerow].render[E.coloff], len);
316 }
317
318 ab_append(ab, "\x1b[K", 3);
319 ab_append(ab, "\r\n", 2);
320 }
321}
322
323void editor_draw_status_bar(struct abuf *ab) {
324 ab_append(ab, "\x1b[7m", 4);
325 char status[80], rstatus[80];
326 int len = snprintf(status, sizeof(status), "%.20s - %d lines", E.filename ? E.filename : "[No Name]", E.numrows);
327 int rlen = snprintf(rstatus, sizeof(rstatus), "%d/%d", E.cy + 1, E.numrows);
328 if (len > E.screencols) len = E.screencols;
329 ab_append(ab, status, len);
330 while (len < E.screencols) {
331 if (E.screencols - len == rlen) {
332 ab_append(ab, rstatus, rlen);
333 break;
334 } else {
335 ab_append(ab, " ", 1);
336 len++;
337 }
338 }
339 ab_append(ab, "\x1b[m", 3);
340 ab_append(ab, "\r\n", 2);
341}
342
343void editor_draw_message_bar(struct abuf *ab) {
344 ab_append(ab, "\x1b[K", 3);
345 int msglen = strlen(E.statusmsg);
346 if (msglen > E.screencols) msglen = E.screencols;
347 if (msglen && time(NULL) - E.statusmsg_time < 5)
348 ab_append(ab, E.statusmsg, msglen);
349}
350
351void editor_refresh_screen() {
352 editor_scroll();
353
354 struct abuf ab = ABUF_INIT;
355
356 ab_append(&ab, "\x1b[?25l", 6);
357 ab_append(&ab, "\x1b[H", 3); // https://vt100.net/docs/vt100-ug/chapter3.html#CUP
358
359 editor_draw_rows(&ab);
360 editor_draw_status_bar(&ab);
361 editor_draw_message_bar(&ab);
362
363 char buf[32];
364 snprintf(buf, sizeof(buf), "\x1b[%d;%dH", (E.cy - E.rowoff) + 1, (E.rx - E.coloff) + 1);
365 ab_append(&ab, buf, strlen(buf));
366
367 ab_append(&ab, "\x1b[?25h", 6);
368
369 write(STDOUT_FILENO, ab.b, ab.len);
370 ab_free(&ab);
371}
372
373void editor_set_status_message(const char *fmt, ...) {
374 va_list ap;
375 va_start(ap, fmt);
376 vsnprintf(E.statusmsg, sizeof(E.statusmsg), fmt, ap);
377 va_end(ap);
378 E.statusmsg_time = time(NULL);
379}
380
381/*** input ***/
382
383void editor_move_cursor(int key) {
384 erow *row = (E.cy >= E.numrows) ? NULL : &E.row[E.cy];
385
386 switch(key) {
387 case ARROW_LEFT:
388 if (E.cx != 0) {
389 E.cx--;
390 } else if (E.cy > 0) {
391 E.cy--;
392 E.cx = E.row[E.cy].size;
393 }
394 break;
395 case ARROW_RIGHT:
396 if (row && E.cx < row->size) {
397 E.cx++;
398 } else if (row && E.cx == row->size) {
399 E.cy++;
400 E.cx = 0;
401 }
402 break;
403 case ARROW_UP:
404 if (E.cy != 0) {
405 E.cy--;
406 }
407 break;
408 case ARROW_DOWN:
409 if (E.cy < E.numrows) {
410 E.cy++;
411 }
412 break;
413 }
414
415 row = (E.cy >= E.numrows) ? NULL : &E.row[E.cy];
416 int rowlen = row ? row->size : 0;
417 if (E.cx > rowlen) {
418 E.cx = rowlen;
419 }
420}
421
422void editor_process_keypress() {
423 int c = editor_read_key();
424
425 switch(c) {
426 case CTRL_KEY('q'):
427 write(STDOUT_FILENO, "\x1b[2J", 4);
428 write(STDOUT_FILENO, "\x1b[H", 3);
429 exit(0);
430 break;
431
432 case HOME_KEY:
433 E.cx = 0;
434 break;
435
436 case END_KEY:
437 if (E.cy < E.numrows)
438 E.cx = E.row[E.cy].size;
439 break;
440
441 case PAGE_UP:
442 case PAGE_DOWN:
443 {
444 if (c == PAGE_UP) {
445 E.cy = E.rowoff;
446 } else if (c == PAGE_DOWN) {
447 E.cy = E.rowoff + E.screenrows - 1;
448 if (E.cy > E.numrows) E.cy = E.numrows;
449 }
450
451 int times = E.screenrows;
452 while (times--)
453 editor_move_cursor(c == PAGE_UP ? ARROW_UP : ARROW_DOWN);
454 }
455 break;
456
457 case ARROW_UP:
458 case ARROW_DOWN:
459 case ARROW_LEFT:
460 case ARROW_RIGHT:
461 editor_move_cursor(c);
462 break;
463
464 default:
465 if (iscntrl(c))
466 printf("%d\r\n", c);
467 else
468 printf("%d ('%c')\r\n", c, c);
469 break;
470 }
471}
472
473/*** init ***/
474
475void init_editor() {
476 E.cx = 0;
477 E.cy = 0;
478 E.rx = 0;
479 E.rowoff = 0;
480 E.coloff = 0;
481 E.numrows = 0;
482 E.row = NULL;
483 E.filename = NULL;
484 E.statusmsg[0] = '\0';
485 E.statusmsg_time = 0;
486
487 if (get_window_size(&E.screenrows, &E.screencols) == -1)
488 die("get_window_size");
489 E.screenrows -= 2;
490}
491
492int main(int argc, char *argv[]) {
493 enable_raw_mode();
494 init_editor();
495 if (argc >= 2) {
496 editor_open(argv[1]);
497 }
498
499 editor_set_status_message("HELP: Ctrl-Q = quit");
500
501 while (1) {
502 editor_refresh_screen();
503 editor_process_keypress();
504 }
505
506 return 0;
507}