Files
tte/tte.c
T

872 lines
24 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*** Include section ***/
// We add them above our includes, because the header
// files we’re including use the macros to decide what
// features to expose. These macros remove some compilation
// warnings. See
// https://www.gnu.org/software/libc/manual/html_node/Feature-Test-Macros.html
// for more info.
#define _DEFAULT_SOURCE
#define _BSD_SOURCE
#define _GNU_SOURCE
#include <ctype.h>
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdarg.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/types.h>
#include <termios.h>
#include <time.h>
#include <unistd.h>
/*** Define section ***/
// This mimics the Ctrl + whatever behavior, setting the
// 3 upper bits of the character pressed to 0.
#define CTRL_KEY(k) ((k) & 0x1f)
// Empty buffer
#define ABUF_INIT {NULL, 0}
// Version code
#define TTE_VERSION "0.0.1"
// Length of a tab stop
#define TTE_TAB_STOP 8
// Times to press Ctrl-Q before exiting
#define TTE_QUIT_TIMES 3
/*** Data section ***/
typedef struct editor_row {
int size;
int render_size;
char* chars;
char* render;
} editor_row;
struct editor_config {
int cursor_x;
int cursor_y;
int render_x;
int row_offset;
int col_offset;
int screen_rows;
int screen_cols;
int num_rows;
editor_row* row;
int dirty; // To know if a file has been modified since opening.
char* file_name;
char status_msg[80];
time_t status_msg_time;
struct termios orig_termios;
} ec;
// Having a dynamic buffer will allow us to write only one
// time once the screen is refreshing, instead of doing
// a lot of write's.
struct a_buf {
char* buf;
int len;
};
enum editor_key {
BACKSPACE = 0x7f, // 127
ARROW_LEFT = 0x3e8, // 1000, large value out of the range of a char.
ARROW_RIGHT,
ARROW_UP,
ARROW_DOWN,
PAGE_UP,
PAGE_DOWN,
HOME_KEY,
END_KEY,
DEL_KEY
};
/*** Declarations section ***/
void editorClearScreen();
void editorSetStatusMessage(const char* msg, ...);
char *editorPrompt(char* prompt);
/*** Terminal section ***/
void die(const char* s) {
editorClearScreen();
// perror looks for global errno variable and then prints
// a descriptive error mesage for it.
perror(s);
printf("\r\n");
exit(1);
}
void disableRawMode() {
if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &ec.orig_termios) == -1)
die("Failed to disable raw mode");
}
void enableRawMode() {
// Save original terminal state into orig_termios.
if (tcgetattr(STDIN_FILENO, &ec.orig_termios) == -1)
die("Failed to get current terminal state");
// At exit, restore the original state.
atexit(disableRawMode);
// Modify the original state to enter in raw mode.
struct termios raw = ec.orig_termios;
// This disables Ctrl-M, Ctrl-S and Ctrl-Q commands.
// (BRKINT, INPCK and ISTRIP are not estrictly mandatory,
// but it is recommended to turn them off in case any
// system needs it).
raw.c_iflag &= ~(BRKINT | ICRNL | INPCK | ISTRIP | IXON);
// Turning off all output processing (\r\n).
raw.c_oflag &= ~(OPOST);
// Setting character size to 8 bits per byte (it should be
// like that on most systems, but whatever).
raw.c_cflag |= (CS8);
// Using NOT operator on ECHO | ICANON | IEXTEN | ISIG and
// then bitwise-AND them with flags field in order to
// force c_lflag 4th bit to become 0. This disables
// chars being printed (ECHO) and let us turn off
// canonical mode in order to read input byte-by-byte
// instead of line-by-line (ICANON), ISIG disables
// Ctrl-C command and IEXTEN the Ctrl-V one.
raw.c_lflag &= ~(ECHO | ICANON | IEXTEN | ISIG);
// read() function now returns as soon as there is any
// input to be read.
raw.c_cc[VMIN] = 0;
// Forcing read() function to return every 1/10 of a
// second if there is nothing to read.
raw.c_cc[VTIME] = 1;
if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw) == -1)
die("Failed to set raw mode");
}
int editorReadKey() {
int nread;
char c;
while ((nread = read(STDIN_FILENO, &c, 1)) != 1) {
// Ignoring EAGAIN to make it work on Cygwin.
if (nread == -1 && errno != EAGAIN)
die("Error reading input");
}
// Check escape sequences, if first byte
// is an escape character then...
if (c == '\x1b') {
char seq[3];
if (read(STDIN_FILENO, &seq[0], 1) != 1 ||
read(STDIN_FILENO, &seq[1], 1) != 1)
return '\x1b';
if (seq[0] == '[') {
if (seq[1] >= '0' && seq[1] <= '9') {
if (read(STDIN_FILENO, &seq[2], 1) != 1)
return '\x1b';
if (seq[2] == '~') {
switch (seq[1]) {
// Home and End keys may be sent in many ways depending on the OS
// \x1b[1~, \x1b[7~, \x1b[4~, \x1b[8~
case '1':
case '7':
return HOME_KEY;
case '4':
case '8':
return END_KEY;
// Del key is sent as \x1b[3~
case '3':
return DEL_KEY;
// Page Up and Page Down send '\x1b', '[', '5' or '6' and '~'.
case '5': return PAGE_UP;
case '6': return PAGE_DOWN;
}
}
} else {
switch (seq[1]) {
// Arrow keys send multiple bytes starting with '\x1b', '[''
// and followed by an 'A', 'B', 'C' or 'D' depending on which
// arrow is pressed.
case 'A': return ARROW_UP;
case 'B': return ARROW_DOWN;
case 'C': return ARROW_RIGHT;
case 'D': return ARROW_LEFT;
// Home key can also be sent as \x1b[H
case 'H': return HOME_KEY;
// End key can also be sent as \x1b[F
case 'F': return END_KEY;
}
}
} else if (seq[0] == 'O') {
switch (seq[1]) {
// Yes, Home key can ALSO be sent as \x1bOH
case 'H': return HOME_KEY;
// And... End key as \x1bOF
case 'F': return END_KEY;
}
}
return '\x1b';
} else {
return c;
}
}
int getWindowSize(int* screen_rows, int* screen_cols) {
struct winsize ws;
// Getting window size thanks to ioctl into the given
// winsize struct.
if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == -1 || ws.ws_col == 0) {
return -1;
} else {
*screen_cols = ws.ws_col;
*screen_rows = ws.ws_row;
return 0;
}
}
/*** Row operations ***/
int editorRowCursorXToRenderX(editor_row* row, int cursor_x) {
int render_x = 0;
int j;
// For each character, if it’s a tab we use rx % TTE_TAB_STOP
// to find out how many columns we are to the right of the last
// tab stop, and then subtract that from TTE_TAB_STOP - 1 to
// find out how many columns we are to the left of the next tab
// stop. We add that amount to rx to get just to the left of the
// next tab stop, and then the unconditional rx++ statement gets
// us right on the next tab stop. Notice how this works even if
// we are currently on a tab stop.
for (j = 0; j < cursor_x; j++) {
if (row -> chars[j] == '\t')
render_x += (TTE_TAB_STOP - 1) - (render_x % TTE_TAB_STOP);
render_x++;
}
return render_x;
}
void editorUpdateRow(editor_row* row) {
// First, we have to loop through the chars of the row
// and count the tabs in order to know how much memory
// to allocate for render. The maximum number of characters
// needed for each tab is 8. row->size already counts 1 for
// each tab, so we multiply the number of tabs by 7 and add
// that to row->size to get the maximum amount of memory we’ll
// need for the rendered row.
int tabs = 0;
int j;
for (j = 0; j < row -> size; j++) {
if (row -> chars[j] == '\t')
tabs++;
}
free(row -> render);
row -> render = malloc(row -> size + tabs * (TTE_TAB_STOP - 1) + 1);
// After allocating the memory, we check whether the current character
// is a tab. If it is, we append one space (because each tab must
// advance the cursor forward at least one column), and then append
// spaces until we get to a tab stop, which is a column that is
// divisible by 8
int idx = 0;
for (j = 0; j < row -> size; j++) {
if (row -> chars[j] == '\t') {
row -> render[idx++] = ' ';
while (idx % TTE_TAB_STOP != 0)
row -> render[idx++] = ' ';
} else
row -> render[idx++] = row -> chars[j];
}
row -> render[idx] = '\0';
row -> render_size = idx;
}
void editorInsertRow(int at, char* s, size_t line_len) {
if (at < 0 || at > ec.num_rows)
return;
ec.row = realloc(ec.row, sizeof(editor_row) * (ec.num_rows + 1));
memmove(&ec.row[at + 1], &ec.row[at], sizeof(editor_row) * (ec.num_rows - at));
ec.row[at].size = line_len;
ec.row[at].chars = malloc(line_len + 1); // We want to add terminator char '\0' at the end
memcpy(ec.row[at].chars, s, line_len);
ec.row[at].chars[line_len] = '\0';
ec.row[at].render_size = 0;
ec.row[at].render = NULL;
editorUpdateRow(&ec.row[at]);
ec.num_rows++;
ec.dirty++;
}
void editorFreeRow(editor_row* row) {
free(row -> render);
free(row -> chars);
}
void editorDelRow(int at) {
if (at < 0 || at >= ec.num_rows)
return;
editorFreeRow(&ec.row[at]);
memmove(&ec.row[at], &ec.row[at + 1], sizeof(editor_row) * (ec.num_rows - at - 1));
ec.num_rows--;
ec.dirty++;
}
void editorRowInsertChar(editor_row* row, int at, int c) {
if (at < 0 || at > row -> size)
at = row -> size;
// We need to allocate 2 bytes because we also have to make room for
// the null byte.
row -> chars = realloc(row -> chars, row -> size + 2);
// memmove it's like memcpy(), but is safe to use when the source and
// destination arrays overlap
memmove(&row -> chars[at + 1], &row -> chars[at], row -> size - at + 1);
row -> size++;
row -> chars[at] = c;
editorUpdateRow(row);
ec.dirty++; // This way we can see "how dirty" a file is.
}
void editorInsertNewline() {
// If we’re at the beginning of a line, all we have to do is insert
// a new blank row before the line we’re on.
if (ec.cursor_x == 0) {
editorInsertRow(ec.cursor_y, "", 0);
// Otherwise, we have to split the line we’re on into two rows.
} else {
editor_row* row = &ec.row[ec.cursor_y];
editorInsertRow(ec.cursor_y + 1, &row -> chars[ec.cursor_x], row -> size - ec.cursor_x);
row = &ec.row[ec.cursor_y];
row -> size = ec.cursor_x;
row -> chars[row -> size] = '\0';
editorUpdateRow(row);
}
ec.cursor_y++;
ec.cursor_x = 0;
}
void editorRowAppendString(editor_row* row, char* s, size_t len) {
row -> chars = realloc(row -> chars, row -> size + len + 1);
memcpy(&row -> chars[row -> size], s, len);
row -> size += len;
row -> chars[row -> size] = '\0';
editorUpdateRow(row);
ec.dirty++;
}
void editorRowDelChar(editor_row* row, int at) {
if (at < 0 || at >= row -> size)
return;
// Overwriting the deleted character with the characters that come
// after it.
memmove(&row -> chars[at], &row -> chars[at + 1], row -> size - at);
row -> size--;
editorUpdateRow(row);
ec.dirty++;
}
/*** Editor operations ***/
void editorInsertChar(int c) {
// If this is true, the cursor is on the tilde line after the end of
// the file, so we need to append a new row to the file before inserting
// a character there.
if (ec.cursor_y == ec.num_rows)
editorInsertRow(ec.num_rows, "", 0);
editorRowInsertChar(&ec.row[ec.cursor_y], ec.cursor_x, c);
ec.cursor_x++; // This way we can see "how dirty" a file is.
}
void editorDelChar() {
// If the cursor is pats the end of the file, there's nothing to delete.
if (ec.cursor_y == ec.num_rows)
return;
// Cursor is at the beginning of a file, there's nothing to delete.
if (ec.cursor_x == 0 && ec.cursor_y == 0)
return;
editor_row* row = &ec.row[ec.cursor_y];
if (ec.cursor_x > 0) {
editorRowDelChar(row, ec.cursor_x - 1);
ec.cursor_x--;
// Deleting a line and moving up all the content.
} else {
ec.cursor_x = ec.row[ec.cursor_y - 1].size;
editorRowAppendString(&ec.row[ec.cursor_y -1], row -> chars, row -> size);
editorDelRow(ec.cursor_y);
ec.cursor_y--;
}
}
/*** File I/O ***/
char* editorRowsToString(int* buf_len) {
int total_len = 0;
int j;
// Adding up the lengths of each row of text, adding 1
// to each one for the newline character we'll add to
// the end of each line.
for (j = 0; j < ec.num_rows; j++) {
total_len += ec.row[j].size + 1;
}
*buf_len = total_len;
char* buf = malloc(total_len);
char* p = buf;
// Copying the contents of each row to the end of the
// buffer, appending a newline character after each
// row.
for (j = 0; j < ec.num_rows; j++) {
memcpy(p, ec.row[j].chars, ec.row[j].size);
p += ec.row[j].size;
*p = '\n';
p++;
}
return buf;
}
void editorOpen(char* file_name) {
free(ec.file_name);
ec.file_name = strdup(file_name);
FILE* file = fopen(file_name, "r");
if (!file)
die("Failed to open the file");
char* line = NULL;
// Unsigned int of at least 16 bit.
size_t line_cap = 0;
// Bigger than int
ssize_t line_len;
while ((line_len = getline(&line, &line_cap, file)) != -1) {
// We already know each row represents one line of text, there's no need
// to keep carriage return and newline characters.
if (line_len > 0 && (line[line_len - 1] == '\n' || line[line_len - 1] == '\r'))
line_len--;
editorInsertRow(ec.num_rows, line, line_len);
}
free(line);
fclose(file);
ec.dirty = 0;
}
void editorSave() {
if (ec.file_name == NULL) {
ec.file_name = editorPrompt("Save as: %s (ESC to cancel)");
if (ec.file_name == NULL) {
editorSetStatusMessage("Save aborted");
return;
}
}
int len;
char* buf = editorRowsToString(&len);
// We want to create if it doesn't already exist (O_CREAT flag), giving
// 0644 permissions (the standard ones). O_RDWR stands for reading and
// writing.
int fd = open(ec.file_name, O_RDWR | O_CREAT, 0644);
if (fd != -1) {
// ftruncate sets the file's size to the specified length.
if (ftruncate(fd, len) != -1) {
// Writing the file.
if (write(fd, buf, len) == len) {
close(fd);
free(buf);
ec.dirty = 0;
editorSetStatusMessage("%d bytes written to disk", len);
return;
}
}
close(fd);
}
free(buf);
editorSetStatusMessage("Cant's save file. Error occurred: %s", strerror(errno));
}
/*** Append buffer section **/
void abufAppend(struct a_buf* ab, const char* s, int len) {
// Using realloc to get a block of free memory that is
// the size of the current string + the size of the string
// to be appended.
char* new = realloc(ab -> buf, ab -> len + len);
if (new == NULL)
return;
// Copying the string s at the end of the current data in
// the buffer.
memcpy(&new[ab -> len], s, len);
ab -> buf = new;
ab -> len += len;
}
void abufFree(struct a_buf* ab) {
// Deallocating buffer.
free(ab -> buf);
}
/*** Output section ***/
void editorScroll() {
ec.render_x = 0;
if (ec.cursor_y < ec.num_rows)
ec.render_x = editorRowCursorXToRenderX(&ec.row[ec.cursor_y], ec.cursor_x);
// The first if statement checks if the cursor is above the visible window,
// and if so, scrolls up to where the cursor is. The second if statement checks
// if the cursor is past the bottom of the visible window, and contains slightly
// more complicated arithmetic because ec.row_offset refers to what’s at the top
// of the screen, and we have to get ec.screen_rows involved to talk about what’s
// at the bottom of the screen.
if (ec.cursor_y < ec.row_offset)
ec.row_offset = ec.cursor_y;
if (ec.cursor_y >= ec.row_offset + ec.screen_rows)
ec.row_offset = ec.cursor_y - ec.screen_rows + 1;
if (ec.render_x < ec.col_offset)
ec.col_offset = ec.render_x;
if (ec.render_x >= ec.col_offset + ec.screen_cols)
ec.col_offset = ec.render_x - ec.screen_cols + 1;
}
void editorDrawStatusBar(struct a_buf* ab) {
// This switches to inverted colors.
// NOTE:
// The m command (Select Graphic Rendition) causes the text printed
// after it to be printed with various possible attributes including
// bold (1), underscore (4), blink (5), and inverted colors (7). An
// argument of 0 clears all attributes (the default one). See
// http://vt100.net/docs/vt100-ug/chapter3.html#SGR for more info.
abufAppend(ab, "\x1b[7m", 4);
char status[80], r_status[80];
// Showing up to 20 characters of the filename, followed by the number of lines.
int len = snprintf(status, sizeof(status), "%.20s - %d lines %s", ec.file_name ? ec.file_name : "New file", ec.num_rows, ec.dirty ? "(modified)" : "");
int r_len = snprintf(r_status, sizeof(r_status), "%d/%d", ec.cursor_y + 1 > ec.num_rows ? ec.num_rows : ec.cursor_y + 1, ec.num_rows);
if (len > ec.screen_cols)
len = ec.screen_cols;
abufAppend(ab, status, len);
while (len < ec.screen_cols) {
if (ec.screen_cols - len == r_len) {
abufAppend(ab, r_status, r_len);
break;
} else {
abufAppend(ab, " ", 1);
len++;
}
}
// This switches back to normal colors.
abufAppend(ab, "\x1b[m", 3);
abufAppend(ab, "\r\n", 2);
}
void editorDrawMessageBar(struct a_buf *ab) {
// Clearing the message bar.
abufAppend(ab, "\x1b[K", 3);
int msg_len = strlen(ec.status_msg);
if (msg_len > ec.screen_cols)
msg_len = ec.screen_cols;
// We only show the message if its less than 5 secons old, but
// remember the screen is only being refreshed after each keypress.
if (msg_len && time(NULL) - ec.status_msg_time < 5)
abufAppend(ab, ec.status_msg, msg_len);
}
void editorDrawWelcomeMessage(struct a_buf* ab) {
char welcome[80];
// Using snprintf to truncate message in case the terminal
// is too tiny to handle the entire string.
int welcome_len = snprintf(welcome, sizeof(welcome),
"tte -- version %s", TTE_VERSION);
if (welcome_len > ec.screen_cols)
welcome_len = ec.screen_cols;
// Centering the message.
int padding = (ec.screen_cols - welcome_len) / 2;
// Remember that everything != 0 is true.
if (padding) {
abufAppend(ab, "~", 1);
padding--;
}
while (padding--)
abufAppend(ab, " ", 1);
abufAppend(ab, welcome, welcome_len);
}
// The ... argument makes editorSetStatusMessage() a variadic function,
// meaning it can take any number of arguments. C’s way of dealing with
// these arguments is by having you call va_start() and va_end() on a
// // value of type va_list. The last argument before the ... (in this
// case, msg) must be passed to va_start(), so that the address of
// the next arguments is known. Then, between the va_start() and
// va_end() calls, you would call va_arg() and pass it the type of
// the next argument (which you usually get from the given format
// string) and it would return the value of that argument. In
// this case, we pass msg and args to vsnprintf() and it takes care
// of reading the format string and calling va_arg() to get each
// argument.
void editorSetStatusMessage(const char* msg, ...) {
va_list args;
va_start(args, msg);
vsnprintf(ec.status_msg, sizeof(ec.status_msg), msg, args);
va_end(args);
ec.status_msg_time = time(NULL);
}
void editorDrawRows(struct a_buf* ab) {
int y;
for (y = 0; y < ec.screen_rows; y++) {
int file_row = y + ec.row_offset;
if(file_row >= ec.num_rows) {
if (ec.num_rows == 0 && y == ec.screen_rows / 3)
editorDrawWelcomeMessage(ab);
else
abufAppend(ab, "~", 1);
} else {
int len = ec.row[file_row].render_size - ec.col_offset;
// len can be a negative number, meaning the user scrolled
// horizontally past the end of the line. In that case, we set
// len to 0 so that nothing is displayed on that line.
if (len < 0)
len = 0;
if (len > ec.screen_cols)
len = ec.screen_cols;
abufAppend(ab, &ec.row[file_row].render[ec.col_offset], len);
}
// Redrawing each line instead of the whole screen.
abufAppend(ab, "\x1b[K", 3);
// Addind a new line
abufAppend(ab, "\r\n", 2);
}
}
void editorRefreshScreen() {
editorScroll();
struct a_buf ab = ABUF_INIT;
// Hiding the cursor while the screen is refreshing.
// See http://vt100.net/docs/vt100-ug/chapter3.html#S3.3.4
// for more info.
abufAppend(&ab, "\x1b[?25l", 6);
abufAppend(&ab, "\x1b[H", 3);
editorDrawRows(&ab);
editorDrawStatusBar(&ab);
editorDrawMessageBar(&ab);
// Moving the cursor where it should be.
char buf[32];
snprintf(buf, sizeof(buf), "\x1b[%d;%dH", (ec.cursor_y - ec.row_offset) + 1, (ec.render_x - ec.col_offset) + 1);
abufAppend(&ab, buf, strlen(buf));
// Showing again the cursor.
abufAppend(&ab, "\x1b[?25h", 6);
// Writing all content at once
write(STDOUT_FILENO, ab.buf, ab.len);
abufFree(&ab);
}
void editorClearScreen() {
// Writing 4 bytes out to the terminal:
// - (1 byte) \x1b : escape character
// - (3 bytes) [2J : Clears the entire screen, see
// http://vt100.net/docs/vt100-ug/chapter3.html#ED
// for more info.
write(STDOUT_FILENO, "\x1b[2J", 4);
// Writing 3 bytes to reposition the cursor back at
// the top-left corner, see
// http://vt100.net/docs/vt100-ug/chapter3.html#CUP
// for more info.
write(STDOUT_FILENO, "\x1b[H", 3);
}
/*** Input section ***/
char* editorPrompt(char* prompt) {
size_t buf_size = 128;
char* buf = malloc(buf_size);
size_t buf_len = 0;
buf[0] = '\0';
while (1) {
editorSetStatusMessage(prompt, buf);
editorRefreshScreen();
int c = editorReadKey();
if (c == DEL_KEY || c == CTRL_KEY('h') || c == BACKSPACE) {
if (buf_len != 0)
buf[--buf_len] = '\0';
} else if (c == '\x1b') {
editorSetStatusMessage("");
free(buf);
return NULL;
} else if (c == '\r') {
if (buf_len != 0) {
editorSetStatusMessage("");
return buf;
}
} else if (isprint(c)) {
if (buf_len == buf_size - 1) {
buf_size *= 2;
buf = realloc(buf, buf_size);
}
buf[buf_len++] = c;
buf[buf_len] = '\0';
}
}
}
void editorMoveCursor(int key) {
editor_row* row = (ec.cursor_y >= ec.num_rows) ? NULL : &ec.row[ec.cursor_y];
switch (key) {
case ARROW_LEFT:
if (ec.cursor_x != 0)
ec.cursor_x--;
// If <- is pressed, move to the end of the previous line
else if (ec.cursor_y > 0) {
ec.cursor_y--;
ec.cursor_x = ec.row[ec.cursor_y].size;
}
break;
case ARROW_RIGHT:
if (row && ec.cursor_x < row -> size)
ec.cursor_x++;
// If -> is pressed, move to the start of the next line
else if (row && ec.cursor_x == row -> size) {
ec.cursor_y++;
ec.cursor_x = 0;
}
break;
case ARROW_UP:
if (ec.cursor_y != 0)
ec.cursor_y--;
break;
case ARROW_DOWN:
if (ec.cursor_y < ec.num_rows)
ec.cursor_y++;
break;
}
// Move cursor_x if it ends up past the end of the line it's on
row = (ec.cursor_y >= ec.num_rows) ? NULL : &ec.row[ec.cursor_y];
int row_len = row ? row -> size : 0;
if (ec.cursor_x > row_len)
ec.cursor_x = row_len;
}
void editorProcessKeypress() {
static int quit_times = TTE_QUIT_TIMES;
int c = editorReadKey();
switch (c) {
case '\r': // Enter key
editorInsertNewline();
break;
case CTRL_KEY('q'):
if (ec.dirty && quit_times > 0) {
editorSetStatusMessage("Warning! File has unsaved changes. Press Ctrl-Q %d more times to quit", quit_times);
quit_times--;
return;
}
editorClearScreen();
exit(0);
break;
case CTRL_KEY('s'):
editorSave();
break;
case ARROW_UP:
case ARROW_DOWN:
case ARROW_LEFT:
case ARROW_RIGHT:
editorMoveCursor(c);
break;
case PAGE_UP:
case PAGE_DOWN:
{ // You can't declare variables directly inside a switch statement.
if (c == PAGE_UP)
ec.cursor_y = ec.row_offset;
else if (c == PAGE_DOWN)
ec.cursor_y = ec.row_offset + ec.screen_rows - 1;
int times = ec.screen_rows;
while (times--)
editorMoveCursor(c == PAGE_UP ? ARROW_UP : ARROW_DOWN);
}
break;
case HOME_KEY:
ec.cursor_x = 0;
break;
case END_KEY:
if (ec.cursor_y < ec.num_rows)
ec.cursor_x = ec.row[ec.cursor_y].size;
break;
case BACKSPACE:
case CTRL_KEY('h'):
case DEL_KEY:
if (c == DEL_KEY)
editorMoveCursor(ARROW_RIGHT);
editorDelChar();
break;
case CTRL_KEY('l'):
case '\x1b': // Escape key
break;
default:
editorInsertChar(c);
break;
}
quit_times = TTE_QUIT_TIMES;
}
/*** Init section ***/
void initEditor() {
ec.cursor_x = 0;
ec.cursor_y = 0;
ec.render_x = 0;
ec.row_offset = 0;
ec.col_offset = 0;
ec.num_rows = 0;
ec.row = NULL;
ec.dirty = 0;
ec.file_name = NULL;
ec.status_msg[0] = '\0';
ec.status_msg_time = 0;
if (getWindowSize(&ec.screen_rows, &ec.screen_cols) == -1)
die("Failed to get window size");
ec.screen_rows -= 2;
}
int main(int argc, char* argv[]) {
enableRawMode();
initEditor();
if (argc >= 2)
editorOpen(argv[1]);
editorSetStatusMessage("Ctrl-Q to quit | Ctrl-S to save");
while (1) {
editorRefreshScreen();
editorProcessKeypress();
}
return 0;
}