diff --git a/AGENTS.md b/AGENTS.md index ebf4462..085a92a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -25,7 +25,7 @@ This repository contains structured skills and reference material for developing | `skills/interrupts/` | IRQ, NMI, CIA timers, raster interrupts | Interrupt-driven code, timing | | `skills/compiler-design/` | Compiler & assembler construction for C64/128 | Writing compilers, assemblers, parsers | | `skills/vice-emulator/` | VICE emulator, autostart, monitor, snapshots, automation | Running, testing, and debugging C64 software in VICE | -| `skills/powerc/` | Power C compiler on the C64 | Writing, compiling, linking, and running C programs (reference/POWERc.d81) | +| `skills/powerc/` | Power C compiler on the C64 | Writing, compiling, linking, and running C programs with Power C (reference/POWERc.d81); load this whenever Power C or on-C64 C development is requested | ### Reference Files @@ -225,7 +225,8 @@ When an AI agent receives a C64 development question: 3. **Cross-reference** `reference/` files for specific register values, addresses, or opcodes 4. **Check examples/** for working code patterns 5. **If the task involves running or debugging software in an emulator**, load `skills/vice-emulator/SKILL.md` and `reference/vice-usage.md` -6. **Apply the 150-expert consensus**: prefer documented Kernal routines over custom implementations; use zero-page for speed; always consider memory banking when accessing $D000–$DFFF +6. **If the task involves writing C for the C64** (user mentions C, Power C, `ced`, `cc`, `link`, or `reference/POWERc.d81`), load `skills/powerc/SKILL.md` first. Power C is K&R C (no prototypes/`void`, 2-byte `int`, 8-char identifiers), sources on disk are PETSCII, and `cc`/`link` need specific steps. Also load `skills/vice-emulator/SKILL.md` to automate the build in VICE (copy `reference/POWERc.d81` first; never write to the original). Note: `examples/moonlander.c` and `examples/linked-list-test.c` use **cc65**, not Power C; pick the toolchain the user asked for. +7. **Apply the 150-expert consensus**: prefer documented Kernal routines over custom implementations; use zero-page for speed; always consider memory banking when accessing $D000–$DFFF --- diff --git a/vim/NOTES.md b/vim/NOTES.md new file mode 100644 index 0000000..a32f0b8 --- /dev/null +++ b/vim/NOTES.md @@ -0,0 +1,137 @@ +# Building a vi/vim clone for the C64 with Power C — hard-won notes + +Everything below was verified on this machine (GTK3VICE 3.10, `reference/POWERc.d81`), +mostly by first getting it wrong. Read this before touching the build or the +keyboard code. + +## The build: one session, start to finish + +Compile every unit, link, then `bye` out of the Power C shell, then `load` and +`run` from BASIC — all typed into **one** emulator session from one `-keybuf` +string: + +``` +cc vim.c ... padding +cc vscreen.c ... padding +link -s +vim.o +vscreen.o +^ up-arrow: stdlib.l + syslib.l + empty RETURN: done +vim output name +bye +load"vim",8,1 +run +``` + +- **Never run two emulators against the same `.d81`.** Both hold the whole image + in memory and write it back on exit, so the second one's exit silently erases + what the first wrote. Symptoms: a compile that "produced nothing", a linker + that "found no object", a directory that changes between two `c1541 -list` + calls. +- Use `Start-Process -Wait` (or `.NET Process` + `WaitForExit`) so a step cannot + overlap the next. +- `-keybuf-delay` is **wall-clock** milliseconds per keystroke while the machine + runs at warp, so 900 ms per key is plenty for the machine. +- **`cc` clears the keyboard buffer when it starts**, so its three disk-prompt + answers have to arrive *after* the prompts appear, not before. That is why a + 300 ms burst of RETURNs fails: the answers are eaten and cc sits at "insert + object disk" forever. 900 ms plus ~20 spare RETURNs after each `cc` works. + Spare RETURNs at the shell prompt are harmless. +- **The linker's `>` prompts are line-based and it loads each object before + asking again**, so no padding RETURNs may sit between the object names — an + empty line means "end of object list". `^` is the up-arrow key and pulls in + the libraries; forget it and you get "unresolved external references" + (`Main`, `SYS`, `MALLOC`, ...). +- `-exitscreenshot` **will not overwrite** an existing file, and `-limitcycles` + is what ends the session (VICE exits with status 1 and prints "cycle limit + reached" — that is normal). +- Keep the disk image a **bare filename** and run from its folder: VICE splits + its `image:file` autostart argument on a colon, so a full path is ambiguous. + +## PowerShell traps + +- `Start-Process -ArgumentList` joins the array with spaces and **quotes + nothing**. `"-keybuf", "cc vim.c\n"` therefore arrived as two arguments and + VICE silently never started (no window, no screenshot). Build the command + line yourself and hand it to `System.Diagnostics.ProcessStartInfo.Arguments`, + escaping inner `"` as `\"` (see `build.ps1`). +- PowerShell does **not** interpret `\n` in a double-quoted string, so + `"cc $u.c\n"` really is backslash-n — which is what VICE wants. + +## Power C specifics + +- `cc name.c` → `name.o`; it prompts for source disk / compiler disk / object + disk. The Shell prompt is `$`; `bye` returns to BASIC. +- **The linker refuses to overwrite** — "63, file exists" — and then gives up + quietly. Build on a fresh copy of the system disk instead. +- `link -s` with no address links at the start of BASIC: the file begins + `01 08`, holds `10 SYS 2062`, and `$080E` is a `JMP` to the C runtime, so + `load"name",8,1` + `run` works. +- Source files on disk are **SEQ PETSCII with CR line endings**: `a-z → $41-$5A`, + `A-Z → $C1-$DA`, CRLF → CR, `{ | } ~ → $DB $DC $DD $DE` (ASCII `$7B-$7F` plus + `$60`). +- **Do not write `||`.** It needs `|` = LOGO+`*` = PETSCII `$DC`, and cc dies + with "*** illegal character" part-way through the file. Use nested `if`s. + (`&&` is fine.) +- `EOF` is not declared unless you `#include `; test + `if (ch < 0) break;` instead. +- K&R only: no prototypes, no `void`, no `//` comments, identifiers significant + to 8 characters, `int` is 16 bits, chars are unsigned and not sign-extended. + Functions returning `int` need no declaration; declare `char *malloc();`. +- `peek()`/`poke()` are C128-only — use pointer casts. +- `malloc()` (declared `char *malloc();`) happily gives 19000 bytes. Compiled + programs and the heap end at `$D000` by default. +- **Keep the readable `.c` authoritative and `.pet` derived.** In one bad + session the converted PETSCII copy ended up written over the host `.c` file + (every letter uppercased, CR line endings). It still compiled — the + conversion round-trips — but the source was gone. + +## Calling machine language: sys() and the keyboard + +- `sys(bank, address, &a, &x, &y)` is documented as + `int bank; unsigned address; char *aptr, *xptr, *yptr;` with `bank` from 0 to + 15 (BASIC 7 style). **Calling KERNAL GETIN through it crashed the program**; + the crash warm-starts the C64, which clears the screen, so the only evidence + was a blank blue screen and `READY.`. +- The library's **`getchar()` is line buffered**: it echoes through the KERNAL's + screen editor and only hands the characters over after RETURN. Useless for an + editor. +- **The answer: read the KERNAL keyboard buffer directly.** `$C6` holds how many + characters are queued and they sit from `$0277`. The KERNAL's IRQ handler + scans the matrix and fills that buffer, so this gets every real press as it + happens, with no line buffering and no echo. Pop by shifting the rest down and + decrementing `$C6`. +- **Clear `$91` at startup.** Bit 7 is the RUN/STOP flag and it can already be + set when the program starts; treating that as "Esc" made the editor's first + keypress a phantom and swallowed the filename. +- VICE's `-keybuf` feeds that same buffer, so a program reading `$C6` can be + driven from the command line — but the keys race the program's start: type a + filename and it can be eaten by BASIC before the program runs. + +## C64 screen facts the editor relies on + +- Screen matrix `$0400`, colour RAM `$D800` (both directly writable). +- `$D018 = $17` → screen at `$0400`, character set at `$1800` (lower/upper case), + so PETSCII `$41-$5A` draws as real lowercase and `$C1-$DA` as uppercase. + Default is `$15` (uppercase/graphics at `$1000`) — restore it on exit. +- PETSCII → screen code: `$20-$3F` unchanged, `$40-$5F` minus `$40`, + `$60-$7F` minus `$20`, `$A0-$BF` minus `$40`, `$C0-$FF` minus `$80`. +- Reverse video is the high bit: screen code `+ $80`. That is the cursor block + and the status row. +- `$00CC = 0` stops the KERNAL cursor blinking over the editor's own cursor. +- Arrow keys are PETSCII `$11` down, `$91` up, `$1D` right, `$9D` left; + RETURN `$0D`, DEL `$14`. + +## Debugging technique that paid off + +- `-exitscreenshot` + `-limitcycles` gives one png per run; read it directly. +- **A crash warm-starts the machine: screen cleared, `READY.`, evidence gone.** + So probe in steps — write a marker to `$0400` (`p[0] = 2` shows as `B`) and + end with `for (;;) { }` instead of going on. Whatever is on screen when it + hangs is the last step that worked. +- Prove the toolchain before debugging your own code: a six-line program that + pokes the top row and spins. +- Keep a screenshot of every build step (`shots\cc-*.png`, `link.png`) and make + the script fail *per step* ("cc vim.c produced no object") instead of at the + end, or a compile failure looks like a linker failure. diff --git a/vim/build-ml.ps1 b/vim/build-ml.ps1 new file mode 100644 index 0000000..81d7c31 --- /dev/null +++ b/vim/build-ml.ps1 @@ -0,0 +1,81 @@ +# build-ml.ps1 - assemble vrow.s with cc65 and turn the result into vml.c, the +# Power C unit that holds the machine code as a byte array and calls it. +# +# powershell -NoProfile -File build-ml.ps1 +# +# Run it whenever vrow.s changes, then build.ps1 as usual. vml.c is generated: +# edit vrow.s, not vml.c. + +$ErrorActionPreference = "Stop" +$here = $PSScriptRoot +$bin = "D:\Development\Commodore\cc65-snapshot-win32\bin" +$ca65 = Join-Path $bin "ca65.exe" +$ld65 = Join-Path $bin "ld65.exe" +$addr = 0xC020 # where the routine lives; the parameter block is at $C000 + +$cfg = Join-Path $env:TEMP "vrow.cfg" +@" +MEMORY { M: start = `$C020, size = `$0100, file = "%O"; } +SEGMENTS { CODE: load = M, type = ro; } +"@ | Set-Content $cfg -Encoding ascii + +$obj = Join-Path $env:TEMP "vrow.o" +$out = Join-Path $here "vrow.bin" +& $ca65 (Join-Path $here "vrow.s") -o $obj +if ($LASTEXITCODE) { throw "ca65 failed" } +& $ld65 -C $cfg -o $out $obj +if ($LASTEXITCODE) { throw "ld65 failed" } + +$b = [IO.File]::ReadAllBytes($out) +$rows = @() +for ($i = 0; $i -lt $b.Length; $i += 12) { + $n = [Math]::Min(12, $b.Length - $i) + $rows += " " + (($b[$i..($i + $n - 1)] | ForEach-Object { "$_" }) -join ", ") +} +$arr = $rows -join ",`n" + +@" +/* vml.c - GENERATED by build-ml.ps1 from vrow.s. Do not edit by hand. + * + * The machine code row painter, kept as a byte array and called in place + * through sys(). It is position independent; its parameters go through zero + * page scratch `$22-`$28, which Power C leaves to ML routines. + */ + +char mlcode[$($b.Length)] = { +$arr +}; + +/* nothing to install: the code runs where the array sits */ +mlinit() +{ + return 0; +} + +/* paint the screen row at address dst: len bytes from src, cursor at column cur + (255 = none), rev = 128 for an all reverse row */ +mlrow(src, len, dst, cur, rev) +unsigned src, dst; +int len, cur, rev; +{ + char *p; + char a, x, y; + + p = (char *) 0x22; + p[0] = src & 255; + p[1] = src >> 8; + p[2] = len; + p[3] = dst & 255; + p[4] = dst >> 8; + p[5] = cur; + p[6] = rev; + a = 0; + x = 0; + y = 0; + sys(15, (unsigned) mlcode, &a, &x, &y); + return 0; +} +"@ | Set-Content (Join-Path $here "vml.c") -Encoding utf8 + +Write-Host ("vrow.bin {0} bytes -> vml.c" -f $b.Length) + diff --git a/vim/build.bat b/vim/build.bat index 99b5625..949f7b8 100644 --- a/vim/build.bat +++ b/vim/build.bat @@ -1,71 +1,5 @@ @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 +REM build.bat [prog] [extra build.ps1 args] - build the editor on a fresh work disk. +REM build.bat build vim (the default program) +REM build.bat vim -Run -Keys "vim.c\njjj" build, then leave it running +powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0build.ps1" %* diff --git a/vim/build.ps1 b/vim/build.ps1 new file mode 100644 index 0000000..d7f0c80 --- /dev/null +++ b/vim/build.ps1 @@ -0,0 +1,241 @@ +# build.ps1 - build a Power C program for the C64 on a fresh work disk, then +# optionally leave it back in BASIC, loaded and running. +# +# powershell -NoProfile -File build.ps1 +# powershell -NoProfile -File build.ps1 -Run -Keys 'vim.c\njjj' +# powershell -NoProfile -File build.ps1 -Shell 'l\n' (poke at the disk) +# +# Everything happens in ONE emulator session: compile each unit, link, then +# "bye" out of the Power C shell, load the program from BASIC and run it. One +# session is not just faster, it is required: two emulators both attached to +# work.d81 write the image back over each other on exit and the files made by +# the other one disappear. +# +# Power C is interactive - cc asks which disk to insert three times, the linker +# wants one object file per ">" line, "^" for the libraries, an empty RETURN +# and then a name - so every prompt is answered from the -keybuf string. VICE +# paces those keystrokes in wall-clock milliseconds while the machine runs at +# warp, so the delay has to be long enough that a step's answers arrive after +# its prompt: cc, for one, clears the keyboard buffer when it starts. + +param( + [string]$Prog = "vim", + [string]$Keys = "", + [string]$Shell = "", + [int]$Delay = 900, # ms per keystroke; wall clock, machine is at warp + [int]$Pad = 20, # spare RETURNs while a step is working + [int]$Tail = 8, # spare RETURNs while the linker writes + [int]$Settle = 0, # extra seconds to run after the last key (screen redraw) + [switch]$Full, # ignore the stamps: fresh disk, recompile everything + [switch]$Run +) + +$ErrorActionPreference = "Stop" + +$vice = "C:\Program Files\GTK3VICE-3.10-win64\bin" +$x64 = Join-Path $vice "x64sc.exe" +$c1541 = Join-Path $vice "c1541.exe" +$here = $PSScriptRoot +$base = Join-Path $here "..\reference\POWERc.d81" +$shots = Join-Path $here "shots" + +# VICE splits "image:file" on a colon, so keep the image name bare and run from +# this folder. +Set-Location $here +$img = "work.d81" + +$units = @{ + vim = @("vim", "vscreen", "vview", "vedit", "vml") + mltest = @("mltest", "vml") +} +if (-not $units.ContainsKey($Prog)) { throw "unknown program '$Prog'" } +$unitList = $units[$Prog] + +if (-not (Test-Path $x64)) { throw "x64sc.exe not found in $vice" } +if (-not (Test-Path $base)) { throw "$base not found - is the repository complete?" } +New-Item -ItemType Directory -Force -Path $shots | Out-Null + +# ---------------------------------------------------------------- petscii + +# Power C sources live on the disk as PETSCII SEQ files with CR line endings: +# a-z -> $41-$5A, A-Z -> $C1-$DA, CRLF -> CR, and the four characters the manual +# says are typed with shift/logo keys: { | } ~ -> $DB $DC $DD $DE. +function ConvertTo-Petscii([string]$in, [string]$out) { + $bytes = [IO.File]::ReadAllBytes($in) + $start = 0 + if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and + $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) { $start = 3 } + $o = New-Object System.Collections.Generic.List[byte] + for ($i = $start; $i -lt $bytes.Length; $i++) { + $b = $bytes[$i] + if ($b -eq 13) { continue } + if ($b -eq 10) { $o.Add([byte]13); continue } + if ($b -ge 0x61 -and $b -le 0x7A) { $o.Add([byte]($b - 0x20)); continue } + if ($b -ge 0x41 -and $b -le 0x5A) { $o.Add([byte]($b + 0x80)); continue } + if ($b -ge 0x7B -and $b -le 0x7F) { $o.Add([byte]($b + 0x60)); continue } + $o.Add([byte]$b) + } + [IO.File]::WriteAllBytes($out, $o.ToArray()) + return $o.Count +} + +# ---------------------------------------------------------------- emulator + +# Quote one argument the way the Windows command line wants it. Start-Process's +# -ArgumentList joins with spaces and does not quote anything, which silently +# split "-keybuf cc vim.c\n" into two arguments, so the command line is built +# here and handed to .NET as a single string. +function Quote-Arg([string]$a) { + if ($a -ne "" -and $a -notmatch '[\s"]') { return $a } + $e = $a -replace '(\\*)"', '$1$1\"' + $e = $e -replace '(\\+)$', '$1$1' + return '"' + $e + '"' +} + +# Exactly one emulator, waited for, so nothing else touches the image. +function Invoke-C64 { + param( + [string]$KeyBuf, + [string]$Shot, + [string]$Target = "shell", + [int]$Cycles = 0, + [int]$KeyDelay = 900 + ) + if (Test-Path $Shot) { Remove-Item $Shot -Force } # VICE won't overwrite + if ($Cycles -le 0) { + $wall = ($KeyBuf.Length * $KeyDelay / 1000.0) + 15 + $Settle + $c = [long]($wall * 13000000) + if ($c -lt 150000000) { $c = 150000000 } + if ($c -gt 2000000000) { $c = 2000000000 } + $Cycles = [int]$c + } + $argv = @( + "-8", $img, + "-autostart", ($img + ":" + $Target), + "-autostart-warp", "-warp", "-sounddev", "dummy", + "-keybuf-delay", "$KeyDelay", + "-keybuf", $KeyBuf, + "-limitcycles", "$Cycles", + "-exitscreenshot", $Shot + ) + $cmdline = ($argv | ForEach-Object { Quote-Arg $_ }) -join ' ' + $psi = New-Object System.Diagnostics.ProcessStartInfo + $psi.FileName = $x64 + $psi.Arguments = $cmdline + $psi.UseShellExecute = $false + $pi = [System.Diagnostics.Process]::Start($psi) + $pi.WaitForExit() + Start-Sleep -Milliseconds 900 # let the image write settle +} + +function Get-DiskDir { + return (& $c1541 $img -list 2>$null) -join "`n" +} + +# ---------------------------------------------------------------- raw shell + +if ($Shell -ne "") { + if (-not (Test-Path $img)) { throw "$img not found - build something first" } + Invoke-C64 -KeyBuf $Shell -Shot (Join-Path $shots "shell.png") -KeyDelay $Delay + Write-Host "shots\shell.png" + exit 0 +} + +# ---------------------------------------------------------------- fresh disk + +# work.stamp remembers the hash of each .c as it was last compiled onto the work +# disk. Only units whose hash differs are converted, copied over and recompiled; +# the others keep the .o already on the disk. The linker then runs again. +$stampFile = Join-Path $here "work.stamp" +$stamps = @{} +if (Test-Path $stampFile) { + foreach ($ln in (Get-Content $stampFile)) { + $p = $ln -split '=' + if ($p.Count -eq 2) { $stamps[$p[0]] = $p[1] } + } +} +if ($Full -or -not (Test-Path $img) -or -not (Test-Path $stampFile)) { + Write-Host "== fresh work disk" + Copy-Item $base $img -Force + $stamps = @{} +} else { + Write-Host "== incremental build on $img" +} + +$changed = @() +foreach ($u in $unitList) { + $src = Join-Path $here "$u.c" + if (-not (Test-Path $src)) { throw "$src not found" } + $hash = (Get-FileHash $src -Algorithm MD5).Hash + if ($stamps[$u] -eq $hash) { continue } + $changed += $u + $pet = Join-Path $here "$u.pet" + $n = ConvertTo-Petscii $src $pet + Write-Host ("== {0}.c changed -> petscii ({1} bytes)" -f $u, $n) + # cc and the linker refuse to overwrite, so scratch the stale copies first + $null = & $c1541 $img -delete "$u.c" "$u.o" 2>$null + $null = & $c1541 $img -write $pet "$u.c,s" 2>$null + $stamps.Remove($u) # only re-stamped once its .o really exists +} +$haveProg = ((& $c1541 $img -list 2>$null) -join "`n") -match ('"' + [regex]::Escape($Prog) + '"\s+prg') +$needLink = ($changed.Count -gt 0) -or (-not $haveProg) +if ($needLink -and $haveProg) { $null = & $c1541 $img -delete $Prog 2>$null } +if ($changed.Count -eq 0) { Write-Host "== no source changed" } + +# ---------------------------------------------------------------- one session + +# cc and RETURNs for its disk prompts, over and over, then the linker: +# ">" object files, ">" up-arrow for the libraries, ">" empty to finish, the +# output name. With -Run the session goes on to leave the shell with bye, load +# the program from BASIC and type -Keys at whatever it puts on the screen. +$kb = "" +foreach ($u in $changed) { + $kb += "cc $u.c\n" + ("\n" * 3) + ("\n" * $Pad) +} +if ($needLink) { + $kb += "link -s\n" + foreach ($u in $unitList) { $kb += "$u.o\n" } + $kb += "^\n\n" + $Prog + "\n" + ("\n" * $Tail) +} +if ((-not $Run) -and ($kb -eq "")) { + Write-Host "build ok - nothing to do" + exit 0 +} +if ($Run) { + $kb += "bye\n" + ("\n" * 6) # let the shell hand back to BASIC + $kb += 'load"' + $Prog + '",8,1' + "\n" + ("\n" * 10) # let the load finish + $kb += "run\n" + $kb += $Keys +} + +$shot = if ($Run) { Join-Path $shots "run.png" } else { Join-Path $shots "build.png" } +Write-Host ("== one session: cc [{0}], {1}{2}" -f ($changed -join ' '), + $(if ($needLink) { 'link' } else { 'no link' }), + $(if ($Run) { ', bye, load, run' } else { '' })) +Invoke-C64 -KeyBuf $kb -Shot $shot -KeyDelay $Delay + +$dir = Get-DiskDir +$found = $false +foreach ($line in ($dir -split "`n")) { + if ($line -match ('"' + [regex]::Escape($Prog) + '"\s+prg')) { $found = $true } + if ($line -match ('"(' + ($unitList -join '|') + ')\.[co]"')) { + Write-Host " $line" + } +} + +# stamp a unit only once its object file is really on the disk +foreach ($u in $changed) { + if ($dir -match ('"' + [regex]::Escape($u) + '\.o"')) { + $stamps[$u] = (Get-FileHash (Join-Path $here "$u.c") -Algorithm MD5).Hash + } else { + Write-Host " $u.c produced no object - read $shot" + } +} +($stamps.GetEnumerator() | ForEach-Object { "{0}={1}" -f $_.Key, $_.Value }) | Set-Content $stampFile +if (-not $found) { throw "'$Prog' was not created - read $shot" } + +if ($Run) { + Write-Host (" {0}" -f (Split-Path $shot -Leaf)) +} else { + Write-Host "build ok - 'build.ps1 -Run -Keys ...' to try it" +} diff --git a/vim/mltest.c b/vim/mltest.c new file mode 100644 index 0000000..cad0807 --- /dev/null +++ b/vim/mltest.c @@ -0,0 +1,32 @@ +/* mltest.c - prove calling machine code from Power C, one marked step at a time. + * Whatever is left on screen when it hangs is the last step that worked. */ + +char txt[8]; +char rts[4]; + +main() +{ + char *p; + char a, x, y; + + p = (char *) 0x0400; + p[0] = 1; /* A: main started */ + + rts[0] = 96; /* RTS */ + a = 0; + x = 0; + y = 0; + p[1] = 2; /* B: about to sys() a bare RTS */ + for (;;) { } + sys(15, (unsigned) rts, &a, &x, &y); + p[2] = 3; /* C: sys + array address work */ + + txt[0] = 72; + txt[1] = 69; + txt[2] = 76; + txt[3] = 76; + txt[4] = 79; + mlrow((unsigned) txt, 5, 1104, 2, 0); + p[3] = 4; /* D: the real routine came back */ + for (;;) { } +} diff --git a/vim/mltest.pet b/vim/mltest.pet new file mode 100644 index 0000000..1e0abdd --- /dev/null +++ b/vim/mltest.pet @@ -0,0 +1 @@ +/* MLTEST.C - PROVE CALLING MACHINE CODE FROM ÐOWER Ã, ONE MARKED STEP AT A TIME. * ×HATEVER IS LEFT ON SCREEN WHEN IT HANGS IS THE LAST STEP THAT WORKED. */ CHAR TXT[8]; CHAR RTS[4]; MAIN() Û CHAR *P; CHAR A, X, Y; P = (CHAR *) 0X0400; P[0] = 1; /* Á: MAIN STARTED */ RTS[0] = 96; /* ÒÔÓ */ A = 0; X = 0; Y = 0; P[1] = 2; /* Â: ABOUT TO SYS() A BARE ÒÔÓ */ FOR (;;) Û Ý SYS(15, (UNSIGNED) RTS, &A, &X, &Y); P[2] = 3; /* Ã: SYS + ARRAY ADDRESS WORK */ TXT[0] = 72; TXT[1] = 69; TXT[2] = 76; TXT[3] = 76; TXT[4] = 79; MLROW((UNSIGNED) TXT, 5, 1104, 2, 0); P[3] = 4; /* Ä: THE REAL ROUTINE CAME BACK */ FOR (;;) Û Ý Ý \ No newline at end of file diff --git a/vim/run.bat b/vim/run.bat new file mode 100644 index 0000000..9132de1 --- /dev/null +++ b/vim/run.bat @@ -0,0 +1,10 @@ +@echo off +REM run.bat - start the editor from work.d81 in VICE. +setlocal +set "VICE=C:\Program Files\GTK3VICE-3.10-win64\bin" +cd /d "%~dp0" +if not exist work.d81 ( + echo work.d81 not found - run build.bat first + exit /b 1 +) +"%VICE%\x64sc.exe" -8 work.d81 -autostart "work.d81:vim" diff --git a/vim/shots/run.png b/vim/shots/run.png new file mode 100644 index 0000000..f9165fc Binary files /dev/null and b/vim/shots/run.png differ diff --git a/vim/shots/shell.png b/vim/shots/shell.png new file mode 100644 index 0000000..35e057d Binary files /dev/null and b/vim/shots/shell.png differ diff --git a/vim/to-petscii.ps1 b/vim/to-petscii.ps1 deleted file mode 100644 index 47a47a8..0000000 --- a/vim/to-petscii.ps1 +++ /dev/null @@ -1,31 +0,0 @@ -# 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/vedit.c b/vim/vedit.c new file mode 100644 index 0000000..1b3f457 --- /dev/null +++ b/vim/vedit.c @@ -0,0 +1,224 @@ +/* vedit.c - buffer editing for the c64 vim clone: insert, delete, save. */ + +extern char *bfr; +extern unsigned blen; +extern unsigned lptr[]; +extern int nlines, cy, cx, top, leftcol, mode, dirty; +extern char *note; +extern char fname[]; +extern char ybuf[]; +extern int ylen, yok; + +/* end offset (exclusive of the $0d) of line ln */ +linend(ln) +int ln; +{ + if (ln + 1 < nlines) return lptr[ln + 1] - 1; + return blen; +} + +/* keep cx inside the current line */ +clampx() +{ + int len; + + len = linend(cy) - lptr[cy]; + if (cx > len) cx = len; + if (cx < 0) cx = 0; + return 0; +} + +/* ------------------------------------------------------------- editing */ + +/* insert byte ch at buffer offset pos. 1 on success, 0 if the buffer is full */ +bins(pos, ch) +unsigned pos; +int ch; +{ + unsigned i; + + if (blen >= 18999) return 0; + for (i = blen; i > pos; i--) bfr[i] = bfr[i - 1]; + bfr[pos] = ch; + blen++; + dirty = 1; + return 1; +} + +/* remove n bytes at offset pos */ +delrange(pos, n) +unsigned pos, n; +{ + unsigned i; + + for (i = pos; i + n < blen; i++) bfr[i] = bfr[i + n]; + blen = blen - n; + dirty = 1; + return 0; +} + +/* type one character at the cursor */ +inschar(ch) +int ch; +{ + int l; + + if (bins(lptr[cy] + cx, ch) == 0) return 0; + for (l = cy + 1; l < nlines; l++) lptr[l]++; + cx++; + return 1; +} + +/* split the line at the cursor */ +newline() +{ + int l; + unsigned pos; + + if (nlines >= 599) return 0; + pos = lptr[cy] + cx; + if (bins(pos, 13) == 0) return 0; + for (l = nlines; l > cy + 1; l--) lptr[l] = lptr[l - 1]; + lptr[cy + 1] = pos + 1; + nlines++; + for (l = cy + 2; l < nlines; l++) lptr[l]++; + cy++; + cx = 0; + return 1; +} + +/* delete the character under the cursor */ +delchar() +{ + int l; + + if (cx >= linend(cy) - lptr[cy]) return 0; + delrange(lptr[cy] + cx, 1); + for (l = cy + 1; l < nlines; l++) lptr[l]--; + return 1; +} + +/* backspace: delete before the cursor, or join onto the previous line */ +bkspace() +{ + int l, pl; + + if (cx > 0) { + cx--; + delchar(); + return 0; + } + if (cy == 0) return 0; + pl = linend(cy - 1) - lptr[cy - 1]; + delrange(lptr[cy] - 1, 1); + for (l = cy; l < nlines - 1; l++) lptr[l] = lptr[l + 1]; + nlines--; + for (l = cy; l < nlines; l++) lptr[l]--; + cy--; + cx = pl; + return 0; +} + +/* dd: yank the current line and remove it */ +delline() +{ + int l, i; + unsigned s, e, n; + + s = lptr[cy]; + e = linend(cy); + ylen = e - s; + if (ylen > 159) ylen = 159; + for (i = 0; i < ylen; i++) ybuf[i] = bfr[s + i]; + yok = 1; + + if (nlines == 1) { + blen = 0; + dirty = 1; + } else if (cy + 1 < nlines) { + n = lptr[cy + 1] - s; + delrange(s, n); + for (l = cy; l < nlines - 1; l++) lptr[l] = lptr[l + 1]; + nlines--; + for (l = cy; l < nlines; l++) lptr[l] -= n; + } else { /* last line: take its leading $0d */ + delrange(s - 1, blen - s + 1); + nlines--; + cy--; + } + cx = 0; + return 0; +} + +/* p: put the yanked line below the cursor line */ +paste() +{ + int i; + + if (yok == 0) return 0; + cx = linend(cy) - lptr[cy]; + if (newline() == 0) return 0; + for (i = 0; i < ylen; i++) inschar(ybuf[i]); + cx = 0; + return 0; +} + +/* cursor keys, the same in both modes. 1 if k was one of them */ +arrow(k) +int k; +{ + if (k == 17) { + if (cy + 1 < nlines) { + cy++; + clampx(); + } + return 1; + } + if (k == 145) { + if (cy > 0) { + cy--; + clampx(); + } + return 1; + } + if (k == 29) { + cx++; + clampx(); + return 1; + } + if (k == 157) { + if (cx > 0) cx--; + return 1; + } + return 0; +} + +/* write the buffer to fname, replacing any old copy. 1 on success */ +savef() +{ + char on[32]; + int i, f; + unsigned j; + + on[0] = 64; /* @ */ + on[1] = 48; /* 0 */ + on[2] = 58; /* : */ + i = 3; + j = 0; + while (fname[j] && j < 20) on[i++] = fname[j++]; + on[i++] = 44; /* , */ + on[i++] = 83; /* S (petscii, not ascii) */ + on[i++] = 44; /* , */ + on[i++] = 87; /* W */ + on[i] = 0; + + f = open(5, 8, 3, on); + if (f == 0) return 0; + for (j = 0; j < blen; j++) putc(bfr[j], 5); + if (blen > 0) { + if (bfr[blen - 1] != 13) putc(13, 5); + } + close(5); + return 1; +} + diff --git a/vim/vedit.pet b/vim/vedit.pet new file mode 100644 index 0000000..8165488 --- /dev/null +++ b/vim/vedit.pet @@ -0,0 +1 @@ +/* VEDIT.C - BUFFER EDITING FOR THE C64 VIM CLONE: INSERT, DELETE, SAVE. */ EXTERN CHAR *BFR; EXTERN UNSIGNED BLEN; EXTERN UNSIGNED LPTR[]; EXTERN INT NLINES, CY, CX, TOP, LEFTCOL, MODE, DIRTY; EXTERN CHAR *NOTE; EXTERN CHAR FNAME[]; EXTERN CHAR YBUF[]; EXTERN INT YLEN, YOK; /* END OFFSET (EXCLUSIVE OF THE $0D) OF LINE LN */ LINEND(LN) INT LN; Û IF (LN + 1 < NLINES) RETURN LPTR[LN + 1] - 1; RETURN BLEN; Ý /* KEEP CX INSIDE THE CURRENT LINE */ CLAMPX() Û INT LEN; LEN = LINEND(CY) - LPTR[CY]; IF (CX > LEN) CX = LEN; IF (CX < 0) CX = 0; RETURN 0; Ý /* ------------------------------------------------------------- EDITING */ /* INSERT BYTE CH AT BUFFER OFFSET POS. 1 ON SUCCESS, 0 IF THE BUFFER IS FULL */ BINS(POS, CH) UNSIGNED POS; INT CH; Û UNSIGNED I; IF (BLEN >= 18999) RETURN 0; FOR (I = BLEN; I > POS; I--) BFR[I] = BFR[I - 1]; BFR[POS] = CH; BLEN++; DIRTY = 1; RETURN 1; Ý /* REMOVE N BYTES AT OFFSET POS */ DELRANGE(POS, N) UNSIGNED POS, N; Û UNSIGNED I; FOR (I = POS; I + N < BLEN; I++) BFR[I] = BFR[I + N]; BLEN = BLEN - N; DIRTY = 1; RETURN 0; Ý /* TYPE ONE CHARACTER AT THE CURSOR */ INSCHAR(CH) INT CH; Û INT L; IF (BINS(LPTR[CY] + CX, CH) == 0) RETURN 0; FOR (L = CY + 1; L < NLINES; L++) LPTR[L]++; CX++; RETURN 1; Ý /* SPLIT THE LINE AT THE CURSOR */ NEWLINE() Û INT L; UNSIGNED POS; IF (NLINES >= 599) RETURN 0; POS = LPTR[CY] + CX; IF (BINS(POS, 13) == 0) RETURN 0; FOR (L = NLINES; L > CY + 1; L--) LPTR[L] = LPTR[L - 1]; LPTR[CY + 1] = POS + 1; NLINES++; FOR (L = CY + 2; L < NLINES; L++) LPTR[L]++; CY++; CX = 0; RETURN 1; Ý /* DELETE THE CHARACTER UNDER THE CURSOR */ DELCHAR() Û INT L; IF (CX >= LINEND(CY) - LPTR[CY]) RETURN 0; DELRANGE(LPTR[CY] + CX, 1); FOR (L = CY + 1; L < NLINES; L++) LPTR[L]--; RETURN 1; Ý /* BACKSPACE: DELETE BEFORE THE CURSOR, OR JOIN ONTO THE PREVIOUS LINE */ BKSPACE() Û INT L, PL; IF (CX > 0) Û CX--; DELCHAR(); RETURN 0; Ý IF (CY == 0) RETURN 0; PL = LINEND(CY - 1) - LPTR[CY - 1]; DELRANGE(LPTR[CY] - 1, 1); FOR (L = CY; L < NLINES - 1; L++) LPTR[L] = LPTR[L + 1]; NLINES--; FOR (L = CY; L < NLINES; L++) LPTR[L]--; CY--; CX = PL; RETURN 0; Ý /* DD: YANK THE CURRENT LINE AND REMOVE IT */ DELLINE() Û INT L, I; UNSIGNED S, E, N; S = LPTR[CY]; E = LINEND(CY); YLEN = E - S; IF (YLEN > 159) YLEN = 159; FOR (I = 0; I < YLEN; I++) YBUF[I] = BFR[S + I]; YOK = 1; IF (NLINES == 1) Û BLEN = 0; DIRTY = 1; Ý ELSE IF (CY + 1 < NLINES) Û N = LPTR[CY + 1] - S; DELRANGE(S, N); FOR (L = CY; L < NLINES - 1; L++) LPTR[L] = LPTR[L + 1]; NLINES--; FOR (L = CY; L < NLINES; L++) LPTR[L] -= N; Ý ELSE Û /* LAST LINE: TAKE ITS LEADING $0D */ DELRANGE(S - 1, BLEN - S + 1); NLINES--; CY--; Ý CX = 0; RETURN 0; Ý /* P: PUT THE YANKED LINE BELOW THE CURSOR LINE */ PASTE() Û INT I; IF (YOK == 0) RETURN 0; CX = LINEND(CY) - LPTR[CY]; IF (NEWLINE() == 0) RETURN 0; FOR (I = 0; I < YLEN; I++) INSCHAR(YBUF[I]); CX = 0; RETURN 0; Ý /* CURSOR KEYS, THE SAME IN BOTH MODES. 1 IF K WAS ONE OF THEM */ ARROW(K) INT K; Û IF (K == 17) Û IF (CY + 1 < NLINES) Û CY++; CLAMPX(); Ý RETURN 1; Ý IF (K == 145) Û IF (CY > 0) Û CY--; CLAMPX(); Ý RETURN 1; Ý IF (K == 29) Û CX++; CLAMPX(); RETURN 1; Ý IF (K == 157) Û IF (CX > 0) CX--; RETURN 1; Ý RETURN 0; Ý /* WRITE THE BUFFER TO FNAME, REPLACING ANY OLD COPY. 1 ON SUCCESS */ SAVEF() Û CHAR ON[32]; INT I, F; UNSIGNED J; ON[0] = 64; /* @ */ ON[1] = 48; /* 0 */ ON[2] = 58; /* : */ I = 3; J = 0; WHILE (FNAME[J] && J < 20) ON[I++] = FNAME[J++]; ON[I++] = 44; /* , */ ON[I++] = 83; /* Ó (PETSCII, NOT ASCII) */ ON[I++] = 44; /* , */ ON[I++] = 87; /* × */ ON[I] = 0; F = OPEN(5, 8, 3, ON); IF (F == 0) RETURN 0; FOR (J = 0; J < BLEN; J++) PUTC(BFR[J], 5); IF (BLEN > 0) Û IF (BFR[BLEN - 1] != 13) PUTC(13, 5); Ý CLOSE(5); RETURN 1; Ý \ No newline at end of file diff --git a/vim/vim.c b/vim/vim.c index 3b92655..689f413 100644 --- a/vim/vim.c +++ b/vim/vim.c @@ -1,420 +1,345 @@ -/* vim.c - a small vi/vim-style screen editor for the Commodore 64 +/* vim.c - a small modal editor for the commodore 64, written for power c. * - * 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. + * Increment 2 edits: it reads a petscii SEQ file into one buffer, keeps a line + * index, shows 24 lines plus a status row, and supports + * move h j k l / arrows, 0 $ gg G + * insert i a A I o O (RUN/STOP leaves insert mode, DEL backspaces) + * change x dd p + * colon :w :w name :wq :x :q :q! * - * 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 ||. + * The buffer holds the file exactly as it sits on disk: petscii bytes with $0d + * between lines. Keys are compared as numbers, never as character constants, + * so nothing depends on how the compiler treats literals. (Unshifted letters + * arrive as $41-$5a, so h = 72; shifted ones as $c1-$da, so G = 199.) */ -#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 */ +char *malloc(); +char *bfr; /* file text, $0d separated */ +unsigned blen; /* bytes used */ +unsigned lptr[600]; /* offset of each line start */ +int nlines; +int cy, cx; /* cursor line and column */ +int top; /* first line on screen */ +int leftcol; /* horizontal scroll */ +char fname[24]; +int mode; /* 0 normal, 1 insert */ +int dirty; /* buffer changed since the last save */ +int pend; /* first key of a two key command: d or g */ +char *note; /* one-shot message for the status row */ +char ybuf[160]; /* the yanked line */ +int ylen, yok; + +/* screen rows are 0..23, the status row is 24 */ + +/* shorten fname to what fits */ +shortn(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; + while (fname[i] && i < n) i++; + fname[i] = 0; + return 0; } -static intend(o) /* offset of the line end */ -int o; +/* a message that waits for a key */ +msg(s) +char *s; { - int e; - - e = o; - while (e < tlen && text[e] != 13) e = e + 1; - return e; + sclr(24, 0, 40); + sps(24, 0, s); + skey(); + return 0; } -static inschr(ch) -int ch; +/* read a line of lowercase petscii into s, echoed from column c of row 24 */ +getln(c, s, max) +int c, max; +char *s; { - int i; + int n, k; - if (tlen >= 1999) { - msgcode = 1; - return; + n = 0; + s[0] = 0; + for (;;) { + k = skey(); + if (k == 13) break; + if (k == 3) { + n = 0; + s[0] = 0; + break; + } + if (k == 20) { + if (n > 0) { + n--; + s[n] = 0; + } + } else { + if (k >= 0xc1 && k <= 0xda) k = k - 0x80; + if (k >= 32 && k != 127 && n < max) { + s[n] = k; + n++; + s[n] = 0; + } + } + sclr(24, c, max); + sps(24, c, s); } - for (i = tlen; i > pos; i--) - text[i] = text[i - 1]; - text[pos] = ch; - tlen = tlen + 1; - pos = pos + 1; + return n; } -static delat() +/* ask which file to open */ +askname() { - 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; + sclr(24, 0, 40); + sps(24, 0, "file: "); + if (getln(6, fname, 20) == 0) fname[0] = 0; + return 0; } -static linehome() +/* read fname into bfr. 1 on success, 0 if it could not be opened */ +loadf() { - pos = lstart(lnof()); -} - -static lineend() -{ - pos = intend(lstart(lnof())); -} - -/* ---------- saving ---------- */ - -static savfil() -{ - FILE *f; + int f, ch, prev, c; + char on[28]; int i; - f = fopen(fname, "w"); - if (!f) { - msgcode = 2; - return; + if (fname[0] == 0) return 1; + i = 0; + while (fname[i] && i < 20) { + on[i] = fname[i]; + i++; } - if (ferror(f)) { - msgcode = 2; - fclose(f); - return; + on[i++] = 44; /* , */ + on[i++] = 83; /* S (petscii, not ascii) */ + on[i++] = 44; /* , */ + on[i++] = 82; /* R */ + on[i] = 0; + + f = open(5, 8, 2, on); + if (f == 0) return 0; + blen = 0; + prev = 0; + for (;;) { + ch = getc(5); + if (ch < 0) break; /* end of file */ + if (ch == 26) break; + if (ch == 10 && prev == 13) { /* crlf seen as two bytes */ + prev = ch; + continue; + } + if (ch == 13) c = 13; + else if (ch == 10) c = 13; + else c = ch; + if (blen < 18999) bfr[blen++] = c; + prev = ch; } - for (i = 0; i < tlen; i++) fputc(text[i], f); - fclose(f); - msgcode = 3; + close(5); + return 1; } -/* ---------- : command line ---------- */ - -static docmd() +/* find the start of every line */ +buildidx() { - if (cmdlen == 0) return; - if (cmd[0] == 'w') { - if (cmdlen > 1 && cmd[1] == 'q') { - savfil(); - quit = 1; - return; + unsigned i; + + nlines = 1; + lptr[0] = 0; + for (i = 0; i < blen; i++) { + if (bfr[i] == 13) { + if (nlines < 599) { + lptr[nlines] = i + 1; + nlines++; + } } - savfil(); - return; - } - if (cmd[0] == 'q') { - quit = 1; - return; } - msgcode = 4; + return 0; } -/* ---------- key handlers ---------- */ - -static insk(k) -int k; +/* the ":" command line. returns 1 when the editor should quit */ +doscmd() { - if (k == 3) { /* ESC: leave insert mode */ - mode = 0; - if (pos > 0) pos = pos - 1; - return; + int n, p, ok, qf; + char cmd[24]; + + sclr(24, 0, 40); + spc(24, 0, 58); /* : */ + n = getln(1, cmd, 20); + sclr(24, 0, 40); + if (n == 0) return 0; + + if (cmd[0] == 81) { /* q, q! */ + if (cmd[1] == 33) return 1; + if (dirty) { + note = "unsaved - :q! quits"; + return 0; + } + return 1; } - 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; + ok = 0; + qf = 0; + p = 1; + if (cmd[0] == 87) ok = 1; /* w, wq, w name */ + if (cmd[0] == 88) { /* x */ + ok = 1; + qf = 1; } - if (k == 'i') { mode = 1; return; } - if (k == 'a') { - if (pos < tlen && text[pos] != 13) pos = pos + 1; - mode = 1; - return; + if (ok == 0) { + note = "unknown command"; + return 0; } - if (k == 'o') { - pos = intend(lstart(lnof())); - inschr(13); - mode = 1; - return; + if (cmd[0] == 87 && cmd[1] == 81) { + qf = 1; + p = 2; } - if (k == 'O') { - pos = lstart(lnof()); - inschr(13); - pos = pos - 1; - mode = 1; - return; + while (cmd[p] == 32) p++; + if (cmd[p]) { + n = 0; + while (cmd[p] && n < 20) fname[n++] = cmd[p++]; + fname[n] = 0; } - 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; + if (fname[0] == 0) { + note = "no file name"; 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; + if (savef()) { + dirty = 0; + note = "written"; + return qf; } + note = "cannot write"; + return 0; } -/* ---------- main loop ---------- */ - main() { - int k; - int i; + int k, ok, p; + + sinit(); + mlinit(); /* machine code row painter */ + bfr = malloc(19000); + if (bfr == 0) { + sps(0, 0, "out of memory"); + skey(); + return; + } - tlen = 0; - pos = 0; + blen = 0; + nlines = 1; + lptr[0] = 0; + cy = 0; + cx = 0; + top = 0; + leftcol = 0; + fname[0] = 0; mode = 0; - cmdlen = 0; - quit = 0; - msgcode = 0; - - i = 0; - while (defnam[i]) { - fname[i] = defnam[i]; - i = i + 1; + dirty = 0; + pend = 0; + note = 0; + ylen = 0; + yok = 0; + + askname(); + ok = loadf(); + shortn(20); + buildidx(); + if (ok == 0) { /* keep the name: :w creates it */ + nlines = 1; + blen = 0; + note = "new file"; } - fname[i] = 0; - while (!quit) { - draw(text, tlen, pos, mode, fname, cmd, cmdlen, msgcode); - msgcode = 0; - k = waitkey(); - if (mode == 2) { - if (k == 13) { + for (;;) { + scroll(); + dall(); + k = skey(); + note = 0; + + if (mode) { /* ---- insert mode */ + if (k == 3) { /* RUN/STOP = Esc */ 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; + if (cx > 0) cx--; + } else if (k == 13) { + newline(); + } else if (k == 20) { + bkspace(); + } else if (arrow(k)) { + cx = cx; + } else if (k >= 32 && k < 128) { + inschar(k); + } else if (k >= 161) { + inschar(k); } continue; } - if (mode == 1) { - insk(k); + + if (pend) { /* ---- second key of dd / gg */ + p = pend; + pend = 0; + if (p == 68 && k == 68) delline(); + if (p == 71 && k == 71) { + cy = 0; + cx = 0; + } + clampx(); continue; } - cmdk(k); - } - clrscr(); - printf("bye\n"); + if (arrow(k)) { + cx = cx; + } else if (k == 72) { /* h */ + if (cx > 0) cx--; + } else if (k == 76) { /* l */ + cx++; + clampx(); + } else if (k == 74) { /* j */ + arrow(17); + } else if (k == 75) { /* k */ + arrow(145); + } else if (k == 48) { /* 0 */ + cx = 0; + } else if (k == 36) { /* $ */ + cx = linend(cy) - lptr[cy]; + } else if (k == 199) { /* G */ + cy = nlines - 1; + cx = 0; + } else if (k == 68) { /* d.. */ + pend = 68; + } else if (k == 71) { /* g.. */ + pend = 71; + } else if (k == 88) { /* x */ + delchar(); + clampx(); + } else if (k == 80) { /* p */ + paste(); + } else if (k == 73) { /* i */ + mode = 1; + } else if (k == 65) { /* a */ + cx++; + clampx(); + mode = 1; + } else if (k == 193) { /* A */ + cx = linend(cy) - lptr[cy]; + mode = 1; + } else if (k == 201) { /* I */ + cx = 0; + mode = 1; + } else if (k == 79) { /* o */ + cx = linend(cy) - lptr[cy]; + newline(); + mode = 1; + } else if (k == 207) { /* O */ + cx = 0; + newline(); + cy--; + mode = 1; + } else if (k == 58) { /* : */ + if (doscmd() == 1) { + sdone(); + return; + } + } + } } diff --git a/vim/vim.pet b/vim/vim.pet new file mode 100644 index 0000000..c957569 --- /dev/null +++ b/vim/vim.pet @@ -0,0 +1 @@ +/* VIM.C - A SMALL MODAL EDITOR FOR THE COMMODORE 64, WRITTEN FOR POWER C. * * ÉNCREMENT 2 EDITS: IT READS A PETSCII ÓÅÑ FILE INTO ONE BUFFER, KEEPS A LINE * INDEX, SHOWS 24 LINES PLUS A STATUS ROW, AND SUPPORTS * MOVE H J K L / ARROWS, 0 $ GG Ç * INSERT I A Á É O Ï (ÒÕÎ/ÓÔÏÐ LEAVES INSERT MODE, ÄÅÌ BACKSPACES) * CHANGE X DD P * COLON :W :W NAME :WQ :X :Q :Q! * * ÔHE BUFFER HOLDS THE FILE EXACTLY AS IT SITS ON DISK: PETSCII BYTES WITH $0D * BETWEEN LINES. ËEYS ARE COMPARED AS NUMBERS, NEVER AS CHARACTER CONSTANTS, * SO NOTHING DEPENDS ON HOW THE COMPILER TREATS LITERALS. (ÕNSHIFTED LETTERS * ARRIVE AS $41-$5A, SO H = 72; SHIFTED ONES AS $C1-$DA, SO Ç = 199.) */ CHAR *MALLOC(); CHAR *BFR; /* FILE TEXT, $0D SEPARATED */ UNSIGNED BLEN; /* BYTES USED */ UNSIGNED LPTR[600]; /* OFFSET OF EACH LINE START */ INT NLINES; INT CY, CX; /* CURSOR LINE AND COLUMN */ INT TOP; /* FIRST LINE ON SCREEN */ INT LEFTCOL; /* HORIZONTAL SCROLL */ CHAR FNAME[24]; INT MODE; /* 0 NORMAL, 1 INSERT */ INT DIRTY; /* BUFFER CHANGED SINCE THE LAST SAVE */ INT PEND; /* FIRST KEY OF A TWO KEY COMMAND: D OR G */ CHAR *NOTE; /* ONE-SHOT MESSAGE FOR THE STATUS ROW */ CHAR YBUF[160]; /* THE YANKED LINE */ INT YLEN, YOK; /* SCREEN ROWS ARE 0..23, THE STATUS ROW IS 24 */ /* SHORTEN FNAME TO WHAT FITS */ SHORTN(N) INT N; Û INT I; I = 0; WHILE (FNAME[I] && I < N) I++; FNAME[I] = 0; RETURN 0; Ý /* A MESSAGE THAT WAITS FOR A KEY */ MSG(S) CHAR *S; Û SCLR(24, 0, 40); SPS(24, 0, S); SKEY(); RETURN 0; Ý /* READ A LINE OF LOWERCASE PETSCII INTO S, ECHOED FROM COLUMN C OF ROW 24 */ GETLN(C, S, MAX) INT C, MAX; CHAR *S; Û INT N, K; N = 0; S[0] = 0; FOR (;;) Û K = SKEY(); IF (K == 13) BREAK; IF (K == 3) Û N = 0; S[0] = 0; BREAK; Ý IF (K == 20) Û IF (N > 0) Û N--; S[N] = 0; Ý Ý ELSE Û IF (K >= 0XC1 && K <= 0XDA) K = K - 0X80; IF (K >= 32 && K != 127 && N < MAX) Û S[N] = K; N++; S[N] = 0; Ý Ý SCLR(24, C, MAX); SPS(24, C, S); Ý RETURN N; Ý /* ASK WHICH FILE TO OPEN */ ASKNAME() Û SCLR(24, 0, 40); SPS(24, 0, "FILE: "); IF (GETLN(6, FNAME, 20) == 0) FNAME[0] = 0; RETURN 0; Ý /* READ FNAME INTO BFR. 1 ON SUCCESS, 0 IF IT COULD NOT BE OPENED */ LOADF() Û INT F, CH, PREV, C; CHAR ON[28]; INT I; IF (FNAME[0] == 0) RETURN 1; I = 0; WHILE (FNAME[I] && I < 20) Û ON[I] = FNAME[I]; I++; Ý ON[I++] = 44; /* , */ ON[I++] = 83; /* Ó (PETSCII, NOT ASCII) */ ON[I++] = 44; /* , */ ON[I++] = 82; /* Ò */ ON[I] = 0; F = OPEN(5, 8, 2, ON); IF (F == 0) RETURN 0; BLEN = 0; PREV = 0; FOR (;;) Û CH = GETC(5); IF (CH < 0) BREAK; /* END OF FILE */ IF (CH == 26) BREAK; IF (CH == 10 && PREV == 13) Û /* CRLF SEEN AS TWO BYTES */ PREV = CH; CONTINUE; Ý IF (CH == 13) C = 13; ELSE IF (CH == 10) C = 13; ELSE C = CH; IF (BLEN < 18999) BFR[BLEN++] = C; PREV = CH; Ý CLOSE(5); RETURN 1; Ý /* FIND THE START OF EVERY LINE */ BUILDIDX() Û UNSIGNED I; NLINES = 1; LPTR[0] = 0; FOR (I = 0; I < BLEN; I++) Û IF (BFR[I] == 13) Û IF (NLINES < 599) Û LPTR[NLINES] = I + 1; NLINES++; Ý Ý Ý RETURN 0; Ý /* THE ":" COMMAND LINE. RETURNS 1 WHEN THE EDITOR SHOULD QUIT */ DOSCMD() Û INT N, P, OK, QF; CHAR CMD[24]; SCLR(24, 0, 40); SPC(24, 0, 58); /* : */ N = GETLN(1, CMD, 20); SCLR(24, 0, 40); IF (N == 0) RETURN 0; IF (CMD[0] == 81) Û /* Q, Q! */ IF (CMD[1] == 33) RETURN 1; IF (DIRTY) Û NOTE = "UNSAVED - :Q! QUITS"; RETURN 0; Ý RETURN 1; Ý OK = 0; QF = 0; P = 1; IF (CMD[0] == 87) OK = 1; /* W, WQ, W NAME */ IF (CMD[0] == 88) Û /* X */ OK = 1; QF = 1; Ý IF (OK == 0) Û NOTE = "UNKNOWN COMMAND"; RETURN 0; Ý IF (CMD[0] == 87 && CMD[1] == 81) Û QF = 1; P = 2; Ý WHILE (CMD[P] == 32) P++; IF (CMD[P]) Û N = 0; WHILE (CMD[P] && N < 20) FNAME[N++] = CMD[P++]; FNAME[N] = 0; Ý IF (FNAME[0] == 0) Û NOTE = "NO FILE NAME"; RETURN 0; Ý IF (SAVEF()) Û DIRTY = 0; NOTE = "WRITTEN"; RETURN QF; Ý NOTE = "CANNOT WRITE"; RETURN 0; Ý MAIN() Û INT K, OK, P; SINIT(); MLINIT(); /* MACHINE CODE ROW PAINTER */ BFR = MALLOC(19000); IF (BFR == 0) Û SPS(0, 0, "OUT OF MEMORY"); SKEY(); RETURN; Ý BLEN = 0; NLINES = 1; LPTR[0] = 0; CY = 0; CX = 0; TOP = 0; LEFTCOL = 0; FNAME[0] = 0; MODE = 0; DIRTY = 0; PEND = 0; NOTE = 0; YLEN = 0; YOK = 0; ASKNAME(); OK = LOADF(); SHORTN(20); BUILDIDX(); IF (OK == 0) Û /* KEEP THE NAME: :W CREATES IT */ NLINES = 1; BLEN = 0; NOTE = "NEW FILE"; Ý FOR (;;) Û SCROLL(); DALL(); K = SKEY(); NOTE = 0; IF (MODE) Û /* ---- INSERT MODE */ IF (K == 3) Û /* ÒÕÎ/ÓÔÏÐ = ÅSC */ MODE = 0; IF (CX > 0) CX--; Ý ELSE IF (K == 13) Û NEWLINE(); Ý ELSE IF (K == 20) Û BKSPACE(); Ý ELSE IF (ARROW(K)) Û CX = CX; Ý ELSE IF (K >= 32 && K < 128) Û INSCHAR(K); Ý ELSE IF (K >= 161) Û INSCHAR(K); Ý CONTINUE; Ý IF (PEND) Û /* ---- SECOND KEY OF DD / GG */ P = PEND; PEND = 0; IF (P == 68 && K == 68) DELLINE(); IF (P == 71 && K == 71) Û CY = 0; CX = 0; Ý CLAMPX(); CONTINUE; Ý IF (ARROW(K)) Û CX = CX; Ý ELSE IF (K == 72) Û /* H */ IF (CX > 0) CX--; Ý ELSE IF (K == 76) Û /* L */ CX++; CLAMPX(); Ý ELSE IF (K == 74) Û /* J */ ARROW(17); Ý ELSE IF (K == 75) Û /* K */ ARROW(145); Ý ELSE IF (K == 48) Û /* 0 */ CX = 0; Ý ELSE IF (K == 36) Û /* $ */ CX = LINEND(CY) - LPTR[CY]; Ý ELSE IF (K == 199) Û /* Ç */ CY = NLINES - 1; CX = 0; Ý ELSE IF (K == 68) Û /* D.. */ PEND = 68; Ý ELSE IF (K == 71) Û /* G.. */ PEND = 71; Ý ELSE IF (K == 88) Û /* X */ DELCHAR(); CLAMPX(); Ý ELSE IF (K == 80) Û /* P */ PASTE(); Ý ELSE IF (K == 73) Û /* I */ MODE = 1; Ý ELSE IF (K == 65) Û /* A */ CX++; CLAMPX(); MODE = 1; Ý ELSE IF (K == 193) Û /* Á */ CX = LINEND(CY) - LPTR[CY]; MODE = 1; Ý ELSE IF (K == 201) Û /* É */ CX = 0; MODE = 1; Ý ELSE IF (K == 79) Û /* O */ CX = LINEND(CY) - LPTR[CY]; NEWLINE(); MODE = 1; Ý ELSE IF (K == 207) Û /* Ï */ CX = 0; NEWLINE(); CY--; MODE = 1; Ý ELSE IF (K == 58) Û /* : */ IF (DOSCMD() == 1) Û SDONE(); RETURN; Ý Ý Ý Ý \ No newline at end of file diff --git a/vim/vml.c b/vim/vml.c new file mode 100644 index 0000000..1e9d418 --- /dev/null +++ b/vim/vml.c @@ -0,0 +1,46 @@ +/* vml.c - GENERATED by build-ml.ps1 from vrow.s. Do not edit by hand. + * + * The machine code row painter, kept as a byte array and called in place + * through sys(). It is position independent; its parameters go through zero + * page scratch $22-$28, which Power C leaves to ML routines. + */ + +char mlcode[76] = { + 165, 40, 9, 32, 160, 39, 145, 37, 136, 16, 251, 160, + 0, 196, 36, 240, 45, 177, 34, 201, 32, 176, 2, 169, + 32, 201, 192, 144, 4, 233, 128, 176, 22, 201, 160, 144, + 4, 233, 64, 176, 14, 201, 96, 144, 4, 233, 32, 176, + 6, 201, 64, 144, 2, 233, 64, 5, 40, 145, 37, 200, + 208, 207, 165, 39, 201, 255, 240, 7, 168, 177, 37, 73, + 128, 145, 37, 96 +}; + +/* nothing to install: the code runs where the array sits */ +mlinit() +{ + return 0; +} + +/* paint the screen row at address dst: len bytes from src, cursor at column cur + (255 = none), rev = 128 for an all reverse row */ +mlrow(src, len, dst, cur, rev) +unsigned src, dst; +int len, cur, rev; +{ + char *p; + char a, x, y; + + p = (char *) 0x22; + p[0] = src & 255; + p[1] = src >> 8; + p[2] = len; + p[3] = dst & 255; + p[4] = dst >> 8; + p[5] = cur; + p[6] = rev; + a = 0; + x = 0; + y = 0; + sys(15, (unsigned) mlcode, &a, &x, &y); + return 0; +} diff --git a/vim/vml.pet b/vim/vml.pet new file mode 100644 index 0000000..3fe6377 --- /dev/null +++ b/vim/vml.pet @@ -0,0 +1 @@ +/* VML.C - ÇÅÎÅÒÁÔÅÄ BY BUILD-ML.PS1 FROM VROW.S. ÄO NOT EDIT BY HAND. * * ÔHE MACHINE CODE ROW PAINTER, KEPT AS A BYTE ARRAY AND CALLED IN PLACE * THROUGH SYS(). ÉT IS POSITION INDEPENDENT; ITS PARAMETERS GO THROUGH ZERO * PAGE SCRATCH $22-$28, WHICH ÐOWER à LEAVES TO ÍÌ ROUTINES. */ CHAR MLCODE[76] = Û 165, 40, 9, 32, 160, 39, 145, 37, 136, 16, 251, 160, 0, 196, 36, 240, 45, 177, 34, 201, 32, 176, 2, 169, 32, 201, 192, 144, 4, 233, 128, 176, 22, 201, 160, 144, 4, 233, 64, 176, 14, 201, 96, 144, 4, 233, 32, 176, 6, 201, 64, 144, 2, 233, 64, 5, 40, 145, 37, 200, 208, 207, 165, 39, 201, 255, 240, 7, 168, 177, 37, 73, 128, 145, 37, 96 Ý; /* NOTHING TO INSTALL: THE CODE RUNS WHERE THE ARRAY SITS */ MLINIT() Û RETURN 0; Ý /* PAINT THE SCREEN ROW AT ADDRESS DST: LEN BYTES FROM SRC, CURSOR AT COLUMN CUR (255 = NONE), REV = 128 FOR AN ALL REVERSE ROW */ MLROW(SRC, LEN, DST, CUR, REV) UNSIGNED SRC, DST; INT LEN, CUR, REV; Û CHAR *P; CHAR A, X, Y; P = (CHAR *) 0X22; P[0] = SRC & 255; P[1] = SRC >> 8; P[2] = LEN; P[3] = DST & 255; P[4] = DST >> 8; P[5] = CUR; P[6] = REV; A = 0; X = 0; Y = 0; SYS(15, (UNSIGNED) MLCODE, &A, &X, &Y); RETURN 0; Ý \ No newline at end of file diff --git a/vim/vrow.bin b/vim/vrow.bin new file mode 100644 index 0000000..491ff51 Binary files /dev/null and b/vim/vrow.bin differ diff --git a/vim/vrow.s b/vim/vrow.s new file mode 100644 index 0000000..810e49f --- /dev/null +++ b/vim/vrow.s @@ -0,0 +1,69 @@ +; vrow.s - draw one screen row for the c64 vim clone (called from power c). +; +; Position independent: no absolute jumps and no absolute data, so the bytes can +; sit in a C array and be called at wherever the linker put that array. +; +; Parameters live in zero page scratch that Power C leaves to ML routines +; (decimal 34-42 = $22-$2A), poked by C before the call (see vml.c): +; $22/$23 src address of the first byte of text (petscii) +; $24 len bytes of text to show, 0..40 +; $25/$26 dst screen RAM address of the row's first cell +; $27 cur column to show reverse (the cursor), $FF = none +; $28 rev $00 normal text, $80 whole row reverse (status row) +; +; The row is cleared to blanks, the text is converted petscii -> screen code +; and copied, then the cursor cell is flipped to reverse video. Codes below +; $20 show as a blank. Colour RAM is set once at start-up, not here. +; +; Build: build-ml.ps1 (ca65 + ld65 -> vrow.bin -> vml.c). + +src = $22 +len = $24 +dst = $25 +cur = $27 +rev = $28 + + .segment "CODE" + +entry: lda rev ; clear the row + ora #$20 + ldy #39 +clr: sta (dst),y + dey + bpl clr + + ldy #0 +cp: cpy len + beq done + lda (src),y + cmp #$20 + bcs ok + lda #$20 +ok: cmp #$C0 ; petscii -> screen code + bcc l1 + sbc #$80 + bcs st ; carry is still set: an unconditional branch +l1: cmp #$A0 + bcc l2 + sbc #$40 + bcs st +l2: cmp #$60 + bcc l3 + sbc #$20 + bcs st +l3: cmp #$40 + bcc st + sbc #$40 +st: ora rev + sta (dst),y + iny + bne cp ; y never wraps (max 40): unconditional + +done: lda cur + cmp #$FF + beq fin + tay + lda (dst),y + eor #$80 + sta (dst),y +fin: rts diff --git a/vim/vscreen.c b/vim/vscreen.c index f2d9cfa..d1cd458 100644 --- a/vim/vscreen.c +++ b/vim/vscreen.c @@ -1,248 +1,165 @@ -/* vscreen.c - screen half of the vim editor for the C64 (Power C) +/* vscreen.c - screen and keyboard for the c64 vim clone. * - * 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. + * The editor writes straight into the screen matrix at $0400 and colour ram at + * $d800 rather than going through the kernal, and it selects the lower/upper + * case character set at $1800 so that petscii $41-$5a draws as real lowercase. * - * 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. + * Everything here returns int, so the other compilation unit can call these + * without declaring them (power c is K&R C). */ -static char stat[40]; /* status line build buffer */ -static int slen; +char *scr; /* screen matrix, $0400 */ +char *colr; /* colour ram, $d800 */ -/* ---------- 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; +/* petscii byte -> screen code */ +s2v(p) +int p; { - while (*s) { - sadd(*s); - s = s + 1; - } + if (p >= 0xc0) return p - 0x80; + if (p >= 0xa0) return p - 0x40; + if (p >= 0x60) return p - 0x20; + if (p >= 0x40) return p - 0x40; + return p; } -static snum(n) -int n; +/* raw screen code at row r, column c */ +sraw(r, c, v) +int r, c, v; { - 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]); - } + if (r < 0) return 0; + if (r > 24) return 0; + if (c < 0) return 0; + if (c > 39) return 0; + i = r * 40 + c; + scr[i] = v; + colr[i] = 1; + return 0; } -/* ---------- PETSCII byte -> screen code ---------- */ - -static sccode(c) -int c; +/* one petscii character */ +spc(r, c, p) +int r, c, p; { - 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; + sraw(r, c, s2v(p)); + return 0; } -/* ---------- screen output ---------- */ - -clrscr() +/* one petscii character, inverted: the cursor and the status row */ +srpc(r, c, p) +int r, c, p; { - char *s; - char *m; - int i; - - s = (char *) 1024; - m = (char *) 55296; - for (i = 0; i < 1000; i++) { - s[i] = 32; - m[i] = 1; - } + sraw(r, c, s2v(p) + 128); + return 0; } -static putchr(x, y, ch, c) -int x; -int y; -int ch; -int c; +/* a reversed space, for the cursor sitting past the end of a line */ +sblk(r, c) +int r, 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; + sraw(r, c, 160); + return 0; } -static putstr(x, y, s, c) -int x; -int y; +/* a zero terminated petscii string */ +sps(r, c, s) +int r, c; char *s; -int c; { - while (*s && x < 40) { - putchr(x, y, sccode(*s), c); - x = x + 1; - s = s + 1; - } + while (*s) spc(r, c++, *s++); + return 0; } -static revchr(o) /* reverse the character in a cell */ -int o; +/* n blanks from (r,c) */ +sclr(r, c, n) +int r, c, n; { - char *s; - - s = (char *) 1024; - if (s[o] < 128) s[o] = s[o] + 128; + while (n-- > 0) sraw(r, c++, 32); + return 0; } -/* ---------- 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; +/* take over the machine: black on blue, lower case, no blinking cursor */ +sinit() { 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; + char *p; + + scr = (char *) 0x0400; + colr = (char *) 0xd800; + p = (char *) 0xd018; + *p = 0x17; + p = (char *) 0xd020; + *p = 0; + p = (char *) 0xd021; + *p = 6; + p = (char *) 0x00cc; + *p = 0; + p = (char *) 0x0091; /* run/stop flag: drop whatever boot left */ + *p = 0; + for (i = 0; i < 1000; i++) { + scr[i] = 32; + colr[i] = 1; } - return len; + return 0; } -static lnofx(tp, cur) /* line holding the cursor */ -char *tp; -int cur; +/* give the machine back the way basic expects it */ +sdone() { int i; - int c; + char *p; + + p = (char *) 0xd018; + *p = 0x15; + p = (char *) 0xd020; + *p = 14; + p = (char *) 0xd021; + *p = 6; + for (i = 0; i < 1000; i++) { + scr[i] = 32; + colr[i] = 1; + } + return 0; +} - c = 0; - for (i = 0; i < cur; i++) - if (tp[i] == 13) c = c + 1; - return c; +/* ---------------------------------------------------------------- keyboard + * + * Keys come straight out of the kernal's keyboard buffer: $c6 says how many + * are waiting and they queue from $0277. The kernal's own irq handler scans + * the keyboard matrix and fills that buffer, so this gets each press as it + * happens - and it also catches anything typed before the program started. + * + * Two obvious alternatives do not work: calling the kernal's GETIN through + * sys() crashed the program, and the library's getchar() is line buffered - it + * echoes through the screen editor and only hands characters over after RETURN. + */ + +/* next key, or 0 if none is waiting */ +skeyp() +{ + int n, k, i; + char *cnt, *buf; + + cnt = (char *) 0x00c6; + n = *cnt; + n = n & 255; + if (n == 0) return 0; + buf = (char *) 0x0277; + k = buf[0]; + k = k & 255; + for (i = 1; i < n; i++) buf[i - 1] = buf[i]; + *cnt = (char) (n - 1); + return k; } -/* ---------- 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; +/* wait for a key */ +skey() { - 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; - } + int k; - 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); + for (;;) { + k = skeyp(); + if (k != 0) return k; } - - 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); } diff --git a/vim/vscreen.pet b/vim/vscreen.pet new file mode 100644 index 0000000..f2720b9 --- /dev/null +++ b/vim/vscreen.pet @@ -0,0 +1 @@ +/* VSCREEN.C - SCREEN AND KEYBOARD FOR THE C64 VIM CLONE. * * ÔHE EDITOR WRITES STRAIGHT INTO THE SCREEN MATRIX AT $0400 AND COLOUR RAM AT * $D800 RATHER THAN GOING THROUGH THE KERNAL, AND IT SELECTS THE LOWER/UPPER * CASE CHARACTER SET AT $1800 SO THAT PETSCII $41-$5A DRAWS AS REAL LOWERCASE. * * ÅVERYTHING HERE RETURNS INT, SO THE OTHER COMPILATION UNIT CAN CALL THESE * WITHOUT DECLARING THEM (POWER C IS Ë&Ò Ã). */ CHAR *SCR; /* SCREEN MATRIX, $0400 */ CHAR *COLR; /* COLOUR RAM, $D800 */ /* PETSCII BYTE -> SCREEN CODE */ S2V(P) INT P; Û IF (P >= 0XC0) RETURN P - 0X80; IF (P >= 0XA0) RETURN P - 0X40; IF (P >= 0X60) RETURN P - 0X20; IF (P >= 0X40) RETURN P - 0X40; RETURN P; Ý /* RAW SCREEN CODE AT ROW R, COLUMN C */ SRAW(R, C, V) INT R, C, V; Û INT I; IF (R < 0) RETURN 0; IF (R > 24) RETURN 0; IF (C < 0) RETURN 0; IF (C > 39) RETURN 0; I = R * 40 + C; SCR[I] = V; COLR[I] = 1; RETURN 0; Ý /* ONE PETSCII CHARACTER */ SPC(R, C, P) INT R, C, P; Û SRAW(R, C, S2V(P)); RETURN 0; Ý /* ONE PETSCII CHARACTER, INVERTED: THE CURSOR AND THE STATUS ROW */ SRPC(R, C, P) INT R, C, P; Û SRAW(R, C, S2V(P) + 128); RETURN 0; Ý /* A REVERSED SPACE, FOR THE CURSOR SITTING PAST THE END OF A LINE */ SBLK(R, C) INT R, C; Û SRAW(R, C, 160); RETURN 0; Ý /* A ZERO TERMINATED PETSCII STRING */ SPS(R, C, S) INT R, C; CHAR *S; Û WHILE (*S) SPC(R, C++, *S++); RETURN 0; Ý /* N BLANKS FROM (R,C) */ SCLR(R, C, N) INT R, C, N; Û WHILE (N-- > 0) SRAW(R, C++, 32); RETURN 0; Ý /* TAKE OVER THE MACHINE: BLACK ON BLUE, LOWER CASE, NO BLINKING CURSOR */ SINIT() Û INT I; CHAR *P; SCR = (CHAR *) 0X0400; COLR = (CHAR *) 0XD800; P = (CHAR *) 0XD018; *P = 0X17; P = (CHAR *) 0XD020; *P = 0; P = (CHAR *) 0XD021; *P = 6; P = (CHAR *) 0X00CC; *P = 0; P = (CHAR *) 0X0091; /* RUN/STOP FLAG: DROP WHATEVER BOOT LEFT */ *P = 0; FOR (I = 0; I < 1000; I++) Û SCR[I] = 32; COLR[I] = 1; Ý RETURN 0; Ý /* GIVE THE MACHINE BACK THE WAY BASIC EXPECTS IT */ SDONE() Û INT I; CHAR *P; P = (CHAR *) 0XD018; *P = 0X15; P = (CHAR *) 0XD020; *P = 14; P = (CHAR *) 0XD021; *P = 6; FOR (I = 0; I < 1000; I++) Û SCR[I] = 32; COLR[I] = 1; Ý RETURN 0; Ý /* ---------------------------------------------------------------- KEYBOARD * * ËEYS COME STRAIGHT OUT OF THE KERNAL'S KEYBOARD BUFFER: $C6 SAYS HOW MANY * ARE WAITING AND THEY QUEUE FROM $0277. ÔHE KERNAL'S OWN IRQ HANDLER SCANS * THE KEYBOARD MATRIX AND FILLS THAT BUFFER, SO THIS GETS EACH PRESS AS IT * HAPPENS - AND IT ALSO CATCHES ANYTHING TYPED BEFORE THE PROGRAM STARTED. * * ÔWO OBVIOUS ALTERNATIVES DO NOT WORK: CALLING THE KERNAL'S ÇÅÔÉÎ THROUGH * SYS() CRASHED THE PROGRAM, AND THE LIBRARY'S GETCHAR() IS LINE BUFFERED - IT * ECHOES THROUGH THE SCREEN EDITOR AND ONLY HANDS CHARACTERS OVER AFTER ÒÅÔÕÒÎ. */ /* NEXT KEY, OR 0 IF NONE IS WAITING */ SKEYP() Û INT N, K, I; CHAR *CNT, *BUF; CNT = (CHAR *) 0X00C6; N = *CNT; N = N & 255; IF (N == 0) RETURN 0; BUF = (CHAR *) 0X0277; K = BUF[0]; K = K & 255; FOR (I = 1; I < N; I++) BUF[I - 1] = BUF[I]; *CNT = (CHAR) (N - 1); RETURN K; Ý /* WAIT FOR A KEY */ SKEY() Û INT K; FOR (;;) Û K = SKEYP(); IF (K != 0) RETURN K; Ý Ý \ No newline at end of file diff --git a/vim/vview.c b/vim/vview.c new file mode 100644 index 0000000..1d10f38 --- /dev/null +++ b/vim/vview.c @@ -0,0 +1,109 @@ +/* vview.c - drawing for the c64 vim clone: scroll, rows, status row. */ + +extern char *bfr; +extern unsigned blen; +extern unsigned lptr[]; +extern int nlines, cy, cx, top, leftcol, mode, dirty; +extern char *note; +extern char fname[]; +extern char ybuf[]; +extern int ylen, yok; + +/* decimal of v into t at p, returns the position after it */ +dnum(t, p, v) +char *t; +int p, v; +{ + char d[6]; + int n; + + n = 0; + if (v == 0) { + t[p++] = 48; + return p; + } + while (v > 0 && n < 5) { + d[n++] = 48 + (v % 10); + v = v / 10; + } + while (n > 0) t[p++] = d[--n]; + return p; +} + +/* bring the cursor back into view */ +scroll() +{ + if (cy < top) top = cy; + if (cy > top + 23) top = cy - 23; + if (top < 0) top = 0; + if (cx < leftcol) leftcol = cx; + if (cx > leftcol + 39) leftcol = cx - 39; + if (leftcol < 0) leftcol = 0; + return 0; +} + +/* draw screen row sr from buffer line top+sr */ +drow(sr) +int sr; +{ + int ln, n, cc; + unsigned i, e, d; + + d = 1024 + sr * 40; /* screen RAM address of the row */ + ln = top + sr; + if (ln >= nlines) { + mlrow(0, 0, d, 255, 0); + return 0; + } + i = lptr[ln] + leftcol; + e = linend(ln); + n = 0; + if (e > i) n = e - i; + if (n > 40) n = 40; + cc = 255; /* cursor column, 255 = not here */ + if (ln == cy) { + cc = cx - leftcol; + if (cc < 0) cc = 255; + if (cc > 39) cc = 255; + } + mlrow((unsigned) bfr + i, n, d, cc, 0); /* machine code does the rest */ + return 0; +} + +/* the status row: message, mode or name on the left; modified flag; line/total */ +dstat() +{ + char t[40]; + char *s; + int i, n, p; + + if (note != 0) s = note; + else if (mode) s = "-- insert --"; + else if (fname[0] == 0) s = "[no name]"; + else s = fname; + + n = 0; + for (i = 0; s[i] && i < 26; i++) t[n++] = s[i]; + while (n < 28) t[n++] = 32; + if (dirty) t[n++] = 43; /* + */ + else t[n++] = 32; + while (n < 31) t[n++] = 32; + p = dnum(t, n, cy + 1); + t[p++] = 47; /* / */ + p = dnum(t, p, nlines); + + mlrow((unsigned) t, p, 1984, 255, 128); /* row 24 = $0400 + 960, reverse */ + return 0; +} + +dall() +{ + int r; + + for (r = 0; r < 24; r++) drow(r); + dstat(); + return 0; +} + + +/* end of vview.c */ diff --git a/vim/vview.pet b/vim/vview.pet new file mode 100644 index 0000000..2df3f6f --- /dev/null +++ b/vim/vview.pet @@ -0,0 +1 @@ +/* VVIEW.C - DRAWING FOR THE C64 VIM CLONE: SCROLL, ROWS, STATUS ROW. */ EXTERN CHAR *BFR; EXTERN UNSIGNED BLEN; EXTERN UNSIGNED LPTR[]; EXTERN INT NLINES, CY, CX, TOP, LEFTCOL, MODE, DIRTY; EXTERN CHAR *NOTE; EXTERN CHAR FNAME[]; EXTERN CHAR YBUF[]; EXTERN INT YLEN, YOK; /* DECIMAL OF V INTO T AT P, RETURNS THE POSITION AFTER IT */ DNUM(T, P, V) CHAR *T; INT P, V; Û CHAR D[6]; INT N; N = 0; IF (V == 0) Û T[P++] = 48; RETURN P; Ý WHILE (V > 0 && N < 5) Û D[N++] = 48 + (V % 10); V = V / 10; Ý WHILE (N > 0) T[P++] = D[--N]; RETURN P; Ý /* BRING THE CURSOR BACK INTO VIEW */ SCROLL() Û IF (CY < TOP) TOP = CY; IF (CY > TOP + 23) TOP = CY - 23; IF (TOP < 0) TOP = 0; IF (CX < LEFTCOL) LEFTCOL = CX; IF (CX > LEFTCOL + 39) LEFTCOL = CX - 39; IF (LEFTCOL < 0) LEFTCOL = 0; RETURN 0; Ý /* DRAW SCREEN ROW SR FROM BUFFER LINE TOP+SR */ DROW(SR) INT SR; Û INT LN, N, CC; UNSIGNED I, E, D; D = 1024 + SR * 40; /* SCREEN ÒÁÍ ADDRESS OF THE ROW */ LN = TOP + SR; IF (LN >= NLINES) Û MLROW(0, 0, D, 255, 0); RETURN 0; Ý I = LPTR[LN] + LEFTCOL; E = LINEND(LN); N = 0; IF (E > I) N = E - I; IF (N > 40) N = 40; CC = 255; /* CURSOR COLUMN, 255 = NOT HERE */ IF (LN == CY) Û CC = CX - LEFTCOL; IF (CC < 0) CC = 255; IF (CC > 39) CC = 255; Ý MLROW((UNSIGNED) BFR + I, N, D, CC, 0); /* MACHINE CODE DOES THE REST */ RETURN 0; Ý /* THE STATUS ROW: MESSAGE, MODE OR NAME ON THE LEFT; MODIFIED FLAG; LINE/TOTAL */ DSTAT() Û CHAR T[40]; CHAR *S; INT I, N, P; IF (NOTE != 0) S = NOTE; ELSE IF (MODE) S = "-- INSERT --"; ELSE IF (FNAME[0] == 0) S = "[NO NAME]"; ELSE S = FNAME; N = 0; FOR (I = 0; S[I] && I < 26; I++) T[N++] = S[I]; WHILE (N < 28) T[N++] = 32; IF (DIRTY) T[N++] = 43; /* + */ ELSE T[N++] = 32; WHILE (N < 31) T[N++] = 32; P = DNUM(T, N, CY + 1); T[P++] = 47; /* / */ P = DNUM(T, P, NLINES); MLROW((UNSIGNED) T, P, 1984, 255, 128); /* ROW 24 = $0400 + 960, REVERSE */ RETURN 0; Ý DALL() Û INT R; FOR (R = 0; R < 24; R++) DROW(R); DSTAT(); RETURN 0; Ý /* END OF VVIEW.C */ \ No newline at end of file diff --git a/vim/vim.d81 b/vim/work.d81 similarity index 88% rename from vim/vim.d81 rename to vim/work.d81 index cc47478..62109cd 100644 Binary files a/vim/vim.d81 and b/vim/work.d81 differ diff --git a/vim/work.stamp b/vim/work.stamp new file mode 100644 index 0000000..5eb49e8 --- /dev/null +++ b/vim/work.stamp @@ -0,0 +1,6 @@ +vim=8E13FCED4A0CAFD92855B9281D268368 +vscreen=FFCDAD177DF3AD43CE2809EA7ACD6029 +vview=F3C7D092B8080C956D9EBD7B95800CD5 +mltest=E4BDE10600DE08AF766FFB32A6D9A3E5 +vedit=8BE6C95F2BDA01EC535A8C51CD666881 +vml=D600B54B6821800D24B2986D920E9B12