├── .gitignore ├── LICENSE ├── MakeFile ├── README.md ├── _config.yml ├── stex.c └── text.txt /.gitignore: -------------------------------------------------------------------------------- 1 | .stex.c.swp 2 | stex 3 | settings.json -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Yash Jain 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MakeFile: -------------------------------------------------------------------------------- 1 | stex : stex.c 2 | $(CC) stex.c -o stex -Wall -Wextra -pedantic -std=C99 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### [![contributions welcome](https://img.shields.io/badge/contributions-welcome-brightgreen.svg?style=flat)](https://github.com/scocoyash/Text-Editor-in-C/issues) 2 | 3 | # Text-Editor-In-C 4 | A basic Text editor in C99 Language 5 | 6 | ## **Running the code** 7 | *** 8 | 9 | * Install c99(or higher) compiler; on Linux, check by executing ``` cc --version``` command on terminal 10 | * Install cmake to build files using MakeFile; on Linux, check by executing ``` make -v ``` command on terminal 11 | * Then run the following code snippet in the same directory where you unzipped this repo: 12 | 13 | Without Make: 14 | ``` 15 | cc stex.c -o stex 16 | ./stex 17 | ``` 18 | 19 | With Make: 20 | ``` 21 | make stex 22 | ./stex 23 | ``` 24 | 25 | ## **Notes** : 26 | 27 | ### Step 1 - Conversion from Canonical Mode to Raw Mode 28 | *** 29 | 30 | * Starting from the main function, we need to change the terminal mode to **'RAW'** mode. By default, the terminal is in **'CANONICAL'** mode i.e. it does not pass the input to the process unless you press the Enter key. 31 | 32 | * We will need the *termios* library to change the terminal attributes so importing that firstly. 33 | 34 | * Now we **disable** various attributes of the terminal using flags : 35 | 36 | * *ECHO* [termios.h] - The ECHO attribute causes each key you type to be printed to the terminal, so you can see what you’re typing. We do not need such feature for the text-editor 37 | 38 | * *ICANON* [termios.h] - Turns off Canonical mode; enables us to read the input byte-by-byte rather than line by line 39 | 40 | * *ISIG* [termios.h] - Disable **Ctrl+Z** (26-byte SIGTSTP signal to the current process) and **Ctrl+C** (3-byte SIGINT signal to the current process) signals from user input 41 | 42 | * *IXON* [termios.h] - Turns off **Ctrl+S** and **Ctrl+Q** software control signals 43 | 44 | * *IEXTEN* [termios.h] - Turns off **Ctrl+V** and **Ctrl+O** software control signals 45 | 46 | * *ICRNL* [termios.h] - Turns off **Ctrl+M** and **ENTER** transformation to Carriage Returns by the terminal. 47 | 48 | * Also disable many extra signals from wrong inputs. 49 | 50 | * Do not forget to disable the RAW mode at program exit; using **atexit()** from [stdlib.h] library. 51 | 52 | * It’s time to clean up the code by adding some *error handling*: 53 | 54 | * perror() [stdio.h] - looks at the global errno variable and prints a descriptive error message 55 | 56 | * exit() [stdlib.h] - exit the program with an exit status of 1 57 | 58 | 59 | ### Step 2 - Enhancing the input and output 60 | *** 61 | 62 | * Adding refresh screen everytime the input is received; do not forget to clear the screen and reposition the cursor on Ctrl+Q press 63 | * *editorRefreshScreen()* - 64 | writes 4 bytes to the output screen; 65 | * **\x1b** - decimal 27 is the escape character, 66 | * [ - start of the escape sequence 67 | * 2J - clears the entire screen 68 | * H - repositions the cursor to the top of the screen 69 | 70 | * Getting to know the size of the terminal - using the **ioctl** library 71 | 72 | * *getWindowSize()* - using the ioctl function, we check whether the winsize struct is present or not. If absent, we return -1. **TIOCGWINSZ** stands for Terminal IOCtl (which itself stands for Input/Output Control) Get WINdow SiZe.) 73 | 74 | * ioctl() isn’t guaranteed to be able to request the window size on all systems, so providing a fallback method of getting the window size using the cursor positon - *getCursorPosition()*. 75 | 76 | * Drawing '~' like VIM at the start of each row 77 | * *editorDrawRows()* - prints '~' at the start of each row 78 | 79 | * Creating **Dynamic Strings** in C to write the buffer to the screen at once for performance enhancements. 80 | Replacing all our **write()** calls with code that appends the string to a buffer, and then **write()** this buffer out at the end. 81 | * Creating a **buffer abuf{}** consisting of a pointer to our buffer in memory, and a length. 82 | * *abAppend()* - appends a string to a buffer 83 | * *abFree()* - frees the memory occupied by the buffer 84 | 85 | * For performance improvements, we can just clear the right part of the cursor when drawing the row instead of clearing the screen everytime when we refresh the screen. 86 | * remove ~~abAppend(&ab, "\x1b[2J", 4)~~ from editor refresh screen for the above said reason. 87 | 88 | ### Step 3 - Focusing on input 89 | *** 90 | 91 | * Get the cursor position : store the x and y co-ordinates of the cursor postion in the editorConfiguration struct. 92 | * E.cx is the horizontal coordinate of the cursor (the column) and E.cy is the vertical coordinate (the row). Initialize both of them to 0, as we want the cursor to start at the top-left of the screen 93 | 94 | * Move the cursor to the x and y co-ordinates of the screen in the editor-refresh screen. 95 | * using **snprintf()** from [stdlib.h] library which formats and stores a series of characters and values in the array buffer 96 | 97 | 98 | * Handle Arrow key escape sequences and change the position of the cursor on arrow key press 99 | 100 | * Handle HOME_KEY, END_KEY, PAGE_UP, PAGE_DOWN AND DEL KEYS support along with their escape sequence handling. Following are escape sequences for some keys : 101 | * *'\x1b'* - Escape sequence starts 102 | * [A - ArrowUp 103 | * [B - ArrowDown 104 | * [C - ArrowRight 105 | * [D - ArrowLeft 106 | * [5~ - PageUp 107 | * [6~ - PageDown 108 | * [1~ , [7~ , [H , [OH - Home 109 | * [4~ , [8~ , [F , [OF - End 110 | * [3~ - Del 111 | * [You can find more escape sequences here :D](http://ascii-table.com/ansi-escape-sequences.php) 112 | 113 | ### Step 4 - Reading from a file 114 | *** 115 | 116 | * To store each row values, we create a **struct erow{}** that stores the size of the row as well as the string of that row. 117 | 118 | * Next, you open a file( **FILE** ) and read it line-by-line using **fopen()** and **getline()** functions from [stdio.h] library 119 | 120 | * Strip off the newline or carriage return at the end of the line before copying it into our erow. We know each erow represents one line of text, so there’s no use storing a newline character at the end of each one. 121 | 122 | ### Step 5 - Adding Vertical Scroll 123 | *** 124 | 125 | * To enable vertical scroll, we use have to keep track of current row position in the editor itself. Therefore, an editor variable rowOffset is added to configuration and initialized to 0 i.e. topmost row. 126 | 127 | * If the current cursor position int the vertical direction is above or below the visible window, we need to set rowOffset variable accoringly. This has been done in **verticalScroll()** function. 128 | 129 | * But as soon as you scroll to bottom of file, cy will no longer refer to the position onsceen. So, the correct cursor position will be (cy - rowOffset) -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-minimal -------------------------------------------------------------------------------- /stex.c: -------------------------------------------------------------------------------- 1 | #define _DEFAULT_SOURCE 2 | #define _BSD_SOURCE 3 | #define _GNU_SOURCE 4 | 5 | /*** includes ***/ 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | #define STEX_VERSION "0.0.1" 17 | // 0x1F - decimal 27 escape sequence 18 | #define CTRL_KEY(k) ((k) & 0x1F) 19 | 20 | /*** DATA STRUCTURES ***/ 21 | 22 | // datatype for storing a row of a text-editor 23 | typedef struct erow { 24 | int size; 25 | char* chars; 26 | } erow; 27 | 28 | struct editorConfiguration { 29 | int cx, cy; 30 | /* row of the file the user is currently scrolled */ 31 | int rowOffset; 32 | 33 | int screencols; 34 | int screenrows; 35 | 36 | int numRows; 37 | erow *row; // array for each row 38 | 39 | /* stores original terminal attributes */ 40 | struct termios original_termios; 41 | }; 42 | 43 | struct editorConfiguration E; 44 | 45 | enum editorKeys{ 46 | ARROW_LEFT = 1000, 47 | ARROW_RIGHT, 48 | ARROW_UP, 49 | ARROW_DOWN, 50 | DEL_KEY, 51 | HOME_KEY, 52 | END_KEY, 53 | PAGE_UP, 54 | PAGE_DOWN 55 | } ; 56 | 57 | /*** TERMINAL ***/ 58 | struct abuf { 59 | char *b; 60 | int len; 61 | }; 62 | 63 | #define ABUF_INIT {NULL, 0} 64 | 65 | /* error logging function */ 66 | void die(const char *s) { 67 | write(STDOUT_FILENO, "\x1b[2J", 4); 68 | write(STDOUT_FILENO, "\x1b[H", 3); 69 | perror(s); 70 | exit(1); 71 | } 72 | 73 | void abAppend(struct abuf *ab,const char *s, int len){ 74 | char *new = realloc(ab->b, ab->len + len); 75 | if(new == NULL) return; 76 | memcpy(&new[ab->len], s, len); 77 | ab->b = new; 78 | ab->len += len; 79 | } 80 | 81 | void abFree(struct abuf *ab){ 82 | free(ab->b); 83 | } 84 | 85 | int keyRead(){ 86 | int nread; 87 | char c; 88 | while ((nread = read(STDIN_FILENO, &c, 1)) != 1) { 89 | if (nread == -1 && errno != EAGAIN) die("read"); 90 | } 91 | 92 | // handle escape sequences 93 | if (c == '\x1b') { 94 | char seq[3]; 95 | if(read(STDIN_FILENO, &seq[0], 1) != 1) 96 | return '\x1b'; 97 | if(read(STDIN_FILENO, &seq[1], 1) != 1) 98 | return '\x1b'; 99 | 100 | if(seq[0] == '['){ 101 | if(seq[1] >= '0' && seq[1] <= '9'){ 102 | if(read(STDIN_FILENO, &seq[2], 1) != 1) 103 | return '\x1b'; 104 | if(seq[2] == '~'){ 105 | switch(seq[1]){ 106 | case '1': return HOME_KEY; 107 | case '3': return DEL_KEY; 108 | case '4': return END_KEY; 109 | case '5': return PAGE_UP; 110 | case '6': return PAGE_DOWN; 111 | case '7': return HOME_KEY; 112 | case '8': return END_KEY; 113 | } 114 | } 115 | }else{ 116 | // escape sequences for arrow keys 117 | switch(seq[1]){ 118 | case 'A' : return ARROW_UP; 119 | case 'B' : return ARROW_DOWN; 120 | case 'C' : return ARROW_RIGHT; 121 | case 'D' : return ARROW_LEFT; 122 | case 'H' : return HOME_KEY; 123 | case 'F' : return END_KEY; 124 | } 125 | } 126 | }else if(seq[0] == 'O'){ 127 | switch (seq[1]) { 128 | case 'H': return HOME_KEY; 129 | case 'F': return END_KEY; 130 | } 131 | } 132 | 133 | return '\x1b'; 134 | } else { 135 | return c; 136 | } 137 | } 138 | 139 | void exitRawMode(){ 140 | /* leave the terminal attributes as they were when exiting */ 141 | if(tcsetattr(STDIN_FILENO, TCSAFLUSH, &E.original_termios) == -1) die("tcsetattr"); 142 | } 143 | 144 | void enterRawMode(){ 145 | if (tcgetattr(STDIN_FILENO, &E.original_termios) == -1) die("tcgetattr"); 146 | /* whenever exiting the program, restore terminal attribute states */ 147 | atexit(exitRawMode); 148 | 149 | struct termios raw = E.original_termios; 150 | // flag - IXON turns ctrl+s && ctrl+q software signals off 151 | // flag - ICRNL turns ctrl+m carriage return off 152 | raw.c_iflag &= ~(BRKINT | ICRNL | INPCK | ISTRIP | IXON); 153 | // flag - OPOST turns post-processing of output off 154 | raw.c_oflag &= ~(OPOST); 155 | raw.c_cflag |= (CS8); 156 | // flag - ICANON turns canonical mode off 157 | // flag - ISIG turns ctrl+c && ctrl+z signals off 158 | raw.c_lflag &= ~(ECHO | ICANON | IEXTEN | ISIG); 159 | 160 | /* adding timeouts for read */ 161 | raw.c_cc[VMIN] = 0; 162 | raw.c_cc[VTIME] = 1; 163 | 164 | if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw) == -1) die("tcsetattr"); 165 | } 166 | 167 | int getCursorPosition(int *rows, int *cols) { 168 | char buf[32]; 169 | unsigned int i = 0; 170 | if (write(STDOUT_FILENO, "\x1b[6n", 4) != 4) return -1; 171 | while (i < sizeof(buf) - 1) { 172 | if (read(STDIN_FILENO, &buf[i], 1) != 1) break; 173 | if (buf[i] == 'R') break; 174 | i++; 175 | } 176 | buf[i] = '\0'; 177 | if (buf[0] != '\x1b' || buf[1] != '[') return -1; 178 | if (sscanf(&buf[2], "%d;%d", rows, cols) != 2) return -1; 179 | return 0; 180 | } 181 | 182 | int getWindowSize(int *rows, int *cols){ 183 | struct winsize ws; 184 | if(ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == -1 || ws.ws_col == 0){ 185 | // moving to the bottom-rightmost pixel to get rows and columns 186 | if (write(STDOUT_FILENO, "\x1b[999C\x1b[999B", 12) != 12) return -1; 187 | return getCursorPosition(rows, cols); 188 | }else{ 189 | *cols = ws.ws_col; 190 | *rows = ws.ws_row; 191 | return 0; 192 | } 193 | } 194 | 195 | /*** ROW OPERATIONS ***/ 196 | void editorAppendRow(char *s, size_t linelen){ 197 | E.row = realloc(E.row, sizeof(erow) * (E.numRows + 1)); 198 | int at = E.numRows; 199 | E.row[at].size = linelen; 200 | E.row[at].chars = malloc(linelen + 1); 201 | memcpy(E.row[at].chars, s, linelen); 202 | E.row[at].chars[linelen] = '\0'; 203 | E.numRows++; 204 | } 205 | 206 | /*** FILE I/O ***/ 207 | void editorFileOpen(char *filename){ 208 | FILE *f = fopen(filename, "r"); 209 | if(!f) die("fopen"); 210 | 211 | char *line = NULL; 212 | size_t linecap = 0; 213 | ssize_t linelen ; 214 | while ((linelen = getline(&line, &linecap, f)) != -1) { 215 | while(linelen > 0 && (line[linelen - 1] == '\n' || 216 | line[linelen - 1] == '\r')) 217 | linelen--; 218 | editorAppendRow(line, linelen); 219 | } 220 | // release memory 221 | free(line); 222 | fclose(f); 223 | } 224 | 225 | /*** INPUT ***/ 226 | void editorMoveCursor(int key){ 227 | switch(key){ 228 | case ARROW_UP : 229 | if(E.cy != 0) 230 | E.cy--; 231 | break; 232 | case ARROW_DOWN : 233 | if(E.cy != E.numRows) 234 | E.cy++; 235 | break; 236 | case ARROW_RIGHT : 237 | if (E.cx != E.screencols - 1) 238 | E.cx++; 239 | break; 240 | case ARROW_LEFT : 241 | if(E.cx != 0) 242 | E.cx--; 243 | break; 244 | default : break; 245 | } 246 | } 247 | 248 | void editorKeyPress(){ 249 | int c = keyRead(); 250 | switch(c){ 251 | case CTRL_KEY('q'): 252 | write(STDOUT_FILENO, "\x1b[2J", 4); 253 | write(STDOUT_FILENO, "\x1b[H", 3); 254 | exit(0); 255 | break; 256 | 257 | case HOME_KEY : 258 | E.cx = 0; 259 | break; 260 | case END_KEY : 261 | E.cx = E.screencols - 1; 262 | break; 263 | case DEL_KEY : 264 | break; // TODO : add logic here 265 | 266 | case PAGE_UP: 267 | case PAGE_DOWN : 268 | { 269 | int times = E.screenrows; 270 | while (times--) 271 | editorMoveCursor(c == PAGE_UP ? ARROW_UP : ARROW_DOWN); 272 | } 273 | break; 274 | 275 | case ARROW_UP : 276 | case ARROW_DOWN : 277 | case ARROW_RIGHT: 278 | case ARROW_LEFT : 279 | editorMoveCursor(c); 280 | break; 281 | } 282 | } 283 | 284 | 285 | /*** OUTPUT ***/ 286 | 287 | void verticalScroll() { 288 | if (E.cy < E.rowOffset) { 289 | // if the cursor is above the visible window 290 | E.rowOffset = E.cy; 291 | } 292 | if (E.cy >= E.rowOffset + E.screenrows) { 293 | // if cursor is below the bottom of visible window 294 | E.rowOffset = E.cy - E.screenrows + 1; 295 | } 296 | } 297 | 298 | /** 299 | * @brief adds '~' character at the start of each row 300 | */ 301 | void editorDrawRows(struct abuf *ab) { 302 | for (int y = 0; y < E.screenrows; y++) { 303 | int filerow = y + E.rowOffset; 304 | if(filerow >= E.numRows){ 305 | if (E.numRows == 0 && y == E.screenrows / 3) { 306 | // display the text message only if no text file to read 307 | char welcome[80]; 308 | int welcomelen = snprintf(welcome, sizeof(welcome), 309 | "Stex editor -- version %s", STEX_VERSION); 310 | if (welcomelen > E.screencols) welcomelen = E.screencols; 311 | int padding = (E.screencols - welcomelen) / 2; 312 | if (padding) { 313 | abAppend(ab, "~", 1); 314 | padding--; 315 | } 316 | while (padding--) abAppend(ab, " ", 1); 317 | 318 | abAppend(ab, welcome, welcomelen); 319 | } else { 320 | abAppend(ab, "~", 1); 321 | } 322 | } else{ 323 | // display file 324 | int len = E.row[filerow].size; 325 | if(len > E.screencols) len = E.screencols; 326 | abAppend(ab, E.row[filerow].chars, len); 327 | } 328 | 329 | // write(STDOUT_FILENO, "~", 1); 330 | 331 | // erasing the right part of each line before drawing 332 | abAppend(ab, "\x1b[K", 3); 333 | if (y < E.screenrows - 1) { 334 | abAppend(ab, "\r\n", 2); 335 | // TODO : terminal status bar display 336 | // write(STDOUT_FILENO, "\r\n", 2); 337 | } 338 | } 339 | } 340 | 341 | void editorRefreshScreen() { 342 | // write(STDOUT_FILENO, "\x1b[2J", 4); 343 | // write(STDOUT_FILENO, "\x1b[H", 3); 344 | verticalScroll(); 345 | struct abuf ab = ABUF_INIT; 346 | 347 | // hide the cursor before drawing screen 348 | abAppend(&ab, "\x1b[?25l", 6); 349 | 350 | // the below commented line clears the whole screen 351 | // abAppend(&ab, "\x1b[2J", 4); 352 | 353 | abAppend(&ab, "\x1b[H", 3); 354 | // drawing screen 355 | editorDrawRows(&ab); 356 | 357 | // postions the cursor at current cx, cy 358 | char buf[32]; 359 | snprintf(buf, sizeof(buf), "\x1b[%d;%dH", E.cy+1, E.cx+1); 360 | abAppend(&ab, buf, strlen(buf)); 361 | 362 | // show the cursor after drawing screen 363 | abAppend(&ab, "\x1b[?25h", 6); 364 | 365 | write(STDOUT_FILENO, ab.b, ab.len); 366 | abFree(&ab); 367 | } 368 | 369 | /*** INIT ***/ 370 | void initEditor() { 371 | E.cx = E.cy = 0; 372 | E.row = NULL; 373 | E.rowOffset = 0; 374 | E.numRows = 0; // TODO : make this dynamic; increment as per lines 375 | if (getWindowSize(&E.screenrows, &E.screencols) == -1) 376 | die("getWindowSize"); 377 | } 378 | 379 | int main(int argc, char *argv[]){ 380 | enterRawMode(); 381 | initEditor(); 382 | if(argc >= 2){ 383 | editorFileOpen(argv[1]); 384 | } 385 | while (1) { 386 | editorRefreshScreen(); 387 | editorKeyPress(); 388 | } 389 | 390 | return 0; 391 | } 392 | -------------------------------------------------------------------------------- /text.txt: -------------------------------------------------------------------------------- 1 | Hi, Yash here! 2 | Good to see you staring at the repo :p 3 | --------------------------------------------------------------------------------