Last active
April 10, 2017 20:05
-
-
Save jdmichaud/ed30c5aa70542ca358b4a485b24ba745 to your computer and use it in GitHub Desktop.
A simple terminal based text editor (inspired by http://viewsourcecode.org/snaptoken/kilo/)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Constant compilation: | |
// echo editor.c | entr bash -c "echo "-----------------" && cc -ggdb3 editor.c -o /tmp/editor && echo OK" | |
#include <ctype.h> | |
#include <stdio.h> | |
#include <stdlib.h> | |
#include <termios.h> | |
#include <unistd.h> | |
#include <errno.h> | |
#include <string.h> | |
#include <sys/ioctl.h> | |
#include <sys/param.h> | |
#define BUF_LEN 16384 | |
// Return the code of CTRL+k | |
#define CTRL_KEY(k) ((k) & 0x1f) | |
#define ESCAPE '\x1b' | |
enum editorKey { | |
ARROW_LEFT = 1000, | |
ARROW_RIGHT, | |
ARROW_UP, | |
ARROW_DOWN, | |
PAGE_UP, | |
PAGE_DOWN, | |
HOME_KEY, | |
END_KEY, | |
DELETE_KEY | |
}; | |
// Escape sequences | |
#define CLEAR_SCR "\x1b[2J" | |
#define CLEAR_LINE_REMAINING "\x1b[0K" | |
#define MOVE_CRS "\x1b[%i;%iH" | |
// Move cursor to top left | |
#define MOVE_CRS_TL "\x1b[H" | |
#define HIDE_CRS "\x1b[?25l" | |
#define SHOW_CRS "\x1b[?25h" | |
typedef struct row { | |
char *chars; | |
int len; | |
} row_t; | |
typedef struct editorModel { | |
struct termios orig_termios; | |
int screenrows; | |
int screencols; | |
int cx, cy; // Cursor position | |
row_t *rows; // line of text | |
int nrows; | |
} editorModel_t; | |
typedef struct screenBuffer { | |
char *b; | |
int len; | |
} screenBuffer_t; | |
// Function declaration | |
void refreshScreen(); | |
void moveCursor(); | |
int getWindowSize(int *rows, int *cols); | |
editorModel_t model = { 0, 0, 0, 0, 0, 0, 0 }; | |
screenBuffer_t sBuffer = { NULL, 0 }; | |
void die(const char *s) { | |
// Clear the screen BEFORE writing the error message... | |
write(STDOUT_FILENO, CLEAR_SCR, 4); | |
write(STDOUT_FILENO, MOVE_CRS_TL, 3); | |
perror(s); | |
exit(1); | |
} | |
void disableRawMode() { | |
if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &model.orig_termios) == -1) | |
die("tcsetattr"); | |
} | |
void enableRawMode() { | |
struct termios raw; | |
if (tcgetattr(STDIN_FILENO, &model.orig_termios) == -1) | |
die("tcgetattr"); | |
raw = model.orig_termios; | |
// Turn off: | |
// ECHO: echo mode (don't print letter) | |
// ICANNON: canonical mode (don't wait for enter) | |
// ISIG: Ctrl-C /Ctrl-Z - send key combination to the application | |
// IEXTEN: Ctrl-V - send key combination to the application | |
raw.c_lflag &= ~(ECHO | ICANON | ISIG | IEXTEN); | |
// Tirn off: | |
// IXON: Ctrl-S /Ctrl-Q - send key combination to the application | |
// ICRNL: Ctrl-M - send key combination to the application | |
raw.c_iflag &= ~(IXON | ICRNL); | |
// Turn off: | |
// OPOST: Don't convert \n to \r\n on the terminal output | |
raw.c_oflag &= ~(OPOST); | |
// Return read as soon as an input is read (no buffering) | |
raw.c_cc[VMIN] = 0; | |
// Set read timeout to 100ms | |
raw.c_cc[VTIME] = 1; | |
if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw) == -1) | |
die("tcsetattr"); | |
atexit(disableRawMode); | |
} | |
// void enableRawMode() { | |
// struct termios termios; | |
// tcgetattr(STDIN_FILENO, &model.orig_termios); | |
// termios = model.orig_termios; | |
// // Turn off: | |
// // ECHO: echo mode (don't print letter) | |
// // ICANNON: canonical mode (don't wait for enter) | |
// // ISIG: Ctrl-C /Ctrl-Z - send key combination to the application | |
// // IEXTEN: Ctrl-V - send key combination to the application | |
// termios.c_lflag &= ~(ECHO | ICANON | ISIG | IEXTEN); | |
// // Tirn off: | |
// // IXON: Ctrl-S /Ctrl-Q - send key combination to the application | |
// // ICRNL: Ctrl-M - send key combination to the application | |
// termios.c_iflag &= ~(IXON | ICRNL); | |
// tcsetattr(STDIN_FILENO, TCSAFLUSH, &termios); | |
// atexit(disableRawMode); | |
// } | |
void init() { | |
enableRawMode(); | |
if (getWindowSize(&model.screenrows, &model.screencols) == -1) | |
die("getWindowSize"); | |
sBuffer.len = 0; | |
if ((sBuffer.b = (char *) malloc(model.screenrows * model.screencols | |
* sizeof (char))) == NULL) | |
die("init"); | |
} | |
int readKey() { | |
char c; | |
int nread; | |
while (nread = read(STDIN_FILENO, &c, 1) != 1) { | |
// read timeout with 0 | |
// Except in cygwin, read timeout with return value -1 and errno EAGAIN | |
if (nread == -1 && errno != EAGAIN) | |
die("read"); | |
} | |
if (c == ESCAPE) { | |
if (read(STDIN_FILENO, &c, 1) != 1) return ESCAPE; | |
if (read(STDIN_FILENO, &c, 1) != 1) return ESCAPE; | |
if (c >= '0' && c <= '9') { | |
char d; | |
if (read(STDIN_FILENO, &d, 1) != 1) return ESCAPE; | |
if (d == '~') { | |
switch (c) { | |
case '3': return DELETE_KEY; | |
case '1': | |
case '7': return HOME_KEY; | |
case '4': | |
case '8': return END_KEY; | |
case '6': return PAGE_DOWN; | |
case '5': return PAGE_UP; | |
} | |
} | |
} else { | |
switch (c) { | |
case 'A': return ARROW_UP; | |
case 'B': return ARROW_DOWN; | |
case 'C': return ARROW_RIGHT; | |
case 'D': return ARROW_LEFT; | |
} | |
} | |
} | |
return c; | |
} | |
int write2screen_l(screenBuffer_t *buffer, char *s, int string_len) { | |
memcpy(&buffer->b[buffer->len], s, string_len); | |
buffer->len += string_len; | |
return string_len; | |
} | |
int write2screen(screenBuffer_t *buffer, char *s) { | |
int remaining_len = strnlen(s, model.screenrows * model.screencols - buffer->len); | |
int string_len = strlen(s); | |
if (string_len > remaining_len) die("write2screen: buffer overflow"); | |
return write2screen_l(buffer, s, string_len); | |
} | |
int blitScreen(screenBuffer_t *buffer) { | |
write(STDOUT_FILENO, buffer->b, buffer->len); | |
buffer->len = 0; | |
} | |
int getWindowSize(int *rows, int *cols) { | |
struct winsize ws; | |
if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == -1 || ws.ws_col == 0) { | |
return -1; | |
} else { | |
*cols = ws.ws_col; | |
*rows = ws.ws_row; | |
return 0; | |
} | |
} | |
void processKeypress() { | |
int c = readKey(); | |
switch (c) { | |
case CTRL_KEY('q'): | |
refreshScreen(); | |
moveCursor(1, 1); | |
exit(0); | |
break; | |
case ARROW_UP: | |
if (model.cy > 0) --model.cy; | |
break; | |
case ARROW_DOWN: | |
if (model.cy < model.screenrows - 1) ++model.cy; | |
break; | |
case ARROW_LEFT: | |
if (model.cx > 0) --model.cx; | |
break; | |
case ARROW_RIGHT: | |
if (model.cx < model.screencols - 1) ++model.cx; | |
break; | |
case PAGE_DOWN: | |
case PAGE_UP: | |
case HOME_KEY: | |
case END_KEY: | |
break; | |
} | |
} | |
void moveCursor(int row, int col) { | |
char cseq[10]; | |
snprintf(cseq, 10, MOVE_CRS, row, col); | |
if (write2screen(&sBuffer, cseq) != strlen(cseq)) | |
die("moveCursor"); | |
} | |
void drawRows() { | |
int y; | |
int lnsize = 3; // For now, no more than 999 lines | |
char ln[10]; // No more than 9.999.999.999 lines | |
for (int index = 0; index < MIN(model.nrows, model.screencols); index++) { | |
// snprintf(ln, 10, "%*s%i\n", 3, "", index); | |
// fprintf(stdout, "%s\r\n", ln); | |
// char x[10]; | |
// sprintf(x, "%i", model.rows[index].len); | |
// die(x); | |
write2screen_l(&sBuffer, model.rows[index].chars, | |
MIN(model.rows[index].len, model.screencols)); | |
write2screen(&sBuffer, CLEAR_LINE_REMAINING); | |
if (index < model.screenrows - 1) | |
write2screen(&sBuffer, "\r\n"); | |
} | |
} | |
void refreshScreen() { | |
write2screen(&sBuffer, HIDE_CRS); | |
moveCursor(2, 1); | |
drawRows(); | |
moveCursor(model.cy + 1, model.cx + 1); | |
write2screen(&sBuffer, SHOW_CRS); | |
// Blit the buffer to the screen | |
blitScreen(&sBuffer); | |
} | |
int countLines(FILE *fp) { | |
char buffer[BUF_LEN]; | |
int lcount = 0; | |
ssize_t rcount = 0; | |
rcount = fread(buffer, 1, BUF_LEN, fp); | |
while (rcount-- > 0) { | |
if (buffer[rcount] == '\n') lcount++; | |
if (rcount == 0) { | |
rcount = fread(buffer, 1, BUF_LEN, fp); | |
} | |
} | |
// Be kind, rewind | |
fseek(fp, 0, SEEK_SET); | |
return lcount + 1; | |
} | |
void openFile(char *filename) { | |
FILE *fp = fopen(filename, "r"); | |
if (!fp) die("openFile"); | |
// Count the lines | |
model.nrows = countLines(fp); | |
model.rows = (row_t *) malloc(sizeof (row_t) * model.nrows); | |
char *line; | |
size_t linelen; | |
int counter = 0; | |
while (getline(&line, &linelen, fp) != -1) { | |
model.rows[counter].chars = (char *) malloc(sizeof (char) * linelen); | |
memcpy(model.rows[counter].chars, line, linelen); | |
model.rows[counter++].len = linelen; | |
} | |
} | |
int main(int argc, char **argv) { | |
init(); | |
if (argc > 1) | |
openFile(argv[1]); | |
while (1) { | |
refreshScreen(); | |
processKeypress(); | |
} | |
return 0; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment