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

Author: Andre Adrian
Date: 2025-02-15

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 computers are the Z80-MBC2, a CP/M computer with Zilog Z80 CPU with 128KByte RAM, K&R standard C compiler and the 68k-MBC, a CP/M 68K computer with Motorola 68008 CPU, 1MByte RAM, two RS232 serial ports and K&R standard C compiler. This is computing like in the 1980s. I had Sinclair ZX81, Sinclair Spectrum and Atari ST at this time. The CP/M 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, Tera Term configuration, compile C source code 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.

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
int congetc() /* get character from keyboard */
{
    int c = _getch();
    if (c != 0xE0) {
        _putch(c);      /* local echo */
        return c;
    }
    return _getch()+META;
}


/* put string to terminal */
#define conputs(s) _cputs(s)

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

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 */
#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 z80
#include <string.h>
#include <conio.h>

int congetc() /* get character from keyboard */
{
    int c = getch();
    if (c != 0x1B) {
        putch(c);      /* local echo */
        return c;
    }
    getch();
    return getch()+META;
}

/* put string to terminal */
#define conputs(s) cputs(s)
#else
#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;
}

/* put string to terminal */
#define conputs(s) _cputs(s)
#endif /* CPM */
#endif /* z80 */

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 'W'-CTRL:  /* CP/M */
        case 'A'+META:  /* CP/M 68K */
        case 'H'+META:  /* MS-Windows */
            if (cury > 0) {
                --cury;
                conputs("\033[A");  /* CUU */
            }
        break;
        case 'S'-CTRL:
        case 'B'+META:
        case 'P'+META:
            if (cury < SCREENY-1) {
                ++cury;
                conputs("\033[B");  /* CUD */
            }
        break;
        case 'D'-CTRL:
        case 'C'+META:
        case 'M'+META:
            if (curx < SCREENX-1) {
                ++curx;
                conputs("\033[C");  /* CUF */
            }
        break;
        case 'A'-CTRL:
        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 */
#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 z80
#include <string.h>
#include <conio.h>

int congetc() /* get character from keyboard */
{
    int c = getch();
    if (c != 0x1B) {
        putch(c);      /* local echo */
        return c;
    }
    getch();
    return getch()+META;
}

/* put string to terminal */
#define conputs(s) cputs(s)
#else
#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;
}

/* put string to terminal */
#define conputs(s) _cputs(s)
#endif /* CPM */
#endif /* z80 */

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 (2 == argc)
        filread(argv[1]);
    txtpryy(0);
    conputs("\033[H");  /* CUP top left (home) */
    for(;;) {
        c = congetc();
        switch (c) {
        case 'W'-CTRL:  /* CP/M */
        case 'A'+META:  /* CP/M 68K */
        case 'H'+META:  /* MS-Windows */
            if (cury > 0) {
                --cury;
                conputs("\033[A");  /* CUU */
            }
        break;
        case 'X'-CTRL:
        case 'B'+META:
        case 'P'+META:
            if (cury < SCREENY-1) {
                ++cury;
                conputs("\033[B");  /* CUD */
            }
        break;
        case 'D'-CTRL:
        case 'C'+META:
        case 'M'+META:
            if (curx < SCREENX-1) {
                ++curx;
                conputs("\033[C");  /* CUF */
            }
        break;
        case 'A'-CTRL:
        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 */
#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 z80
#include <string.h>
#include <conio.h>

int congetc() /* get character from keyboard */
{
    int c = getch();
    if (c != 0x1B) {
        putch(c);      /* local echo */
        return c;
    }
    getch();
    return getch()+META;
}

/* put string to terminal */
#define conputs(s) cputs(s)
#else
#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;
}

/* put string to terminal */
#define conputs(s) _cputs(s)
#endif /* CPM */
#endif /* z80 */

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 (2 == argc)
        filread(argv[1]);
    txtpryy(0);
    conputs("\033[H");  /* CUP top left (home) */
    for(;;) {
        c = congetc();
        switch (c) {
        case 'W'-CTRL:  /* CP/M */
        case 'A'+META:  /* CP/M 68K up */
        case 'H'+META:  /* MS-Windows */
            if (cury > 0) {
                --cury;
                conputs("\033[A");  /* CUU */
            }
        break;
        case 'X'-CTRL:
        case 'B'+META:  /* down */
        case 'P'+META:
            if (cury < SCREENY-1) {
                ++cury;
                conputs("\033[B");  /* CUD */
            }
        break;
        case 'D'-CTRL:
        case 'C'+META:  /* right */
        case 'M'+META:
            if (curx < SCREENX-1) {
                ++curx;
                conputs("\033[C");  /* CUF */
            }
        break;
        case 'A'-CTRL:
        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", 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. The Z80-MBC2 with CP/M 2.2 can handle 500 lines or 40500 bytes.

/* e3.c */
#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    500     /* editor max number of lines */

#ifdef z80
#include <string.h>
#include <conio.h>

int congetc() /* get character from keyboard */
{
    int c = getch();
    if (c != 0x1B) {
        putch(c);      /* local echo */
        return c;
    }
    getch();
    return getch()+META;
}

/* put string to terminal */
#define conputs(s) cputs(s)
#else
#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;
}

/* put string to terminal */
#define conputs(s) _cputs(s)
#endif /* CPM */
#endif /* z80 */

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 (2 == argc)
        filread(argv[1]);
    txtpryy(0);
    conputs("\033[H");  /* CUP top left (home) */
    for(;;) {
        c = congetc();
        switch (c) {
        case 'W'-CTRL:  /* CP/M */
        case 'A'+META:  /* CP/M 68K 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 'X'-CTRL:
        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 'D'-CTRL:
        case 'C'+META:  /* right */
        case 'M'+META:
            if (curx < SCREENX-1) {
                ++curx;
                conputs("\033[C");  /* CUF */
            }
        break;
        case 'A'-CTRL:
        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.

CP/M 2.2 program sizes

The sizes of programs e0 to e3 on CP/M 2.2 with C compiler flag -O (optimize) are:

C>a:stat e*.com

 Recs  Bytes  Ext Acc
   38     8k    1 R/W C:E0.COM
   77    12k    1 R/W C:E1.COM
   86    12k    1 R/W C:E2.COM
   91    12k    1 R/W C:E3.COM

Note: the FAT32 block size is 4 KByte. One record is 128 bytes.
The program sizes go from 38 records or 4.8 KBytes to 91 records or 11.4 KBytes. Program e1 is the first that uses fopen(). This uses some program space. The step from 25 lines to 500 lines between e2 and e3 needs only small additional program space.

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 */
#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

Output on CP/M 2.2 Z80 computer:

sizeof(char)=1
sizeof(short)=2
sizeof(int)=2
sizeof(long)=4
sizeof(float)=4
sizeof(double)=4
sizeof(char*)=2
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 2.2 and CP/M 68K the double has the same length as float. This is seldom. The Hi-Soft C and Alcyon C have only float numbers. Type char is signed for all compilers. The implementation differences are not important for the Tiny Screen Editor. As they say: "Write once with #ifdef, compile anywhere".