/* MOP text editor =============== MOP (Modern Obsolete Pad) is a line oriented text editor like ED. Mostly a subset of ED with few adjustments to my personal liking. Commands -------- q Quit Q Force quit e [x] Edit file E [x] Force edit r [x] Read file after dot w [x] Write file or region W [x] Write and quit z[20] Scroll /x/ids Search ?x?ids Reverse search s/x/y/i Substitute [x] Replace dot a [x] Append after dot d Delete c Change j Join m[x] Move k[x] Keep dot ![x] Run shell command se '\\/ Key differences between MOP and ED ---------------------------------- 0. When file opens then number of lines in that file is printed and line 0 is set as current line. From my observation, the first thing I do after openning new file is either scrolling through it with 'z' or searching for specific text. The default ED behavior of having last line active is a better default when your most common action is append at the end of the file. I don't do that. So starting from the top gives me advantage. Also the ED default of printing file size feels less useful than knowing the number of lines in open file, especially in line oriented editor. Write 'w' command will print number of written lines. 1. There is line 0 with file name, can be edited to change file name. Ignored when writing file. It makes scrolling with 'z' after file was open more natural, searching and appending from top of the file feels less magical. In ED line 0 magically exist for some commands. In MOP it's just there and have some purpose. 2. No 'i' insert command, only 'a' append to avoid mistakes which I often made in ED when typing fast. I know, it's embarrassing, but by having only append, and less magical line 0, adding text needs fewer decisions. 3. Searching is a command, not a part of address. This makes code much simpler and I can live without it. 4. 'W' instead of 'wq'. Felt more natural next to 'Q' and 'E'. 5. Providing address range without command prints all lines within that range. Printing always include line numbers. Those two made 'p' print and 'n' print with line numbers ED commands obsolete. The 'l' command is also not present as I never used it. 6. Address range is only defined with ','. There is no ';'. Similar like with other changes, I'm trying to make things more basic by avoiding extra edge cases. 7. Keep 'k' command will list stored addresses when invoked without argument. In ED I kept forgetting which addresses I stored. 8. New command, a single whitespace in form of space or tab replaces current line (or range) with text that follows that whitespace. I found that most small changes in single line I made not with multiple substitution commands but by retyping whole line. This command makes it easier and faster than 'c' and also when tab is used then new typed line will aling with printed lines as each line have line number and tab as prefix. It's my favorite command. 9. Append 'a' command works as usual but when followed by whitespace it will append single line in fasion descibed in previous point. 10. Empty command repeats previous command. License ------- Copyright (C) June 2026 irek@gabr.pl This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, see https://www.gnu.org/licenses */ #define VERSION "v0.9" #include #include #include #include #include #include #define LEN(arr) (int)((sizeof (arr)) / (sizeof (arr)[0])) typedef const char* Err; typedef struct line Line; struct line { /* double linked list, classic */ char str[4096]; /* line text without trailing \n */ Line* prev; Line* next; }; static struct { int dirty; int scroll; /* how many lines to scroll */ int icase; /* last search icase flag */ Err error; /* last error */ char read[4096]; /* last read or write region filename */ char input[4096]; /* last input */ char shell[4096]; /* last shell command */ char search[4096]; /* last search phrase */ char replace[4096]; /* last replace phrase */ Line* head; /* store filename in line 0 */ Line* last; /* last printed line */ Line* keep[26]; /* kept lines [a-z] */ } g = {0}; static size_t getlinenum (Line*); static char* parsearg (char**, char delim); static Line* parseaddr (char**); static char* parsereplace (char*, regmatch_t m[10]); static Err oninput (char*); static Line* append (Line* after, char*); static Line* delete (Line*); size_t getlinenum(Line* line) { Line *node = g.head; size_t ln = 0; for (; node != line; node = node->next) ln++; return ln; } char* parsearg(char **pt, char delim) { static char buf[4096]; size_t pi=0, bi=0; while ((*pt)[pi] && bi + 1 < sizeof buf) { if ((*pt)[pi] == '\\' && (*pt)[pi] == delim) pi++; else if ((*pt)[pi] == delim) break; buf[bi++] = (*pt)[pi++]; } buf[bi] = 0; (*pt) += (*pt)[pi] ? pi + 1 : pi; return buf; } Line* parseaddr(char** input) { Line *node = 0; size_t ln; int i; while (1) { i = 0; ln = 0; switch (**input) { case '\'': (*input)++; if (**input < 'a' || **input > 'z') return 0; i = *(*input)++; if (!g.keep[i - 'a']) return 0; node = g.keep[i - 'a']; continue; case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': sscanf(*input, "%lu%n", &ln, &i); (*input) += i; for (node = g.head; ln && node; node = node->next) ln--; if (!node) return 0; continue; case '+': case '-': sscanf((*input) + 1, "%lu%n", &ln, &i); if (ln == 0) ln = 1; if (!node) node = g.last; while (ln-- && node) node = **input == '+' ? node->next : node->prev; (*input) += i + 1; if (!node) return 0; continue; case '.': (*input)++; node = g.last; continue; case '$': (*input)++; for (node = g.head; node->next; node = node->next); continue; } break; /* end on no-address character */ } return node; } char* parsereplace(char *str, regmatch_t match[10]) { static char buf[4096]; char *pt; size_t bi, n; regmatch_t m; bi = 0; pt = g.replace; while (*pt && bi < sizeof buf - 1) { if (*pt != '\\') { buf[bi++] = *pt++; continue; } pt++; switch (*pt) { case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': m = match[*pt - '0']; if (m.rm_so == -1) break; n = m.rm_eo - m.rm_so; if (n > (sizeof buf -1) - bi) n = (sizeof buf -1) - bi; memcpy(buf + bi, str + m.rm_so, n); bi += n; break; case 't': buf[bi++] = '\t'; break; default: buf[bi++] = '\\'; buf[bi++] = *pt; } pt++; } buf[bi] = 0; return buf; } Err oninput(char *input) { static char error[256]; char delim, *pt, *name, *str, buf[4096]; FILE *fp; Line *first, *last, *node, *tmp; size_t len, ln; int i, previous, flags, code; regex_t preg; regmatch_t match[10]; if (*input) strncpy(g.input, input, sizeof g.input); input = g.input; /* parse address range [addr][,[addr]] (default: 1,$) */ last = 0; pt = input; first = parseaddr(&input); if (first) g.last = first; else if (pt != input) return "Invalid first address"; if (*input == ',') { pt = "1"; if (!first) first = parseaddr(&pt); input++; pt = input; last = parseaddr(&input); if (!last) { if (pt != input) return "Invalid last address"; pt = "$"; last = parseaddr(&pt); } /* "last" can't be before "first" */ for (node = last; node && node != first; node = node->next); if (node == first) return "Invalid range"; g.last = last; } switch (*input) { case 0: if (!first) first = g.last; if (!last) last = first; ln = getlinenum(first); for (node = first; ; node = node->next, ln++) { printf("%lu\t%s\n", ln, node->str); if (node == last) break; } break; case 'h': puts(g.error ? g.error : "Ok"); break; case 'q': if (g.dirty) return "Unsaved changes, use Q to force"; /* fallthrough */ case 'Q': exit(0); case 'e': if (g.dirty) return "Unsaved changes, use E to force"; /* fallthrough */ case 'E': while (g.head->next) g.last = delete(g.head->next); /* fallthrough */ case 'r': name = *input == 'r' ? g.read : g.head->str; if (input[1] && input[2]) strncpy(name, input+2, 4096); fp = fopen(name, "r"); if (!fp) return "Failed to open file"; node = g.last; for (ln=1; fgets(buf, sizeof buf, fp); ln++) { len = strlen(buf); if (len + 1 == sizeof buf) fprintf(stderr, "Line %lu truncated", ln); if (len && buf[len - 1] == '\n') buf[--len] = 0; if (len && buf[len - 1] == '\r') buf[--len] = 0; node = append(node, buf); } if (*input != 'r') g.dirty = 0; printf("%lu\n", ln - 1); if (fclose(fp)) return "Failed to close file"; strncpy(g.input, "z", sizeof g.input); break; case 'w': case 'W': if (first) { if (!last) last = first; if (!input[1] || !input[2]) return "Writing region requires file name"; strncpy(g.read, input+2, sizeof g.read); fp = fopen(g.read, "w"); } else { if (input[1] && input[2]) strncpy(g.head->str, input + 2, sizeof g.head->str); fp = fopen(g.head->str, "w"); } if (!fp) return "Failed to open file"; if (!first) first = g.head->next; ln = 0; for (node = first; 1; node = node->next) { fprintf(fp, "%s\n", node->str); ln++; if ((last && node == g.last) || !node->next) break; } printf("%lu\n", ln); g.dirty = 0; if (fclose(fp)) return "Failed to close file"; if (*input == 'W') exit(0); break; case 'z': i = atoi(input + 1); if (i > 0) g.scroll = i; ln = getlinenum(g.last); node = g.last; if (!first) { node = node->next; ln++; if (!node) return "EOF"; } for (i=0; inext, i++) { printf("%lu\t%s\n", ln, node->str); ln++; g.last = node; } break; case '?': case '/': delim = *input++; flags = REG_EXTENDED | REG_NEWLINE; str = parsearg(&input, delim); if (*str) { strncpy(g.search, str, sizeof g.search); g.icase = *input == 'i'; if (g.icase) input++; } if (g.icase) flags |= REG_ICASE; code = regcomp(&preg, g.search, flags); if (code) { regerror(code, &preg, error, sizeof error); regfree(&preg); return error; } i = -1; /* find as many as possible */ if (!last) { /* non range search */ i = 1; /* find only one, first match */ if (delim == '?') { first = g.head; if (g.last == g.head) break; last = g.last->prev; } else { first = g.last->next; if (!first) break; } } node = delim == '?' ? last : first; ln = getlinenum(node); while (1) { code = regexec(&preg, node->str, 0, 0, 0); if (!code) { printf("%lu\t%s\n", ln, node->str); g.last = node; if (--i == 0) break; } if (delim == '?') { if (node == first) break; node = node->prev; ln--; } else { if (node == last) break; node = node->next; if (!node) break; ln++; } } regfree(&preg); break; case 's': input++; if (*input) { delim = *input++; str = parsearg(&input, delim); if (*str) strncpy(g.search, str, sizeof g.search); str = parsearg(&input, delim); if (*str) strncpy(g.replace, str, sizeof g.replace); g.icase = *input == 'i'; } if (!first) first = g.last; if (!last) last = first; flags = REG_EXTENDED | REG_NEWLINE; if (g.icase) flags |= REG_ICASE; code = regcomp(&preg, g.search, flags); if (code) { regerror(code, &preg, error, sizeof error); regfree(&preg); return error; } ln = getlinenum(first); for (node = first; 1; node = node->next, ln++) { flags = 0; tmp = 0; previous = -1; for (i=0; node->str[i];) { code = regexec(&preg, node->str + i, LEN(match), match, flags); if (code) break; tmp = node; flags |= REG_NOTBOL; str = parsereplace(node->str + i, match); len = strlen(str); snprintf(buf, sizeof buf, "%.*s%.*s%s", match[0].rm_so + i, node->str, (int)len, str, node->str + match[0].rm_eo + i); strncpy(node->str, buf, sizeof node->str); i += len; if (i + match[0].rm_so == previous) i++; previous = i + match[0].rm_so; } if (tmp) { /* found */ g.last = tmp; printf("%lu\t%s\n", ln, g.last->str); } if (node == last) break; } regfree(&preg); break; case ' ': case '\t': if (!first) first = g.last; if (!last) last = first; for (node = first; 1; node = node->next) { strncpy(node->str, input + 1, sizeof node->str); if (node == last) break; } break; case 'd': case 'c': if (!first) first = g.last; if (!last) last = first; /* remember if we are deleting very last line */ i = !!last->next; if (first == g.head) return "Can't remove line 0"; node = first; while (1) { if (node == last) { g.last = delete(node); break; } node = delete(node); } if (*input == 'd') { ln = getlinenum(g.last); printf("%lu\t%s\n", ln, g.last->str); break; } /* in case of 'c' we continue with append logic */ if (i) g.last = g.last->prev; /* fallthrough */ case 'a': if (input[1]) { g.last = append(g.last, input + 2); break; } /* insert mode */ while (fgets(buf, sizeof buf, stdin)) { if (buf[0] == '.' && buf[1] == '\n') break; len = strlen(buf); buf[--len] = 0; g.last = append(g.last, buf); } break; case 'm': if (!first) first = g.last; if (!last) last = first; pt = ++input; node = parseaddr(&input); if (pt != input && !node) return "Invalid address"; if (!node) node = last->next; if (!node) return "Invalid address"; for (tmp = first; 1; tmp = tmp->next) { if (tmp == node) return "Invalid address"; if (tmp == last) break; } while (1) { if (last->prev) last->prev->next = last->next; if (last->next) last->next->prev = last->prev; if (node->next) node->next->prev = last; tmp = last->prev; last->next = node->next; last->prev = node; node->next = last; if (last == first) break; last = tmp; } break; case 'j': if (!first) first = g.last; if (!last) last = first->next; if (!last || last == first) return "Nothing to join"; do { node = last->prev; snprintf(buf, sizeof buf, "%s%s", node->str, last->str); strncpy(node->str, buf, sizeof node->str); delete(last); last = node; } while (last != first); g.last = last; break; case 'k': if (input[1]) { i = input[1]; if (i<'a' || i>'z') return "Invalid address"; g.keep[i - 'a'] = g.last; break; } for (i=0; istr); } break; case '!': if (input[1]) strncpy(g.shell, input + 1, sizeof g.shell); printf("!%d\n", system(g.shell)); break; case '#': break; default: return "Unknown command"; } return 0; } Line* append(Line* after, char* str) { Line *node = malloc(sizeof (*node)); strncpy(node->str, str, sizeof node->str); node->prev = after; node->next = after->next; if (node->next) node->next->prev = node; after->next = node; g.dirty++; return node; } Line* delete(Line *node) { Line *prev; int i; for (i=0; iprev; prev->next = node->next; if (node->next) node->next->prev = prev; free(node); g.dirty++; return prev->next ? prev->next : prev; } int main(int argc, char **argv) { char buf[4096]; size_t len; int i; while ((i = getopt(argc, argv, "hv")) != -1) switch (i) { case 'v': printf("mop "VERSION" by irek@gabr.pl GPLv2\n"); return 0; case 'h': default: printf("%s [-hv] [file]\n", argv[0]); return 0; } g.head = calloc(1, sizeof *g.head); strncpy(g.head->str, "scratch", sizeof g.head->str); g.last = g.head; g.scroll = 20; if (argc - optind > 0) { snprintf(buf, sizeof buf, "e %s", argv[optind++]); goto runcmd; } while (fgets(buf, sizeof buf, stdin)) { len = strlen(buf); buf[--len] = 0; /* remove \n */ runcmd: g.error = oninput(buf); if (g.error) puts("?"); } return 0; }