CP/M 68K, MS-Windows Tiny Screen editor

Author: Andre Adrian
Date: 2025-01-30

Introduction

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.

Download

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).

Development environments

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.

Raw mode input

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.

Cursor keys

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.

ANSI escape codes

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.

Source code

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.

Version 0 Cursor movement

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 ASCII art

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.

Version 2 insert mode, enter, backspace

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;
        }
    }
}

 

Version 3 large file

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).



Picture: e3 editor displays e3.c file, CP/M 68K version.

Design trade offs

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.

Summary

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.

Appendix A: C compiler differences

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".