// Copyright (c) 2026 Charles KWON OhJun (charleskwonohjun@gmail.com) // All rights reserved. // Terminal I/O functions for Five. // Implements Harbour's @..SAY, @..GET, Inkey(), SetPos(), Row(), Col(), // and screen control using Go's ANSI escape sequences. // // No external dependency — uses standard ANSI/VT100 escape codes. // Works on Linux, macOS, Windows Terminal, WSL. // // Reference: /mnt/d/harbour-core/src/rtl/gtcrs/, gtstd/, gttrm/ package hbrtl import ( "bufio" "five/hbrt" "fmt" "os" "strings" ) // Terminal state var ( termRow int = 0 termCol int = 0 ) // --- Screen positioning (ANSI escape) --- // SetPos positions cursor at row, col (0-based). // Harbour: SetPos(nRow, nCol) func SetPos(t *hbrt.Thread) { t.Frame(2, 0) defer t.EndProc() row := int(t.Local(1).AsNumInt()) col := int(t.Local(2).AsNumInt()) termRow = row termCol = col fmt.Printf("\033[%d;%dH", row+1, col+1) // ANSI is 1-based t.RetNil() } // Row returns current cursor row. func Row(t *hbrt.Thread) { t.Frame(0, 0) defer t.EndProc() t.RetInt(int64(termRow)) } // Col returns current cursor column. func Col(t *hbrt.Thread) { t.Frame(0, 0) defer t.EndProc() t.RetInt(int64(termCol)) } // DevPos positions cursor (alias for SetPos). // Harbour: DevPos(nRow, nCol) func DevPos(t *hbrt.Thread) { SetPos(t) } // --- Screen output --- // DevOut outputs value at current position. // Harbour: DevOut(xValue) func DevOut(t *hbrt.Thread) { t.Frame(1, 0) defer t.EndProc() v := t.Local(1) s := valueToDisplay(v) fmt.Print(s) termCol += len(s) t.RetNil() } // AtSay implements @ nRow, nCol SAY expr // In Five: compiled as SetPos(r,c) + DevOut(expr) func AtSay(t *hbrt.Thread) { nParams := t.ParamCount() t.Frame(nParams, 0) defer t.EndProc() row := int(t.Local(1).AsNumInt()) col := int(t.Local(2).AsNumInt()) termRow = row termCol = col fmt.Printf("\033[%d;%dH", row+1, col+1) if nParams >= 3 { s := valueToDisplay(t.Local(3)) fmt.Print(s) termCol += len(s) } t.RetNil() } // DevOutPict outputs a value with PICTURE formatting. // Harbour: DevOutPict(xValue, cPicture [, cColor]) // Expands to: DevOut(Transform(xValue, cPicture), cColor) func DevOutPict(t *hbrt.Thread) { nParams := t.ParamCount() t.Frame(nParams, 0) defer t.EndProc() val := t.Local(1) pic := "" if nParams >= 2 && !t.Local(2).IsNil() { pic = t.Local(2).AsString() } s := transformHbValue(val, pic) fmt.Print(s) termCol += len(s) t.RetNil() } // --- Screen control --- // Cls clears the screen. // Harbour: CLS or @ 0,0 CLEAR func Cls(t *hbrt.Thread) { t.Frame(0, 0) defer t.EndProc() fmt.Print("\033[2J\033[H") // clear screen + home termRow = 0 termCol = 0 t.RetNil() } // Scroll scrolls a screen region. // Harbour: Scroll(nTop, nLeft, nBottom, nRight, nRows) func Scroll(t *hbrt.Thread) { t.Frame(5, 0) defer t.EndProc() // Simplified: just move cursor t.RetNil() } // --- Keyboard --- // InkeyWait waits for a keypress and returns key code. // Harbour: Inkey(nSeconds) → nKeyCode func InkeyWait(t *hbrt.Thread) { nParams := t.ParamCount() t.Frame(nParams, 0) defer t.EndProc() // Simple: read one byte from stdin reader := bufio.NewReader(os.Stdin) ch, err := reader.ReadByte() if err != nil { t.RetInt(0) return } // Handle escape sequences (arrow keys etc.) if ch == 27 { // ESC // Try reading more for escape sequence if reader.Buffered() > 0 { ch2, _ := reader.ReadByte() if ch2 == '[' { ch3, _ := reader.ReadByte() switch ch3 { case 'A': t.RetInt(5) // K_UP return case 'B': t.RetInt(24) // K_DOWN return case 'C': t.RetInt(4) // K_RIGHT return case 'D': t.RetInt(19) // K_LEFT return } } } t.RetInt(27) // K_ESC return } t.RetInt(int64(ch)) } // --- Color --- // SetColor sets screen colors. // Harbour: SetColor(cColorString) // Simplified: map to ANSI colors. func SetColor(t *hbrt.Thread) { nParams := t.ParamCount() t.Frame(nParams, 0) defer t.EndProc() if nParams > 0 { // Parse Harbour color string like "W+/B" (white on blue) // For now, just reset _ = t.Local(1).AsString() } t.RetNil() } // --- Cursor --- // SetCursor sets cursor shape. // Harbour: SetCursor(nCursorShape) func SetCursor(t *hbrt.Thread) { nParams := t.ParamCount() t.Frame(nParams, 0) defer t.EndProc() if nParams > 0 { shape := int(t.Local(1).AsNumInt()) if shape == 0 { fmt.Print("\033[?25l") // hide } else { fmt.Print("\033[?25h") // show } } t.RetInt(1) // return old cursor } // MaxRow returns max screen row. func MaxRow(t *hbrt.Thread) { t.Frame(0, 0) defer t.EndProc() t.RetInt(24) // standard 25 rows, 0-based } // MaxCol returns max screen col. func MaxCol(t *hbrt.Thread) { t.Frame(0, 0) defer t.EndProc() t.RetInt(79) // standard 80 cols, 0-based } // --- String display helpers --- // DispOut outputs string at current position (like DevOut). func DispOut(t *hbrt.Thread) { DevOut(t) } // DispBox draws a box. Simplified version. // Harbour: DispBox(nTop, nLeft, nBottom, nRight, cChars) func DispBox(t *hbrt.Thread) { nParams := t.ParamCount() t.Frame(nParams, 0) defer t.EndProc() nTop := int(t.Local(1).AsNumInt()) nLeft := int(t.Local(2).AsNumInt()) nBottom := int(t.Local(3).AsNumInt()) nRight := int(t.Local(4).AsNumInt()) width := nRight - nLeft + 1 if width < 2 { width = 2 } // Draw top border fmt.Printf("\033[%d;%dH", nTop+1, nLeft+1) fmt.Print("+" + strings.Repeat("-", width-2) + "+") // Draw sides for r := nTop + 1; r < nBottom; r++ { fmt.Printf("\033[%d;%dH", r+1, nLeft+1) fmt.Print("|" + strings.Repeat(" ", width-2) + "|") } // Draw bottom border fmt.Printf("\033[%d;%dH", nBottom+1, nLeft+1) fmt.Print("+" + strings.Repeat("-", width-2) + "+") t.RetNil() }