Files
five/hbrt/debugtui.go
Charles KWON OhJun 59568f3301 Five v0.9 — Harbour + Go fusion language
- 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>
2026-03-31 09:41:50 +09:00

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])
}