- Compiler: PP → Lexer → Parser → Analyzer → Gengo pipeline - Parser: 232/236 (98%) Harbour compatibility, registry-based dispatch - RTL: 351 Harbour-compatible functions - RDD: DBF/NTX/CDX engines with Rushmore bitmap optimization - Go Interop: IMPORT + pkg.Func() + obj:Method() with FastPath (15M calls/sec) - HB_FUNC API: Full Harbour C API compatible Go bridge - Concurrency: SPAWN/LAUNCH/GOROUTINE, <-, WATCH, PARALLEL FOR, ASYNC/AWAIT - Extensions: Multi-return, DEFER, Slice, f-string, Nil-safe ?:, CONST - Macro Compiler: Runtime AST parsing and evaluation - Debugger: TUI debugger with source display, breakpoints, stepping - FRB: Native + Pcode dual mode runtime binary - Tests: 13 packages ALL PASS Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
335 lines
8.2 KiB
Go
335 lines
8.2 KiB
Go
// Copyright (c) 2026 Charles KWON OhJun (charleskwonohjun@gmail.com)
|
|
// All rights reserved.
|
|
|
|
// Full-screen TUI debugger for Five — Harbour/Clipper debugger style.
|
|
// Uses ANSI escape codes for terminal rendering.
|
|
|
|
package hbrt
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
"syscall"
|
|
"unsafe"
|
|
)
|
|
|
|
// TUIDebugger creates a full-screen terminal debugger callback.
|
|
func TUIDebugger() DebugCallback {
|
|
var sourceCache map[string][]string // file → lines cache
|
|
sourceCache = make(map[string][]string)
|
|
|
|
return func(event *DebugEvent) int {
|
|
// Switch to cooked mode for debugger
|
|
restoreCooked()
|
|
defer reenterRaw()
|
|
|
|
// Load source file
|
|
srcDir := ""
|
|
if event.Thread.VM().Debugger != nil {
|
|
srcDir = event.Thread.VM().Debugger.SourceDir
|
|
}
|
|
lines := loadSource(sourceCache, event.Module, srcDir)
|
|
|
|
// Get terminal size
|
|
rows, cols := termSize()
|
|
if rows < 10 {
|
|
rows = 24
|
|
}
|
|
if cols < 40 {
|
|
cols = 80
|
|
}
|
|
|
|
// Layout:
|
|
// Row 1: title bar
|
|
// Row 2 ~ srcEnd: source code
|
|
// srcEnd+1 ~ panelEnd: locals + stack side by side
|
|
// Last row: command bar
|
|
srcHeight := rows - 10
|
|
if srcHeight < 5 {
|
|
srcHeight = 5
|
|
}
|
|
panelHeight := rows - srcHeight - 3 // title + src + cmdbar
|
|
if panelHeight < 3 {
|
|
panelHeight = 3
|
|
}
|
|
|
|
for {
|
|
// Clear screen
|
|
fmt.Print("\033[2J\033[H")
|
|
|
|
// === Title Bar ===
|
|
title := fmt.Sprintf(" Five Debugger - %s:%d %s() ", event.Module, event.Line, event.Function)
|
|
if event.Reason == "breakpoint" {
|
|
title += "[BREAKPOINT] "
|
|
}
|
|
fmt.Printf("\033[7m%-*s\033[0m\r\n", cols, title)
|
|
|
|
// === Source Window ===
|
|
drawSourceWindow(lines, event.Line, srcHeight, cols, event.Thread.VM().Debugger)
|
|
|
|
// === Panels: Locals | Call Stack ===
|
|
localW := cols / 2
|
|
stackW := cols - localW
|
|
drawPanels(event, panelHeight, localW, stackW)
|
|
|
|
// === Command Bar ===
|
|
fmt.Printf("\033[7m%-*s\033[0m", cols,
|
|
" F5:Go F7:Into F8:Step F9:Break F10:Over F11:Out L:Locals ESC:Quit")
|
|
|
|
// Wait for key
|
|
key := readDebugKey()
|
|
|
|
switch key {
|
|
case 0x1B, 'q', 'Q': // ESC or Q — quit
|
|
fmt.Print("\033[2J\033[H")
|
|
restoreCooked()
|
|
os.Exit(0)
|
|
|
|
case 0xF5, 'g', 'G': // F5 or G — Go/Continue
|
|
return DbgContinue
|
|
|
|
case 0xF7: // F7 — Step Into
|
|
return DbgStepLine
|
|
|
|
case 0xF8, 's', 'S', 10, 13: // F8 / s / Enter — Step
|
|
return DbgStepLine
|
|
|
|
case 0xF9, 'b', 'B': // F9 or B — Toggle Breakpoint
|
|
dbg := event.Thread.VM().Debugger
|
|
found := false
|
|
for i, bp := range dbg.Breakpoints {
|
|
if bp.Line == event.Line {
|
|
dbg.RemoveBreakpoint(i)
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
dbg.AddBreakpoint(event.Module, event.Line)
|
|
}
|
|
continue // redraw
|
|
|
|
case 0xFA, 'n', 'N': // F10 or N — Step Over
|
|
return DbgStepOver
|
|
|
|
case 0xFB, 'o', 'O': // F11 or O — Step Out
|
|
return DbgStepOut
|
|
|
|
case 'c', 'C': // Continue
|
|
return DbgContinue
|
|
|
|
case 0xE0, 0xE1, 0xE2, 0xE3: // Arrow keys — ignore
|
|
continue
|
|
|
|
default:
|
|
continue // unknown key, redraw
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func drawSourceWindow(lines []string, curLine, height, width int, dbg *Debugger) {
|
|
// Calculate visible range centered on current line
|
|
start := curLine - height/2
|
|
if start < 1 {
|
|
start = 1
|
|
}
|
|
end := start + height - 1
|
|
if end > len(lines) {
|
|
end = len(lines)
|
|
}
|
|
|
|
// Top border
|
|
fmt.Printf("\033[36m\u250C\u2500 Source \u2500%s\u2510\033[0m\r\n", strings.Repeat("\u2500", width-12))
|
|
|
|
for i := start; i <= end; i++ {
|
|
lineText := ""
|
|
if i-1 < len(lines) {
|
|
lineText = lines[i-1]
|
|
}
|
|
// Truncate to fit
|
|
if len(lineText) > width-10 {
|
|
lineText = lineText[:width-10]
|
|
}
|
|
|
|
// Breakpoint marker
|
|
bpMark := " "
|
|
if dbg != nil {
|
|
for _, bp := range dbg.Breakpoints {
|
|
if bp.Enabled && bp.Line == i {
|
|
bpMark = "\033[31m\u25CF\033[0m" // red dot
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
// Current line marker
|
|
if i == curLine {
|
|
fmt.Printf("\033[36m\u2502\033[0m%s\033[33m>> %4d:\033[0m \033[7m%-*s\033[0m\033[36m\u2502\033[0m\r\n",
|
|
bpMark, i, width-10, lineText)
|
|
} else {
|
|
fmt.Printf("\033[36m\u2502\033[0m%s %4d: %-*s\033[36m\u2502\033[0m\r\n",
|
|
bpMark, i, width-10, lineText)
|
|
}
|
|
}
|
|
|
|
// Pad remaining lines
|
|
for i := end - start + 1; i < height; i++ {
|
|
fmt.Printf("\033[36m\u2502\033[0m%*s\033[36m\u2502\033[0m\r\n", width-2, "")
|
|
}
|
|
|
|
// Bottom border
|
|
fmt.Printf("\033[36m\u2514%s\u2518\033[0m\r\n", strings.Repeat("\u2500", width-2))
|
|
}
|
|
|
|
func drawPanels(event *DebugEvent, height, localW, stackW int) {
|
|
// Headers
|
|
localHeader := fmt.Sprintf("\u250C\u2500 Locals %s\u2510", strings.Repeat("\u2500", localW-11))
|
|
stackHeader := fmt.Sprintf("\u250C\u2500 Stack %s\u2510", strings.Repeat("\u2500", stackW-10))
|
|
fmt.Printf("\033[36m%s%s\033[0m\r\n", localHeader, stackHeader)
|
|
|
|
// Content rows
|
|
for i := 0; i < height-2; i++ {
|
|
// Left: locals
|
|
localLine := ""
|
|
if i < len(event.Locals) {
|
|
v := event.Locals[i]
|
|
val := v.Value.String()
|
|
if len(val) > localW-8 {
|
|
val = val[:localW-11] + "..."
|
|
}
|
|
localLine = fmt.Sprintf(" %s = %s", v.Name, val)
|
|
}
|
|
if len(localLine) > localW-2 {
|
|
localLine = localLine[:localW-2]
|
|
}
|
|
|
|
// Right: call stack
|
|
stackLine := ""
|
|
if i < len(event.CallStack) {
|
|
f := event.CallStack[i]
|
|
if f.Module != "" {
|
|
stackLine = fmt.Sprintf(" %s() %s:%d", f.Function, f.Module, f.Line)
|
|
} else {
|
|
stackLine = fmt.Sprintf(" %s()", f.Function)
|
|
}
|
|
}
|
|
if len(stackLine) > stackW-2 {
|
|
stackLine = stackLine[:stackW-2]
|
|
}
|
|
|
|
fmt.Printf("\033[36m\u2502\033[0m%-*s\033[36m\u2502\033[0m%-*s\033[36m\u2502\033[0m\r\n",
|
|
localW-2, localLine, stackW-2, stackLine)
|
|
}
|
|
|
|
// Bottom borders
|
|
localFooter := fmt.Sprintf("\u2514%s\u2518", strings.Repeat("\u2500", localW-2))
|
|
stackFooter := fmt.Sprintf("\u2514%s\u2518", strings.Repeat("\u2500", stackW-2))
|
|
fmt.Printf("\033[36m%s%s\033[0m\r\n", localFooter, stackFooter)
|
|
}
|
|
|
|
func loadSource(cache map[string][]string, filename string, sourceDir string) []string {
|
|
if lines, ok := cache[filename]; ok {
|
|
return lines
|
|
}
|
|
|
|
// Try as-is first
|
|
data, err := os.ReadFile(filename)
|
|
if err != nil && sourceDir != "" {
|
|
// Try relative to source directory
|
|
joined := sourceDir + "/" + filename
|
|
data, err = os.ReadFile(joined)
|
|
if err != nil {
|
|
// Try just the basename in source dir
|
|
base := filename
|
|
if idx := strings.LastIndexAny(filename, "/\\"); idx >= 0 {
|
|
base = filename[idx+1:]
|
|
}
|
|
data, err = os.ReadFile(sourceDir + "/" + base)
|
|
}
|
|
}
|
|
if err != nil {
|
|
return []string{"(source not available: " + filename + ")"}
|
|
}
|
|
lines := strings.Split(string(data), "\n")
|
|
cache[filename] = lines
|
|
return lines
|
|
}
|
|
|
|
func termSize() (int, int) {
|
|
type winsize struct {
|
|
Row, Col, Xpixel, Ypixel uint16
|
|
}
|
|
var ws winsize
|
|
_, _, _ = syscall.Syscall(syscall.SYS_IOCTL, uintptr(1),
|
|
uintptr(syscall.TIOCGWINSZ), uintptr(unsafe.Pointer(&ws)))
|
|
return int(ws.Row), int(ws.Col)
|
|
}
|
|
|
|
// readDebugKey reads a key in raw mode for the debugger.
|
|
// Returns ASCII for normal keys, 0xF5-0xFB for F5-F11.
|
|
func readDebugKey() int {
|
|
// Temporarily set raw mode for key reading
|
|
fd := int(os.Stdin.Fd())
|
|
var t syscall.Termios
|
|
syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), 0x5401, uintptr(unsafe.Pointer(&t)), 0, 0, 0)
|
|
raw := t
|
|
raw.Lflag &^= syscall.ICANON | syscall.ECHO
|
|
raw.Cc[syscall.VMIN] = 1
|
|
raw.Cc[syscall.VTIME] = 0
|
|
syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), 0x5402, uintptr(unsafe.Pointer(&raw)), 0, 0, 0)
|
|
defer syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), 0x5402, uintptr(unsafe.Pointer(&t)), 0, 0, 0)
|
|
|
|
buf := make([]byte, 8)
|
|
n, _ := syscall.Read(fd, buf)
|
|
if n == 0 {
|
|
return 0
|
|
}
|
|
|
|
// ESC sequence
|
|
if buf[0] == 0x1B {
|
|
if n == 1 {
|
|
return 0x1B // bare ESC
|
|
}
|
|
if n >= 3 && buf[1] == '[' {
|
|
// Arrow keys: ESC [ A/B/C/D
|
|
switch buf[2] {
|
|
case 'A':
|
|
return 0xE0 // Up
|
|
case 'B':
|
|
return 0xE1 // Down
|
|
case 'C':
|
|
return 0xE2 // Right
|
|
case 'D':
|
|
return 0xE3 // Left
|
|
}
|
|
// F5-F11: ESC [ 1 5 ~ through ESC [ 2 4 ~
|
|
if n >= 4 && buf[n-1] == '~' {
|
|
code := string(buf[2 : n-1])
|
|
switch code {
|
|
case "15":
|
|
return 0xF5 // F5
|
|
case "17":
|
|
return 0xF6 // F6
|
|
case "18":
|
|
return 0xF7 // F7
|
|
case "19":
|
|
return 0xF8 // F8
|
|
case "20":
|
|
return 0xF9 // F9
|
|
case "21":
|
|
return 0xFA // F10
|
|
case "23":
|
|
return 0xFB // F11
|
|
case "24":
|
|
return 0xFC // F12
|
|
}
|
|
}
|
|
}
|
|
return 0 // ignore unknown ESC sequences (don't quit)
|
|
}
|
|
|
|
return int(buf[0])
|
|
}
|