Skip to content

Instantly share code, notes, and snippets.

@jdmichaud
Last active April 10, 2017 20:05
Show Gist options
  • Save jdmichaud/ed30c5aa70542ca358b4a485b24ba745 to your computer and use it in GitHub Desktop.
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/)
// 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