Author: Andre Adrian
Date: 2025-01-30
UNIX hackers have tribes. The source code editors vi and emacs
camps are one example. I am in neither team. I prefer
Nedit, a X11 GUI source
code editor. My latest computer is the 68k-MBC, a CP/M 68K computer
with Motorola 68008 CPU, 1MByte RAM, two RS232 serial ports and a
K&R standard C compiler. This is computing like in the mid
1980s. I had an Atari ST at this time. The CP/M 68K editor
ED is horrible, worse then vi. Therefore I decided to write my
own tiny screen editor. Current program name is "e3" for "Editor
version 3".
From the 1940s to the 1970s, a computer terminal like the
ASR33 was
like an electromechanical(!) typewriter: a keyboard for input and
printed paper as output. The vi editor shows its "printing
terminal" legacy in the commands. A "glass terminal" had a keyboard
and a CRT tube. The CRT screen showed 80 columns and 25 rows of
text. The computer connects to a terminal through a serial cable
with minimum three wires: transmit (TX), receive (RX) and ground
(GND). The early glass terminals like the VT52 had no
microprocessor, but could execute simple commands from the computer
like "move cursor one row up". The VT100 glass terminal was
one of the first glass terminals to implement the
ANSI escape
sequences for these "move the cursor around" commands.
The source code files and executeables of e0 to e3 for CP/M 68K
and MS-Windows 64-bit are in editor.zip. License is 3-clause BSD.
See my 68k-MBC, CP/M
68k web page about 68k-MBC computer setup and download/upload
files between 68k-MBC and host computer (e.g. MS-Windows
PC).
The 68k-MBC computer is slow, like any microcomputer from the
1980s. A current MS-Windows PC is much faster. For a fast
development cycle, I decided to create the tiny screen editor for
MS-Windows and CP/M 68K. I use MS-Windows for prototyping, the real
target is CP/M 68K. The development environments are:
A terminal emulation is a computer program to make the computer
into a glass terminal. I have no real VT100 terminal. See my
webpage 68k-MBC, CP/M
68k for Tera Term configuration and compile C source
code.
The MS-Windows computer and the 68k-MBC have both a C compiler,
therefore it should be "Write once, compile anywhere". This is true
for the largest part of the TSE program. But C uses by default the
"canonical mode" or "cooked mode" for input. Keyboard input is
buffered until a "newline" character flushes the buffer. A screen
editor needs "raw mode" to react directly to every key stroke. C
functions that use "raw mode" are not part of the C library
standard, neither the functions that switch standard C functions
like getchar() from "canonical mode" to "raw mode". See
"Entering
raw mode" from the Kilo screen editor in detail source code
discussion for UNIX (Linux).
The MS-Windows "raw mode" character user interface (CUI) function
for keyboard input is
_getch() and for terminal (console) output is
_cputs(). The CP/M 68K "raw mode" functions are part of BDOS. I
use the __OSIF(CONIO, 0xFF) function to get keyboard input and
__OSIF(CONIO, char) for one character output.
I use the functions congetc(), conputs() and conditional
compilation to decouple OS specific raw mode details in the
program:
MS-Windows |
CP/M 68K |
short congetc() // get character from
keyboard { short c = _getch(); if (c != 0xE0) { _putch(c); // local echo return c; } return _getch()+META; } void conputs(s) // put string to terminal char* s; { _cputs(s); } |
short congetc() // get character from
keyboard { short c = __OSIF(CONIO, 0xFF); if (c != 0x1B) { __OSIF(CONIO, c); // local echo return c; } __OSIF(CONIO, 0xFF); return __OSIF(CONIO, 0xFF)+META; } void conputs(s) // put string to terminal char* s; { while(*s) { __OSIF(CONIO, *s++); } } |
Note: This is K&R standard C, not ANSI standard C. The
Alcyon C compiler understands only K&R, the gcc compiler still
understands it. The C language and the C library aged very well in
the last 40 years.
The cursor keys on the keyboard are special. There is no ASCII
coding for these keys. The CP/M terminal emulator does the same a
VT100 terminal does: It sends the ANSI escape codes CUU, CUD, CUF,
CUB. MS-Windows uses a different coding. First character is 0xE0,
second character is H for up, P for down, M for right and K for
left for the "inverted T" cursor keys. The cursor keys in the
numeric pad have different codes. The congetc() function changes
the cursor key codes into a META character. The META character
value is ASCII-value plus META. META value is 256. Therefore all
ASCII characters are below 256, all special characters are above.
The congetc() function produces local echo (show the typed
printable character), because this is fast feedback to the
user.
There are many ANSI escape codes. Not every terminal implements
every escape code, neither every terminal emulation.The tiny screen
editor uses these ANSI codes:
Name | Code |
Comment |
CUU Cursor Up |
ESC [ A |
|
CUD Cursor Down |
ESC [ B |
|
CUF Cursor Forward |
ESC [ C |
Cursor right |
CUB Cursor Back |
ESC [ D |
Cursor left |
CUP Cursor Position |
ESC [ n ; m H |
Row n, column m. These numbers are
coded as ASCII decimals. Left top position is 1 ; 1. |
ED Erase in Display |
ESC [ n J |
If n is missing, erase display from cursor
position to end of screen. |
EL Erase in Line |
ESC [ n K |
If n is missing, erase line from cursor
position to end of line. |
SGR Select Graphic Rendition |
ESC [ n m |
n=0 is normal, n=31 is red
foreground, n=32 is green foreground color |
SU Scroll Up |
ESC [ n S |
If n missing, scroll whole page up one
line |
SD Scroll Down |
ESC [ n T |
If n missing, scroll whole page down one line |
Note: ESC has the ASCII code 0x1B hexadecimal or 033 octal.
I present the source code in iterations (development snapshots).
The development was incremental: think about the next evolution
step including how to test it, write some new lines of code, test
it, debug it, repeat.
The first iteration is "proof of concept" for cursor movement.
// e0.c // copyright 3-clause BSD 2025 Andre Adrian // CP/M68k, MS-Windows ANSI Tiny Screen Editor, K&R C // Cursor movement #include <stdio.h> #define SCREENX 80 // terminal columns #define SCREENY 24 // terminal rows without status line #define CTRL 64 // Ctrl + alpha key offset #define META 256 // Cursor key offset #ifdef CPM #include <osif.h> int congetc() // get character from keyboard { int c = __OSIF(CONIO, 0xFF); if (c != 0x1B) { __OSIF(CONIO, c); // local echo return c; } __OSIF(CONIO, 0xFF); return __OSIF(CONIO, 0xFF)+META; } void conputs(s) // put string to terminal char* s; { while(*s) { __OSIF(CONIO, *s++); } } #else #include <stdlib.h> #include <string.h> #include <conio.h> int congetc() // get character from keyboard { int c = _getch(); if (c != 0xE0) { _putch(c); // local echo return c; } return _getch()+META; } void conputs(s) // put string to terminal char* s; { _cputs(s); } #endif // CPM char buf[SCREENX+2]; // +2 for \n\0 void curpos(y, x) // move cursor int y, x; { sprintf(buf, "\033[%d;%dH", y+1, x+1); // ANSI top left is 1;1 conputs(buf); } int main() { int curx=0, cury=0; // state int c; conputs("\033[H\033[J"); // CUP, ED for(;;) { c = congetc(); switch (c) { case 'A'+META: // CP/M case 'H'+META: // MS-Windows if (cury > 0) { --cury; conputs("\033[A"); // CUU } break; case 'B'+META: case 'P'+META: if (cury < SCREENY-1) { ++cury; conputs("\033[B"); // CUD } break; case 'C'+META: case 'M'+META: if (curx < SCREENX-1) { ++curx; conputs("\033[C"); // CUF } break; case 'D'+META: case 'K'+META: if (curx > 0) { --curx; conputs("\033[D"); // CUB } break; case 'Q'-CTRL: exit(0); break; default: if (c < ' ' || c > 127) { printf("<0x%02x>", c); break; } ++curx; if (curx > SCREENX-1) { --curx; curpos(cury, curx); } break; } } } |
Cursor movement is with the "inverted T" cursor keys. The
traditional keys were CTRL key plus K, H, J, L, because the "dumb
terminal" ADM-3A had cursor
markings on these keyboard keys. Program exit is with CTRL-Q. You
can move the cursor within the 80 columns times 25 rows rectangle,
and enter printable ASCII characters. Non-printing ASCII characters
like CTRL-X are printed as hexadecimal numbers like
"<0x18>".
Version 1 is an "ASCII art" editor. The program is started with
a file name, like "e1 a.txt". Function filread() reads the file
contents into two dimensional array txt. Line length and number of
lines are fixed through constants SCREENX and SCREENY. As printable
characters are entered, the txt array gets updated. The CTRL-L
input refreshes the display from array txt. The CTRL-S input writes
array txt to the file, using function filwrite(). The newline
character '\n' is appended to every line in filwrite() and is
stripped from every line in filread(). The C library functions like
printf() and fgets() expand '\n' to '\r\n' or shrink it from '\r\n'
to '\n'.
Note: That we have carriage return '\r' and newline '\n' is a left
over from the "printing terminal" times. This device could not
perform both actions simultaneous.
// e1.c // copyright 3-clause BSD 2025 Andre Adrian // CP/M68k, MS-Windows ANSI Tiny Screen Editor, K&R C // Cursor movement, status line, one screen memory, load&save #include <stdio.h> #define SCREENX 80 // terminal columns #define SCREENY 24 // terminal rows without status line #define CTRL 64 // Ctrl + alpha key offset #define META 256 // Cursor key offset #ifdef CPM #include <osif.h> extern FILE *fopen(); int congetc() // get character from keyboard { int c = __OSIF(CONIO, 0xFF); if (c != 0x1B) { __OSIF(CONIO, c); // local echo return c; } __OSIF(CONIO, 0xFF); return __OSIF(CONIO, 0xFF)+META; } void conputs(s) // put string to terminal char* s; { while(*s) { __OSIF(CONIO, *s++); } } #else #include <stdlib.h> #include <string.h> #include <conio.h> int congetc() // get character from keyboard { int c = _getch(); if (c != 0xE0) { _putch(c); // local echo return c; } return _getch()+META; } void conputs(s) // put string to terminal char* s; { _cputs(s); } #endif // CPM char txt[SCREENY][SCREENX+1]; // state, +1 for \0 char buf[SCREENX+2]; // +2 for \n\0 void curpos(y, x) // move cursor int y, x; { sprintf(buf, "\033[%d;%dH", y+1, x+1); // ANSI top left is 1;1 conputs(buf); } void txtinit() // init txt with spaces { int y, x; char* p = txt[0]; for (y = 0; y < SCREENY; ++y) { for (x=0; x < SCREENX; ++x) { *p++ = ' '; } *p++ = '\0'; } } void txtpry(y) // print one line int y; { curpos(y, 0); conputs(txt[y]); } void txtpryy(y) // print many lines int y; { for (; y < SCREENY; ++y) { txtpry(y); } } void errfile() // print error message { curpos(SCREENY, 8); conputs("\033[31mCan't open file\033[0m"); } void errchar(i) // print error message int i; { curpos(SCREENY, 8); sprintf(buf, "\033[31mChar 0x%02x\033[0m", i); conputs(buf); } int filread(s) // Read file. If okay then return 0, else !=0 char* s; { int i; FILE* fp = fopen(s, "r"); if (NULL == fp) { errfile(); return 1; } for (i=0; i < SCREENY; ++i) { fgets(buf, sizeof(buf), fp); strncpy(txt[i], buf, strlen(buf)-1); // -1 for \n } return fclose(fp); } int filwrite(s) // Write file. If okay then return 0, else !=0 char* s; { int i; FILE* fp = fopen(s, "w"); if (NULL == fp) { errfile(); return 1; } for (i=0; i < SCREENY; ++i) { fprintf(fp, "%s\n", txt[i]); } return fclose(fp); } int main(argc, argv) int argc; char *argv[]; { int curx=0, cury=0; // state int c; conputs("\033[H\033[J"); // CUP, ED txtinit(); if (argc) filread(argv[1]); txtpryy(0); conputs("\033[H"); // CUP top left (home) for(;;) { c = congetc(); switch (c) { case 'A'+META: // CP/M case 'H'+META: // MS-Windows if (cury > 0) { --cury; conputs("\033[A"); // CUU } break; case 'B'+META: case 'P'+META: if (cury < SCREENY-1) { ++cury; conputs("\033[B"); // CUD } break; case 'C'+META: case 'M'+META: if (curx < SCREENX-1) { ++curx; conputs("\033[C"); // CUF } break; case 'D'+META: case 'K'+META: if (curx > 0) { --curx; conputs("\033[D"); // CUB } break; case 'Q'-CTRL: exit(0); break; case 'L'-CTRL: txtpryy(0); curpos(SCREENY, 0); // Status line sprintf(buf, "\033[31m%3d:%3d\033[0m\033[K", cury+1, curx+1); conputs(buf); curpos(cury, curx); break; case 'S'-CTRL: filwrite(argv[1]); curpos(cury, curx); break; default: if (c < ' ' || c > 127) { errchar(c); curpos(cury, curx); break; } txt[cury][curx++] = c; if (curx > SCREENX-1) { --curx; curpos(cury, curx); } break; } } } |
Note: txt[y] is shorthand notation for &txt[y][0].
The version 1 ASCII art editor is limited in text size and can only
operate in overwrite mode. This is good for ASCII art, but not for
source code editing.
Picture: simple ASCII art drawing. Program e1 with MS-Windows
terminal emulator PowerShell.
Picture: simple ASCII art drawing. Program e1 with CP/M 68K
terminal emulator Tera Term.
The minimum feature set of a source code editor is insert mode,
split a line with the Enter key and join two consecutive lines with
the Backspace key.
// e2.c // copyright 3-clause BSD 2025 Andre Adrian // CP/M68k, MS-Windows ANSI Tiny Screen Editor, K&R C // Cursor movement, status line, one screen memory, load&save, // insert mode, backspace, enter #include <stdio.h> #define SCREENX 80 // terminal columns #define SCREENY 24 // terminal rows without status line #define CTRL 64 // Ctrl + alpha key offset #define META 256 // Cursor key offset #ifdef CPM #include <osif.h> extern FILE *fopen(); int congetc() // get character from keyboard { int c = __OSIF(CONIO, 0xFF); if (c != 0x1B) { __OSIF(CONIO, c); // local echo return c; } __OSIF(CONIO, 0xFF); return __OSIF(CONIO, 0xFF)+META; } void conputs(s) // put string to terminal char* s; { while(*s) { __OSIF(CONIO, *s++); } } #else #include <stdlib.h> #include <string.h> #include <conio.h> int congetc() // get character from keyboard { int c = _getch(); if (c != 0xE0) { _putch(c); // local echo return c; } return _getch()+META; } void conputs(s) // put string to terminal char* s; { _cputs(s); } #endif // CPM char txt[SCREENY][SCREENX+1]; // state, +1 for \0 char buf[SCREENX+2]; // +2 for \n\0 void curpos(y, x) // move cursor int y, x; { sprintf(buf, "\033[%d;%dH", y+1, x+1); // ANSI top left is 1;1 conputs(buf); } void txtinit() // init txt with spaces { int y, x; char* p = txt[0]; for (y = 0; y < SCREENY; ++y) { for (x=0; x < SCREENX; ++x) { *p++ = ' '; } *p++ = '\0'; } } void txtpry(y) // print one line int y; { curpos(y, 0); conputs(txt[y]); } void txtpryy(y) // print many lines int y; { for (; y < SCREENY; ++y) { txtpry(y); } } void errfile() // print error message { curpos(SCREENY, 8); conputs("\033[31mCan't open file\033[0m"); } void errchar(i) // print error message int i; { curpos(SCREENY, 8); sprintf(buf, "\033[31mChar 0x%02x\033[0m", i); conputs(buf); } int filread(s) // Read file. If okay then return 0, else !=0 char* s; { int i; FILE* fp = fopen(s, "r"); if (NULL == fp) { errfile(); return 1; } for (i=0; i < SCREENY; ++i) { fgets(buf, sizeof(buf), fp); strncpy(txt[i], buf, strlen(buf)-1); // -1 for \n } return fclose(fp); } int filwrite(s) // Write file. If okay then return 0, else !=0 char* s; { int i; FILE* fp = fopen(s, "w"); if (NULL == fp) { errfile(); return 1; } for (i=0; i < SCREENY; ++i) { fprintf(fp, "%s\n", txt[i]); } return fclose(fp); } void lininit(y) // init one line with spaces int y; { int x; char *p = txt[y]; for (x=0; x < SCREENX; ++x) { *p++ = ' '; } *p++ = '\0'; } void txtright(y, x) // move characters right in line int y, x; { char *q = &txt[y][SCREENX]; char *p = q-1; int i; for (i=x-1; i < SCREENX-2; ++i) { *--q = *--p; } } void txtleft(y, x) // move characters left in line int y, x; { char *q = &txt[y][x]; char *p = q+1; int i; for (i=x-1; i < SCREENX-2; ++i) { *q++ = *p++; } *q = ' '; } int txtjoin(y) // join two consecutive lines int y; { int x; char *p = &txt[y][SCREENX]; for (x = SCREENX-1; x >= -1; --x) { if (*--p != ' ') break; } strncpy(p+1, txt[y+1], SCREENX-1-x); return ++x; } void txtsplit(y, x) // split two consecutive lines int y, x; { char *p = &txt[y][x]; char *q = txt[y+1]; for (; x < SCREENX; ++x) { *q++ = *p; *p++ = ' '; } } void txtdown(y) // move lines down, delete line int y; { int i; char *q = txt[y]; char *p = txt[y+1]; for (i = y; i < SCREENY; ++i) { strcpy(q, p); q += SCREENX+1; p += SCREENX+1; } } void txtup(y) // move lines up, insert line int y; { int i; char *p = txt[SCREENY-2]; char *q = txt[SCREENY-1]; for (i = SCREENY; i > y; --i) { strcpy(q, p); q -= SCREENX+1; p -= SCREENX+1; } } int main(argc, argv) int argc; char *argv[]; { int curx=0, cury=0; // state int c; conputs("\033[H\033[J"); // CUP, ED txtinit(); if (argc) filread(argv[1]); txtpryy(0); conputs("\033[H"); // CUP top left (home) for(;;) { c = congetc(); switch (c) { case 'A'+META: // CP/M case 'H'+META: // MS-Windows if (cury > 0) { --cury; conputs("\033[A"); // CUU } break; case 'B'+META: case 'P'+META: if (cury < SCREENY-1) { ++cury; conputs("\033[B"); // CUD } break; case 'C'+META: case 'M'+META: if (curx < SCREENX-1) { ++curx; conputs("\033[C"); // CUF } break; case 'D'+META: case 'K'+META: if (curx > 0) { --curx; conputs("\033[D"); // CUB } break; case 'Q'-CTRL: exit(0); break; case 'L'-CTRL: txtpryy(0); curpos(SCREENY, 0); // Status line sprintf(buf, "\033[31m%3d:%3d\033[0m\033[K", cury+1, curx+1); conputs(buf); curpos(cury, curx); break; case 'S'-CTRL: filwrite(argv[1]); curpos(cury, curx); break; case 'H'-CTRL: // Backspace if (curx > 0) { txtleft(cury, --curx); txtpry(cury); } else if (cury > 0) { curx = txtjoin(--cury); txtdown(cury+1); lininit(SCREENY-1); txtpryy(cury); } curpos(cury, curx); break; case '\r': // Enter if (cury < SCREENY-1) { txtup(cury+1); lininit(cury+1); txtsplit(cury, curx); txtpryy(cury); ++cury; curx = 0; curpos(cury, curx); } break; default: if (c < ' ' || c > 127) { errchar(c); curpos(cury, curx); break; } txtright(cury, curx); txt[cury][curx++] = c; txtpry(cury); if (curx > SCREENX-1) { --curx; } curpos(cury, curx); break; } } } |
The editor shall handle files that have more then SCREENY lines.
I keep the line length to SCREENX characters and limit the number
of lines to 1000. Hard limits make easy programs and good testing.
The size of array txt becomes 81000 bytes. This would be a problem
for Intel 8088, but not for Motorola 68008.
// e3.c // copyright 3-clause BSD 2025 Andre Adrian // CP/M68k, MS-Windows ANSI Tiny Screen Editor, K&R C // Cursor movement, status line, load&save, // insert mode, backspace, enter, 1000 lines editor #include <stdio.h> #define SCREENX 80 // terminal columns #define SCREENY 24 // terminal rows without status line #define CTRL 64 // Ctrl + alpha key offset #define META 256 // Cursor key offset #define TXTY 1000 // editor max number of lines #ifdef CPM #include <osif.h> extern FILE *fopen(); int congetc() // get character from keyboard { int c = __OSIF(CONIO, 0xFF); if (c != 0x1B) { __OSIF(CONIO, c); // local echo return c; } __OSIF(CONIO, 0xFF); return __OSIF(CONIO, 0xFF)+META; } void conputs(s) // put string to terminal char* s; { while(*s) { __OSIF(CONIO, *s++); } } #else #include <stdlib.h> #include <string.h> #include <conio.h> int congetc() // get character from keyboard { int c = _getch(); if (c != 0xE0) { _putch(c); // local echo return c; } return _getch()+META; } void conputs(s) // put string to terminal char* s; { _cputs(s); } #endif // CPM char txt[TXTY][SCREENX+1]; // state, +1 for \0 char buf[SCREENX+2]; // +2 for \n\0 int offy = 0; // state void curpos(y, x) // move cursor int y, x; { sprintf(buf, "\033[%d;%dH", y+1, x+1); // ANSI top left is 1;1 conputs(buf); } void txtinit() // init txt with spaces { int y, x; char *p = txt[0]; for (y = 0; y < TXTY; ++y) { for (x=0; x < SCREENX; ++x) { *p++ = ' '; } *p++ = '\0'; } } void txtpry(y) // print one line int y; { curpos(y, 0); conputs(txt[offy+y]); } void txtpryy(y) // print many lines int y; { for (; y < SCREENY; ++y) { txtpry(y); } } void errsave() // print error message { curpos(SCREENY, 8); conputs("\033[32mFile saved\033[0m"); } void errfile() // print error message { curpos(SCREENY, 8); conputs("\033[31mFile error\033[0m"); } void errchar(i) // print error message int i; { curpos(SCREENY, 8); sprintf(buf, "\033[31mChar 0x%02x\033[0m", i); conputs(buf); } int filread(s) // Read file. If okay then return 0, else !=0 char* s; { int i; FILE* fp = fopen(s, "r"); if (NULL == fp) { errfile(); return 1; } for (i=0; i < TXTY; ++i) { if (NULL == fgets(buf, sizeof(buf), fp)) break; strncpy(txt[i], buf, strlen(buf)-1); // -1 for \n } return fclose(fp); } int txtylast() // find last non-empty line { int x, i; char *p = buf; // create empty line for (x=0; x < SCREENX; ++x) { *p++ = ' '; } *p++ = '\0'; for(i = TXTY-1; i >= 0; --i) { if (strcmp(buf, txt[i])) break; } return i+1; } int filwrite(s) // Write file. If okay then return 0, else !=0 char* s; { int i, x; FILE* fp = fopen(s, "w"); char *p; int ylast = txtylast(); if (NULL == fp) { errfile(); return 1; } for (i=0; i < ylast; ++i) { strcpy(buf, txt[i]); p = &buf[SCREENX]; for (x = SCREENX; x >= 0; --x) { if (*--p > ' ') break; } *(p+1) = '\0'; // truncate trailing spaces if (fprintf(fp, "%s\n", buf) < 0) return 1; } return fclose(fp); } void lininit(y) // init one line with spaces int y; { int x; char *p = txt[offy+y]; for (x=0; x < SCREENX; ++x) { *p++ = ' '; } *p++ = '\0'; } void txtright(y, x) // move characters right in line int y, x; { char *q = &txt[offy+y][SCREENX]; char *p = q-1; int i; for (i=x-1; i < SCREENX-2; ++i) { *--q = *--p; } } void txtleft(y, x) // move characters left in line int y, x; { char *q = &txt[offy+y][x]; char *p = q+1; int i; for (i=x-1; i < SCREENX-2; ++i) { *q++ = *p++; } *q = ' '; } int txtjoin(y) // join two consecutive lines, return new curx int y; { int x; char *p = &txt[offy+y][SCREENX]; for (x = SCREENX-1; x >= -1; --x) { if (*--p != ' ') break; } strncpy(p+1, txt[offy+y+1], SCREENX-1-x); return ++x; } void txtsplit(y, x) // split two consecutive lines int y, x; { char *p = &txt[offy+y][x]; char *q = txt[offy+y+1]; for (; x < SCREENX; ++x) { *q++ = *p; *p++ = ' '; } } void txtdown(y) // move lines down, delete line int y; { int i; char *q = txt[offy+y]; char *p = txt[offy+y+1]; for (i = offy+y; i < TXTY; ++i) { strcpy(q, p); q += SCREENX+1; p += SCREENX+1; } } void txtup(y) // move lines up, insert line int y; { int i; char *p = txt[TXTY-2]; char *q = txt[TXTY-1]; for (i = TXTY; i > offy+y; --i) { strcpy(q, p); q -= SCREENX+1; p -= SCREENX+1; } } int main(argc, argv) int argc; char *argv[]; { int curx=0, cury=0; // state int c; conputs("\033[H\033[J"); // CUP, ED txtinit(); if (argc) filread(argv[1]); txtpryy(0); conputs("\033[H"); // CUP top left (home) for(;;) { c = congetc(); switch (c) { case 'A'+META: // CP/M up case 'H'+META: // MS-Windows if (cury > 0) { --cury; conputs("\033[A"); // CUU } else if (offy >= 1) { --offy; conputs("\033[T\033[25;1H\033[K"); // SD, CUP, EL txtpry(0); curpos(cury, curx); } break; case 'B'+META: // down case 'P'+META: if (cury < SCREENY-1) { ++cury; conputs("\033[B"); // CUD } else if (offy < TXTY-SCREENY) { ++offy; conputs("\033[S"); // SU txtpry(SCREENY-1); curpos(cury, curx); } break; case 'C'+META: // right case 'M'+META: if (curx < SCREENX-1) { ++curx; conputs("\033[C"); // CUF } break; case 'D'+META: // left case 'K'+META: if (curx > 0) { --curx; conputs("\033[D"); // CUB } break; case 'Q'-CTRL: exit(0); break; case 'L'-CTRL: txtpryy(0); curpos(SCREENY, 0); // Status line sprintf(buf, "\033[31m%3d:%3d\033[0m\033[K", offy+cury+1, curx+1); conputs(buf); curpos(cury, curx); break; case 'S'-CTRL: if (filwrite(argv[1])) errfile(); else errsave(); curpos(cury, curx); break; case 'H'-CTRL: // Backspace if (curx > 0) { txtleft(cury, --curx); txtpry(cury); } else if (cury > 0) { curx = txtjoin(--cury); txtdown(cury+1); lininit(TXTY-1); txtpryy(cury); } curpos(cury, curx); break; case '\r': // Enter if (cury < SCREENY-1) { txtup(cury+1); lininit(cury+1); txtsplit(cury, curx); txtpryy(cury); ++cury; } else if (offy < TXTY-SCREENY) { ++offy; conputs("\033[S"); // SU txtup(cury); lininit(cury); txtsplit(cury-1, curx); txtpryy(cury-1); } curx = 0; curpos(cury, curx); break; default: if (c < ' ' || c > 127) { errchar(c); curpos(cury, curx); break; } txtright(cury, curx); txt[offy+cury][curx++] = c; txtpry(cury); if (curx > SCREENX-1) { --curx; } curpos(cury, curx); break; } } } |
There were some bugs with e3.c prior 2025-01-29 version: in
txtup() I did not use txt[TXTY-1] as last line. At processing enter
key I forgot the special case hit enter in the last row. These bugs
are hopefully fixed and no new bugs introduced.
Because there is no flow control (handshake) between Tera Term and
68k-MBC, keyboard repeat and buffer overflow can corrupt ANSI
escape sequences. For example: pressing long time cursor down can
"sprinkle" B into your text because "ESC [ B" gets corrupted to
"B".
With 374 lines of code, comment lines and empty lines incluced, the
screen editor is tiny. CP/M 68K source code size is 8KByte,
executable size is 36 KByte (290 records of 128bytes).
The Tiny Screen Editor has a primitive data structure. There is
one fixed size two dimensional array for the text. Other editors
use other data structures. The Kilo editor uses dynamic memory
using malloc(), realloc() and free() functions. This sophisticated
solution should save memory. But malloc() can suffer from "memory
fragmentation", specially if every second line in the text grows
through editing. Further any malloc() data structure needs
additional memory for "housekeeping". Another complication is
entering characters in the middle of a line. Our primitive "an
empty line is filled with spaces" approach helps in this case. A
line that has no trailing spaces needs more housekeeping if a new
character is added after trailing spaces. All in all, the
performance (memory/speed trade off) of primitive solutions is
often under-estimated, the performance of sophisticated solutions
is often over-estimated.
Last, but not least, sophisticated solutions have sophisticated
bugs, primitive solutions have primitive bugs. Testing is nasty if
the bug shows only after hundred editor actions.
Between my first C compiler in 1986 and today are 39 years. I
used C and C++ all these years. Basic, Logo, (Turbo) Pascal and
Prolog are long gone, C survived. I had to look into my old "The C
programming language" book for details of K&R C. The CP/M 68K
1.3 version with the Alcyon C compiler is copyright 1985. Over the
years, every programmer should learn "how to skin the cat" or solve
the problem. I call it "thinking with your gut" - experience
condensed not to conscious know how but to "semi-conscious" know
how. You need years of practise, like in every
art/craft/science/trade.
Some C language details are compiler implementation dependent. A
little program shows some of these details.
// impl.c // copyright 3-clause BSD 2025 Andre Adrian // check C compiler implementation #include <stdio.h> int main() { int i = sizeof(char); char c = 0xFF; printf("sizeof(char)=%d\n", i); i = sizeof(short); printf("sizeof(short)=%d\n", i); i = sizeof(int); printf("sizeof(int)=%d\n", i); i = sizeof(long); printf("sizeof(long)=%d\n", i); i = sizeof(float); printf("sizeof(float)=%d\n", i); i = sizeof(double); printf("sizeof(double)=%d\n", i); i = sizeof(char*); printf("sizeof(char*)=%d\n", i); i = c; printf("char signed if -1=%d\n", i); } |
Output on 64-bit MS-Windows computer:
sizeof(char)=1 sizeof(short)=2 sizeof(int)=4 sizeof(long)=4 sizeof(float)=4 sizeof(double)=8 sizeof(char*)=8 char signed if -1=-1 |
Output on CP/M 68K computer:
sizeof(char)=1 sizeof(short)=2 sizeof(int)=2 sizeof(long)=4 sizeof(float)=4 sizeof(double)=4 sizeof(char*)=4 char signed if -1=-1 |
One typical difference between C compilers is the length of int.
Typical for a 64-bit computer is the char* length of 8. On the CP/M
68K the double has the same length as float. This is seldom. The
Alcyon C has only float numbers. Type char is signed for both
computers. The implementation differences are not important for the
Tiny Screen Editor. As they say: "Write once with #ifdef, compile
anywhere".