The editor runs a single process, which runs in a loop, alternately refreshing the screen and processing user input, blocking until the input is available.
editorRefreshScreen();
editorProcessKeypress();
The main function puts the terminal into raw mode, reads the command line arguments, sets up the editor, and then enters the main loop.
enableRawMode();
initEditor();
if (argc >= 2) {
if (argc >= 4) {
editor.log = fileLog;
editor.logFile = fopen(argv[3], "w");
} else {
editor.log = noLog;
}
editorOpen(
argv[1],
&activePane(&editor.display)->file->filename,
activePane(&editor.display)->file->buffer,
&activePane(&editor.display)->file->undo,
&activePane(&editor.display)->file->unsavedChanges,
&activePane(&editor.display)->file->numberOfRows,
activePane(&editor.display)->file->cursorX,
activePane(&editor.display)->file->cursorY
);
splitBelow(&editor.display);
}
editorSetStatusMessage("Ctrl-q to quit, Ctrl-s to save");
while (1) {
<<main-loop>>
}
if (argc >= 4) {
fclose(editor.logFile);
}
return 0;
/*** includes ***/
#define _DEFAULT_SOURCE
#define _GNU_SOURCE
#define _BSD_SOURCE
#include <ctype.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <termios.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <string.h>
#include <sys/types.h>
#include <time.h>
#include <stdarg.h>
#include <fcntl.h>
#include "display.h"
#include "edit.h"
#include "editorRow.h"
#include "fileData.h"
#include "pane.h"
#include "undo.h"
#include "zipperBuffer.h"
#include "lists/DisplayRow.h"
/*** defines ***/
#define CTRL_KEY(k) ((k) & 0x1f)
#define KIBI_VERSION "0.0.1"
#define tabSize 4
enum EditorKey {
BACKSPACE = 127,
ARROW_LEFT = 1000,
ARROW_RIGHT,
ARROW_DOWN,
ARROW_UP,
PAGE_UP,
PAGE_DOWN,
HOME_KEY,
END_KEY,
DELETE_KEY
};
/*** data ***/
typedef struct EditorConfig {
Display display;
char statusMessage[80];
time_t statusMessageTime;
struct termios original_termios;
void (*log)(char *format, ...);
FILE *logFile;
} EditorConfig;
EditorConfig editor;
/*** prototypes ***/
void editorForwardLine();
void editorSetStatusMessage(const char *format, ...);
/*** logging ***/
void stderrLog(char *format, ...) {
va_list args;
va_start(args, format);
vprintf(format, args);
puts("\n");
va_end(args);
}
void fileLog(char *format, ...) {
va_list args;
va_start(args, format);
vfprintf(editor.logFile, format, args);
va_end(args);
fputs("\n", editor.logFile);
}
void noLog(char *format, ...) {
return;
}
/*** terminal ***/
void die(const char *s) {
write(STDOUT_FILENO, "\x1b[2J", 4);
write(STDOUT_FILENO, "\x1b[H", 3);
perror(s);
exit(1);
}
<<disableRawMode>>
<<enableRawMode>>
int editorReadKey() {
int nread;
char c;
while ((nread = read(STDIN_FILENO, &c, 1)) != 1) {
if (nread == -1 && errno != EAGAIN) die("Error while reading input");
}
if (c == '\x1b') {
char seq[3];
if (read(STDIN_FILENO, &seq[0], 1) != 1) return '\x1b';
if (read(STDIN_FILENO, &seq[1], 1) != 1) return '\x1b';
if (seq[0] == '[' || seq[0] == 'O') {
if (seq[1] >= '0' && seq[1] <= '9') {
if (read(STDIN_FILENO, &seq[2], 1) != 1) return '\x1b';
if (seq[2] == '~') {
switch (seq[1]) {
case '1': return HOME_KEY;
case '3': return DELETE_KEY;
case '4': return END_KEY;
case '5': return PAGE_UP;
case '6': return PAGE_DOWN;
case '7': return HOME_KEY;
case '8': return END_KEY;
}
}
} else {
switch (seq[1]) {
case 'A': return ARROW_UP;
case 'B': return ARROW_DOWN;
case 'C': return ARROW_RIGHT;
case 'D': return ARROW_LEFT;
case 'F': return END_KEY;
case 'H': return HOME_KEY;
}
}
}
return '\x1b';
} else {
return c;
}
}
int getCursorPosition(int *rows, int *columns) {
char buf[32];
unsigned int i = 0;
if (write(STDOUT_FILENO, "\x1b[6n", 4) != 4) return -1;
while (i < sizeof(buf) - 1) {
if (read(STDIN_FILENO, &buf[i], 1) != 1) break;
if (buf[i] == 'R') break;
i++;
}
buf[i] = '\0';
if (buf[0] != '\x1b' || buf[1] != '[') return -1;
if (sscanf(&buf[2], "%d;%d", rows, columns) != 2) return -1;
return 0;
}
int getWindowSize(int *rows, int *cols) {
struct winsize ws;
if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == -1 || ws.ws_col == 0) {
if (write(STDOUT_FILENO, "\x1b[999C\x1b[999B", 12) != 12) return -1;
return getCursorPosition(rows, cols);
} else {
*cols = ws.ws_col;
*rows = ws.ws_row;
return 0;
}
}
/*** undo ***/
void editorUndoSteps(UndoStack *undo) {
int n = 0;
while (undo != NULL) {
n++;
undo = undo->tail;
}
editorSetStatusMessage("%d undo steps.", n);
}
/*** row operations ***/
void editorInsertRow(
char *s,
size_t length,
bool pushUndo,
ZipperBuffer *buffer,
int *numberOfRows,
int *unsavedChanges,
UndoStack **undo,
int cursorX,
int cursorY
) {
if (pushUndo) {
editorPushUndo(buffer, undo, cursorX, cursorY);
}
zipperInsertRow(buffer, newRow(s, length, tabSize));
*numberOfRows = *numberOfRows + 1;
*unsavedChanges = *unsavedChanges + 1;
}
void editorInsertRowAfter(
char *s,
size_t length,
bool pushUndo,
ZipperBuffer *buffer,
int *numberOfRows,
int *unsavedChanges,
UndoStack **undo,
int cursorX,
int *cursorY
) {
if (pushUndo) {
editorPushUndo(buffer, undo, cursorX, *cursorY);
}
editorForwardLine();
editorInsertRow(s, length, false, buffer, numberOfRows, unsavedChanges, undo, cursorX, *cursorY);
if (*cursorY < activeHeight(&editor.display) - 1) {
*cursorY = *cursorY + 1;
}
}
void editorAppendRow(
char *s,
size_t length,
bool pushUndo,
ZipperBuffer *buffer,
int *numberOfRows,
int *unsavedChanges,
UndoStack **undo,
int cursorX,
int cursorY
) {
int i = 0;
while (buffer->forwards != NULL) {
zipperForwardRow(buffer);
i++;
}
editorInsertRow(s, length, pushUndo, buffer, numberOfRows, unsavedChanges, undo, cursorX, cursorY);
while (i > 0) {
zipperBackwardRow(buffer);
i--;
}
}
void editorDeleteBetween(int startRow, int startColumn, int endRow, int endColumn) {
}
void editorDeleteCurrentRow(
ZipperBuffer *buffer,
UndoStack **undo,
int *numberOfRows,
int *unsavedChanges,
int cursorX,
int cursorY
) {
if (buffer->forwards == NULL) return;
editorPushUndo(buffer, undo, cursorX, cursorY);
buffer->forwards = buffer->forwards->tail;
numberOfRows--;
unsavedChanges++;
}
void editorDeleteRow(
ZipperBuffer *buffer,
UndoStack **undo,
int at,
int *numberOfRows,
int *unsavedChanges,
int cursorX,
int cursorY
) {
if (at < 0 || at >= *numberOfRows) {
return;
}
int moves = 0;
while (buffer->backwards != NULL) {
zipperBackwardRow(buffer);
moves--;
}
moves += at;
while (at > 0) {
zipperForwardRow(buffer);
at--;
}
editorDeleteCurrentRow(buffer, undo, numberOfRows, unsavedChanges, cursorX, cursorY);
while (moves < -1) {
zipperForwardRow(buffer);
moves++;
}
while (moves > 0) {
zipperBackwardRow(buffer);
}
}
EditorRow *editorRowInsertChar(EditorRow *row, int at, int c) {
if (at < 0 || at > row->size) at = row->size;
char *newChars = malloc(row->size + 2);
memcpy(newChars, row->chars, at);
memcpy(&newChars[at + 1], &row->chars[at], row->size - at);
newChars[at] = c;
newChars[row->size + 1] = '\0';
return newRow(newChars, row->size + 1, tabSize);
}
EditorRow *editorRowAppendString(EditorRow *row, char *s, size_t length) {
char *newChars = malloc(row->size + length + 1);
memcpy(newChars, row->chars, row->size);
memcpy(&newChars[row->size], s, length);
newChars[row->size + length] = '\0';
return newRow(newChars, row->size + length, tabSize);
}
EditorRow *editorRowDeleteChar(EditorRow *row, int at) {
if (at < 0 || at >= row->size) return row;
char *newChars = malloc(row->size);
memcpy(newChars, row->chars, at);
memcpy(&newChars[at], &row->chars[at + 1], row->size - at);
newChars[row->size - 1] = '\0';
return newRow(newChars, row->size - 1, tabSize);
}
/**
* Create a new row with the first n characters of row.
*/
EditorRow *editorRowTake(EditorRow *row, unsigned int n) {
char *newChars = malloc(n + 1);
memcpy(newChars, row->chars, n);
newChars[n] = '\0';
return newRow(newChars, n, tabSize);
}
/**
* Create a new row with all characters of row after the first n.
*/
EditorRow *editorRowDrop(EditorRow *row, unsigned int n) {
char *newChars = malloc(row->size - n + 1);
memcpy(newChars, &row->chars[n], row->size - n);
newChars[row->size - n] = '\0';
return newRow(newChars, row->size - n, tabSize);
}
/**
* Split a row at an index, return a RowList of the two new rows.
*/
RowList *editorRowSplit(EditorRow *row, unsigned int at) {
EditorRow *first = editorRowTake(row, at);
EditorRow *second = editorRowDrop(row, at);
return rowListCons(first, rowListCons(second, NULL));
}
EditorRow *editorCurrentRow(ZipperBuffer *buffer) {
return buffer->forwards ? buffer->forwards->head : NULL;
}
EditorRow *editorPreviousRow(ZipperBuffer *buffer) {
return buffer->backwards ? buffer->backwards->head : NULL;
}
/*** editor operations ***/
void editorForwardLine(ZipperBuffer *buffer, int *cursorY) {
if (editorCurrentRow(buffer) != NULL) {
*cursorY += 1;
zipperForwardRow(buffer);
}
}
void editorBackwardLine(ZipperBuffer *buffer, int *cursorY) {
if (editorPreviousRow(buffer) != NULL) {
*cursorY -= 1;
zipperBackwardRow(buffer);
}
}
/**
* Replace the current row with a new one.
*/
void editorReplaceRow(
ZipperBuffer *buffer,
UndoStack **undo,
int cursorX,
int cursorY,
int *unsavedChanges,
EditorRow *row
) {
if (row == NULL) return;
editorPushUndo(buffer, undo, cursorX, cursorY);
RowList *old = buffer->forwards;
if (old == NULL) {
buffer->forwards = rowListCons(row, NULL);
} else {
buffer->forwards = rowListCons(row, old->tail);
}
*unsavedChanges = *unsavedChanges + 1;
}
void editorInsertChar(
int c,
ZipperBuffer *buffer,
UndoStack **undo,
int *numberOfRows,
int *unsavedChanges,
int *cursorX,
int cursorY
) {
EditorRow *row = editorCurrentRow(buffer);
if (row == NULL) {
editorInsertRow("", 0, true, buffer, numberOfRows, unsavedChanges, undo, *cursorX, cursorY);
row = editorCurrentRow(buffer);
}
EditorRow *new = editorRowInsertChar(row, *cursorX, c);
editorReplaceRow(buffer, undo, *cursorX, cursorY, unsavedChanges, new);
*cursorX = *cursorX + 1;
}
void editorInsertRows(ZipperBuffer *buffer, UndoStack **undo, int cursorX, int cursorY, RowList *new, int *unsavedChanges) {
if (new == NULL) return;
editorPushUndo(buffer, undo, cursorX, cursorY);
RowList *end = new;
int added = 1;
while (end->tail != NULL) {
end = end->tail;
added++;
}
end->tail = buffer->forwards;
buffer->forwards = new;
*unsavedChanges += added;
}
void editorInsertNewline(
ZipperBuffer *buffer,
UndoStack **undo,
int *cursorX,
int *cursorY,
int *numberOfRows,
int *unsavedChanges
) {
EditorRow *row = editorCurrentRow(buffer);
if (*cursorX == 0 || row == NULL) {
editorInsertRowAfter("", 0, true, buffer, numberOfRows, unsavedChanges, undo, *cursorX, cursorY);
} else {
RowList *new = editorRowSplit(row, *cursorX);
editorDeleteCurrentRow(buffer, undo, numberOfRows, unsavedChanges, *cursorX, *cursorY);
editorInsertRows(buffer, undo, *cursorX, *cursorY, new, unsavedChanges);
editorForwardLine(buffer, cursorY);
*cursorX = 0;
}
}
void editorDeleteChar(
ZipperBuffer *buffer,
UndoStack **undo,
int *cursorX,
int *cursorY,
int *unsavedChanges,
int *numberOfRows
) {
EditorRow *current = editorCurrentRow(buffer);
if (current == NULL) return;
EditorRow *previous = editorPreviousRow(buffer);
if (previous == NULL && *cursorX == 0) return;
if (*cursorX > 0) {
EditorRow *new = editorRowDeleteChar(current, *cursorX - 1);
editorReplaceRow(buffer, undo, *cursorX, *cursorY, unsavedChanges, new);
*cursorX -= 1;
} else {
*cursorX = previous->size;
EditorRow *new = editorRowAppendString(previous,
current->chars,
current->size);
editorDeleteCurrentRow(buffer, undo, numberOfRows, unsavedChanges, *cursorX, *cursorY);
editorBackwardLine(buffer, cursorY);
editorReplaceRow(buffer, undo, *cursorX, *cursorY, unsavedChanges, new);
}
}
void editorJumpToEnd(
ZipperBuffer *buffer,
int *cursorY
) {
while (editorCurrentRow(buffer) != NULL) {
editorForwardLine(buffer, cursorY);
}
}
void editorJumpToStart(
ZipperBuffer *buffer,
int *cursorY
) {
while (editorPreviousRow(buffer) != NULL) {
editorBackwardLine(buffer, cursorY);
}
}
/*** file i/o ***/
char *editorRowsToString(ZipperBuffer *editorBuffer, int *bufferLength) {
int rowsToEnd = 0;
while (editorBuffer->forwards != NULL) {
zipperForwardRow(editorBuffer);
rowsToEnd++;
}
int totalLength = 0;
while (editorBuffer->backwards != NULL) {
totalLength += editorBuffer->backwards->head->size + 1;
zipperBackwardRow(editorBuffer);
}
*bufferLength = totalLength;
char *buffer = malloc(totalLength);
char *p = buffer;
while (editorBuffer->forwards != NULL) {
memcpy(p, editorBuffer->forwards->head->chars,
editorBuffer->forwards->head->size);
p += editorBuffer->forwards->head->size;
*p = '\n';
p++;
zipperForwardRow(editorBuffer);
}
while (rowsToEnd > 0) {
zipperBackwardRow(editorBuffer);
rowsToEnd--;
}
return buffer;
}
void editorOpen(
char *filename,
char **editorFilename,
ZipperBuffer *buffer,
UndoStack **undo,
int *unsavedChanges,
int *numberOfRows,
int cursorX,
int cursorY
) {
free(*editorFilename);
*editorFilename = strdup(filename);
FILE *fp = fopen(filename, "r");
if (!fp) die("Couldn't open file");
char *line = NULL;
size_t linecap = 0;
ssize_t lineLength;
while ((lineLength = getline(&line, &linecap, fp)) != -1) {
while (lineLength > 0 &&
(line[lineLength - 1] == '\n' || line[lineLength - 1] == '\r')) {
lineLength--;
}
char *rowChars = malloc(lineLength + 1);
memcpy(rowChars, line, lineLength);
rowChars[lineLength] = '\0';
editorInsertRow(rowChars, lineLength, false, buffer, numberOfRows, unsavedChanges, undo, cursorX, cursorY);
}
buffer->forwards = rowListReverse(buffer->forwards);
free(line);
fclose(fp);
*unsavedChanges = 0;
}
void editorSave(ZipperBuffer *editorBuffer, char *filename, int *unsavedChanges) {
if (filename == NULL) return;
int length;
char *buffer = editorRowsToString(editorBuffer, &length);
int fileDescriptor = open(filename, O_RDWR | O_CREAT, 0644);
if (fileDescriptor != -1) {
if (ftruncate(fileDescriptor, length) != -1) {
if (write(fileDescriptor, buffer, length) == length) {
close(fileDescriptor);
free(buffer);
editorSetStatusMessage("%d bytes written to disk", length);
*unsavedChanges = 0;
return;
}
}
close(fileDescriptor);
}
free(buffer);
editorSetStatusMessage("Can't save! I/O error: %s", strerror(errno));
}
/*** append buffer ***/
struct abuf {
char *b;
int len;
};
#define ABUF_INIT {NULL, 0}
void abAppend(struct abuf *ab, const char *s, int len) {
char *new = realloc(ab->b, ab->len + len);
if (new == NULL) {
return;
}
memcpy(&new[ab->len], s, len);
ab->b = new;
ab->len += len;
}
void abFree(struct abuf *ab) {
free(ab->b);
}
/*** output ***/
/**
* Split the current pane in two, with the new (non-focused) split below the
* current one.
*/
void splitBelow(Display *display) {
int upperHeight = display->height / 2;
int lowerHeight = display->height - upperHeight;
int x = display->panes->active->active->cursorX;
int y = display->panes->active->active->cursorY;
int top = display->panes->active->active->top;
int left = display->panes->active->active->left;
FileData *file = display->panes->active->active->file;
Pane *newPane = makePane(x, y, top, left, file);
DisplayRow *newRow = makeDisplayRow(NULL, newPane, NULL);
display->panes->down = ListF(DisplayRow).cons(newRow, display->panes->down);
}
void editorScroll(Pane *pane) {
pane->cursorX = 0;
EditorRow *current = editorCurrentRow(pane->file->buffer);
if (current != NULL) {
pane->cursorX = editorCursorToRender(current, pane->file->cursorX, tabSize);
}
if (pane->cursorX < pane->left) {
pane->left = pane->cursorX;
}
if (pane->cursorX >= pane->left + activeWidth(&editor.display)) {
pane->left = pane->cursorX - activeWidth(&editor.display) + 1;
}
pane->cursorY = pane->file->cursorY - pane->top;
if (pane->cursorY < 0) {
pane->top += pane->cursorY;
}
if (pane->cursorY >= activeHeight(&editor.display)) {
pane->top = pane->file->cursorY - activeHeight(&editor.display) + 1;
}
pane->cursorY = pane->file->cursorY - pane->top;
}
void editorDrawString(struct abuf *ab, char *s, int length) {
abAppend(ab, s, length);
}
void editorDrawBlanks(struct abuf *ab, int n) {
for (; n > 0; n--) {
abAppend(ab, " ", 1);
}
}
void editorDrawNewline(struct abuf *ab) {
abAppend(ab, "\r\n", 2);
}
void editorDrawLine(struct abuf *ab, char *s, int length) {
editorDrawString(ab, s, length);
editorDrawNewline(ab);
}
void editorDrawEmpties(struct abuf *ab, int numberOfLines) {
editorDrawLine(ab, "~", 1);
if (numberOfLines > 1) {
editorDrawEmpties(ab, numberOfLines - 1);
}
}
void editorDrawWelcome(struct abuf *ab) {
editorDrawEmpties(ab, editor.display.height / 3 - 1);
char welcome[80];
int welcomeLength = snprintf(
welcome,
sizeof(welcome),
"Kibi editor - version %s",
KIBI_VERSION
);
if (welcomeLength > editor.display.width) {
welcomeLength = editor.display.width;
}
int padding = (editor.display.width - welcomeLength) / 2;
if (padding) {
abAppend(ab, "~", 1);
padding--;
}
while (padding--) abAppend(ab, " ", 1);
abAppend(ab, welcome, welcomeLength);
}
void editorDrawRows(struct abuf *ab) {
if (activePane(&editor.display)->file->numberOfRows == 0) {
editorDrawWelcome(ab);
} else {
List(List(List(PaneRow))) *paneRows =
drawDisplayColumn(editor.display.panes, editor.display.height, editor.display.width);
int linesDrawn = 0;
List(List(List(PaneRow))) *rows = paneRows;
// for each column
while (rows != NULL && linesDrawn < editor.display.height) {
List(List(PaneRow)) *panes = rows->head;
// for each row in the column
while (panes->head != NULL && linesDrawn < editor.display.height) {
int charactersDrawn = 0;
List(List(PaneRow)) *panes2 = panes;
// for each pane in the row, print the current line
while (panes2 != NULL) {
List(PaneRow) *pane = panes2->head;
int proposedWidth = pane->head->width + pane->head->blanks;
int widthAvailable = editor.display.width - charactersDrawn;
int rowWidth = pane->head->width > widthAvailable ? widthAvailable : pane->head->width;
int totalWidth =
proposedWidth > widthAvailable ? widthAvailable : proposedWidth;
editorDrawString(ab, pane->head->row, rowWidth);
if (rowWidth < totalWidth) {
editorDrawBlanks(ab, totalWidth - rowWidth);
}
charactersDrawn += totalWidth;
// move pane pointer to next row
List(PaneRow) *current = panes2->head;
panes2->head = panes2->head->tail;
// that row (cons cell) is no longer needed
free(current);
// move to next pane
panes2 = panes2->tail;
}
editorDrawNewline(ab);
linesDrawn++;
}
// we've done all the panes in this row
ListF(List(PaneRow)).free(panes);
List(List(List(PaneRow))) *finishedRow = rows;
rows = rows->tail;
free(finishedRow);
}
if (linesDrawn < editor.display.height) {
editorDrawEmpties(ab, editor.display.height - linesDrawn);
}
}
}
void editorDrawMessageBar(struct abuf *ab) {
abAppend(ab, "\x1b[K", 3);
int messageLength = strlen(editor.statusMessage);
if (messageLength > editor.display.width) messageLength = editor.display.width;
if (messageLength && time(NULL) - editor.statusMessageTime < 5) {
abAppend(ab, editor.statusMessage, messageLength);
}
}
void editorUpdateWindowSize() {
if (getWindowSize(&editor.display.height, &editor.display.width) == -1)
die("Failed to get window size");
editor.display.height -= 1;
}
void editorRefreshScreen() {
editorUpdateWindowSize();
editorScroll(activePane(&editor.display));
struct abuf ab = ABUF_INIT;
abAppend(&ab, "\x1b[?25l", 6);
abAppend(&ab, "\x1b[H", 3);
editorDrawRows(&ab);
editorDrawMessageBar(&ab);
char buf[32];
ScreenCursor c = activeCursor(&editor.display);
snprintf(buf, sizeof(buf), "\x1b[%d;%dH", c.y, c.x);
abAppend(&ab, buf, strlen(buf));
abAppend(&ab, "\x1b[?25h", 6);
write(STDOUT_FILENO, ab.b, ab.len);
abFree(&ab);
}
void editorSetStatusMessage(const char *format, ...) {
va_list ap;
va_start(ap, format);
vsnprintf(editor.statusMessage, sizeof(editor.statusMessage), format, ap);
va_end(ap);
editor.statusMessageTime = time(NULL);
}
/*** input ***/
void editorSwitchPane() {
// TODO
}
void editorMoveCursor(ZipperBuffer *buffer, int *cursorX, int *cursorY, int key) {
EditorRow *row = editorCurrentRow(buffer);
switch (key) {
case ARROW_DOWN:
case CTRL_KEY('n'): {
struct Navigation n = {.type = ToNext, .objectType = Line};
struct String s = navigationToString(n);
editor.log(s.s);
editorForwardLine(buffer, cursorY);
break;
}
case ARROW_UP:
case CTRL_KEY('p'): {
struct Navigation n = {.type = ToPrevious, .objectType = Line};
struct String s = navigationToString(n);
editor.log(s.s);
editorBackwardLine(buffer, cursorY);
break;
}
case ARROW_RIGHT:
case CTRL_KEY('f'): {
struct Navigation n = {.type = ToNext, .objectType = Character};
struct String s = navigationToString(n);
editor.log(s.s);
if (row && *cursorX < row->size) {
*cursorX += 1;
} else if (row && *cursorX == row->size) {
editorForwardLine(buffer, cursorY);
*cursorX = 0;
}
break;
}
case ARROW_LEFT:
case CTRL_KEY('b'): {
struct Navigation n = {.type = ToPrevious, .objectType = Line};
struct String s = navigationToString(n);
editor.log(s.s);
if (*cursorX > 0) {
*cursorX -= 1;
} else if (editorPreviousRow(buffer) != NULL) {
editorBackwardLine(buffer, cursorY);
*cursorX = buffer->forwards->head->size;
}
break;
}
}
row = editorCurrentRow(buffer);
int rowLength = row ? row->size : 0;
if (*cursorX > rowLength) {
*cursorX = rowLength;
}
}
void editorProcessKeypress() {
static int quitTimes = 1;
int c = editorReadKey();
FileData *fileData = activePane(&editor.display)->file;
switch (c) {
case '\r': {
struct Edit e = {.type = InsertText, .insert = {.text = "\n"}};
struct String s = editToString(e);
editor.log(s.s);
free(s.s);
editorInsertNewline(
fileData->buffer,
&fileData->undo,
&fileData->cursorX,
&fileData->cursorY,
&fileData->numberOfRows,
&fileData->unsavedChanges
);
break;
}
case CTRL_KEY('z'): {
onFailure(editorUndo(fileData), editorSetStatusMessage);
break;
}
case CTRL_KEY('y'):
onFailure(editorRedo(fileData), editorSetStatusMessage);
break;
case CTRL_KEY('x'):
editorUndoSteps(fileData->undo);
break;
case CTRL_KEY('q'):
if (fileData->unsavedChanges && quitTimes > 0) {
editorSetStatusMessage("There are unsaved changes. Press Ctrl-q again to quit.");
quitTimes = 0;
return;
}
write(STDOUT_FILENO, "\x1b[2J", 4);
write(STDOUT_FILENO, "\x1b[H", 3);
exit(0);
break;
case CTRL_KEY('s'):
editorSave(fileData->buffer, fileData->filename, &fileData->unsavedChanges);
break;
case HOME_KEY:
case CTRL_KEY('a'): {
struct Navigation n = {.type = ToStartOf, .objectType = Line};
struct String s = navigationToString(n);
editor.log(s.s);
fileData->cursorX = 0;
break;
}
case END_KEY:
case CTRL_KEY('e'): {
struct Navigation n = {.type = ToEndOf, .objectType = Line};
struct String s = navigationToString(n);
editor.log(s.s);
EditorRow *current = editorCurrentRow(fileData->buffer);
if (current != NULL) {
fileData->cursorX = current->size;
}
break;
}
case BACKSPACE:
case CTRL_KEY('h'): {
struct Edit e = {.type = DeleteText, .delete = {.object = {.type = Character}}};
struct String s = editToString(e);
editor.log(s.s);
free(s.s);
editorDeleteChar(
fileData->buffer,
&fileData->undo,
&fileData->cursorX,
&fileData->cursorY,
&fileData->unsavedChanges,
&fileData->numberOfRows
);
break;
}
case DELETE_KEY:
editorMoveCursor(fileData->buffer, &fileData->cursorX, &fileData->cursorY, ARROW_RIGHT);
editorDeleteChar(
fileData->buffer,
&fileData->undo,
&fileData->cursorX,
&fileData->cursorY,
&fileData->unsavedChanges,
&fileData->numberOfRows
);
break;
case PAGE_UP:
case PAGE_DOWN:
case CTRL_KEY('u'):
case CTRL_KEY('d'):
{
if (c == PAGE_UP || c == CTRL_KEY('u')) {
struct Navigation n = {.type = ToPrevious, .objectType = Page};
struct String s = navigationToString(n);
editor.log(s.s);
fileData->cursorY = activePane(&editor.display)->top;
} else {
struct Navigation n = {.type = ToNext, .objectType = Page};
struct String s = navigationToString(n);
editor.log(s.s);
fileData->cursorY = activePane(&editor.display)->top + activeHeight(&editor.display) - 1;
if (fileData->cursorY > fileData->numberOfRows) {
fileData->cursorY = fileData->numberOfRows;
}
}
int times = activeHeight(&editor.display);
while (times--) {
editorMoveCursor(
fileData->buffer,
&fileData->cursorX,
&fileData->cursorY,
(c == PAGE_UP || c == CTRL_KEY('u')) ? ARROW_UP : ARROW_DOWN
);
}
}
break;
case CTRL_KEY('g'): {
struct Navigation n = {.type = ToEndOf, .objectType = Buffer};
struct String s = navigationToString(n);
editor.log(s.s);
editorJumpToEnd(fileData->buffer, &fileData->cursorY);
break;
}
case ARROW_DOWN:
case ARROW_UP:
case ARROW_RIGHT:
case ARROW_LEFT:
case CTRL_KEY('n'):
case CTRL_KEY('p'):
case CTRL_KEY('f'):
case CTRL_KEY('b'):
editorMoveCursor(fileData->buffer, &fileData->cursorX, &fileData->cursorY, c);
break;
case '\x1b':
case CTRL_KEY('l'):
break;
case CTRL_KEY('w'):
editorSwitchPane();
break;
default: {
char *text = malloc(sizeof(char) * 2);
text[0] = c; text[1] = 0;
struct Edit e = {.type = InsertText, .insert = {.text = text}};
struct String s = editToString(e);
editor.log(s.s);
free(s.s);
free(text);
editorInsertChar(
c,
fileData->buffer,
&fileData->undo,
&fileData->numberOfRows,
&fileData->unsavedChanges,
&fileData->cursorX,
fileData->cursorY
);
}
}
quitTimes = 1;
}
/*** init ***/
void initEditor() {
ZipperBuffer *emptyBuffer = malloc(sizeof(ZipperBuffer));
emptyBuffer->forwards = NULL;
emptyBuffer->backwards = NULL;
emptyBuffer->newest = NULL;
FileData *emptyFile = fileData(0, 0, 0, emptyBuffer, NULL, 0, NULL, NULL);
Pane *pane = makePane(0, 0, 0, 0, emptyFile);
DisplayRow *row = makeDisplayRow(NULL, pane, NULL);
DisplayColumn *column = makeDisplayColumn(NULL, row, NULL);
editor.display = (Display){column, 0, 0};
editor.statusMessage[0] = '\0';
editor.statusMessageTime = 0;
editor.log = stderrLog;
editorUpdateWindowSize();
}
int main(int argc, char *argv[]) {
<<main>>
}
The program puts the terminal into raw mode to get more control of the screen, where normally the terminal would just accumulate everything.
I don’t really know much about the details. The code gets the current terminal settings, stores them, then makes a modified copy to set. It also arranges for disableRawMode
to be called when the program exits.
void enableRawMode() {
if (tcgetattr(STDIN_FILENO, &editor.original_termios) == -1) {
die("Failed to get terminal attributes while enabling raw mode");
}
atexit(disableRawMode);
struct termios raw = editor.original_termios;
raw.c_iflag &= ~(BRKINT | INPCK | ISTRIP | ICRNL | IXON);
raw.c_oflag &= ~(OPOST);
raw.c_cflag |= (CS8);
raw.c_lflag &= ~(ECHO | ICANON | IEXTEN | ISIG);
raw.c_cc[VMIN] = 0;
raw.c_cc[VTIME] = 1;
if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw) == -1) {
die("Failed to set terminal attributes while enabling raw mode");
}
}
void disableRawMode() {
if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &editor.original_termios) == -1) {
die("Failed to disable raw mode");
}
}