{"id":21945849,"url":"https://github.com/mattduck/kilo","last_synced_at":"2026-03-03T18:02:37.918Z","repository":{"id":69133123,"uuid":"234915447","full_name":"mattduck/kilo","owner":"mattduck","description":"My implementation of the kilo text editor","archived":false,"fork":false,"pushed_at":"2021-03-28T15:17:51.000Z","size":195,"stargazers_count":38,"open_issues_count":0,"forks_count":5,"subscribers_count":3,"default_branch":"master","last_synced_at":"2025-04-22T21:47:33.108Z","etag":null,"topics":["c","kilo"],"latest_commit_sha":null,"homepage":null,"language":"C","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/mattduck.png","metadata":{"files":{"readme":"README.org","changelog":"history.c","contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2020-01-19T14:44:06.000Z","updated_at":"2025-04-22T03:16:43.000Z","dependencies_parsed_at":null,"dependency_job_id":"8551220a-fd92-493b-83c1-6e8371d00c39","html_url":"https://github.com/mattduck/kilo","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/mattduck/kilo","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mattduck%2Fkilo","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mattduck%2Fkilo/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mattduck%2Fkilo/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mattduck%2Fkilo/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/mattduck","download_url":"https://codeload.github.com/mattduck/kilo/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mattduck%2Fkilo/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":30054003,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-03-03T17:46:22.538Z","status":"ssl_error","status_checked_at":"2026-03-03T17:46:22.036Z","response_time":61,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["c","kilo"],"created_at":"2024-11-29T04:19:44.980Z","updated_at":"2026-03-03T18:02:37.908Z","avatar_url":"https://github.com/mattduck.png","language":"C","funding_links":[],"categories":[],"sub_categories":[],"readme":"*Disclaimer*\n\nThis project is something fun that I did at the start of 2020. It might be\nuseful to help understand how you can build new features on top of Kilo, but\nplease read the changes with scepticism as they haven't been thoroughly\ntested. In particular, the basic undo/redo implementation that I started writing\nis full of memory leaks and makes this code unusable as a real editor (see\nhttps://github.com/mattduck/kilo/issues/1 for more details).\n\n* Kilo\n\nThis is my implementation of the Kilo text editor, written by following [[https://viewsourcecode.org/snaptoken/kilo/index.html][Build\nyour own text editor]].\n\nIt was initially written as an org-mode file, as an exercise for me to learn a\nbit more about writing terminal applications with C, and to see whether the\nliterate programming approach with org-mode is useful.\n\nOverall I think embedding the code in this file actually made it harder to keep\nthe overall structure in my head as I went, because I was only operating on\nindividuals parts at a time. Next time I will just write notes separately.\n\nI've now renamed the org-mode version to ~kilo-org.c~. For future edits I'll work\non ~kilo.c~ directly.\n\n* New features\n\nI've extended ~kilo.c~ with a few things that I'm used to from vim/emacs:\n\n- Splitting user input into ~normal~ and ~insert~ modes.\n- Word-based cursor movement that is normally found with ~w/W/b/B~\n- A new prompt to simulate ~:wq~ and ~:q!~.\n- Standard cursor movement with ~hjkl~, ~^/$~, ~C-f/C-b~, ~gg~ and ~G~.\n- Using ~dd~ to remove lines, and ~J~ to join lines.\n- Adding the ~jj~ and ~jk~ bindings that I use in ~insert~ mode to exit to ~normal~ mode\n  (which means waiting for a follow-up key to ~j~, and inserting it into the row\n  if it doesn't come after a set timeout).\n\n* Compile with org-mode\n\nThis just concatenates all the C snippets to ~kilo.c~, and then runs ~make~.\n\n#+begin_src emacs-lisp :results silent\n  (interactive)\n  (setq-local org-confirm-babel-evaluate nil)\n  (org-babel-tangle nil \"kilo-org.c\" \"c\")\n  (compile \"make\")\n#+end_src\n\n* Code\n** Feature test macros\n\nThere are various macros that you can define that control what features are\navailable to the compiler. There is more info in the [[https://www.gnu.org/software/libc/manual/html_node/Feature-Test-Macros.html][GNU libc\ndocumentation]]. Some are added in step 59, to remove a warning about implicit\ndeclaration of ~getline()~.\n\n#+begin_src c :results silent\n# define _DEFAULT_SOURCE\n# define _BSD_SOURCE\n# define _GNU_SOURCE\n#+end_src\n\n** Includes\n\n#+begin_src c\n  #include \u003cctype.h\u003e\n  #include \u003cerrno.h\u003e\n  #include \u003cfcntl.h\u003e\n  #include \u003cstdio.h\u003e\n  #include \u003cstdarg.h\u003e\n  #include \u003cstdlib.h\u003e\n  #include \u003cstring.h\u003e\n  #include \u003csys/ioctl.h\u003e\n  #include \u003csys/types.h\u003e\n  #include \u003ctime.h\u003e\n  #include \u003ctermios.h\u003e\n  #include \u003cunistd.h\u003e\n#+end_src\n\n** Constants\n\n#+begin_src c\n  #define KILO_VERSION \"0.0.1\"\n  #define KILO_TAB_STOP 4\n  #define KILO_QUIT_TIMES 2\n#+end_src\n\nSome of these macros (like ~CTRL_KEY~ below) take a parameter, similar to\nfunctions. The main advantage of doing this is that the preprocessor replaces\nthe template so there's no stack or function call needed. There are downsides\ntoo: if you have a lot of macros it can increase the binary size, and they're\nlimited because they're not functions - you can't return a parameter, you can't\ndo recursion, etc.\n\nIn ASCII, the CTRL character strips bits 5 and 6 from whatever key you\npress. For example, ~h~ is 01101000, and ~C-h~ is 00001000. We define this below:\n\n#+begin_src c\n  #define CTRL_KEY(k) ((k) \u0026 0x1F)\n#+end_src\n\n#+begin_src c\n  enum editorKey {\n                  BACKSPACE = 127,\n                  ARROW_LEFT = 1000,\n                  ARROW_RIGHT,\n                  ARROW_UP,\n                  ARROW_DOWN,\n                  DEL_KEY,\n                  HOME_KEY,\n                  END_KEY,\n                  PAGE_UP,\n                  PAGE_DOWN\n  };\n\n  enum editorHighlight {\n                        HL_NORMAL = 0,\n                        HL_COMMENT,\n                        HL_MLCOMMENT,\n                        HL_KEYWORD1,\n                        HL_KEYWORD2,\n                        HL_STRING,\n                        HL_NUMBER,\n                        HL_MATCH\n  };\n#+end_src\n\n#+begin_src c\n  #define HL_HIGHLIGHT_NUMBERS (1\u003c\u003c0)\n  #define HL_HIGHLIGHT_STRINGS (1\u003c\u003c1)\n#+end_src\n\n** State\n\nThe global editor state is stored in ~editorConfig~. This stores data like the\ncursor position, screen offset, size of the terminal, whether the buffer has\nbeen modified, the associated filename, etc. It also contains some setup and\nteardown data (like the properties of the user's terminal),\n\n~erow~ represents a single line of text. User input results in a lot of mutation\nof ~editorConfig~, particularly the rows.\n\n~editorSyntax~ OTOH just contains information associated with a particular\nfiletype, and is not affected by user input. The buffer can be associated with a\nsingle ~editorSyntax~ struct.\n\n#+begin_src c\n  struct editorSyntax {\n    char *filetype;\n    char **filematch;\n    char **keywords;\n    char *singleline_comment_start;\n    char *multiline_comment_start;\n    char *multiline_comment_end;\n    int flags;\n  };\n\n  typedef struct erow {\n    int idx;  // which row in the buffer it represents\n    int size;  // the row length, excluding the null byte at the end.\n    char *chars;  // the characters in the line\n    int rsize; // the length of the \"rendered\" line, where eg. \\t will expand to n spaces\n    char *render;  // the \"rendered\" characters in the line\n    unsigned char *hl;  // the highlight property of a character\n    int hl_open_comment;  // whether this line begins or is part of a multiline comment\n  } erow;\n\n  struct editorConfig {\n    int cx, cy;  // cursor\n    int rx;  // render index, as some chars are multi-width (eg. tabs)\n    int rowoff; // file offset\n    int coloff; // same as above\n    int screenrows; // size of the terminal\n    int screencols; // size of the terminal\n    int numrows;  // size of the buffer\n    erow *row;  // current row\n    int dirty;  // is modified?\n    char *filename;  // name of file linked to the buffer\n    char statusmsg[80];  // status message displayed on at bottom of buffer\n    time_t statusmsg_time;  // how long ago status message was written\n    struct editorSyntax *syntax;  // the syntax rules that apply to the buffer\n    struct termios orig_termios;  // the terminal state taken at startup; used to restore on exit\n  };\n\n  struct editorConfig E;  // the global state\n#+end_src\n\n** Filetypes\n\nThe tutorial specifies an entry for C:\n\n#+begin_src c\n  char *C_HL_extensions[] = { \".c\", \".h\", \".cpp\", NULL };\n  char *C_HL_keywords[] = {\n    \"switch\", \"if\", \"while\", \"for\", \"break\", \"continue\", \"return\", \"else\",\n    \"struct\", \"union\", \"typedef\", \"static\", \"enum\", \"class\", \"case\",\n    \"int|\", \"long|\", \"double|\", \"float|\", \"char|\", \"unsigned|\", \"signed|\",\n    \"void|\", NULL\n  };\n\n  struct editorSyntax HLDB[] = {\n                                {\"c\",\n                                 C_HL_extensions,\n                                 C_HL_keywords,\n                                 \"//\", \"/*\", \"*/\",\n                                 HL_HIGHLIGHT_NUMBERS | HL_HIGHLIGHT_STRINGS\n                                },\n  };\n\n  #define HLDB_ENTRIES (sizeof(HLDB) / sizeof(HLDB[0]))\n#+end_src\n\n** Exiting\n\nMost C library functions that fail set the global ~errno~. ~perror()~ looks at this\nand prints a descriptive message for it - for example, \"inappropriate ioctl for\ndevice\".\n\n#+begin_src c\n  void die(const char *s) {\n    write(STDOUT_FILENO, \"\\x1b[2J\", 4);  // clear screen\n    write(STDOUT_FILENO, \"\\x1b[H\", 3);  // reposition cursor\n    perror(s);\n    exit(1);\n  }\n#+end_src\n\n** Prototypes\n\nC compiles in a single pass, so you can't always call functions that aren't\ndefined yet. We can define the signature though. These are the few functions\nthat are required:\n\n#+begin_src c\n  void editorSetStatusMessage(const char *fmt, ...);\n  void editorRefreshScreen();\n  char *editorPrompt(char *prompt, void (*callback)(char *, int));\n#+end_src\n\n** Append buffer\n\nRather than calling ~write()~ regularly to modify the terminal output, we instead\nbuffer everything in ~abuf~, and only write to the terminal once our update is\ncomplete. This reduces the number of updates, can prevent screen flickering,\netc.\n\n#+begin_src c\n  struct abuf {\n    char *b;\n    int len;\n  };\n\n  #define ABUF_INIT {NULL, 0}  // Represents an empty buffer\n\n  void abAppend(struct abuf *ab, const char *s, int len) {\n    // Get a block of memory that is the size of the current string, plus the\n    // string we're appending.\n    char *new = realloc(ab-\u003eb, ab-\u003elen + len);\n\n    if (new == NULL) return;\n    memcpy(\u0026new[ab-\u003elen], s, len);  // copy \"s\" after the current data\n    ab-\u003eb = new;\n    ab-\u003elen += len;\n  }\n\n  void abFree(struct abuf *ab) {\n    free(ab-\u003eb);\n  }\n#+end_src\n\n** Terminal\n\nThere are a few functions here that just get information from the\nterminal. ~editorReadKey()~ translates ANSI codes into an ~editorKey()~ enum:\n\n#+begin_src c\n  int editorReadKey() {\n    int nread;\n    char c;\n    // read() returns the number of bytes read\n    while ((nread = read(STDIN_FILENO, \u0026c, 1)) != 1) {\n      if (nread == -1 \u0026\u0026 errno != EAGAIN) die(\"read\");\n    }\n\n    if (c == '\\x1b') {\n      char seq[3];\n      if (read(STDIN_FILENO, \u0026seq[0], 1) != 1) return '\\x1b';\n      if (read(STDIN_FILENO, \u0026seq[1], 1) != 1) return '\\x1b';\n      if (seq[0] == '[') {\n\n        // Page up / down, which are represented by \\x1b[5~ and \\x1b[6~\n        if (seq[1] \u003e= '0' \u0026\u0026 seq[1] \u003c= '9') {\n          if (read(STDIN_FILENO, \u0026seq[2], 1) != 1) return '\\x1b';\n          if (seq[2] == '~') {\n            switch (seq[1]) {\n            case '1': return HOME_KEY;\n            case '3': return DEL_KEY;\n            case '4': return END_KEY;\n            case '5': return PAGE_UP;\n            case '6': return PAGE_DOWN;\n            case '7': return HOME_KEY;\n            case '8': return END_KEY;\n            }\n          }\n        } else {\n\n          // Arrows\n          switch (seq[1]) {\n          case 'A': return ARROW_UP;\n          case 'B': return ARROW_DOWN;\n          case 'C': return ARROW_RIGHT;\n          case 'D': return ARROW_LEFT;\n          case 'H': return HOME_KEY;\n          case 'F': return END_KEY;\n          }\n        }\n      } else if (seq[0] == '0') {\n        switch (seq[1]) {\n        case 'H': return HOME_KEY;\n        case 'F': return END_KEY;\n        }\n      }\n      return '\\x1b';\n    } else {\n      return c;\n    }\n  }\n#+end_src\n\nControl characters are prefixed by ESC. If we read ESC, immediately read two\nmore bytes into ~seq~. If the reads timeout, then assume the user just pressed\nescape.\n\n~getCursorPosition~ below doesn't really need to exist for me. It is only used in\n~getWindowSize~ if ~TIOCGWINSZ~ isn't supported by the terminal.\n\n#+begin_src c\n  int getCursorPosition (int *rows, int *cols) {\n    char buf[32];\n    unsigned int i = 0;\n    // 6n (in the line below) asks for the cursor position. 6 is a function that\n    // queries for terminal status info.\n    if (write(STDOUT_FILENO, \"\\x1b[6n\", 4) != 4) return -1;\n    while (i \u003c sizeof(buf) -1){\n      if (read(STDIN_FILENO, \u0026buf[i], 1) != 1) break;\n      if (buf[i] == 'R') break;\n      i++;\n    }\n    buf[i] = '\\0';  // printf expects strings to end with a 0 byte\n\n    if (buf[0] != '\\x1b' || buf[1] != '[') return -1;\n\n    // sscanf will parse out two integers (\"%d;%d\") and put them into rows/cols.\n    if (sscanf(\u0026buf[2], \"%d;%d\", rows, cols) != 2) return -1;\n\n    printf(\"\\r\\n\u0026buf[1]: '%s'\\r\\n\", \u0026buf[1]);\n    editorReadKey();\n    return -1;\n  }\n#+end_src\n\n#+begin_src c\n  int getWindowSize(int *rows, int *cols) {\n    struct winsize ws;\n    if (ioctl(STDOUT_FILENO, TIOCGWINSZ, \u0026ws) == -1 || ws.ws_col == 0) {\n    // ~C~ is cursor forward, and ~B~ is cursor down. We assume that 999 is a large\n    // enough value to position to the bottom right.\n      if (write(STDOUT_FILENO, \"\\x1b[999C\\x1b[999B\", 12) != 12) return -1;\n      return getCursorPosition(rows, cols);\n    } else {\n      ,*cols = ws.ws_col;\n      ,*rows = ws.ws_row;\n      return 0;\n    }\n  }\n#+end_src\n\nTIOCGWINSZ tells the terminal to return the window size. We check for 0 in the\ncolumn value because \"apparently\" that's a possible outcome.\n\n*** Raw mode\n\n#+begin_src c\n  struct termios orig_termios;\n\n  void disableRawMode() {\n    if (tcsetattr(STDIN_FILENO, TCSAFLUSH, \u0026E.orig_termios) == -1) die(\"tcsetattr\");\n  }\n\n  void enableRawMode() {\n    if (tcgetattr(STDIN_FILENO, \u0026E.orig_termios) == -1) die(\"tcgetatr\");\n    atexit(disableRawMode);\n\n    struct termios raw = E.orig_termios;\n    raw.c_iflag \u0026= ~(BRKINT | ICRNL | INPCK | ISTRIP | IXON);\n    raw.c_oflag \u0026= ~(OPOST);\n    raw.c_cflag |= ~(CS8);\n    raw.c_lflag \u0026= ~(ECHO | ICANON | IEXTEN | ISIG);\n\n    raw.c_cc[VMIN] = 0;\n    raw.c_cc[VTIME] = 1;  // 100ms\n    if (tcsetattr(STDIN_FILENO, TCSAFLUSH, \u0026raw) == -1) die(\"tcsetattr\");\n  }\n#+end_src\n\n- TCSAFLUSH specifies when to apply the ~setattr~ change.\n\n- ECHO is a bitflag - ~\u0026= ~~(ECHO)~ flips the echo bit off\n  (00000000000000000000000000001000). We also do this to the ICANON flag, which\n  disables canonical mode, making us read one byte at a time rather than reading\n  the whole line when enter is pressed.\n\n  IEXTEN controls ~C-v~, and ISIG controls the ~C-c~ and ~C-z~ signals.\n\n  IXON controls ~C-s~ and ~C-q~, and ICRNL controls a feature where ~\\r~\n  (character 13) is turned into a newline (character 10).\n\n  OPOST controls some output processing. The main thing we want to disable here\n  (and possibly the only thing enabled by default) is the output translation of\n  ~\\n~ into ~\\r\\n~. The terminal requires these as distinct characters to begin a\n  new line.\n\n- The CS8 line is not a flag, it's a bit mask with multiple bits. Here we set\n  the character size (CS) to 8 bits per byte. This is often a default.\n\n- ~c_lflag~ stores \"local\" flags, which is apparently a dumping ground for a few\n  miscellaneous things. There are also ~iflag~ (input), ~oflag~ (output) and ~clfag~\n  (control flags).\n\n- ~c_cc~ stands for \"control characters\". VMIN sets the minimum number of bytes of\n  input needed before ~read()~ can return - we use 0 so that ~read()~ will return as\n  soon as there's any input to read. VTIME is the timeout value in 10ths of a\n  second.\n** Syntax highlighting\n\nThis is one of the bigger features. ~editorUpdateSyntax~ operates on a single row,\nsetting each column of the ~hl~ array according to that column's syntax\nproperty. When following the steps, we initially only supported syntax state\nwithin a single line. Afterwards the multi-line feature was added.\n\nThis implementation could easily get unwieldy if you wanted to add support for\nmore syntax features, because there's a lot of state to keep track of in the\nmain loop.\n\n#+begin_src c\n  int is_separator(int c) {\n    return isspace(c) || c == '\\0' || strchr(\",.()+-/*=~%\u003c\u003e[];\", c) != NULL;\n  }\n\n  void editorUpdateSyntax(erow *row) {\n    // The hl array is the same size as the render array\n    row-\u003ehl = realloc(row-\u003ehl, row-\u003ersize);\n    memset(row-\u003ehl, HL_NORMAL, row-\u003ersize);\n\n    if (E.syntax == NULL) return;\n\n    char **keywords = E.syntax-\u003ekeywords;\n\n    char *scs = E.syntax-\u003esingleline_comment_start;\n    char *mcs = E.syntax-\u003emultiline_comment_start;\n    char *mce = E.syntax-\u003emultiline_comment_end;\n\n    int scs_len = scs ? strlen(scs) : 0;\n    int mcs_len = mcs ? strlen(mcs) : 0;\n    int mce_len = mce ? strlen(mce) : 0;\n\n    int prev_sep = 1; // beginning of line can be considered a separator\n    int in_string = 0;  // we store the string char in here so we know when it closes\n    int in_comment = (row-\u003eidx \u003e 0 \u0026\u0026 E.row[row-\u003eidx - 1].hl_open_comment);\n\n    int i = 0;\n    while (i \u003c row-\u003esize) {\n      char c = row-\u003erender[i];\n      unsigned char prev_hl = (i \u003e 0) ? row-\u003ehl[i - 1] : HL_NORMAL;\n\n      // single line comments\n      if (scs_len \u0026\u0026 !in_string \u0026\u0026 !in_comment) {\n        if (!strncmp(\u0026row-\u003erender[i], scs, scs_len)) {\n            memset(\u0026row-\u003ehl[i], HL_COMMENT, row-\u003ersize - i);\n            break;\n        }\n      }\n\n      // multiline comments\n      if (mcs_len \u0026\u0026 mce_len \u0026\u0026 !in_string){\n        if (in_comment) {\n          row-\u003ehl[i] = HL_MLCOMMENT; // highlight\n          if (!strncmp(\u0026row-\u003erender[i], mce, mce_len)) { // match end?\n            memset(\u0026row-\u003ehl[i], HL_MLCOMMENT, mce_len);  // highlight end token\n            i += mce_len;\n            in_comment = 0;\n            prev_sep = 1;\n            continue;\n          } else {\n            i++;\n            continue;\n          }\n        } else if (!strncmp(\u0026row-\u003erender[i], mcs, mcs_len)) { // match multiline start?\n          memset(\u0026row-\u003ehl[i], HL_MLCOMMENT, mcs_len);  // highlight the start token\n          i += mcs_len;\n          in_comment = 1;\n          continue;\n        }\n      }\n\n      if (E.syntax-\u003eflags \u0026 HL_HIGHLIGHT_STRINGS) {\n        if (in_string) {\n          row-\u003ehl[i] = HL_STRING;\n          // backslashes should keep this as a string\n          if (c == '\\\\' \u0026\u0026 i + 1 \u003c row-\u003ersize) {\n            row-\u003ehl[i+1] = HL_STRING;\n            i += 2;\n            continue;\n          }\n\n          if (c == in_string) in_string = 0;  // this is the closing quote\n          i ++;\n          prev_sep = 1;\n          continue;\n        } else {\n          if (c == '\"' || c == '\\''){\n            in_string = c;\n            row-\u003ehl[i] = HL_STRING;\n            i++;\n            continue;\n          }\n        }\n      }\n\n      if (E.syntax-\u003eflags \u0026 HL_HIGHLIGHT_NUMBERS) {\n        if ((isdigit(c) \u0026\u0026 (prev_sep || prev_hl == HL_NUMBER)) ||\n            (c == '.' \u0026\u0026 prev_hl == HL_NUMBER)) {  // support if number is a decimal\n          row-\u003ehl[i] = HL_NUMBER;\n          i ++;\n          prev_sep = 0;  // it wasn't a separator because we know it was number\n          continue;\n        }\n      }\n\n      if (prev_sep) {\n        int j;\n        for (j = 0; keywords[j]; j++) {\n          int klen = strlen(keywords[j]);\n          int kw2 = keywords[j][klen - 1] == '|';\n          if (kw2) klen--;\n\n          if (!strncmp(\u0026row-\u003erender[i], keywords[j], klen) \u0026\u0026\n              is_separator(row-\u003erender[i + klen])) {\n            memset(\u0026row-\u003ehl[i], kw2 ? HL_KEYWORD2 : HL_KEYWORD1, klen);\n            i += klen;\n            break;\n          }\n        }\n        if (keywords[j] != NULL) {\n          prev_sep = 0;\n          continue;\n        }\n      }\n\n      prev_sep = is_separator(c);\n      i++;\n    }\n\n    // set hl_open_comment appropriately\n    int changed = (row-\u003ehl_open_comment != in_comment);\n    row-\u003ehl_open_comment = in_comment;\n    if (changed \u0026\u0026 row-\u003eidx + 1 \u003c E.numrows)\n      // Recursive iteration over the rest of the file as the highlighting may\n      // have changed.\n      editorUpdateSyntax(\u0026E.row[row-\u003eidx + 1]);\n  }\n\n  int editorSyntaxToColor(int hl) {\n    switch (hl) {\n    case HL_COMMENT:\n    case HL_MLCOMMENT: return 36;\n    case HL_KEYWORD1: return 33;\n    case HL_KEYWORD2: return 32;\n    case HL_STRING: return 35;\n    case HL_NUMBER: return 31;\n    case HL_MATCH: return 34;\n    default: return 37;\n    }\n  }\n\n  void editorSelectSyntaxHighlight() {\n    /*Sets E.syntax based on E.filename */\n    E.syntax = NULL;\n    if (E.filename == NULL) return;\n    char *ext = strchr(E.filename, '.');\n    for (unsigned int j = 0; j \u003c HLDB_ENTRIES; j++) {\n      struct editorSyntax *s = \u0026HLDB[j];\n      unsigned int i = 0;\n      while (s-\u003efilematch[i]){\n        int is_ext = (s-\u003efilematch[i][0] == '.');\n        if ((is_ext \u0026\u0026 !strcmp(ext, s-\u003efilematch[i])) ||\n            (!is_ext \u0026\u0026 strstr(E.filename, s-\u003efilematch[i]))) {\n          E.syntax = s;\n\n          int filerow;\n          for (filerow = 0; filerow \u003c E.numrows; filerow++) {\n            editorUpdateSyntax(\u0026E.row[filerow]);\n          }\n\n        }\n        i++;\n      }\n    }\n  }\n#+end_src\n\n** Row operations\n\nThese functions operate on rows - eg. to insert a row in the buffer, or insert a\ncharacter into a row. They do /not/ operate on the cursor position or the file\noffset.\n\nTranslation between Cx\u003c-\u003eRx below is quite simple because there is only one character\nsupported (tab). Having to hard-code every translation isn't ideal though.\n\n#+begin_src c\n  int editorRowCxToRx(erow *row, int cx) {\n    int rx = 0;\n    int j;\n    for (j=0; j\u003ccx; j++) {\n      if (row-\u003echars[j] == '\\t')\n        rx += (KILO_TAB_STOP - 1) - (rx % KILO_TAB_STOP);\n      rx++;\n    }\n    return rx;\n  }\n\n  int editorRowRxToCx(erow *row, int rx) {\n    // For a given row, converts the given rx value to the corresponding cx\n    int cur_rx = 0;\n    int cx;\n    for (cx = 0; cx \u003c row-\u003esize; cx++) {\n      if (row-\u003echars[cx] == '\\t')\n        cur_rx += (KILO_TAB_STOP - 1) - (cur_rx % KILO_TAB_STOP);\n      cur_rx++;\n      if (cur_rx \u003e rx) return cx;\n    }\n    return cx;\n  }\n#+end_src\n\n#+begin_src c\n\n  void editorUpdateRow(erow *row) {\n    int tabs = 0;\n    int j;\n    for (j = 0; j \u003c row-\u003esize; j++) {\n      if (row-\u003echars[j] == '\\t') tabs++;\n    }\n\n    free(row-\u003erender);\n    row-\u003erender = malloc(row-\u003esize + tabs*(KILO_TAB_STOP - 1) + 1);\n\n    int idx =0;\n    for (j = 0; j \u003c row-\u003esize; j++) {\n      if (row-\u003echars[j] == '\\t') {\n        // insert spaces until the next % 8 is hit.\n        row-\u003erender[idx++] = ' ';\n        while (idx % KILO_TAB_STOP != 0) row-\u003erender[idx++] = ' ';\n      } else {\n        // Print the character\n        row-\u003erender[idx++] = row-\u003echars[j];\n      }\n    }\n    row-\u003erender[idx] = '\\0';\n    row-\u003ersize = idx; // idx contains the number of characters we copied into row-\u003erender\n\n    editorUpdateSyntax(row);\n  }\n\n  void editorInsertRow(int at, char *s, size_t len) {\n    if (at \u003c 0 || at \u003e E.numrows) return;\n\n    E.row = realloc(E.row, sizeof(erow) * (E.numrows + 1));\n    memmove(\u0026E.row[at + 1], \u0026E.row[at], sizeof(erow) * (E.numrows - at));\n    for (int j = at + 1; j \u003c= E.numrows; j++) E.row[j].idx++;\n\n    E.row[at].idx = at;\n\n    E.row[at].size = len;\n    E.row[at].chars = malloc(len + 1);\n    memcpy(E.row[at].chars, s, len);\n    E.row[at].chars[len] = '\\0';\n\n    E.row[at].rsize = 0;\n    E.row[at].render = NULL;\n    E.row[at].hl = NULL;\n    E.row[at].hl_open_comment = 0;\n    editorUpdateRow(\u0026E.row[at]);\n\n    E.numrows++;\n    E.dirty++;\n  }\n\n  void editorFreeRow(erow *row) {\n    free(row-\u003erender);\n    free(row-\u003echars);\n    free(row-\u003ehl);\n  }\n\n  void editorDelRow(int at) {\n    if (at \u003c 0 || at \u003e= E.numrows) return;\n    editorFreeRow(\u0026E.row[at]);\n    memmove(\u0026E.row[at], \u0026E.row[at + 1], sizeof(erow) * (E.numrows - at - 1));\n    for (int j = at; j \u003c E.numrows - 1; j++) E.row[j].idx--;\n    E.numrows--;\n    E.dirty++;\n  }\n\n  void editorRowInsertChar(erow *row, int at, int c) {\n    if (at \u003c 0 || at \u003e row-\u003esize) at = row-\u003esize; // bounds\n    row-\u003echars = realloc(row-\u003echars, row-\u003esize + 2); // the new character + null byte\n    // shift later chars along\n    memmove(\u0026row-\u003echars[at + 1], \u0026row-\u003echars[at], row-\u003esize - at + 1);\n    row-\u003esize++;\n    row-\u003echars[at] = c;\n    editorUpdateRow(row);\n    E.dirty++;\n  }\n\n  void editorRowAppendString(erow *row, char *s, size_t len) {\n    row-\u003echars = realloc(row-\u003echars, row-\u003esize + len + 1);\n    memcpy(\u0026row-\u003echars[row-\u003esize], s, len);\n    row-\u003esize += len;\n    row-\u003echars[row-\u003esize] = '\\0';\n    editorUpdateRow(row);\n    E.dirty++;\n  }\n\n  void editorRowDelChar(erow *row, int at) {\n    if (at \u003c 0 || at \u003e= row-\u003esize) return;\n    memmove(\u0026row-\u003echars[at], \u0026row-\u003echars[at + 1], row-\u003esize - at);\n    row-\u003esize--;\n    editorUpdateRow(row);\n    E.dirty++;\n  }\n#+end_src\n\n** Editor operations\n\nThese are more user-focused operations that can perform row operations but also\nmanaged the cursor at the same time. They do /not/ manage the file offset though.\n\n#+begin_src c\n  void editorInsertChar(int c){\n    if (E.cy == E.numrows) { // the cursor is on the tilde after the last line\n      editorInsertRow(E.numrows, \"\", 0);\n    }\n    editorRowInsertChar(\u0026E.row[E.cy], E.cx, c);\n    E.cx++;\n  }\n\n  void editorInsertNewline() {\n    if (E.cx == 0) {\n      editorInsertRow(E.cy, \"\", 0);\n    } else {\n      erow *row = \u0026E.row[E.cy];\n      editorInsertRow(E.cy + 1, \u0026row-\u003echars[E.cx], row-\u003esize - E.cx);\n      row = \u0026E.row[E.cy];\n      row-\u003esize = E.cx;\n      row-\u003echars[row-\u003esize] = '\\0';\n      editorUpdateRow(row);\n    }\n    E.cy++;\n    E.cx=0;\n  }\n\n  void editorDelChar() {\n    if (E.cy == E.numrows) return;\n    if (E.cx == 0 \u0026\u0026 E.cy == 0) return;\n\n    erow *row = \u0026E.row[E.cy];\n    if (E.cx \u003e 0) {\n      editorRowDelChar(row, E.cx -1);\n      E.cx--;\n    } else {\n      E.cx = E.row[E.cy - 1].size;\n      editorRowAppendString(\u0026E.row[E.cy - 1], row-\u003echars, row-\u003esize);\n      editorDelRow(E.cy);\n      E.cy--;\n    }\n  }\n#+end_src\n\n** File I/O\n\n#+begin_src c\n  char *editorRowsToString(int *buflen) {\n    int totlen = 0;\n    int j;\n    for (j=0; j \u003c E.numrows; j++)\n      totlen += E.row[j].size + 1; // + 1 for newline\n    *buflen = totlen; // so the caller can inspect how long the string is\n\n    char *buf = malloc(totlen);\n    char *p = buf;\n    for (j=0; j\u003cE.numrows; j++) {\n      memcpy(p, E.row[j].chars, E.row[j].size);\n      p += E.row[j].size;\n      ,*p = '\\n';\n      p++;\n    }\n\n    return buf;\n  }\n#+end_src\n\n#+begin_src c\n  void editorOpen(char *filename) {\n    free(E.filename);\n    E.filename = strdup(filename); // copies the given string to new memory loc.\n\n    editorSelectSyntaxHighlight();\n\n    FILE *fp = fopen(filename, \"r\");\n    if (!fp) die(\"fopen\");\n\n    char *line = NULL;\n    size_t linecap = 0;\n    ssize_t linelen;\n    while ((linelen = getline(\u0026line, \u0026linecap, fp)) != -1) { // iterate over lines\n      while (linelen \u003e 0 \u0026\u0026 (line[linelen -1] == '\\n' || line[linelen -1] == '\\r'))\n        linelen--;\n      editorInsertRow(E.numrows, line, linelen);\n    }\n    free(line);\n    fclose(fp);\n    E.dirty = 0;\n  }\n\n  void editorSave() {\n    if (E.filename == NULL) {\n      E.filename = editorPrompt(\"Save as: %s (ESC to cancel)\", NULL);\n      if (E.filename == NULL) {\n        editorSetStatusMessage(\"Save aborted\");\n        return;\n      }\n      editorSelectSyntaxHighlight();\n    }\n\n    int len;\n    char *buf = editorRowsToString(\u0026len);\n\n    int fd = open(E.filename, O_RDWR | O_CREAT, 0644);\n    if (fd != -1) {\n      if (ftruncate(fd, len) != -1) {\n        if (write(fd, buf, len) == len) {\n          close(fd);\n          free(buf);\n          E.dirty = 0;\n          editorSetStatusMessage(\"%d bytes written to disk\", len);\n          return;\n        }\n      }\n      close(fd);\n    }\n    free(buf);\n    editorSetStatusMessage(\"Can't save! I/O error: %s\", strerror(errno));\n  }\n#+end_src\n\n- ~getline()~ can be used to read lines from a file when we don't know how much\n  memory to allocate for each line. It allocates memory for the next line it\n  reads, and sets the second argument to point to that memory. You can then feed\n  it the pointer back, to try to reuse the memory next time you use ~getline()~.\n\n- We strip out the newline and CR before copying it into erow - we know that\n  every erow represents a single line of text, so we don't need to actually\n  store those characters at the end.\n\n** Search\n\nSearch is implemented using the prompt. It loops through all the rows in the\nfile, uses ~strstr()~ to see if there is a substring match, and then if so scrolls\nand moves the cursor to the row.\n\n#+begin_src c\n  void editorFindCallback(char *query, int key) {\n    static int last_match = -1;\n    static int direction = 1;\n\n    static int saved_hl_line;\n    static char *saved_hl = NULL;\n\n    if (saved_hl) {\n      memcpy(E.row[saved_hl_line].hl, saved_hl, E.row[saved_hl_line].rsize);\n      free(saved_hl);\n      saved_hl = NULL;\n    }\n\n    if (key == '\\r' || key == '\\x1b') {\n      last_match = -1;\n      direction = 1;\n      return;\n    } else if (key == ARROW_RIGHT || key == ARROW_DOWN) {\n      direction = 1;\n    } else if (key == ARROW_LEFT || key == ARROW_UP) {\n      direction = -1;\n    } else {\n      last_match = -1;\n      direction = 1;\n    }\n\n    if (last_match == -1) direction = 1;\n    int current = last_match;\n    int i;\n    for (i = 0; i \u003c E.numrows; i++) {\n      current += direction;\n\n      // loops around the file\n      if (current == -1) current = E.numrows - 1;\n      else if (current == E.numrows) current = 0;\n\n      erow *row = \u0026E.row[current];\n      char *match = strstr(row-\u003erender, query);\n      if (match) {\n        last_match = current;\n        E.cy = current;\n        E.cx = editorRowRxToCx(row, match - row-\u003erender);\n        E.rowoff = E.numrows;\n\n        saved_hl_line = current;\n        saved_hl = malloc(row-\u003ersize);\n        memcpy(saved_hl, row-\u003ehl, row-\u003ersize);\n        memset(\u0026row-\u003ehl[match - row-\u003erender], HL_MATCH, strlen(query));\n        break;\n      }\n    }\n  }\n\n  void editorFind(){\n    int saved_cx = E.cx;\n    int saved_cy = E.cy;\n    int saved_coloff = E.coloff;\n    int saved_rowoff = E.rowoff;\n\n    char *query = editorPrompt(\"Search: %s (ESC/Arrows/Enter)\", editorFindCallback);\n    if (query) {\n      free(query);\n    } else { // NULL query means they pressed ESC.\n      E.cx = saved_cx;\n      E.cy = saved_cy;\n      E.coloff = saved_coloff;\n      E.rowoff = saved_rowoff;\n    }\n  }\n#+end_src\n** Output\n\nThere are a few functions here that handle drawing the terminal output,\nscrolling,  refreshing the screen, drawing the status bar, etc.\n\n#+begin_src c\n  void editorScroll() {\n    E.rx = 0;\n    if (E.cy \u003c E.numrows) {\n      E.rx = editorRowCxToRx(\u0026E.row[E.cy], E.cx);\n    }\n    if (E.cy \u003c E.rowoff) { // is the cursor above the visible window?\n      E.rowoff = E.cy;\n    }\n    if (E.cy \u003e= E.rowoff + E.screenrows) {\n      E.rowoff = E.cy - E.screenrows + 1;\n    }\n    if (E.rx \u003c E.coloff) {\n      E.coloff = E.rx;\n    }\n    if (E.rx \u003e= E.coloff + E.screencols) {\n      E.coloff = E.rx - E.screencols + 1;\n    }\n  }\n#+end_src\n\n#+begin_src c\n  void editorDrawRows(struct abuf *ab) {\n    int y;\n    for (y = 0; y \u003c E.screenrows; y++) {\n      int filerow = y + E.rowoff;\n      if (filerow \u003e= E.numrows) {\n        // Draw things that come after the rows\n        if (E.numrows == 0 \u0026\u0026 y == E.screenrows / 3) {\n          char welcome[80];\n          int welcomelen = snprintf(welcome, sizeof(welcome),\n                                    \"Kilo editor -- version %s\", KILO_VERSION);\n          if (welcomelen \u003e E.screencols) welcomelen = E.screencols;\n          // Add spaces for padding to center the welcome message\n          int padding = (E.screencols - welcomelen) / 2;\n          if (padding) {\n            abAppend(ab, \"~\", 1);\n            padding--;\n          }\n          while (padding--) abAppend(ab, \" \", 1);\n          abAppend(ab, welcome, welcomelen);\n        } else {\n          abAppend(ab, \"~\", 1);\n        }\n      } else {\n        // Draw the row\n        int len = E.row[filerow].rsize - E.coloff;\n        if (len \u003c 0) len = 0;\n        if (len \u003e E.screencols) len = E.screencols;  // Truncate the len\n        char *c = \u0026E.row[filerow].render[E.coloff];\n        unsigned char *hl = \u0026E.row[filerow].hl[E.coloff];\n        int j;\n        int current_color = -1; // keep track of colour to keep number of resets down\n        for (j=0; j\u003clen; j++){\n          // control characters\n          if (iscntrl(c[j])) {\n            char sym = (c[j] \u003c= 26) ? '@' + c[j] : '?';\n            abAppend(ab, \"\\x1b[7m\", 4); // invert colours\n            abAppend(ab, \u0026sym, 1);\n            abAppend(ab, \"\\x1b[m\", 3);  // reset\n            if (current_color != -1) {\n              char buf[16];\n              int clen = snprintf(buf, sizeof(buf), \"\\x1b[%dm\", current_color);\n              abAppend(ab, buf, clen);\n            }\n\n          } else if (hl[j] == HL_NORMAL) {\n            if (current_color != -1) {\n              abAppend(ab, \"\\x1b[39m\", 5);\n              current_color = -1;\n            }\n            abAppend(ab, \u0026c[j], 1);\n          } else {\n            int color = editorSyntaxToColor(hl[j]);\n            if (color != current_color) {\n              current_color = color;\n              char buf[16];\n              int clen = snprintf(buf, sizeof(buf), \"\\x1b[%dm\", color);\n              abAppend(ab, buf, clen);\n            }\n            abAppend(ab, \u0026c[j], 1);\n          }\n        }\n        abAppend(ab, \"\\x1b[39m\", 5); // reset at end of line\n      }\n      abAppend(ab, \"\\x1b[K\", 3);  // clear the rest of the row before drawing\n      abAppend(ab, \"\\r\\n\", 2);  // this means there's always an empty row at the\n                                // bottom of the screen\n    }\n  }\n#+end_src\n\n~filerow~ above represents the offset row, whereas ~y~ represents the absolute\nrow.\n\n#+begin_src c\n  void editorDrawStatusBar(struct abuf *ab) {\n    abAppend(ab, \"\\x1b[7m\", 4);\n    char status[80], rstatus[80];\n    int len = snprintf(status, sizeof(status), \"%.20s - %d lines %s\",\n                       E.filename ? E.filename : \"[No Name]\", E.numrows,\n                       E.dirty ? \"(modified)\" : \"\");\n    int rlen = snprintf(rstatus, sizeof(rstatus), \"%s | %d/%d\",\n                        E.syntax ? E.syntax-\u003efiletype : \"no ft\", E.cy + 1, E.numrows);\n    if (len \u003e E.screencols) len = E.screencols; // bounds\n    abAppend(ab, status, len);\n    while (len \u003c E.screencols) {\n      if (E.screencols - len == rlen) { // The starting column index to start\n                                        // printing rstatus\n        abAppend(ab, rstatus, rlen);\n        break;\n      } else {\n        abAppend(ab, \" \", 1);\n        len++;\n      }\n    }\n    abAppend(ab, \"\\x1b[m\", 3);\n    abAppend(ab, \"\\r\\n\", 2);\n  }\n\n  void editorDrawMessageBar(struct abuf *ab) {\n    abAppend(ab, \"\\x1b[K\", 3);\n    int msglen = strlen(E.statusmsg);\n    if (msglen \u003e E.screencols) msglen = E.screencols; // bounds\n    if (msglen \u0026\u0026 time(NULL) - E.statusmsg_time \u003c 5)\n      abAppend(ab, E.statusmsg, msglen);\n  }\n#+end_src\n\n#+begin_src c\n  void editorRefreshScreen() {\n    editorScroll();\n\n    struct abuf ab = ABUF_INIT;\n    abAppend(\u0026ab, \"\\x1b[?25l\", 6);  // hide cursor\n    abAppend(\u0026ab, \"\\x1b[H\", 3);  // reposition cursor\n    editorDrawRows(\u0026ab);\n    editorDrawStatusBar(\u0026ab);\n    editorDrawMessageBar(\u0026ab);\n\n    // Move the cursor\n    char buf[32];\n    // The ~[H~ escape sequence moves the cursor to the position given by the\n    // coordinates. The +1 is to convert because the terminal uses 1-indexed values.\n    snprintf(buf, sizeof(buf), \"\\x1b[%d;%dH\", (E.cy - E.rowoff) + 1, (E.rx - E.coloff) + 1);\n    abAppend(\u0026ab, buf, strlen(buf));\n\n    abAppend(\u0026ab, \"\\x1b[?25h\", 6);  // show cursor\n    write(STDOUT_FILENO, ab.b, ab.len);\n    abFree(\u0026ab);\n  }\n#+end_src\n\nBelow, the ~...~ takes a varying number of arguments. Between ~va_start()~ and\n~va_end()~ you can use ~va_arg()~ to get the next argument. ~va_start()~ needs to know\nthe last argument before the variable arguments list starts, so it can know the\naddress of the next arguments. In our case we don't use ~va_arg()~, but instead\njust pass ~ap~ to ~vsnprintf~, which can format the string with a varying number of\narguments.\n\n#+begin_src c\n  void editorSetStatusMessage(const char *fmt, ...) {\n    va_list ap;\n    va_start(ap, fmt);\n    vsnprintf(E.statusmsg, sizeof(E.statusmsg), fmt, ap);\n    va_end(ap);\n    E.statusmsg_time = time(NULL);\n  }\n#+end_src\n\n** Input\n\nThese are the main user input functions. ~editorPrompt~ is similar to the main\nloop - it waits for user input and then runs a callback function on\nRET. ~editorProcessKeypress~ is basically a big case statement that checks the key\nenum and performs appropriate operations.\n\n#+begin_src c\n  char *editorPrompt(char *prompt, void (*callback)(char *, int)) {\n    size_t bufsize = 128;\n    char *buf = malloc(bufsize);\n\n    size_t buflen = 0;\n    buf[0] = '\\0';\n\n    while (1) {\n      editorSetStatusMessage(prompt, buf);\n      editorRefreshScreen();\n\n      int c = editorReadKey();\n      if (c == DEL_KEY || c == CTRL_KEY('h') || c == BACKSPACE) {\n        if (buflen !=0) buf[--buflen] = '\\0';\n      } else if (c == '\\x1b') {\n        editorSetStatusMessage(\"\");\n        if (callback) callback(buf, c);\n        free(buf);\n        return NULL;\n      } else if (c == '\\r') {\n        if (buflen != 0) {\n          // clear status message, return the user input\n          editorSetStatusMessage(\"\");\n          if (callback) callback(buf, c);\n          return buf;\n        }\n      } else if (!iscntrl(c) \u0026\u0026 c \u003c 128) {\n        if (buflen == bufsize - 1) {\n          bufsize *= 2; // dynamically increase memory as user input grows\n          buf = realloc(buf, bufsize);\n        }\n        buf[buflen++] = c;\n        buf[buflen] = '\\0';\n      }\n      if (callback) callback(buf, c);\n    }\n  }\n\n  void editorMoveCursor(int key) {\n    erow *row = (E.cy \u003e= E.numrows) ? NULL : \u0026E.row[E.cy]; // get current row\n\n    switch (key) {\n    case ARROW_LEFT:\n      if (E.cx != 0) {\n        E.cx--;\n      } else if (E.cy \u003e 0) {\n          // Move to the row above\n          E.cy--;\n          E.cx = E.row[E.cy].size;\n      }\n      break;\n    case ARROW_RIGHT:\n      if (row \u0026\u0026 E.cx \u003c row-\u003esize) { // limit horizontal scrolling by column width\n        E.cx++;\n      } else if (row \u0026\u0026 E.cx == row-\u003esize) {\n        // Move to the row below\n        E.cy++;\n        E.cx = 0;\n      }\n      break;\n    case ARROW_UP:\n      if (E.cy != 0) {\n        E.cy--;\n      }\n      break;\n    case ARROW_DOWN:\n      if (E.cy != E.numrows - 1) {  // Allow advancing past the screen, but not the file.\n        E.cy++;\n      }\n      break;\n    }\n\n    // Limit the cursor to the end of the row. Fixes the case where\n    // different rows have different widths and you move to the row above/below.\n    row = (E.cy \u003e= E.numrows) ? NULL : \u0026E.row[E.cy];\n    int rowlen = row ? row-\u003esize : 0;\n    if (E.cx \u003e rowlen) {\n      E.cx = rowlen;\n    }\n\n  }\n#+end_src\n\n#+begin_src c\n  void editorProcessKeypress() {\n    static int quit_times = KILO_QUIT_TIMES;\n\n    int c = editorReadKey();\n    switch (c) {\n    case '\\r':\n      editorInsertNewline();\n      break;\n    case CTRL_KEY('q'):\n      if (E.dirty \u0026\u0026 quit_times \u003e 0){\n        editorSetStatusMessage(\"Warning! File has unsaved changes. \"\n                               \"Press C-q %d more times to quit.\", quit_times);\n        quit_times --;\n        return;\n      }\n      write(STDOUT_FILENO, \"\\x1b[2J\", 4);  // clear screen\n      write(STDOUT_FILENO, \"\\x1b[H\", 3);  // reposition cursor\n      exit(0);\n      break;\n    case CTRL_KEY('s'):\n      editorSave();\n      break;\n    case HOME_KEY:\n      E.cx = 0;\n      break;\n    case END_KEY:\n      if (E.cy \u003c E.numrows)\n        E.cx = E.row[E.cy].size;  // move to end of the line\n      break;\n    case CTRL_KEY('f'):\n      editorFind();\n      break;\n    case BACKSPACE:\n    case CTRL_KEY('h'): // legacy - C-h produces \"8\", which used to represent backspace\n    case DEL_KEY:\n      if (c == DEL_KEY) editorMoveCursor(ARROW_RIGHT);\n      editorDelChar();\n      break;\n    case PAGE_UP:\n    case PAGE_DOWN:\n      {\n\n        // Set cursor y position to simulate scrolling the page\n        if (c == PAGE_UP) {\n          E.cy = E.rowoff;\n        } else if (c == PAGE_DOWN) {\n          E.cy = E.rowoff + E.screenrows - 1;\n          if (E.cy \u003e E.numrows) E.cy = E.numrows; // cap to end of file\n        }\n\n        // move the cursor\n        int times = E.screenrows;\n        while (times--)\n          editorMoveCursor(c == PAGE_UP ? ARROW_UP : ARROW_DOWN);\n      }\n      break;\n    case ARROW_UP:\n    case ARROW_DOWN:\n    case ARROW_LEFT:\n    case ARROW_RIGHT:\n      editorMoveCursor(c);\n      break;\n\n    // C-l traditionally refreshes the screen. don't do anything as we refresh by\n    // default after each keypress.\n    case CTRL_KEY('l'):\n    case '\\x1b':\n      break;\n\n    default:\n      editorInsertChar(c);\n      break;\n    }\n\n    quit_times = KILO_QUIT_TIMES;  // reset to 3\n  }\n#+end_src\n** Main\n\nThe entry point. ~initEditor()~ initialises all the fields in the E struct. ~main()~\nhandles arguments and enters the main loop.\n\n#+begin_src c\n  void initEditor () {\n    E.cx = 0;  // horizontal cursor\n    E.cy = 0;  // vertical cursor\n    E.rx = 0;  // cursor index\n    E.rowoff = 0;\n    E.coloff = 0;\n    E.numrows = 0;\n    E.row = NULL;\n    E.dirty = 0;\n    E.filename = NULL;\n    E.statusmsg[0] = '\\0';\n    E.statusmsg_time = 0;\n    E.syntax = NULL;\n    if (getWindowSize(\u0026E.screenrows, \u0026E.screencols) == -1) die(\"getWindowSize\");\n    E.screenrows -= 2;  // For the status bar and message bar\n  }\n#+end_src\n\n#+begin_src c\n  int main(int argc, char *argv[]) {\n    enableRawMode();\n    initEditor();\n\n    if (argc \u003e= 2) {\n      editorOpen(argv[1]);\n    }\n\n    editorSetStatusMessage(\"HELP: C-Q: quit | C-S: save | C-f: find\");\n\n    while (1) {\n      editorRefreshScreen();\n      editorProcessKeypress();\n    }\n    return 0;\n  }\n#+end_src\n\n\n\n* Log\n\nNotes that I'm writing as I go.\n\n** Raw mode\n\nBy default the terminal starts in canonical/cooked mode, which captures a lot of\nuser input rather than passing it straight to the program. Input is only sent to\nthe program when you hit enter, and various keys have special terminal\nbehaviour, like ~C-c~ and ~C-z~.\n\nInterestingly you can \"break\" your terminal by running Step 5, which sets some\ntermios flags, and it has to be reset by the ~reset~ trick.\n\nStep 15 disables various flags that nowadays are usually disabled by default\n(but it's still good practice to disable them to enable \"raw mode\").\n\n** C-s and C-q\n\n~C-s~ stops data from being transmitted to the terminal, and ~C-q~ resumes it. I\nhaven't used these before. Then can be disabled with the IXON termios flag.\n\n** EAGAIN\n\nEAGAIN is returned by ~read()~ on timeout in Cygwin, instead of just\nreturning 0. I'm not using Cygwin so I suspect it's safe to remove that part.\n\n** VT100 escape sequences\n\nIn an escape sequence like ~\\x1b[2J~, ~J~ is the function and ~2~ is an argument to\nit. I hadn't thought about this before - I think I had just treated \"2J\" as a\nwhole.\n\nThe ~m~ command controls text attributes like bold (~1~), underscore (~4~), blink (~5~)\nand inverted colours (~7~).\n\n~ncurses~ uses the ~terminfo~ database to figure out the capabilities of a terminal\nand what the escape sequences for that terminal are. In our case we're just\nhardcoding the VT100 sequences.\n\n*** Home and End\n\nHome and End can have multiple representations depending on the OS, which is why\nthey're added in multiple places in ~editorReadyKey()~ in step 52.\n\n** Hide the cursor when drawing\n\nThis is standard practice - the cursor might jump around the screen if we're\nwriting to it. This can be controlled with ~?25h~ and ~?25l~, at least in later VT\nmodels.\n\n** Enums\n\nIf you set the first constant in an enum (as we do in step 48), then the\nremaining constants are incremented automatically.\n\n** Saving the file\n\nA safer way to write the file would be to write it to a temporary file, ensure\nit succeeds safely, and then rename it to the desired location. This is\nmentioned in step 106.\n** openemacs\n\nThere's a [[https://github.com/practicalswift/openemacs/blob/master/openemacs.c][fork of the project]] that implements some emacs-like features (eg. the\nmovement bindings).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmattduck%2Fkilo","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmattduck%2Fkilo","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmattduck%2Fkilo/lists"}