editor

a terminal text editor written in C


editor / editor.c

editor.c (SIZE) raw | download
1#define _DARWIN_C_SOURCE 1
2#define _XOPEN_SOURCE 700
3#include <errno.h>
4#include <fcntl.h>
5#include <limits.h>
6#include <locale.h>
7#include <signal.h>
8#include <stdarg.h>
9#include <stdint.h>
10#include <stdio.h>
11#include <stdlib.h>
12#include <string.h>
13#include <sys/ioctl.h>
14#include <sys/stat.h>
15#include <termios.h>
16#include <unistd.h>
17#include <wchar.h>
18
19#define KEY_CTRL(c) ((c) & 31)
20#define TABSTOP 8
21
22enum {
23 KEY_NONE = 256, KEY_LEFT, KEY_RIGHT, KEY_UP, KEY_DOWN,
24 KEY_HOME, KEY_END, KEY_DELETE, KEY_PAGEUP, KEY_PAGEDOWN, KEY_RESIZE
25};
26struct row { char *data; size_t len; };
27struct frame { char *data; size_t len, cap; };
28static struct {
29 struct row *rows;
30 size_t count, capacity, x, y, top, left, goal;
31 int width, height, dirty, raw, screen;
32 mode_t new_mode;
33 char *path;
34 const char *name;
35 char message[512];
36 struct termios original;
37 struct frame frame;
38} ed;
39static volatile sig_atomic_t resized, stopped;
40
41static int write_all(int fd, const char *data, size_t len)
42{
43 while (len) {
44 size_t chunk = len > (size_t)SSIZE_MAX ? (size_t)SSIZE_MAX : len;
45 ssize_t n = write(fd, data, chunk);
46 if (n < 0 && errno == EINTR) continue;
47 if (n <= 0) { if (!n) errno = EIO; return -1; }
48 data += n;
49 len -= (size_t)n;
50 }
51 return 0;
52}
53
54static void restore_terminal(void)
55{
56 if (ed.raw) {
57 int rc;
58 do { rc = tcsetattr(STDIN_FILENO, TCSAFLUSH, &ed.original); }
59 while (rc < 0 && errno == EINTR);
60 ed.raw = 0;
61 if (rc < 0) perror("editor: restore terminal");
62 }
63 if (ed.screen) {
64 const char reset[] = "\x1b[0m\x1b[?25h\x1b[?1049l";
65 ed.screen = 0;
66 if (write_all(STDOUT_FILENO, reset, sizeof reset - 1) < 0)
67 perror("editor: restore screen");
68 }
69}
70
71static void cleanup(void)
72{
73 restore_terminal();
74 for (size_t i = 0; i < ed.count; i++) free(ed.rows[i].data);
75 free(ed.rows);
76 free(ed.path);
77 free(ed.frame.data);
78}
79
80static void fatal(const char *what)
81{
82 int error = errno;
83 restore_terminal();
84 fprintf(stderr, "editor: %s: %s\n", what, strerror(error));
85 exit(EXIT_FAILURE);
86}
87
88static void *resize(void *p, size_t count, size_t size)
89{
90 if (size && count > SIZE_MAX / size) { errno = ENOMEM; fatal("allocation"); }
91 size_t bytes = count * size;
92 void *q = realloc(p, bytes ? bytes : 1);
93 if (!q) fatal("allocation");
94 return q;
95}
96
97static void message(const char *fmt, ...)
98{
99 va_list ap;
100 va_start(ap, fmt);
101 vsnprintf(ed.message, sizeof ed.message, fmt, ap);
102 va_end(ap);
103}
104
105static void on_signal(int signo)
106{
107 if (signo == SIGWINCH) resized = 1;
108 else stopped = signo;
109}
110
111static void window_size(void)
112{
113 struct winsize ws;
114 if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) < 0) fatal("window size");
115 if (!ws.ws_row || !ws.ws_col) { errno = EINVAL; fatal("window size"); }
116 ed.width = ws.ws_col;
117 ed.height = ws.ws_row;
118 resized = 0;
119}
120
121static void start_terminal(void)
122{
123 struct sigaction sa = {0};
124 int signals[] = {SIGINT, SIGTERM, SIGHUP, SIGQUIT, SIGPIPE, SIGWINCH};
125 sa.sa_handler = on_signal;
126 sigemptyset(&sa.sa_mask);
127 for (size_t i = 0; i < sizeof signals / sizeof signals[0]; i++)
128 if (sigaction(signals[i], &sa, NULL) < 0) fatal("sigaction");
129 /* Suspension is deliberately unsupported: do not leave a stopped raw tty. */
130 sa.sa_handler = SIG_IGN;
131 if (sigaction(SIGTSTP, &sa, NULL) < 0) fatal("sigaction");
132 if (tcgetattr(STDIN_FILENO, &ed.original) < 0) fatal("tcgetattr");
133 struct termios raw = ed.original;
134 raw.c_iflag &= ~(tcflag_t)(BRKINT | ICRNL | INLCR | IGNCR | INPCK | ISTRIP | IXON | PARMRK);
135 raw.c_oflag &= ~(tcflag_t)OPOST;
136 raw.c_cflag = (raw.c_cflag & ~(tcflag_t)CSIZE) | CS8;
137 raw.c_lflag &= ~(tcflag_t)(ECHO | ECHONL | ICANON | IEXTEN | ISIG);
138 raw.c_cc[VMIN] = 0;
139 raw.c_cc[VTIME] = 1;
140 if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw) < 0) fatal("raw mode");
141 ed.raw = 1;
142 ed.screen = 1;
143 const char enter[] = "\x1b[?1049h\x1b[2J\x1b[H";
144 if (write_all(STDOUT_FILENO, enter, sizeof enter - 1) < 0) fatal("screen output");
145}
146
147/* VTIME bounds escape-sequence reads and lets signals wake the main loop. */
148static int read_byte(void)
149{
150 unsigned char c;
151 for (;;) {
152 if (stopped) return -1;
153 ssize_t n = read(STDIN_FILENO, &c, 1);
154 if (n == 1) return c;
155 if (!n || (n < 0 && errno == EINTR)) return -1;
156 fatal("terminal input");
157 }
158}
159
160static int read_key(void)
161{
162 int c;
163 do {
164 if (stopped) return KEY_NONE;
165 if (resized) return KEY_RESIZE;
166 c = read_byte();
167 } while (c < 0);
168 if (c != 27) return c;
169 c = read_byte();
170 if (c < 0) return 27;
171 if (c == ']' || c == 'P' || c == '^' || c == '_') {
172 int previous = 0;
173 while ((c = read_byte()) >= 0) {
174 if (c == 7 || (previous == 27 && c == '\\')) break;
175 previous = c;
176 }
177 return KEY_NONE;
178 }
179 if (c != '[' && c != 'O') return KEY_NONE;
180 char sequence[32];
181 size_t len = 0;
182 int overflow = 0;
183 while ((c = read_byte()) >= 0) {
184 if (len < sizeof sequence - 1) sequence[len++] = (char)c;
185 else overflow = 1;
186 if (c >= 0x40 && c <= 0x7e) break;
187 }
188 if (c < 0 || overflow) return KEY_NONE;
189 sequence[len] = '\0';
190 switch (c) {
191 case 'A': return KEY_UP;
192 case 'B': return KEY_DOWN;
193 case 'C': return KEY_RIGHT;
194 case 'D': return KEY_LEFT;
195 case 'H': return KEY_HOME;
196 case 'F': return KEY_END;
197 case '~':
198 switch (strtol(sequence, NULL, 10)) {
199 case 1: case 7: return KEY_HOME;
200 case 4: case 8: return KEY_END;
201 case 3: return KEY_DELETE;
202 case 5: return KEY_PAGEUP;
203 case 6: return KEY_PAGEDOWN;
204 }
205 }
206 return KEY_NONE;
207}
208
209static void add_row(size_t at, const char *text, size_t len)
210{
211 if (len == SIZE_MAX || ed.count == SIZE_MAX) { errno = ENOMEM; fatal("row size"); }
212 struct row row = {resize(NULL, len + 1, 1), len};
213 memcpy(row.data, text, len);
214 row.data[len] = '\0';
215 if (ed.count == ed.capacity) {
216 size_t capacity = ed.capacity ? ed.capacity * 2 : 16;
217 if (capacity < ed.capacity) { errno = ENOMEM; fatal("row count"); }
218 ed.rows = resize(ed.rows, capacity, sizeof *ed.rows);
219 ed.capacity = capacity;
220 }
221 memmove(ed.rows + at + 1, ed.rows + at, (ed.count - at) * sizeof *ed.rows);
222 ed.rows[at] = row;
223 ed.count++;
224}
225
226static void load_file(void)
227{
228 int fd = open(ed.name, O_RDONLY | O_NONBLOCK);
229 if (fd < 0) {
230 if (errno != ENOENT) fatal(ed.name);
231 struct stat st;
232 /* A dangling symlink is not a new regular file. */
233 if (lstat(ed.name, &st) == 0) { errno = ELOOP; fatal(ed.name); }
234 if (errno != ENOENT) fatal(ed.name);
235 ed.path = resize(NULL, strlen(ed.name) + 1, 1);
236 strcpy(ed.path, ed.name);
237 add_row(0, "", 0);
238 message("New file | Ctrl-S save | Ctrl-Q quit | Ctrl-F search");
239 return;
240 }
241 struct stat st;
242 if (fstat(fd, &st) < 0) fatal(ed.name);
243 if (!S_ISREG(st.st_mode)) { errno = EINVAL; fatal("not a regular file"); }
244 FILE *file = fdopen(fd, "rb");
245 if (!file) fatal("fdopen");
246 ed.path = realpath(ed.name, NULL);
247 if (!ed.path) fatal(ed.name);
248 char *line = NULL;
249 size_t cap = 0;
250 ssize_t n;
251 int final_newline = 0;
252 while ((n = getline(&line, &cap, file)) >= 0) {
253 final_newline = n > 0 && line[n - 1] == '\n';
254 add_row(ed.count, line, (size_t)n - (size_t)final_newline);
255 }
256 free(line);
257 if (ferror(file)) { if (!errno) errno = EIO; fatal("read file"); }
258 if (fclose(file) == EOF) fatal("close file");
259 /* An empty final row preserves a final newline without inventing one. */
260 if (!ed.count || final_newline) add_row(ed.count, "", 0);
261 message("Ctrl-S save | Ctrl-Q quit | Ctrl-F search");
262}
263
264static void save_file(void)
265{
266 struct stat st;
267 mode_t mode = ed.new_mode;
268 int fd, error = 0, remove_error = 0;
269 size_t n = strlen(ed.path);
270 if (n > SIZE_MAX - 8) { errno = ENOMEM; fatal("filename"); }
271 char *temp = resize(NULL, n + 8, 1);
272 snprintf(temp, n + 8, "%s.XXXXXX", ed.path);
273 if (lstat(ed.path, &st) == 0) {
274 if (!S_ISREG(st.st_mode)) { error = EINVAL; goto done; }
275 mode = st.st_mode & 0777;
276 /* Respect the target's write permissions before replacing its inode. */
277 if (access(ed.path, W_OK) < 0) { error = errno; goto done; }
278 } else if (errno != ENOENT) { error = errno; goto done; }
279 fd = mkstemp(temp);
280 if (fd < 0) { error = errno; goto done; }
281 for (size_t i = 0; i < ed.count; i++) {
282 if (write_all(fd, ed.rows[i].data, ed.rows[i].len) < 0 ||
283 (i + 1 < ed.count && write_all(fd, "\n", 1) < 0)) {
284 error = errno;
285 break;
286 }
287 }
288 if (!error && fchmod(fd, mode) < 0) error = errno;
289 if (!error && fsync(fd) < 0) error = errno;
290 if (close(fd) < 0 && !error) error = errno;
291 if (!error && rename(temp, ed.path) < 0) error = errno;
292 if (error && unlink(temp) < 0) remove_error = errno;
293done:
294 if (remove_error) message("Save failed: %s; temporary file remains: %s", strerror(error), temp);
295 else if (error) message("Save failed: %s", strerror(error));
296 else { ed.dirty = 0; message("Saved %s", ed.name); }
297 free(temp);
298}
299
300/* Decode one character for cursor movement and display. Invalid bytes are kept
301 * in the file but shown as '?'; controls use caret notation and tabs use spaces. */
302static size_t character(const char *s, size_t len, size_t column, int *width, int *valid)
303{
304 unsigned char c = (unsigned char)*s;
305 *valid = 1;
306 if (c == '\t') { *width = TABSTOP - (int)(column % TABSTOP); return 1; }
307 if (c < 32 || c == 127) { *width = 2; return 1; }
308 if (c < 128) { *width = 1; return 1; }
309 mbstate_t state = {0};
310 wchar_t wc;
311 size_t n = mbrtowc(&wc, s, len, &state);
312 if (n == (size_t)-1 || n == (size_t)-2 || !n || (*width = wcwidth(wc)) < 0) {
313 *width = 1;
314 *valid = 0;
315 return 1;
316 }
317 return n;
318}
319
320static size_t previous_char(const char *s, size_t len, size_t at)
321{
322 if (!at) return 0;
323 size_t start = at - 1;
324 while (start && at - start < MB_LEN_MAX && ((unsigned char)s[start] & 0xc0) == 0x80)
325 start--;
326 int width, valid;
327 size_t n = character(s + start, len - start, 0, &width, &valid);
328 return start + n == at ? start : at - 1;
329}
330
331static size_t column_at(const struct row *row, size_t at)
332{
333 size_t x = 0, column = 0;
334 while (x < at) {
335 int width, valid;
336 x += character(row->data + x, row->len - x, column, &width, &valid);
337 column += (size_t)width;
338 }
339 return column;
340}
341
342static size_t offset_at(const struct row *row, size_t goal)
343{
344 size_t x = 0, column = 0;
345 while (x < row->len) {
346 int width, valid;
347 size_t n = character(row->data + x, row->len - x, column, &width, &valid);
348 if (column + (size_t)width > goal) break;
349 column += (size_t)width;
350 x += n;
351 }
352 return x;
353}
354
355static void insert_byte(int c)
356{
357 struct row *row = &ed.rows[ed.y];
358 if (row->len > SIZE_MAX - 2) { errno = ENOMEM; fatal("row size"); }
359 row->data = resize(row->data, row->len + 2, 1);
360 memmove(row->data + ed.x + 1, row->data + ed.x, row->len - ed.x + 1);
361 row->data[ed.x++] = (char)c;
362 row->len++;
363 ed.dirty = 1;
364}
365
366static void split_row(void)
367{
368 struct row *row = &ed.rows[ed.y];
369 add_row(ed.y + 1, row->data + ed.x, row->len - ed.x);
370 row = &ed.rows[ed.y];
371 row->len = ed.x;
372 row->data[ed.x] = '\0';
373 ed.y++;
374 ed.x = 0;
375 ed.dirty = 1;
376}
377
378static void delete_forward(void)
379{
380 struct row *row = &ed.rows[ed.y];
381 if (ed.x < row->len) {
382 int width, valid;
383 size_t n = character(row->data + ed.x, row->len - ed.x, 0, &width, &valid);
384 memmove(row->data + ed.x, row->data + ed.x + n, row->len - ed.x - n + 1);
385 row->len -= n;
386 } else if (ed.y + 1 < ed.count) {
387 struct row *next = row + 1;
388 if (next->len >= SIZE_MAX - row->len) { errno = ENOMEM; fatal("row size"); }
389 row->data = resize(row->data, row->len + next->len + 1, 1);
390 memcpy(row->data + row->len, next->data, next->len + 1);
391 row->len += next->len;
392 free(next->data);
393 memmove(next, next + 1, (ed.count - ed.y - 2) * sizeof *next);
394 ed.count--;
395 } else return;
396 ed.dirty = 1;
397}
398
399static int text_height(void)
400{
401 return ed.height > 2 ? ed.height - 2 : 1;
402}
403
404static void move_cursor(int key)
405{
406 struct row *row = &ed.rows[ed.y];
407 if (key == KEY_UP || key == KEY_DOWN || key == KEY_PAGEUP || key == KEY_PAGEDOWN) {
408 size_t amount = (key == KEY_UP || key == KEY_DOWN) ? 1 : (size_t)text_height();
409 if (ed.goal == SIZE_MAX) ed.goal = column_at(row, ed.x);
410 if (key == KEY_UP || key == KEY_PAGEUP)
411 ed.y = amount > ed.y ? 0 : ed.y - amount;
412 else ed.y += amount < ed.count - 1 - ed.y ? amount : ed.count - 1 - ed.y;
413 ed.x = offset_at(&ed.rows[ed.y], ed.goal);
414 return;
415 }
416 ed.goal = SIZE_MAX;
417 switch (key) {
418 case KEY_LEFT:
419 if (ed.x) ed.x = previous_char(row->data, row->len, ed.x);
420 else if (ed.y) ed.x = ed.rows[--ed.y].len;
421 break;
422 case KEY_RIGHT:
423 if (ed.x < row->len) {
424 int width, valid;
425 ed.x += character(row->data + ed.x, row->len - ed.x, 0, &width, &valid);
426 } else if (ed.y + 1 < ed.count) { ed.y++; ed.x = 0; }
427 break;
428 case KEY_HOME: ed.x = 0; break;
429 case KEY_END: ed.x = row->len; break;
430 }
431}
432
433static void emit(const char *s, size_t len)
434{
435 struct frame *f = &ed.frame;
436 if (len > SIZE_MAX - f->len) { errno = ENOMEM; fatal("screen size"); }
437 size_t needed = f->len + len;
438 if (needed > f->cap) {
439 size_t cap = needed <= SIZE_MAX / 2 ? needed * 2 : needed;
440 f->data = resize(f->data, cap, 1);
441 f->cap = cap;
442 }
443 memcpy(f->data + f->len, s, len);
444 f->len = needed;
445}
446
447static void sequence(const char *s)
448{
449 emit(s, strlen(s));
450}
451
452static void position(int y, int x)
453{
454 char s[64];
455 int n = snprintf(s, sizeof s, "\x1b[%d;%dH", y, x);
456 emit(s, (size_t)n);
457}
458
459static void draw_text(const char *s, size_t len, size_t left)
460{
461 size_t x = 0, column = 0, end = left + (size_t)ed.width;
462 int base_visible = 0;
463 while (x < len && column < end) {
464 int width, valid;
465 size_t n = character(s + x, len - x, column, &width, &valid);
466 size_t after = column + (size_t)width;
467 if (!width) {
468 if (base_visible) emit(s + x, n);
469 } else if (after > left) {
470 size_t first = column < left ? left : column;
471 size_t last = after > end ? end : after;
472 unsigned char c = (unsigned char)s[x];
473 base_visible = column >= left && after <= end;
474 if (c == '\t' || !base_visible) {
475 for (size_t i = first; i < last; i++) sequence(" ");
476 } else if (c < 32 || c == 127) {
477 char control[2] = {'^', c == 127 ? '?' : (char)(c + 64)};
478 emit(control, 2);
479 } else if (!valid) sequence("?");
480 else emit(s + x, n);
481 } else base_visible = 0;
482 column = after;
483 x += n;
484 }
485 sequence("\x1b[K");
486}
487
488static void refresh(void)
489{
490 if (resized) window_size();
491 ed.frame.len = 0;
492 sequence("\x1b[?25l");
493 if (ed.height < 3 || ed.width < 10) {
494 sequence("\x1b[H\x1b[2J");
495 draw_text("Window too small", 16, 0);
496 position(1, 1);
497 } else {
498 size_t column = column_at(&ed.rows[ed.y], ed.x);
499 size_t height = (size_t)text_height();
500 if (ed.y < ed.top) ed.top = ed.y;
501 if (ed.y - ed.top >= height) ed.top = ed.y - height + 1;
502 if (column < ed.left) ed.left = column;
503 if (column - ed.left >= (size_t)ed.width) ed.left = column - (size_t)ed.width + 1;
504 for (int screen_y = 0; screen_y < text_height(); screen_y++) {
505 size_t y = ed.top + (size_t)screen_y;
506 position(screen_y + 1, 1);
507 if (y < ed.count) draw_text(ed.rows[y].data, ed.rows[y].len, ed.left);
508 else sequence("~\x1b[K");
509 }
510 char status[512], details[96];
511 const char *name = strrchr(ed.name, '/');
512 name = name ? name + 1 : ed.name;
513 snprintf(details, sizeof details, " | %zu lines | %zu:%zu",
514 ed.count, ed.y + 1, column + 1);
515 int room = ed.width - (int)strlen(details) - 4;
516 snprintf(status, sizeof status, "%s %.*s%s", ed.dirty ? "[+]" : "[ ]",
517 room > 0 ? room : 0, name, details);
518 position(ed.height - 1, 1);
519 sequence("\x1b[7m");
520 draw_text(status, strlen(status), 0);
521 sequence("\x1b[0m");
522 position(ed.height, 1);
523 draw_text(ed.message, strlen(ed.message), 0);
524 position((int)(ed.y - ed.top) + 1, (int)(column - ed.left) + 1);
525 }
526 sequence("\x1b[?25h");
527 if (write_all(STDOUT_FILENO, ed.frame.data, ed.frame.len) < 0) fatal("screen output");
528}
529
530/* Search bytes, including past embedded NULs. Forward/backward search wraps. */
531static int find_match(const char *query, size_t len, int direction, int include)
532{
533 size_t chosen_y = SIZE_MAX, chosen_x = 0, wrap_y = SIZE_MAX, wrap_x = 0;
534 for (size_t y = 0; y < ed.count; y++) {
535 const struct row *row = &ed.rows[y];
536 if (len > row->len) continue;
537 for (size_t x = 0; x <= row->len - len; x++) {
538 if (memcmp(row->data + x, query, len)) continue;
539 int equal = y == ed.y && x == ed.x;
540 int eligible = direction > 0 ? (y > ed.y || (y == ed.y && x > ed.x)) :
541 (y < ed.y || (y == ed.y && x < ed.x));
542 if (wrap_y == SIZE_MAX || direction < 0) { wrap_y = y; wrap_x = x; }
543 if (eligible || (include && equal)) {
544 chosen_y = y; chosen_x = x;
545 if (direction > 0) goto found;
546 }
547 }
548 }
549found:
550 if (chosen_y == SIZE_MAX) { chosen_y = wrap_y; chosen_x = wrap_x; }
551 if (chosen_y == SIZE_MAX) return 0;
552 ed.y = chosen_y;
553 ed.x = chosen_x;
554 return 1;
555}
556
557static void search(void)
558{
559 char query[256] = "";
560 size_t len = 0, x = ed.x, y = ed.y, top = ed.top, left = ed.left;
561 int found = 1;
562 while (!stopped) {
563 message("Search: %s%s | arrows next/prev, Enter accept, Esc cancel",
564 query, found ? "" : " [not found]");
565 refresh();
566 int key = read_key();
567 if (key == 27 || key == KEY_CTRL('q')) {
568 ed.x = x; ed.y = y; ed.top = top; ed.left = left;
569 message("Search cancelled");
570 return;
571 }
572 if (key == '\r' || key == '\n') { message("Search: %s", query); return; }
573 if (key == KEY_UP || key == KEY_LEFT || key == KEY_DOWN || key == KEY_RIGHT) {
574 if (len) found = find_match(query, len, key == KEY_UP || key == KEY_LEFT ? -1 : 1, 0);
575 continue;
576 }
577 if (key == 127 || key == KEY_CTRL('h')) {
578 if (len) len = previous_char(query, len, len);
579 } else if ((key >= 32 && key < 256) || key == '\t') {
580 if (len + 1 < sizeof query) query[len++] = (char)key;
581 } else continue;
582 query[len] = '\0';
583 ed.x = x; ed.y = y;
584 found = !len || find_match(query, len, 1, 1);
585 }
586}
587
588int main(int argc, char **argv)
589{
590 if (argc != 2) { fprintf(stderr, "usage: %s file\n", argv[0]); return 1; }
591 if (!isatty(STDIN_FILENO) || !isatty(STDOUT_FILENO)) {
592 fprintf(stderr, "editor: standard input and output must be terminals\n");
593 return 1;
594 }
595 if (atexit(cleanup)) { fprintf(stderr, "editor: cannot register cleanup\n"); return 1; }
596 setlocale(LC_CTYPE, "");
597 ed.name = argv[1];
598 ed.goal = SIZE_MAX;
599 mode_t mask = umask(0);
600 umask(mask);
601 ed.new_mode = 0666 & ~mask;
602 load_file();
603 window_size();
604 start_terminal();
605 int confirm_quit = 0;
606 while (!stopped) {
607 refresh();
608 int key = read_key();
609 if (key == KEY_NONE || key == KEY_RESIZE) continue;
610 if (key == KEY_CTRL('q')) {
611 if (!ed.dirty || confirm_quit) break;
612 message("Unsaved changes. Ctrl-Q again discards them; Ctrl-S saves.");
613 confirm_quit = 1;
614 continue;
615 }
616 if (confirm_quit) message("Ctrl-S save | Ctrl-Q quit | Ctrl-F search");
617 confirm_quit = 0;
618 switch (key) {
619 case KEY_CTRL('s'): save_file(); break;
620 case KEY_CTRL('f'): search(); break;
621 case '\r': case '\n': split_row(); break;
622 case 127: case KEY_CTRL('h'):
623 if (ed.x || ed.y) { move_cursor(KEY_LEFT); delete_forward(); }
624 break;
625 case KEY_DELETE: delete_forward(); break;
626 case KEY_LEFT: case KEY_RIGHT: case KEY_UP: case KEY_DOWN:
627 case KEY_HOME: case KEY_END: case KEY_PAGEUP: case KEY_PAGEDOWN:
628 move_cursor(key);
629 continue;
630 default:
631 if (key == '\t' || (key >= 32 && key < 256)) insert_byte(key);
632 }
633 ed.goal = SIZE_MAX;
634 }
635 return stopped ? 128 + stopped : 0;
636}