Screen refreshing and arrow navigation is complete.
This commit is contained in:
@@ -4,16 +4,59 @@
|
||||
#include <errno.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/ioctl.h>
|
||||
#include <termios.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"
|
||||
|
||||
/*** Data section ***/
|
||||
|
||||
struct termios orig_termios;
|
||||
struct editor_config {
|
||||
int cursor_x;
|
||||
int cursor_y;
|
||||
int rows;
|
||||
int cols;
|
||||
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 {
|
||||
ARROW_LEFT = 0x3e8, // 1000, large value out of range of a char.
|
||||
ARROW_RIGHT,
|
||||
ARROW_UP,
|
||||
ARROW_DOWN,
|
||||
PAGE_UP,
|
||||
PAGE_DOWN,
|
||||
HOME_KEY,
|
||||
END_KEY,
|
||||
DEL_KEY
|
||||
};
|
||||
|
||||
/*** Declarations section ***/
|
||||
|
||||
void editorClearScreen();
|
||||
|
||||
/*** 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);
|
||||
@@ -21,19 +64,19 @@ void die(const char* s) {
|
||||
}
|
||||
|
||||
void disableRawMode() {
|
||||
if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &orig_termios) == -1)
|
||||
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, &orig_termios) == -1)
|
||||
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 = orig_termios;
|
||||
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
|
||||
@@ -63,24 +106,260 @@ void enableRawMode() {
|
||||
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* rows, int* 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 {
|
||||
*cols = ws.ws_col;
|
||||
*rows = ws.ws_row;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/*** 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 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.cols)
|
||||
welcome_len = ec.cols;
|
||||
// Centering the message.
|
||||
int padding = (ec.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);
|
||||
}
|
||||
|
||||
void editorDrawRows(struct a_buf* ab) {
|
||||
int y;
|
||||
for (y = 0; y < ec.rows; y++) {
|
||||
if (y == ec.rows / 3)
|
||||
editorDrawWelcomeMessage(ab);
|
||||
else
|
||||
abufAppend(ab, "~", 1);
|
||||
|
||||
// Redrawing each line instead of the whole screen.
|
||||
abufAppend(ab, "\x1b[K", 3);
|
||||
// Addind a new line only if we are not printing the
|
||||
// last tilde.
|
||||
if (y < ec.rows - 1)
|
||||
abufAppend(ab, "\r\n", 2);
|
||||
}
|
||||
}
|
||||
|
||||
void editorRefreshScreen() {
|
||||
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);
|
||||
|
||||
// Moving the cursor where it should be.
|
||||
char buf[32];
|
||||
snprintf(buf, sizeof(buf), "\x1b[%d;%dH", ec.cursor_y + 1, ec.cursor_x + 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 ***/
|
||||
|
||||
void editorMoveCursor(int key) {
|
||||
switch (key) {
|
||||
case ARROW_LEFT:
|
||||
if (ec.cursor_x != 0)
|
||||
ec.cursor_x--;
|
||||
break;
|
||||
case ARROW_RIGHT:
|
||||
if (ec.cursor_x != ec.cols - 1)
|
||||
ec.cursor_x++;
|
||||
break;
|
||||
case ARROW_UP:
|
||||
if (ec.cursor_y != 0)
|
||||
ec.cursor_y--;
|
||||
break;
|
||||
case ARROW_DOWN:
|
||||
if (ec.cursor_y != ec.rows - 1)
|
||||
ec.cursor_y++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void editorProcessKeypress() {
|
||||
int c = editorReadKey();
|
||||
|
||||
switch (c) {
|
||||
case CTRL_KEY('q'):
|
||||
editorClearScreen();
|
||||
exit(0);
|
||||
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.
|
||||
int times = ec.rows;
|
||||
while (times--)
|
||||
editorMoveCursor(c == PAGE_UP ? ARROW_UP : ARROW_DOWN);
|
||||
}
|
||||
break;
|
||||
case HOME_KEY:
|
||||
ec.cursor_x = 0;
|
||||
break;
|
||||
case END_KEY:
|
||||
ec.cursor_x = ec.cols - 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/*** Init section ***/
|
||||
|
||||
void initEditor() {
|
||||
ec.cursor_x = 0;
|
||||
ec.cursor_y = 0;
|
||||
|
||||
if (getWindowSize(&ec.rows, &ec.cols) == -1)
|
||||
die("Failed to get window size");
|
||||
}
|
||||
|
||||
int main() {
|
||||
enableRawMode();
|
||||
initEditor();
|
||||
|
||||
while (1) {
|
||||
char c = '\0';
|
||||
// Ignoring EAGAIN to make it work on Cygwin.
|
||||
if (read(STDIN_FILENO, &c, 1) == -1 && errno != EAGAIN)
|
||||
die("Error reading input");
|
||||
|
||||
if (isprint(c))
|
||||
printf("%d ('%c')\r\n", c, c);
|
||||
else
|
||||
printf("%d\n\r", c);
|
||||
|
||||
if (c == 'q')
|
||||
break;
|
||||
editorRefreshScreen();
|
||||
editorProcessKeypress();
|
||||
}
|
||||
|
||||
return 0;
|
||||
|
||||
Reference in New Issue
Block a user