The telnet echo suppression was reactive — the server only announced
WILL ECHO / WILL SGA after the client sent an IAC first. But a classic
telnet client sits in line-mode-with-local-echo and waits for the
server to negotiate, so the announcement never went out, the client
kept echoing locally, and every line appeared twice (its echo plus the
machine's own authentic echo).
A telnet server that wants remote echo has to offer it proactively on
connect. The new --tty-telnet flag (implies --tty) does exactly that:
it writes WILL ECHO + WILL SGA before the banner and marks the filter
active so the reactive path doesn't double-announce. Bare --tty stays
byte-clean for local terminals and nc (kickoff option B); the reactive
fallback still helps a plain --tty client that does negotiate first.
The systemd @.service switches its ExecStart to --tty-telnet.
The growing session-behavior params (throttle, banner, telnet) fold
into a TtyConfig struct. Tests: proactive announce leads the stream
(before the banner) and doesn't double-announce when the client then
sends its own IAC; the flag implies --tty and parses. Verified live:
`ewm one --tty-telnet | xxd` shows FF FB 01 FF FB 03 then the prompt.
Docs: scripts/systemd/README.md and notes/APPLE1.md explain proactive
--tty-telnet vs byte-clean --tty, with troubleshooting for the
double-echo and the nc negotiation bytes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The first real deployment surfaced everything a header comment
couldn't hold: SDL's Linux build dependencies (and the XTEST error a
partial set produces), the stale-CMake-cache gotcha after a failed
configure (cargo clean -p sdl3-sys), what sdl-static-headless actually
builds and its --tty-only caveat, the install paths the units expect,
how to watch sessions (systemctl list-units / journalctl), operating
knobs (machine, port, limits, banner), the security posture, and a
troubleshooting section written from the actual failures.
The guide lives next to the units as scripts/systemd/README.md; the
socket header shrinks to a pointer (one source of truth), and the main
README's Experimental bullet and notes/APPLE1.md link to it.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Deploying the telnet Apple 1 hit two gaps. First, the default build
links SDL3 dynamically and sdl-static compiles SDL with full video —
which demands X11 dev packages (XTEST et al) that a headless server
neither has nor needs. The new sdl-static-headless feature stacks
sdl3's build-from-source-unix-console on sdl-static: a static SDL
console build with no X11/Wayland at all. Only --tty runs on such a
binary (documented); it builds in a fraction of the time and links no
dynamic SDL.
Second, ewm-one.socket's header said "install both units" without
saying how much more there is: it now carries the full checklist —
the headless build command, binary/banner/unit installs,
daemon-reload, enable, and where the per-instance logs live.
Verified: the headless binary builds, serves a banner + monitor
session over a pipe, and carries no dynamic SDL reference.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Phase T3 of plans/20260719-04-apple1-telnet.md (added after T2): a
--tty-banner <path> text file prints to the session before the machine
says anything — where the operator puts the instructions a bare Woz
Monitor prompt doesn't offer. Line endings are CRLF-normalized for raw
terminals; a missing file fails loud at startup (a systemd misconfig
should be visible in the journal, not silently banner-less).
The flag is tty-session tooling, not machine description — it lives
with --tty, outside the config document, like the debug flags do.
scripts/systemd/banner.txt ships as the example (monitor and BASIC
starters, the reset chord, how to leave telnet) and the service unit
mounts it from /usr/local/share/ewm — readable under
ProtectSystem=strict.
Test: the banner precedes the machine's prompt, CRLF-normalized, and
the flag without a value falls into usage. Verified live: piped
session shows banner then prompt; a bad path exits 1 with the error.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Phase T2, closing plans/20260719-04-apple1-telnet.md.
The tty session gains a hand-rolled RFC 854 subset: a state-machine
filter that stays dormant until the first inbound IAC — nc and local
terminals never see a protocol byte — then announces WILL ECHO + WILL
SGA (character-at-a-time, remote echo), refuses every other
negotiation, swallows subnegotiations, drops IAC IAC (a literal 0xFF
is not an Apple 1 key), and maps BRK/IP — telnet's `send brk`, the
serial-terminal attention — to the RESET button: immediate, flushing
typed-ahead, like the real thing.
scripts/systemd/ carries the inetd-style units: ewm-one.socket
(ListenStream=6502, Accept=yes, MaxConnectionsPerSource) and
ewm-one@.service (StandardInput=socket, DynamicUser, ProtectSystem=
strict, MemoryMax, RuntimeMaxSec) — the sandbox is tight because the
binary needs no filesystem; configs and ROMs are embedded. README's
Experimental section gains the entry; notes/APPLE1.md records the
tty/telnet design as built; the plan is complete.
Tests: the filter unit-tested byte for byte (activation volley, the
no-reply-to-DO-ECHO loop guard, WONT/DONT refusals, subnegotiation
swallowing, BRK, IAC IAC); a session-level test where a staged reader
delivers the volley, enters BASIC, then BREAKs back to the monitor —
staged because BREAK is deliberately out-of-band. Verified live:
piping an IAC volley plus a command through `ewm one --tty` yields
prompt, WILLs, echo, and the dump in one stream.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
scripts/make-app.sh assembles dist/EWM.app, a self-contained
double-clickable bundle:
- SDL3 is compiled from source and linked statically (a new sdl-static
cargo feature; dev/CI builds keep the Homebrew pkg-config path as the
default feature). Verified: otool -L shows only system frameworks.
- The icon is "][" rendered by the emulator's own Apple ][ character
generator (examples/icon.rs), green phosphor on a dark bezel, drawn at
integer scales per icon size and written as PNGs by a hand-rolled
~40-line PNG encoder (flate2 as a dev-dependency for the zlib stream);
iconutil turns the set into EWM.icns at bundle time, so no binary icon
is committed.
- Info.plist declares CFBundleDocumentTypes for .dsk/.do/.po/.nib/.woz
and .hdv at Alternate rank, ad-hoc codesign for local use (real
signing/notarization is Phase 2).
Opening a disk image from the Finder arrives as an SDL drop event, which
also buys drag-and-drop: the boo menu boots the ][+ with a dropped floppy
in drive 1 (or .hdv on slot 7), and dropping a floppy on a running
machine swaps drive 1. The new media module classifies images -- the one
subtlety is .po, which is a floppy at exactly 143,360 bytes and a ProDOS
hard-drive volume when larger.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Phase 9: remove the C tree, promote the Rust workspace to the root
Pure git mv / git rm plus the mechanical path fixes:
- Delete src/*.c, src/*.h, src/Makefile, src/CMakeLists.txt, the Lua
scripts, .travis.yml, and scripts/gen-golden-trace.* (the golden
trace regeneration needed the C tree; the checked-in golden file is
now frozen — regenerating requires the C from git history).
- git mv src/rom -> rom and rust/* -> the repo root.
- Fix include_bytes! and test paths for the new layout (src/rom ->
rom, one directory level less). No other code changes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Phase 9: CI drops the C job; ignore /target
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Phase 9: rewrite README for the Rust build; REWRITE.md complete
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Phase 9: rustfmt after the path shortening
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Completes the CPU core port:
- ins.rs: the 65C02 override handlers ((zp) modes, BRA, STZ, PHX/PHY/
PLX/PLY, TRB/TSB, RMB/SMB, BBR/BBS, INC/DEC A, BIT imm/zpgx/absx,
JMP (abs,X)) and the override table transcribed verbatim from
instructions_65C02 — including its 1-cycle NOPs and the stray 0x03
opcode fields. The table is overlaid onto the 6502 table once, the
Rust version of cpu_initialize()'s back-fill. C's arity-cast NOPs
become explicit nop_byte/nop_word wrappers.
- fmt.rs: cpu_format_state, cpu_format_stack, and the disassembler
from fmt.c, bugs preserved (0x9c prints JMP; BBR/BBS targets are
computed from pc+2).
- tests/dormann.rs: dormann_65c02 passes at $24a8 after 65,094,140
cycles — exactly the C emulator's count.
- Golden-trace harness: scripts/gen-golden-trace.{c,sh} builds a
capture program against the unmodified C sources and records the
first 100k instructions of the functional test as normalized
'PC A X Y SP P' lines; checked in gzipped. trace_compare.rs replays
it on the Rust core and reports the first divergent instruction
(currently: zero divergence). flate2 is a dev-dependency only.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>