diff --git a/vim/build.bat b/vim/build.bat new file mode 100644 index 0000000..99b5625 --- /dev/null +++ b/vim/build.bat @@ -0,0 +1,71 @@ +@echo off +setlocal +REM ------------------------------------------------------------------ +REM Build the vim editor with Spinnaker Power C on a private copy of +REM the system disk at ..\reference\POWERc.d81. +REM +REM The editor is two source files because Power C's compiler reports +REM "program too big" when the whole thing is one unit; both objects +REM are linked into vim. +REM +REM Power C is interactive: "cc" asks for the source, compiler and +REM object disks, and "link" prompts for object files, the "^" library +REM line and the output file name. -keybuf answers all of it; the runs +REM of \n are those prompts. -keybuf-delay spaces the answers out: fed +REM all at once they land before the later disk prompts appear and are +REM lost, which leaves cc waiting for the object disk. +REM ------------------------------------------------------------------ +set "VICE=C:\Program Files\GTK3VICE-3.10-win64\bin" +set "HERE=%~dp0" +set "BASE=%HERE%..\reference\POWERc.d81" +set "IMG=%HERE%vim.d81" + +if not exist "%VICE%\x64sc.exe" goto novice +if not exist "%BASE%" goto nobody + +echo === fresh work disk +copy /y "%BASE%" "%IMG%" >nul + +echo === ASCII source -^> PETSCII +powershell -NoProfile -ExecutionPolicy Bypass -File "%HERE%to-petscii.ps1" "%HERE%vim.c" "%HERE%vim.pet" +if errorlevel 1 goto bad +powershell -NoProfile -ExecutionPolicy Bypass -File "%HERE%to-petscii.ps1" "%HERE%vscreen.c" "%HERE%vscreen.pet" +if errorlevel 1 goto bad + +echo === write both sources to the disk +for %%f in (vim.c vim.o vscreen.c vscreen.o vim) do "%VICE%\c1541.exe" "%IMG%" -delete %%f >nul 2>&1 +"%VICE%\c1541.exe" "%IMG%" -write "%HERE%vim.pet" "vim.c,s" >nul +if errorlevel 1 goto bad +"%VICE%\c1541.exe" "%IMG%" -write "%HERE%vscreen.pet" "vscreen.c,s" >nul +if errorlevel 1 goto bad + +echo === cc vim.c (a minute or so in warp mode) +"%VICE%\x64sc.exe" -8 "%IMG%" -autostart "%IMG%:shell" -autostart-warp -warp -sounddev dummy -keybuf-delay 300 -keybuf "cc vim.c\n\n\n\n\n\n\n\n\n\n\n\n" -limitcycles 600000000 + +echo === cc vscreen.c +"%VICE%\x64sc.exe" -8 "%IMG%" -autostart "%IMG%:shell" -autostart-warp -warp -sounddev dummy -keybuf-delay 300 -keybuf "cc vscreen.c\n\n\n\n\n\n\n\n\n\n\n\n" -limitcycles 600000000 + +echo === link -s -^> vim +"%VICE%\x64sc.exe" -8 "%IMG%" -autostart "%IMG%:shell" -autostart-warp -warp -sounddev dummy -keybuf-delay 300 -keybuf "link -s\nvim.o\nvscreen.o\n^\n\nvim\n\n\n" -limitcycles 600000000 + +del "%HERE%vim.pet" >nul 2>&1 +del "%HERE%vscreen.pet" >nul 2>&1 + +echo === disk contents +"%VICE%\c1541.exe" "%IMG%" -list | findstr /i "vim vscreen" +echo. +echo Run it with: +echo x64sc -autostart "%IMG%:vim" +exit /b 0 + +:novice +echo x64sc.exe not found in "%VICE%" +exit /b 1 + +:nobody +echo %BASE% not found - is the repository complete? +exit /b 1 + +:bad +echo BUILD FAILED +exit /b 1 diff --git a/vim/to-petscii.ps1 b/vim/to-petscii.ps1 new file mode 100644 index 0000000..47a47a8 --- /dev/null +++ b/vim/to-petscii.ps1 @@ -0,0 +1,31 @@ +# to-petscii.ps1 - convert an ASCII C source file into the PETSCII form +# Power C expects on a C64 disk: uppercase letters shifted into $C1-$DA, +# lowercase into $41-$5A, { } into $DB/$DD, and CR ($0D) line endings. +# +# Usage: +# powershell -NoProfile -File to-petscii.ps1 vim.c vim.pet + +param( + [Parameter(Mandatory = $true)][string]$InFile, + [Parameter(Mandatory = $true)][string]$OutFile +) + +$bytes = [System.IO.File]::ReadAllBytes($InFile) +$start = 0 +if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) { + $start = 3 # skip a UTF-8 BOM if an editor added one +} + +$out = New-Object System.Collections.Generic.List[byte] +for ($i = $start; $i -lt $bytes.Length; $i++) { + $b = $bytes[$i] + if ($b -eq 13) { continue } # CRLF -> single CR + if ($b -eq 10) { $out.Add([byte]13); continue } + if ($b -ge 0x61 -and $b -le 0x7A) { $out.Add([byte]($b - 0x20)); continue } # a-z + if ($b -ge 0x41 -and $b -le 0x5A) { $out.Add([byte]($b + 0x80)); continue } # A-Z + if ($b -ge 0x7B -and $b -le 0x7F) { $out.Add([byte]($b + 0x60)); continue } # { | } ~ -> $DB-$DF + $out.Add([byte]$b) +} + +[System.IO.File]::WriteAllBytes($OutFile, $out.ToArray()) +Write-Host ("wrote {0} PETSCII bytes to {1}" -f $out.Count, $OutFile) diff --git a/vim/vim.c b/vim/vim.c new file mode 100644 index 0000000..3b92655 --- /dev/null +++ b/vim/vim.c @@ -0,0 +1,420 @@ +/* vim.c - a small vi/vim-style screen editor for the Commodore 64 + * + * Core half of the program: text buffer, cursor motion, key handling + * and saving. Drawing lives in vscreen.c because Power C's compiler + * reports "program too big" when the whole editor is one unit. The two + * halves share functions only - every piece of state stays private to + * this file and is handed to draw() as arguments - because sharing + * variables across Power C object files proved unreliable. + * + * Modal editing, like vi: + * + * COMMAND mode INSERT mode + * i insert here any key type text + * a append after char RETURN split the line + * o open line below DEL backspace + * O open line above RUN/STOP back to COMMAND mode + * x delete character cursor keys move + * h j k l or cursor keys CLR/HOME start of line + * 0 start of line + * $ end of line + * : command line + * + * : commands + * :w write "notes" to disk (SEQ file, drive 8) + * :wq write and quit + * :q quit + * + * Special C characters, on the same keys the Power C manual gives for ced: + * + * { SHIFT + } SHIFT - \ English pound key + * ~ C= (logo) P _ C= (logo) @ | C= (logo) * + * + * The Kernal already produces the codes Power C expects for these + * ($DB, $DD, $5C, $AF, $A4, $DF - read from the key tables at $EB81), + * so insert mode only has to accept them. The character ROM has no + * brace or bar glyphs, so on screen they appear as the nearest glyph. + * + * ESCAPE is the C64 RUN/STOP key, which the Kernal reports in the + * zero-page STOP flag at $91. + * + * The text buffer holds 2000 bytes with 13 ($0D) as the line break. + * Keys come from the Kernal keyboard buffer at $0277, count at $c6. + * + * Build with build.bat in this folder: + * + * cc vim.c -> vim.o + * cc vscreen.c -> vscreen.o + * link -s + * > vim.o + * > vscreen.o + * > ^ (stdlib.l + syslib.l) + * > + * output file name: vim + * + * Then run with: x64sc -autostart "vim.d81:vim" + * or from BASIC: LOAD"VIM",8,1 : RUN + * + * Note: this Power C build rejects both $7C and $DC as "illegal + * character" for the | operator, so nested tests are used instead of ||. + */ + +#include + +static char text[2000]; /* document, 13 = line break */ +static int tlen; /* bytes in use */ +static int pos; /* cursor, byte offset into text */ +static int mode; /* 0 command, 1 insert, 2 : prompt */ +static char fname[17]; /* file name used by :w */ +static char cmd[9]; /* text after the ':' */ +static int cmdlen; +static int quit; +static int msgcode; /* message for draw(): 0 none, 1-4 */ + +static char *defnam = "notes"; + +/* ---------- provided by vscreen.c ---------- */ + +int draw(); +int clrscr(); + +/* ---------- buffer helpers ---------- */ + +static lstart(n) /* offset of the first byte of line n */ +int n; +{ + int i; + int c; + + if (n <= 0) return 0; + c = 0; + i = 0; + while (i < tlen) { + if (text[i] == 13) { + c = c + 1; + if (c == n) return i + 1; + } + i = i + 1; + } + return tlen; +} + +static lcount() /* number of line breaks */ +{ + int i; + int c; + + c = 0; + for (i = 0; i < tlen; i++) + if (text[i] == 13) c = c + 1; + return c; +} + +static lnof() /* line holding the cursor */ +{ + int i; + int c; + + c = 0; + for (i = 0; i < pos && i < tlen; i++) + if (text[i] == 13) c = c + 1; + return c; +} + +static intend(o) /* offset of the line end */ +int o; +{ + int e; + + e = o; + while (e < tlen && text[e] != 13) e = e + 1; + return e; +} + +static inschr(ch) +int ch; +{ + int i; + + if (tlen >= 1999) { + msgcode = 1; + return; + } + for (i = tlen; i > pos; i--) + text[i] = text[i - 1]; + text[pos] = ch; + tlen = tlen + 1; + pos = pos + 1; +} + +static delat() +{ + int i; + + if (pos >= tlen) return; + for (i = pos; i < tlen - 1; i++) + text[i] = text[i + 1]; + tlen = tlen - 1; +} + +static backsp() +{ + if (pos <= 0) return; + pos = pos - 1; + delat(); +} + +/* ---------- cursor motion ---------- */ + +static movleft() +{ + if (pos > 0) pos = pos - 1; +} + +static movright() +{ + if (pos < tlen) pos = pos + 1; +} + +static movdown() +{ + int ln; + int cs; + int o; + int e; + + ln = lnof(); + if (ln + 1 > lcount()) return; + cs = pos - lstart(ln); + o = lstart(ln + 1); + e = intend(o); + pos = o + cs; + if (pos > e) pos = e; +} + +static movup() +{ + int ln; + int cs; + int o; + int e; + + ln = lnof(); + if (ln <= 0) return; + cs = pos - lstart(ln); + o = lstart(ln - 1); + e = intend(o); + pos = o + cs; + if (pos > e) pos = e; +} + +static linehome() +{ + pos = lstart(lnof()); +} + +static lineend() +{ + pos = intend(lstart(lnof())); +} + +/* ---------- saving ---------- */ + +static savfil() +{ + FILE *f; + int i; + + f = fopen(fname, "w"); + if (!f) { + msgcode = 2; + return; + } + if (ferror(f)) { + msgcode = 2; + fclose(f); + return; + } + for (i = 0; i < tlen; i++) fputc(text[i], f); + fclose(f); + msgcode = 3; +} + +/* ---------- : command line ---------- */ + +static docmd() +{ + if (cmdlen == 0) return; + if (cmd[0] == 'w') { + if (cmdlen > 1 && cmd[1] == 'q') { + savfil(); + quit = 1; + return; + } + savfil(); + return; + } + if (cmd[0] == 'q') { + quit = 1; + return; + } + msgcode = 4; +} + +/* ---------- key handlers ---------- */ + +static insk(k) +int k; +{ + if (k == 3) { /* ESC: leave insert mode */ + mode = 0; + if (pos > 0) pos = pos - 1; + return; + } + if (k == 13) { inschr(13); return; } + if (k == 20) { backsp(); return; } + if (k == 157) { movleft(); return; } + if (k == 29) { movright(); return; } + if (k == 145) { movup(); return; } + if (k == 17) { movdown(); return; } + if (k == 19) { linehome(); return; } + if (k >= 32 && k < 128) { inschr(k); return; } + if (k == 219) { inschr(k); return; } /* SHIFT + : { */ + if (k == 221) { inschr(k); return; } /* SHIFT - : } */ + if (k == 223) { inschr(k); return; } /* logo * : | */ + if (k == 175) { inschr(k); return; } /* logo P : ~ */ + if (k == 164) { inschr(k); return; } /* logo @ : _ */ +} + +static cmdk(k) +int k; +{ + if (k >= 65 && k <= 90) k = k + 32; /* letters arrive as PETSCII $41-$5A */ + if (k == ':') { + mode = 2; + cmdlen = 0; + cmd[0] = 0; + return; + } + if (k == 'i') { mode = 1; return; } + if (k == 'a') { + if (pos < tlen && text[pos] != 13) pos = pos + 1; + mode = 1; + return; + } + if (k == 'o') { + pos = intend(lstart(lnof())); + inschr(13); + mode = 1; + return; + } + if (k == 'O') { + pos = lstart(lnof()); + inschr(13); + pos = pos - 1; + mode = 1; + return; + } + if (k == 'x') { delat(); return; } + if (k == 'h') { movleft(); return; } + if (k == 157) { movleft(); return; } + if (k == 'l') { movright(); return; } + if (k == 29) { movright(); return; } + if (k == 'j') { movdown(); return; } + if (k == 17) { movdown(); return; } + if (k == 'k') { movup(); return; } + if (k == 145) { movup(); return; } + if (k == '0') { linehome(); return; } + if (k == 19) { linehome(); return; } + if (k == '$') { lineend(); return; } +} + +/* ---------- keyboard ---------- */ + +static getkey() /* one byte from the Kernal keyboard + buffer, $0277, count at $c6 */ +{ + char *cnt; + char *buf; + int i; + int k; + + cnt = (char *) 198; + buf = (char *) 631; + if (*cnt == 0) return 0; + if (*cnt > 10) { + *cnt = 0; + return 0; + } + k = buf[0]; + for (i = 0; i < 9; i++) buf[i] = buf[i + 1]; + *cnt = *cnt - 1; + return k; +} + +static waitkey() +{ + char *stop; + int k; + + for (;;) { + stop = (char *) 145; /* $91: RUN/STOP flag */ + if (*stop) { + *stop = 0; + return 3; + } + k = getkey(); + if (k) return k; + } +} + +/* ---------- main loop ---------- */ + +main() +{ + int k; + int i; + + tlen = 0; + pos = 0; + mode = 0; + cmdlen = 0; + quit = 0; + msgcode = 0; + + i = 0; + while (defnam[i]) { + fname[i] = defnam[i]; + i = i + 1; + } + fname[i] = 0; + + while (!quit) { + draw(text, tlen, pos, mode, fname, cmd, cmdlen, msgcode); + msgcode = 0; + k = waitkey(); + if (mode == 2) { + if (k == 13) { + mode = 0; + docmd(); + } + else if (k == 20) { + if (cmdlen > 0) cmdlen = cmdlen - 1; + } + else if (k >= 32 && cmdlen < 8) { + if (k >= 65 && k <= 90) k = k + 32; + cmd[cmdlen] = k; + cmdlen = cmdlen + 1; + } + continue; + } + if (mode == 1) { + insk(k); + continue; + } + cmdk(k); + } + + clrscr(); + printf("bye\n"); +} diff --git a/vim/vim.d81 b/vim/vim.d81 new file mode 100644 index 0000000..cc47478 Binary files /dev/null and b/vim/vim.d81 differ diff --git a/vim/vscreen.c b/vim/vscreen.c new file mode 100644 index 0000000..f2d9cfa --- /dev/null +++ b/vim/vscreen.c @@ -0,0 +1,248 @@ +/* vscreen.c - screen half of the vim editor for the C64 (Power C) + * + * Draws the text window, the status line and the cursor straight into + * the screen matrix at $0400 and colour RAM at $d800. Kept in its own + * file because Power C overflows its compiler when the whole editor is + * one source. + * + * This module owns no program state: draw() receives everything it + * needs from vim.c as arguments. Sharing variables between Power C + * object files proved unreliable, so only functions cross the boundary. + */ + +static char stat[40]; /* status line build buffer */ +static int slen; + +/* ---------- status line text ---------- */ + +static sclr() +{ + slen = 0; + stat[0] = 0; +} + +static sadd(c) +char c; +{ + if (slen < 39) { + stat[slen] = c; + slen = slen + 1; + stat[slen] = 0; + } +} + +static sstr(s) +char *s; +{ + while (*s) { + sadd(*s); + s = s + 1; + } +} + +static snum(n) +int n; +{ + char b[6]; + int i; + + if (n < 0) { + sadd('-'); + n = -n; + } + if (n == 0) { + sadd('0'); + return; + } + i = 0; + while (n > 0) { + b[i] = '0' + (n % 10); + i = i + 1; + n = n / 10; + } + while (i > 0) { + i = i - 1; + sadd(b[i]); + } +} + +/* ---------- PETSCII byte -> screen code ---------- */ + +static sccode(c) +int c; +{ + if (c >= 193 && c <= 218) return c - 192; /* shifted letters $C1-$DA */ + if (c >= 96 && c <= 127) return c - 32; + if (c >= 64 && c <= 95) return c - 64; + /* the C-only characters have no glyph of their own in the ROM */ + if (c == 219) return 91; /* { -> [ */ + if (c == 221) return 93; /* } -> ] */ + if (c == 223) return 94; /* | -> up arrow */ + if (c == 175) return 126; /* ~ -> its graphics glyph */ + if (c == 164) return 95; /* _ -> left arrow */ + return c; +} + +/* ---------- screen output ---------- */ + +clrscr() +{ + char *s; + char *m; + int i; + + s = (char *) 1024; + m = (char *) 55296; + for (i = 0; i < 1000; i++) { + s[i] = 32; + m[i] = 1; + } +} + +static putchr(x, y, ch, c) +int x; +int y; +int ch; +int c; +{ + char *s; + char *m; + int o; + + if (x < 0) return; + if (x > 39) return; + if (y < 0) return; + if (y > 24) return; + o = y * 40 + x; + s = (char *) 1024; + m = (char *) 55296; + s[o] = ch; + m[o] = c; +} + +static putstr(x, y, s, c) +int x; +int y; +char *s; +int c; +{ + while (*s && x < 40) { + putchr(x, y, sccode(*s), c); + x = x + 1; + s = s + 1; + } +} + +static revchr(o) /* reverse the character in a cell */ +int o; +{ + char *s; + + s = (char *) 1024; + if (s[o] < 128) s[o] = s[o] + 128; +} + +/* ---------- buffer scans over the caller's text ---------- */ + +static lstartx(tp, len, n) /* offset of the first byte of line n */ +char *tp; +int len; +int n; +{ + int i; + int c; + + if (n <= 0) return 0; + c = 0; + i = 0; + while (i < len) { + if (tp[i] == 13) { + c = c + 1; + if (c == n) return i + 1; + } + i = i + 1; + } + return len; +} + +static lnofx(tp, cur) /* line holding the cursor */ +char *tp; +int cur; +{ + int i; + int c; + + c = 0; + for (i = 0; i < cur; i++) + if (tp[i] == 13) c = c + 1; + return c; +} + +/* ---------- full redraw ---------- */ + +draw(tp, len, cur, md, name, cp, cl, mc) +char *tp; +int len; +int cur; +int md; +char *name; +char *cp; +int cl; +int mc; +{ + int ln; + int top; + int r; + int i; + int to; + int c; + int cs; + int cr; + + ln = lnofx(tp, cur); + top = ln - 11; + if (top < 0) top = 0; + + clrscr(); + + to = lstartx(tp, len, top); + for (r = 0; r < 23; r++) { + i = 0; + while (to < len && i < 40) { + c = tp[to]; + if (c == 13) break; + putchr(i, r, sccode(c), 1); + i = i + 1; + to = to + 1; + } + while (to < len && tp[to] != 13) to = to + 1; + if (to < len) to = to + 1; + } + + for (i = 0; i < 40; i++) putchr(i, 24, 32, 11); + + sclr(); + if (md == 1) sstr("INSERT"); + else if (md == 2) sstr(": "); + else sstr("COMMAND"); + sstr(" line "); + snum(ln + 1); + sstr(" "); + sstr(name); + if (mc == 1) sstr(" full"); + else if (mc == 2) sstr(" save err"); + else if (mc == 3) sstr(" saved"); + else if (mc == 4) sstr(" ?cmd"); + putstr(0, 24, stat, 11); + + if (md == 2) { + putchr(0, 23, sccode(':'), 1); + for (i = 0; i < cl; i++) putchr(i + 1, 23, sccode(cp[i]), 1); + } + + cs = cur - lstartx(tp, len, ln); + if (cs > 39) cs = 39; + if (cs < 0) cs = 0; + cr = ln - top; + if (cr >= 0 && cr < 23) revchr(cr * 40 + cs); +}