Files
five/hbrtl/tbrowse.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

728 lines
17 KiB
Go

// Copyright (c) 2026 Charles KWON OhJun (charleskwonohjun@gmail.com)
// All rights reserved.
// TBrowse class implementation in Go.
// Ported from Harbour src/rtl/tbrowse.prg (2719 lines).
// This is the core subset needed for dbEdit functionality.
//
// Harbour TBrowse behavior:
// - Cursor row moves within visible area (nRowPos: 1..nRowCount)
// - When at bottom row, down() scrolls data up
// - When at top row, up() scrolls data down
// - Columns scroll horizontally when current column goes off-screen
// - stabilize() redraws the entire visible area
// - forceStable() loops until stable
package hbrtl
import (
"five/hbrt"
"fmt"
"strings"
)
// TBrowse Five class registered at init.
var tbrowseClassID uint16
var tbcolumnClassID uint16
func init() {
// Register TBColumn class
tbcolCls := hbrt.NewClassDef("TBCOLUMN")
tbcolCls.AddData("CHEADING", hbrt.MakeString(""))
tbcolCls.AddData("BBLOCK", hbrt.MakeNil())
tbcolCls.AddData("CCOLSEP", hbrt.MakeString(""))
tbcolCls.AddData("CHEADSEP", hbrt.MakeString(""))
tbcolCls.AddData("CFOOTSEP", hbrt.MakeString(""))
tbcolCls.AddData("CFOOTING", hbrt.MakeString(""))
tbcolCls.AddData("NWIDTH", hbrt.MakeInt(0))
tbcolCls.AddData("CPICTURE", hbrt.MakeString(""))
tbcolCls.AddMethod("INIT", tbcolInit)
tbcolumnClassID = tbcolCls.Register()
// Register TBrowse class
cls := hbrt.NewClassDef("TBROWSE")
cls.AddData("NTOP", hbrt.MakeInt(0))
cls.AddData("NLEFT", hbrt.MakeInt(0))
cls.AddData("NBOTTOM", hbrt.MakeInt(24))
cls.AddData("NRIGHT", hbrt.MakeInt(79))
cls.AddData("ACOLUMNS", hbrt.MakeArray(0))
cls.AddData("NCOLPOS", hbrt.MakeInt(1))
cls.AddData("NROWPOS", hbrt.MakeInt(1))
cls.AddData("NROWCOUNT", hbrt.MakeInt(0))
cls.AddData("NCOLOFFSET", hbrt.MakeInt(1))
cls.AddData("BSKIPBLOCK", hbrt.MakeNil())
cls.AddData("BGOTOPBLOCK", hbrt.MakeNil())
cls.AddData("BGOBOTTOMBLOCK", hbrt.MakeNil())
cls.AddData("CHEADSEP", hbrt.MakeString("-"))
cls.AddData("CCOLSEP", hbrt.MakeString(" | "))
cls.AddData("CFOOTSEP", hbrt.MakeString(""))
cls.AddData("CCOLORSPEC", hbrt.MakeString(""))
cls.AddData("LSTABLE", hbrt.MakeBool(false))
cls.AddData("LHITTOP", hbrt.MakeBool(false))
cls.AddData("LHITBOTTOM", hbrt.MakeBool(false))
cls.AddData("LAUTOLITE", hbrt.MakeBool(true))
cls.AddMethod("INIT", tbrowseInit)
cls.AddMethod("ADDCOLUMN", tbrowseAddColumn)
cls.AddMethod("GETCOLUMN", tbrowseGetColumn)
cls.AddMethod("COLCOUNT", tbrowseColCount)
cls.AddMethod("DOWN", tbrowseDown)
cls.AddMethod("UP", tbrowseUp)
cls.AddMethod("PAGEDOWN", tbrowsePageDown)
cls.AddMethod("PAGEUP", tbrowsePageUp)
cls.AddMethod("GOTOP", tbrowseGoTop)
cls.AddMethod("GOBOTTOM", tbrowseGoBottom)
cls.AddMethod("LEFT", tbrowseLeft)
cls.AddMethod("RIGHT", tbrowseRight)
cls.AddMethod("HOME", tbrowseHome)
cls.AddMethod("END", tbrowseEnd)
cls.AddMethod("STABILIZE", tbrowseStabilize)
cls.AddMethod("FORCESTABLE", tbrowseForceStable)
cls.AddMethod("REFRESHALL", tbrowseRefreshAll)
cls.AddMethod("REFRESHCURRENT", tbrowseRefreshCurrent)
cls.AddMethod("HILITE", tbrowseHiLite)
cls.AddMethod("DEHILITE", tbrowseDeHilite)
tbrowseClassID = cls.Register()
}
// --- Helper: get object fields ---
func getObjInt(obj hbrt.Value, field string) int {
arr := obj.AsArray()
cls := hbrt.GetClass(arr.Class)
if idx := cls.FieldIndex(field); idx >= 0 {
return int(arr.Items[idx].AsNumInt())
}
return 0
}
func setObjInt(obj hbrt.Value, field string, val int) {
arr := obj.AsArray()
cls := hbrt.GetClass(arr.Class)
if idx := cls.FieldIndex(field); idx >= 0 {
arr.Items[idx] = hbrt.MakeInt(val)
}
}
func setObjBool(obj hbrt.Value, field string, val bool) {
arr := obj.AsArray()
cls := hbrt.GetClass(arr.Class)
if idx := cls.FieldIndex(field); idx >= 0 {
arr.Items[idx] = hbrt.MakeBool(val)
}
}
func getObjBlock(obj hbrt.Value, field string) *hbrt.HbBlock {
arr := obj.AsArray()
cls := hbrt.GetClass(arr.Class)
if idx := cls.FieldIndex(field); idx >= 0 {
return arr.Items[idx].AsBlock()
}
return nil
}
func getObjArray(obj hbrt.Value, field string) *hbrt.HbArray {
arr := obj.AsArray()
cls := hbrt.GetClass(arr.Class)
if idx := cls.FieldIndex(field); idx >= 0 {
return arr.Items[idx].AsArray()
}
return nil
}
func getObjString(obj hbrt.Value, field string) string {
arr := obj.AsArray()
cls := hbrt.GetClass(arr.Class)
if idx := cls.FieldIndex(field); idx >= 0 {
return arr.Items[idx].AsString()
}
return ""
}
// --- TBColumn methods ---
func tbcolInit(t *hbrt.Thread) {
t.Frame(2, 0)
defer t.EndProc()
self := t.GetSelf()
heading := t.Local(1)
block := t.Local(2)
arr := self.AsArray()
cls := hbrt.GetClass(arr.Class)
if idx := cls.FieldIndex("CHEADING"); idx >= 0 {
arr.Items[idx] = heading
}
if idx := cls.FieldIndex("BBLOCK"); idx >= 0 {
arr.Items[idx] = block
}
t.PushSelf()
t.RetValue()
}
// --- TBrowse methods ---
func tbrowseInit(t *hbrt.Thread) {
t.Frame(4, 0)
defer t.EndProc()
self := t.GetSelf()
setObjInt(self, "NTOP", int(t.Local(1).AsNumInt()))
setObjInt(self, "NLEFT", int(t.Local(2).AsNumInt()))
setObjInt(self, "NBOTTOM", int(t.Local(3).AsNumInt()))
setObjInt(self, "NRIGHT", int(t.Local(4).AsNumInt()))
nRowCount := int(t.Local(3).AsNumInt()) - int(t.Local(1).AsNumInt()) - 1
if nRowCount < 1 {
nRowCount = 1
}
setObjInt(self, "NROWCOUNT", nRowCount)
t.PushSelf()
t.RetValue()
}
func tbrowseAddColumn(t *hbrt.Thread) {
t.Frame(1, 0)
defer t.EndProc()
self := t.GetSelf()
col := t.Local(1)
cols := getObjArray(self, "ACOLUMNS")
if cols != nil {
cols.Items = append(cols.Items, col)
}
t.PushSelf()
t.RetValue()
}
func tbrowseGetColumn(t *hbrt.Thread) {
t.Frame(1, 0)
defer t.EndProc()
self := t.GetSelf()
n := int(t.Local(1).AsNumInt())
cols := getObjArray(self, "ACOLUMNS")
if cols != nil && n >= 1 && n <= len(cols.Items) {
t.PushValue(cols.Items[n-1])
} else {
t.PushNil()
}
t.RetValue()
}
func tbrowseColCount(t *hbrt.Thread) {
t.Frame(0, 0)
defer t.EndProc()
self := t.GetSelf()
cols := getObjArray(self, "ACOLUMNS")
if cols != nil {
t.RetInt(int64(len(cols.Items)))
} else {
t.RetInt(0)
}
}
// --- Navigation: Harbour TBrowse behavior ---
func callSkipBlock(t *hbrt.Thread, self hbrt.Value, nRecs int) int {
blk := getObjBlock(self, "BSKIPBLOCK")
if blk == nil {
return 0
}
t.PushValue(hbrt.MakeInt(nRecs))
t.PendingParams2(1)
blk.Fn(t)
return int(t.GetRetValue().AsNumInt())
}
func tbrowseDown(t *hbrt.Thread) {
t.Frame(0, 0)
defer t.EndProc()
self := t.GetSelf()
rowPos := getObjInt(self, "NROWPOS")
rowCount := getObjInt(self, "NROWCOUNT")
nSkipped := callSkipBlock(t, self, 1)
if nSkipped > 0 {
if rowPos < rowCount {
// Cursor moves down within screen
setObjInt(self, "NROWPOS", rowPos+1)
}
// else: cursor at bottom, data scrolls (rowPos stays)
} else {
setObjBool(self, "LHITBOTTOM", true)
}
setObjBool(self, "LSTABLE", false)
t.PushSelf()
t.RetValue()
}
func tbrowseUp(t *hbrt.Thread) {
t.Frame(0, 0)
defer t.EndProc()
self := t.GetSelf()
rowPos := getObjInt(self, "NROWPOS")
nSkipped := callSkipBlock(t, self, -1)
if nSkipped < 0 {
if rowPos > 1 {
// Cursor moves up within screen
setObjInt(self, "NROWPOS", rowPos-1)
}
// else: cursor at top, data scrolls down (rowPos stays at 1)
} else {
setObjBool(self, "LHITTOP", true)
}
setObjBool(self, "LSTABLE", false)
t.PushSelf()
t.RetValue()
}
func tbrowsePageDown(t *hbrt.Thread) {
t.Frame(0, 0)
defer t.EndProc()
self := t.GetSelf()
rowCount := getObjInt(self, "NROWCOUNT")
rowPos := getObjInt(self, "NROWPOS")
// Try to skip a full page from current position
nSkipped := callSkipBlock(t, self, rowCount)
if nSkipped <= 0 {
// Already at bottom: move cursor to last data row
setObjBool(self, "LHITBOTTOM", true)
// Find last data row: skip forward from first visible row
callSkipBlock(t, self, -(rowPos - 1)) // go to first visible
count := 1
for {
s := callSkipBlock(t, self, 1)
if s <= 0 {
break
}
count++
if count >= rowCount {
break
}
}
// Now skip back to put cursor at last row
callSkipBlock(t, self, -(count - 1)) // back to first visible
callSkipBlock(t, self, count-1) // forward to last data
setObjInt(self, "NROWPOS", count)
} else if nSkipped < rowCount {
// Partial page: cursor stays at row 1, but we didn't move a full page
// Skip back so screen doesn't shift too much — just move cursor down
callSkipBlock(t, self, -nSkipped) // undo the skip
// Instead: move cursor to last row
callSkipBlock(t, self, -(rowPos - 1)) // go to first visible
count := 1
for {
s := callSkipBlock(t, self, 1)
if s <= 0 {
break
}
count++
if count >= rowCount {
break
}
}
callSkipBlock(t, self, -(count - 1))
callSkipBlock(t, self, count-1)
setObjInt(self, "NROWPOS", count)
}
// else: full page skip succeeded, rowPos stays
setObjBool(self, "LSTABLE", false)
t.PushSelf()
t.RetValue()
}
func tbrowsePageUp(t *hbrt.Thread) {
t.Frame(0, 0)
defer t.EndProc()
self := t.GetSelf()
rowCount := getObjInt(self, "NROWCOUNT")
nSkipped := callSkipBlock(t, self, -rowCount)
if nSkipped >= 0 {
setObjBool(self, "LHITTOP", true)
}
setObjBool(self, "LSTABLE", false)
t.PushSelf()
t.RetValue()
}
func tbrowseGoTop(t *hbrt.Thread) {
t.Frame(0, 0)
defer t.EndProc()
self := t.GetSelf()
blk := getObjBlock(self, "BGOTOPBLOCK")
if blk != nil {
t.PendingParams2(0)
blk.Fn(t)
}
setObjInt(self, "NROWPOS", 1)
setObjBool(self, "LSTABLE", false)
t.PushSelf()
t.RetValue()
}
func tbrowseGoBottom(t *hbrt.Thread) {
t.Frame(0, 0)
defer t.EndProc()
self := t.GetSelf()
blk := getObjBlock(self, "BGOBOTTOMBLOCK")
if blk != nil {
t.PendingParams2(0)
blk.Fn(t)
}
setObjInt(self, "NROWPOS", getObjInt(self, "NROWCOUNT"))
setObjBool(self, "LSTABLE", false)
t.PushSelf()
t.RetValue()
}
func tbrowseLeft(t *hbrt.Thread) {
t.Frame(0, 0)
defer t.EndProc()
self := t.GetSelf()
pos := getObjInt(self, "NCOLPOS")
if pos > 1 {
setObjInt(self, "NCOLPOS", pos-1)
off := getObjInt(self, "NCOLOFFSET")
if pos-1 < off {
setObjInt(self, "NCOLOFFSET", pos-1)
}
}
setObjBool(self, "LSTABLE", false)
t.PushSelf()
t.RetValue()
}
func tbrowseRight(t *hbrt.Thread) {
t.Frame(0, 0)
defer t.EndProc()
self := t.GetSelf()
pos := getObjInt(self, "NCOLPOS")
cols := getObjArray(self, "ACOLUMNS")
if cols != nil && pos < len(cols.Items) {
setObjInt(self, "NCOLPOS", pos+1)
}
setObjBool(self, "LSTABLE", false)
t.PushSelf()
t.RetValue()
}
func tbrowseHome(t *hbrt.Thread) {
t.Frame(0, 0)
defer t.EndProc()
self := t.GetSelf()
setObjInt(self, "NCOLPOS", 1)
setObjInt(self, "NCOLOFFSET", 1)
setObjBool(self, "LSTABLE", false)
t.PushSelf()
t.RetValue()
}
func tbrowseEnd(t *hbrt.Thread) {
t.Frame(0, 0)
defer t.EndProc()
self := t.GetSelf()
cols := getObjArray(self, "ACOLUMNS")
if cols != nil {
setObjInt(self, "NCOLPOS", len(cols.Items))
}
setObjBool(self, "LSTABLE", false)
t.PushSelf()
t.RetValue()
}
// --- Display ---
func tbrowseStabilize(t *hbrt.Thread) {
t.Frame(0, 0)
defer t.EndProc()
self := t.GetSelf()
nTop := getObjInt(self, "NTOP")
nLeft := getObjInt(self, "NLEFT")
nRight := getObjInt(self, "NRIGHT")
nRowCount := getObjInt(self, "NROWCOUNT")
nRowPos := getObjInt(self, "NROWPOS")
nColPos := getObjInt(self, "NCOLPOS")
nColOffset := getObjInt(self, "NCOLOFFSET")
cols := getObjArray(self, "ACOLUMNS")
headSep := getObjString(self, "CHEADSEP")
colSep := getObjString(self, "CCOLSEP")
screenWidth := nRight - nLeft + 1
if cols == nil || len(cols.Items) == 0 {
t.PushBool(true)
t.RetValue()
return
}
// Adjust colOffset for visibility
if nColPos < nColOffset {
nColOffset = nColPos
}
for {
used := 0
visible := false
for i := nColOffset - 1; i < len(cols.Items); i++ {
w := tbColWidth(cols.Items[i])
if i > nColOffset-1 && len(colSep) > 0 {
used += len(colSep)
}
if used+w > screenWidth {
break
}
used += w
if i == nColPos-1 {
visible = true
break
}
}
if visible || nColOffset >= nColPos {
break
}
nColOffset++
}
setObjInt(self, "NCOLOFFSET", nColOffset)
// Draw header
fmt.Printf("\033[%d;%dH", nTop+1, nLeft+1)
x := 0
for i := nColOffset - 1; i < len(cols.Items) && x < screenWidth; i++ {
w := tbColWidth(cols.Items[i])
if i > nColOffset-1 && len(colSep) > 0 {
if x+len(colSep) >= screenWidth {
break
}
fmt.Printf("\033[7m%s\033[0m", colSep)
x += len(colSep)
}
if x+w > screenWidth {
w = screenWidth - x
}
heading := tbColHeading(cols.Items[i])
cell := padRightS(heading, w)
if i == nColPos-1 {
fmt.Printf("\033[1;7m%s\033[0m", cell)
} else {
fmt.Printf("\033[7m%s\033[0m", cell)
}
x += w
}
for x < screenWidth {
fmt.Printf("\033[7m \033[0m")
x++
}
// Header separator
hasHeadSep := len(headSep) > 0
if hasHeadSep {
fmt.Printf("\033[%d;%dH\033[36m%s\033[0m", nTop+2, nLeft+1, padRightS(strings.Repeat(string(headSep[0]), screenWidth), screenWidth))
}
// Skip back to first visible row from current position
actualBack := callSkipBlock(t, self, -(nRowPos - 1))
// actualBack is negative (e.g., -4 means we went back 4)
actualFirstToPos := -(actualBack) // how many rows from first to curPos
// If we couldn't go back enough, adjust rowPos
if actualFirstToPos < nRowPos-1 {
nRowPos = actualFirstToPos + 1
setObjInt(self, "NROWPOS", nRowPos)
}
// Draw data rows
dataStartRow := nTop + 2
if hasHeadSep {
dataStartRow = nTop + 3
}
actualDataRows := 0 // count of real data rows drawn
pastEOF := false
for r := 1; r <= nRowCount; r++ {
fmt.Printf("\033[%d;%dH", dataStartRow+r-1, nLeft+1)
x = 0
if pastEOF {
fmt.Printf("%-*s", screenWidth, "~")
} else {
actualDataRows = r
for i := nColOffset - 1; i < len(cols.Items) && x < screenWidth; i++ {
w := tbColWidth(cols.Items[i])
if i > nColOffset-1 && len(colSep) > 0 {
if x+len(colSep) >= screenWidth {
break
}
fmt.Print(colSep)
x += len(colSep)
}
if x+w > screenWidth {
w = screenWidth - x
}
val := tbEvalBlock(t, cols.Items[i])
cell := padRightS(val, w)
if r == nRowPos && i == nColPos-1 {
fmt.Printf("\033[7m%s\033[0m", cell)
} else if r == nRowPos {
fmt.Printf("\033[47;30m%s\033[0m", cell)
} else {
fmt.Print(cell)
}
x += w
}
for x < screenWidth {
if r == nRowPos {
fmt.Printf("\033[47;30m \033[0m")
} else {
fmt.Print(" ")
}
x++
}
if r < nRowCount {
skipped := callSkipBlock(t, self, 1)
if skipped == 0 {
pastEOF = true
}
}
}
}
// Clamp rowPos to actual data rows
if nRowPos > actualDataRows && actualDataRows > 0 {
nRowPos = actualDataRows
setObjInt(self, "NROWPOS", nRowPos)
}
// Restore to current row position: skip back from wherever we are to first, then forward to rowPos
if !pastEOF {
callSkipBlock(t, self, -(nRowCount - nRowPos))
} else {
// We're at last data record. Skip back to first visible, then forward to rowPos.
callSkipBlock(t, self, -(actualDataRows - 1))
if nRowPos > 1 {
callSkipBlock(t, self, nRowPos-1)
}
}
setObjBool(self, "LSTABLE", true)
setObjBool(self, "LHITTOP", false)
setObjBool(self, "LHITBOTTOM", false)
t.PushBool(true)
t.RetValue()
}
func tbrowseForceStable(t *hbrt.Thread) {
t.Frame(0, 0)
defer t.EndProc()
// Just call stabilize once (simplified)
self := t.GetSelf()
_ = self
tbrowseStabilize(t)
// Discard stabilize result, push self
t.Pop()
t.PushSelf()
t.RetValue()
}
func tbrowseRefreshAll(t *hbrt.Thread) {
t.Frame(0, 0)
defer t.EndProc()
self := t.GetSelf()
setObjBool(self, "LSTABLE", false)
t.PushSelf()
t.RetValue()
}
func tbrowseRefreshCurrent(t *hbrt.Thread) {
t.Frame(0, 0)
defer t.EndProc()
self := t.GetSelf()
setObjBool(self, "LSTABLE", false)
t.PushSelf()
t.RetValue()
}
func tbrowseHiLite(t *hbrt.Thread) {
t.Frame(0, 0)
defer t.EndProc()
t.PushSelf()
t.RetValue()
}
func tbrowseDeHilite(t *hbrt.Thread) {
t.Frame(0, 0)
defer t.EndProc()
t.PushSelf()
t.RetValue()
}
// --- Helpers ---
func tbColWidth(colVal hbrt.Value) int {
colArr := colVal.AsArray()
if colArr == nil {
return 10
}
cls := hbrt.GetClass(colArr.Class)
if cls == nil {
return 10
}
if idx := cls.FieldIndex("NWIDTH"); idx >= 0 {
w := int(colArr.Items[idx].AsNumInt())
if w > 0 {
return w
}
}
if idx := cls.FieldIndex("CHEADING"); idx >= 0 {
w := len(colArr.Items[idx].AsString())
if w < 10 {
w = 10
}
return w
}
return 10
}
func tbColHeading(colVal hbrt.Value) string {
colArr := colVal.AsArray()
if colArr == nil {
return ""
}
cls := hbrt.GetClass(colArr.Class)
if cls == nil {
return ""
}
if idx := cls.FieldIndex("CHEADING"); idx >= 0 {
return colArr.Items[idx].AsString()
}
return ""
}
func tbEvalBlock(t *hbrt.Thread, colVal hbrt.Value) string {
colArr := colVal.AsArray()
if colArr == nil {
return ""
}
cls := hbrt.GetClass(colArr.Class)
if cls == nil {
return ""
}
if idx := cls.FieldIndex("BBLOCK"); idx >= 0 {
blk := colArr.Items[idx].AsBlock()
if blk != nil {
t.PendingParams2(0)
blk.Fn(t)
v := t.GetRetValue()
return strings.TrimRight(valueToDisplay(v), " ")
}
}
return ""
}
func padRightS(s string, w int) string {
if len(s) >= w {
return s[:w]
}
return s + strings.Repeat(" ", w-len(s))
}