Implement the following plan:
Plan: Raw terminal mode for keyboard channel (Issue #90)
Context
The generated entry harness reads keyboard input via bufio.NewReader(os.Stdin), which operates in the terminal's default line-buffered mode. Characters aren't available until Enter is pressed, breaking programs like echoing.occ that expect character-by-character input. We need to put the terminal in raw mode so bytes are available immediately as typed.
Approach
Use golang.org/x/term to switch stdin to raw mode in the generated entry harness. Fall back to the current buffered behavior when stdin is not a terminal (piped input).
Changes
1. Add dependency
go get golang.org/x/term — adds the package to go.mod/go.sum
2. codegen/codegen.go — Generator struct (line ~21)
3. codegen/codegen.go — flag initialization (line ~106)
4. codegen/codegen.go — entry harness detection (lines 216-220)
- Set
g.needTerm = true when entry harness is detected
5. codegen/codegen.go — import generation (lines 228-256)
- Add condition for
needTerm in the import block guard
- When
needTerm, emit imports for "os/signal", "syscall", and "golang.org/x/term" (alphabetical order within the block)
6. codegen/codegen.go — generateEntryHarness() (lines 419-512)
Rewrite to generate code structured like this:
func main() {
keyboard := make(chan byte, 256)
screen := make(chan byte, 256)
_error := make(chan byte, 256)
// Raw terminal mode
var rawMode bool
var oldState *term.State
fd := int(os.Stdin.Fd())
if term.IsTerminal(fd) {
var err error
oldState, err = term.MakeRaw(fd)
if err == nil {
rawMode = true
defer term.Restore(fd, oldState)
// Restore terminal on signals (SIGINT won't fire from Ctrl+C in
// raw mode, but can arrive via external kill; SIGTERM from kill)
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
go func() {
<-sigCh
term.Restore(fd, oldState)
os.Exit(1)
}()
}
}
var wg sync.WaitGroup
wg.Add(2)
// Screen writer — in raw mode, insert CR before LF
go func() {
defer wg.Done()
w := bufio.NewWriter(os.Stdout)
for b := range screen {
if b == 255 {
w.Flush()
} else {
if rawMode && b == '\n' {
w.WriteByte('\r')
}
w.WriteByte(b)
}
}
w.Flush()
}()
// Error writer — same CR/LF handling
go func() { /* same pattern as screen, writing to os.Stderr */ }()
// Keyboard reader
go func() {
if rawMode {
buf := make([]byte, 1)
for {
n, err := os.Stdin.Read(buf)
if err != nil || n == 0 {
close(keyboard)
return
}
if buf[0] == 3 { // Ctrl+C
term.Restore(fd, oldState)
os.Exit(1)
}
keyboard <- buf[0]
}
} else {
r := bufio.NewReader(os.Stdin)
for {
b, err := r.ReadByte()
if err != nil {
close(keyboard)
return
}
keyboard <- b
}
}
}()
entryProc(keyboard, screen, _error)
close(screen)
close(_error)
wg.Wait()
}
Key design decisions:
- Ctrl+C handling: In raw mode, ISIG is disabled so Ctrl+C sends byte 3 instead of SIGINT. The keyboard reader intercepts byte 3 to restore terminal and exit cleanly.
- LF→CRLF in output: Raw mode disables OPOST, so \n no longer auto-translates to \r\n. The screen/error writers insert CR before LF. This is harmless if the occam program already sends both *c and *n (the extra CR is a no-op).
- Non-terminal fallback: When stdin is piped, skip raw mode and use the existing bufio.NewReader approach for efficient buffered reading.
- Signal handler: Catches external SIGINT/SIGTERM to restore terminal state before exit.
7. codegen/codegen_test.go — unit test
- Add test that verifies entry-harness-style program generates code containing
term.MakeRaw, term.IsTerminal, and term.Restore.
8. codegen/e2e_helpers_test.go — new helper + e2e test
- Add
transpileCompileRunWithInput() helper that:
- Initializes a Go module in the temp dir (
go mod init test && go mod tidy)
- Pipes stdin to the binary
- Add e2e test: transpile
echoing.occ-style program, pipe input "hello Z", verify output matches.
Verification
go test ./... — all existing tests pass
- Manual test with
echoing.occ:
bash
go build -o occam2go . && ./occam2go -I kroc/modules/course/libsrc -o /tmp/echo.go kroc/modules/course/examples/echoing.occ
cd /tmp && go mod init echo_test && go mod tidy && go run echo.go
# Type characters — they should echo immediately, one at a time
# Type 'Z' to exit
If you need specific details from before exiting plan mode (like exact code snippets, error messages, or content you generated), read the full transcript at: /home/david/.claude/projects/-home-david-projects-code-associates-occam2go/0114918c-281e-45dd-98ac-dfc9baf171b5.jsonl