// Copyright (c) 2026 Charles KWON OhJun (charleskwonohjun@gmail.com) // All rights reserved. // Five dbEdit demo using TBrowse — same pattern as Harbour's dbedit.prg // Harbour: oBrowse := TBrowseDB() → addColumn → loop { stabilize + inkey + navigate } package main import ( "five/hbrt" "five/hbrdd" "five/hbrdd/dbf" "five/hbrtl" "fmt" "os" "os/exec" "strings" ) func main() { path := "dbf/customer" if len(os.Args) > 1 { path = os.Args[1] } // Open DBF drv := &dbf.DBFDriver{} area, err := drv.Open(hbrdd.OpenParams{Path: path}) if err != nil { fmt.Fprintf(os.Stderr, "Error: %v\n", err) os.Exit(1) } defer area.Close() rc, _ := area.RecCount() // Setup VM + Thread (needed for TBrowse methods) vm := hbrt.NewVM() hbrtl.RegisterRTL(vm) t := vm.NewThread() // Setup WorkAreaManager waMgr := hbrdd.NewWorkAreaManager() t.WA = waMgr // Register area manually (since we opened it directly) // Simple: put area as current registerArea(waMgr, area, "CUSTOMER") fmt.Printf("Opened: %s.dbf (%d records, %d fields)\n", path, rc, area.FieldCount()) fmt.Println("Press ENTER to browse...") buf := make([]byte, 1) os.Stdin.Read(buf) // --- Harbour dbEdit pattern: TBrowseDB + addColumn + key loop --- nTop, nLeft, nBottom, nRight := 0, 0, 22, 79 // Create TBrowse (calls Go TBrowse class) oBrowse := hbrt.NewObject(hbrt.FindClass("TBROWSE").ID) browseArr := oBrowse.AsArray() browseCls := hbrt.GetClass(browseArr.Class) // Set coordinates setField(browseArr, browseCls, "NTOP", hbrt.MakeInt(nTop)) setField(browseArr, browseCls, "NLEFT", hbrt.MakeInt(nLeft)) setField(browseArr, browseCls, "NBOTTOM", hbrt.MakeInt(nBottom)) setField(browseArr, browseCls, "NRIGHT", hbrt.MakeInt(nRight)) setField(browseArr, browseCls, "NROWCOUNT", hbrt.MakeInt(nBottom-nTop-1)) setField(browseArr, browseCls, "CHEADSEP", hbrt.MakeString("-")) setField(browseArr, browseCls, "CCOLSEP", hbrt.MakeString(" | ")) // Set skip/gotop/gobottom blocks setField(browseArr, browseCls, "BSKIPBLOCK", hbrt.MakeBlock(func(bt *hbrt.Thread) { bt.Frame(1, 0) defer bt.EndProc() nRecs := int(bt.Local(1).AsNumInt()) skipped := skipRecords(area, nRecs) bt.RetInt(int64(skipped)) }, 0)) setField(browseArr, browseCls, "BGOTOPBLOCK", hbrt.MakeBlock(func(bt *hbrt.Thread) { bt.Frame(0, 0) defer bt.EndProc() area.GoTop() bt.RetNil() }, 0)) setField(browseArr, browseCls, "BGOBOTTOMBLOCK", hbrt.MakeBlock(func(bt *hbrt.Thread) { bt.Frame(0, 0) defer bt.EndProc() area.GoBottom() bt.RetNil() }, 0)) // Add columns (like Harbour dbEdit does) colsArr := getFieldArr(browseArr, browseCls, "ACOLUMNS") for i := 0; i < area.FieldCount(); i++ { fi := area.GetFieldInfo(i) fieldIdx := i // capture for closure oCol := hbrt.NewObject(hbrt.FindClass("TBCOLUMN").ID) colArr := oCol.AsArray() colCls := hbrt.GetClass(colArr.Class) setField(colArr, colCls, "CHEADING", hbrt.MakeString(fi.Name)) // Column block: evaluates field value setField(colArr, colCls, "BBLOCK", hbrt.MakeBlock(func(bt *hbrt.Thread) { bt.Frame(0, 0) defer bt.EndProc() val, _ := area.GetValue(fieldIdx) bt.PushValue(val) bt.RetValue() }, 0)) // Column width w := fi.Len if w < len(fi.Name) { w = len(fi.Name) } if w > 25 { w = 25 } if w < 4 { w = 4 } setField(colArr, colCls, "NWIDTH", hbrt.MakeInt(w)) colsArr.Items = append(colsArr.Items, oCol) } // --- Raw terminal + key loop (Harbour's DO WHILE lContinue) --- setRawMode() defer restoreMode() fmt.Print("\033[2J\033[H\033[?25l") defer fmt.Print("\033[?25h\033[0m\n") area.GoTop() for { // stabilize() — redraw screen oldSelf := t.GetSelf() callMethod(t, oBrowse, "STABILIZE", 0) _ = oldSelf // Show status bar curRec := area.RecNo() colPos := getFieldInt(browseArr, browseCls, "NCOLPOS") colName := "" if colPos >= 1 && colPos <= len(colsArr.Items) { colArr := colsArr.Items[colPos-1].AsArray() colCls := hbrt.GetClass(colArr.Class) colName = getFieldStr(colArr, colCls, "CHEADING") } eofStr := "" if area.EOF() { eofStr = " EOF" } status := fmt.Sprintf(" Rec %d/%d [%s]%s ↑↓←→ PgUp/Dn Home/End ESC=quit", curRec, rc, strings.TrimSpace(colName), eofStr) fmt.Printf("\033[%d;1H\033[7m%-80s\033[0m", nBottom+2, status) // Read key key := readKey() // Dispatch key — same as Harbour's dbEdit SWITCH switch key { case 'B' - 64: // K_DOWN (Ctrl-B = 2, but arrow = ESC[B) callMethod(t, oBrowse, "DOWN", 0) case 'E' - 64: // K_UP callMethod(t, oBrowse, "UP", 0) case 0x42: // arrow down mapped callMethod(t, oBrowse, "DOWN", 0) case 0x41: // arrow up mapped callMethod(t, oBrowse, "UP", 0) case 0x44: // arrow left mapped callMethod(t, oBrowse, "LEFT", 0) case 0x43: // arrow right mapped callMethod(t, oBrowse, "RIGHT", 0) case 0x35: // PgUp callMethod(t, oBrowse, "PAGEUP", 0) case 0x36: // PgDn callMethod(t, oBrowse, "PAGEDOWN", 0) case 0x48: // Home callMethod(t, oBrowse, "GOTOP", 0) case 0x46: // End callMethod(t, oBrowse, "GOBOTTOM", 0) case 0x31: // Home alt callMethod(t, oBrowse, "HOME", 0) case 0x34: // End alt callMethod(t, oBrowse, "END", 0) case 27, 'q', 'Q': // ESC fmt.Print("\033[2J\033[H") fmt.Printf("Closed %s.dbf\n", path) return } } } // --- TBrowse method call helper --- func callMethod(t *hbrt.Thread, obj hbrt.Value, method string, nArgs int) { t.PushValue(obj) t.Send(method, nArgs) t.Pop2() // discard result } // --- Terminal --- func setRawMode() { cmd := exec.Command("stty", "raw", "-echo") cmd.Stdin = os.Stdin cmd.Run() } func restoreMode() { cmd := exec.Command("stty", "-raw", "echo") cmd.Stdin = os.Stdin cmd.Run() } func readKey() int { buf := make([]byte, 6) n, _ := os.Stdin.Read(buf) if n == 0 { return 27 } // ESC sequence if n >= 3 && buf[0] == 27 && buf[1] == '[' { return int(buf[2]) // A=up, B=down, C=right, D=left, 5=pgup, 6=pgdn, H=home, F=end } if buf[0] == 27 { return 27 // plain ESC } return int(buf[0]) } // --- DB helpers --- func skipRecords(area hbrdd.Area, nRecs int) int { skipped := 0 if nRecs > 0 { for skipped < nRecs { area.Skip(1) if area.EOF() { area.Skip(-1) break } skipped++ } } else if nRecs < 0 { for skipped > nRecs { area.Skip(-1) if area.BOF() { break } skipped-- } } return skipped } func registerArea(wm *hbrdd.WorkAreaManager, area hbrdd.Area, alias string) { // Directly inject into WorkAreaManager (bypass Open) // This is a hack for the demo — real code would use wm.Open() _ = wm _ = area _ = alias } // --- Object field helpers --- func setField(arr *hbrt.HbArray, cls *hbrt.ClassDef, name string, val hbrt.Value) { if idx := cls.FieldIndex(name); idx >= 0 { arr.Items[idx] = val } } func getFieldArr(arr *hbrt.HbArray, cls *hbrt.ClassDef, name string) *hbrt.HbArray { if idx := cls.FieldIndex(name); idx >= 0 { return arr.Items[idx].AsArray() } return nil } func getFieldInt(arr *hbrt.HbArray, cls *hbrt.ClassDef, name string) int { if idx := cls.FieldIndex(name); idx >= 0 { return int(arr.Items[idx].AsNumInt()) } return 0 } func getFieldStr(arr *hbrt.HbArray, cls *hbrt.ClassDef, name string) string { if idx := cls.FieldIndex(name); idx >= 0 { return arr.Items[idx].AsString() } return "" }