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}