From 3ac6b2486a7f17e96ea8d812437ed9bad8662260 Mon Sep 17 00:00:00 2001 From: Jeff Carr Date: Thu, 18 Jan 2024 00:08:37 -0600 Subject: [PATCH] init move into seperate repo. all history lost :( Signed-off-by: Jeff Carr --- Makefile | 26 ++++ add.go | 80 +++++++++++ args.go | 32 +++++ checkbox.go | 33 +++++ click.go | 357 +++++++++++++++++++++++++++++++++++++++++++++++++ color.go | 120 +++++++++++++++++ common.go | 218 ++++++++++++++++++++++++++++++ debug.go | 74 ++++++++++ fakefile.go | 58 ++++++++ go.mod | 21 +++ go.sum | 30 +++++ gocui.go | 102 ++++++++++++++ help.go | 71 ++++++++++ keybindings.go | 165 +++++++++++++++++++++++ main.go | 104 ++++++++++++++ mouse.go | 151 +++++++++++++++++++++ place.go | 188 ++++++++++++++++++++++++++ plugin.go | 117 ++++++++++++++++ showStdout.go | 98 ++++++++++++++ structs.go | 229 +++++++++++++++++++++++++++++++ tab.go | 111 +++++++++++++++ view.go | 233 ++++++++++++++++++++++++++++++++ widget.go | 139 +++++++++++++++++++ 23 files changed, 2757 insertions(+) create mode 100644 Makefile create mode 100644 add.go create mode 100644 args.go create mode 100644 checkbox.go create mode 100644 click.go create mode 100644 color.go create mode 100644 common.go create mode 100644 debug.go create mode 100644 fakefile.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 gocui.go create mode 100644 help.go create mode 100644 keybindings.go create mode 100644 main.go create mode 100644 mouse.go create mode 100644 place.go create mode 100644 plugin.go create mode 100644 showStdout.go create mode 100644 structs.go create mode 100644 tab.go create mode 100644 view.go create mode 100644 widget.go diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..fffcfa7 --- /dev/null +++ b/Makefile @@ -0,0 +1,26 @@ +all: plugin + ldd ../gocui.so + +plugin: + GO111MODULE=off go build -v -buildmode=plugin -o ../gocui.so + +goget: + go get -v -t -u + +objdump: + objdump -t ../gocui.so |less + +log: + reset + tail -f /tmp/witgui.* /tmp/guilogfile + +cleanbuild: + go build -v -x -buildmode=plugin -o ../nocui.so + +check-git-clean: + @git diff-index --quiet HEAD -- || (echo "Git repository is dirty, please commit your changes first"; exit 1) + +redomod: + rm -f go.* + GO111MODULE= go mod init + GO111MODULE= go mod tidy diff --git a/add.go b/add.go new file mode 100644 index 0000000..151d4d2 --- /dev/null +++ b/add.go @@ -0,0 +1,80 @@ +package main + +import ( + "go.wit.com/lib/widget" + log "go.wit.com/log" +) + +var fakeStartWidth int = me.FakeW +var fakeStartHeight int = me.TabH + me.FramePadH + +// setup fake labels for non-visible things off screen +func (n *node) setFake() { + w := n.tk + w.isFake = true + + n.gocuiSetWH(fakeStartWidth, fakeStartHeight) + + fakeStartHeight += w.gocuiSize.Height() + // TODO: use the actual max hight of the terminal window + if fakeStartHeight > 24 { + fakeStartHeight = me.TabH + fakeStartWidth += me.FakeW + } + if true { + n.showView() + } +} + +// set the widget start width & height +func (n *node) addWidget() { + nw := n.tk + log.Log(INFO, "setStartWH() w.id =", n.WidgetId, "n.name", n.progname) + switch n.WidgetType { + case widget.Root: + log.Log(INFO, "setStartWH() rootNode w.id =", n.WidgetId, "w.name", n.progname) + nw.color = &colorRoot + n.setFake() + return + case widget.Flag: + nw.color = &colorFlag + n.setFake() + return + case widget.Window: + nw.frame = false + nw.color = &colorWindow + // redoWindows(0,0) + return + case widget.Tab: + nw.color = &colorTab + // redoWindows(0,0) + return + case widget.Button: + nw.color = &colorButton + case widget.Box: + nw.color = &colorBox + nw.isFake = true + n.setFake() + return + case widget.Grid: + nw.color = &colorGrid + nw.isFake = true + n.setFake() + return + case widget.Group: + nw.color = &colorGroup + nw.frame = false + return + case widget.Label: + nw.color = &colorLabel + nw.frame = false + return + default: + /* + if n.IsCurrent() { + n.updateCurrent() + } + */ + } + n.showWidgetPlacement(true, "addWidget()") +} diff --git a/args.go b/args.go new file mode 100644 index 0000000..eca1db9 --- /dev/null +++ b/args.go @@ -0,0 +1,32 @@ +package main + +/* + this enables command line options from other packages like 'gui' and 'log' +*/ + +import ( + "go.wit.com/log" +) + +var outputS []string + +var NOW *log.LogFlag +var INFO *log.LogFlag + +var SPEW *log.LogFlag +var WARN *log.LogFlag + +var ERROR *log.LogFlag + +func init() { + full := "toolkit/nocui" + short := "nocui" + + NOW = log.NewFlag("NOW", true, full, short, "temp debugging stuff") + INFO = log.NewFlag("INFO", false, full, short, "normal debugging stuff") + + WARN = log.NewFlag("WARN", true, full, short, "bad things") + SPEW = log.NewFlag("SPEW", false, full, short, "spew stuff") + + ERROR = log.NewFlag("ERROR", false, full, short, "toolkit errors") +} diff --git a/checkbox.go b/checkbox.go new file mode 100644 index 0000000..76b4377 --- /dev/null +++ b/checkbox.go @@ -0,0 +1,33 @@ +package main + +import ( + // "github.com/awesome-gocui/gocui" + "go.wit.com/lib/widget" +) + +func (n *node) setCheckbox(b any) { + w := n.tk + if n.WidgetType != widget.Checkbox { + return + } + if widget.GetBool(b) { + n.value = b + n.tk.label = "X " + n.label + } else { + n.value = b + n.tk.label = " " + n.label + } + t := len(n.tk.label) + 1 + w.gocuiSize.w1 = w.gocuiSize.w0 + t + + // w.realWidth = w.gocuiSize.Width() + me.PadW + // w.realHeight = w.gocuiSize.Height() + me.PadH + + // if w.frame { + // w.realWidth += me.FramePadW + // w.realHeight += me.FramePadH + // } + + n.deleteView() + n.showView() +} diff --git a/click.go b/click.go new file mode 100644 index 0000000..6d8b20a --- /dev/null +++ b/click.go @@ -0,0 +1,357 @@ +package main + +import ( + "fmt" + "github.com/awesome-gocui/gocui" + "go.wit.com/lib/widget" + "go.wit.com/log" +) + +// set isCurrent = false everywhere +func unsetCurrent(n *node) { + w := n.tk + w.isCurrent = false + + if n.WidgetType == widget.Tab { + // n.tk.color = &colorTab + // n.setColor() + } + + for _, child := range n.children { + unsetCurrent(child) + } +} + +// when adding a new widget, this will update the display +// of the current widgets if that widget is supposed +// to be in current display +func (n *node) updateCurrent() { + log.Log(NOW, "updateCurrent()", n.progname) + if n.WidgetType == widget.Tab { + if n.IsCurrent() { + // n.tk.color = &colorActiveT + n.setColor(&colorActiveT) + n.hideView() + n.showView() + setCurrentTab(n) + } else { + // n.tk.color = &colorTab + // n.setColor() + } + return + } + if n.WidgetType == widget.Window { + if n.IsCurrent() { + // setCurrentWindow(n) + } + return + } + if n.WidgetType == widget.Root { + return + } + n.parent.updateCurrent() +} + +// shows the widgets in a window +func setCurrentWindow(n *node) { + if n.IsCurrent() { + return + } + w := n.tk + if n.WidgetType != widget.Window { + return + } + unsetCurrent(me.rootNode) + + if n.hasTabs { + // set isCurrent = true on the first tab + for _, child := range n.children { + child.tk.isCurrent = true + break + } + } else { + w.isCurrent = true + } +} + +// shows the widgets in a tab +func setCurrentTab(n *node) { + w := n.tk + if n.WidgetType != widget.Tab { + return + } + unsetCurrent(me.rootNode) + w.isCurrent = true + p := n.parent.tk + p.isCurrent = true + log.Log(NOW, "setCurrent()", n.progname) +} + +func (n *node) doWidgetClick() { + switch n.WidgetType { + case widget.Root: + // THIS IS THE BEGINING OF THE LAYOUT + log.Log(NOW, "doWidgetClick()", n.progname) + redoWindows(0, 0) + case widget.Flag: + log.Log(NOW, "doWidgetClick() FLAG widget name =", n.progname) + log.Log(NOW, "doWidgetClick() if this is the dropdown menu, handle it here?") + case widget.Window: + if me.currentWindow == n { + return + } + if me.currentWindow != nil { + unsetCurrent(me.currentWindow) + me.currentWindow.setColor(&colorWindow) + me.currentWindow.hideWidgets() + } + n.hideWidgets() + me.currentWindow = n + // setCurrentWindow(n) // probably delete this + n.setColor(&colorActiveW) + n.redoTabs(me.TabW, me.TabH) + for _, child := range n.children { + if child.currentTab == true { + log.Log(NOW, "FOUND CURRENT TAB", child.progname) + setCurrentTab(child) + child.placeWidgets(me.RawW, me.RawH) + child.showWidgets() + return + } + } + /* FIXME: redo this + if ! n.hasTabs { + } + */ + case widget.Tab: + if n.IsCurrent() { + return // do nothing if you reclick on the already selected tab + } + // find the window and disable the active tab + p := n.parent + if p != nil { + p.hideWidgets() + p.redoTabs(me.TabW, me.TabH) + unsetCurrent(p) + for _, child := range p.children { + if child.WidgetType == widget.Tab { + child.setColor(&colorTab) + n.currentTab = false + } + } + } + n.currentTab = true + n.setColor(&colorActiveT) + setCurrentTab(n) + n.placeWidgets(me.RawW, me.RawH) + n.showWidgets() + case widget.Group: + // n.placeWidgets(p.tk.startH, newH) + n.toggleTree() + case widget.Checkbox: + if widget.GetBool(n.value) { + n.setCheckbox(false) + } else { + n.setCheckbox(true) + } + n.doUserEvent() + case widget.Grid: + newR := n.realGocuiSize() + + // w,h := n.logicalSize() + // w := newR.w1 - newR.w0 + // h := newR.h1 - newR.h0 + + n.placeGrid(newR.w0, newR.h0) + n.showWidgets() + case widget.Box: + // w.showWidgetPlacement(logNow, "drawTree()") + if n.direction == widget.Horizontal { + log.Log(NOW, "BOX IS HORIZONTAL", n.progname) + } else { + log.Log(NOW, "BOX IS VERTICAL", n.progname) + } + // n.placeWidgets() + n.toggleTree() + case widget.Button: + n.doUserEvent() + case widget.Dropdown: + log.Log(NOW, "do the dropdown here") + if me.ddview == nil { + me.ddview = addDropdown() + tk := me.ddview.tk + tk.gocuiSize.w0 = 20 + tk.gocuiSize.w1 = 40 + tk.gocuiSize.h0 = 10 + tk.gocuiSize.h1 = 25 + tk.v, _ = me.baseGui.SetView("ddview", + tk.gocuiSize.w0, + tk.gocuiSize.h0, + tk.gocuiSize.w1, + tk.gocuiSize.h1, 0) + if tk.v == nil { + return + } + tk.v.Wrap = true + tk.v.Frame = true + tk.v.Clear() + fmt.Fprint(tk.v, "example.com\nwit.com") + me.ddview.SetVisible(true) + return + } + log.Log(NOW, "doWidgetClick() visible =", me.ddview.Visible()) + if me.ddview.Visible() { + me.ddview.SetVisible(false) + me.baseGui.DeleteView("ddview") + me.ddview.tk.v = nil + } else { + var dnsList string + for i, s := range n.vals { + log.Log(NOW, "AddText()", n.progname, i, s) + dnsList += s + "\n" + } + me.ddNode = n + log.Log(NOW, "new dns list should be set to:", dnsList) + me.ddview.label = dnsList + me.ddview.SetText(dnsList) + me.ddview.SetVisible(true) + } + for i, s := range n.vals { + log.Log(NOW, "AddText()", n.progname, i, s) + } + default: + } +} + +var toggle bool = true + +func (n *node) toggleTree() { + if toggle { + n.drawTree(toggle) + toggle = false + } else { + n.hideWidgets() + toggle = true + } +} + +// display the widgets in the binary tree +func (n *node) drawTree(draw bool) { + w := n.tk + if w == nil { + return + } + n.showWidgetPlacement(true, "drawTree()") + if draw { + // w.textResize() + n.showView() + } else { + n.deleteView() + } + + for _, child := range n.children { + child.drawTree(draw) + } +} + +func click(g *gocui.Gui, v *gocui.View) error { + // var l string + // var err error + + log.Log(INFO, "click() START", v.Name()) + // n := me.rootNode.findWidgetName(v.Name()) + n := findUnderMouse() + if n != nil { + log.Log(NOW, "click() Found widget =", n.WidgetId, n.progname, ",", n.label) + if n.progname == "DropBox" { + log.Log(NOW, "click() this is the dropdown menu. set a flag here what did I click? where is the mouse?") + log.Log(NOW, "click() set a global dropdown clicked flag=true here") + me.ddClicked = true + } + n.doWidgetClick() + } else { + log.Log(NOW, "click() could not find node name =", v.Name()) + } + + if _, err := g.SetCurrentView(v.Name()); err != nil { + log.Log(NOW, "click() END err =", err) + return err + } + + log.Log(NOW, "click() END") + return nil +} + +func findUnderMouse() *node { + var found *node + var widgets []*node + var f func(n *node) + w, h := me.baseGui.MousePosition() + + // find buttons that are below where the mouse button click + f = func(n *node) { + widget := n.tk + // ignore widgets that are not visible + if n.Visible() { + if (widget.gocuiSize.w0 <= w) && (w <= widget.gocuiSize.w1) && + (widget.gocuiSize.h0 <= h) && (h <= widget.gocuiSize.h1) { + widgets = append(widgets, n) + found = n + } + } + if n == me.ddview { + log.Log(NOW, "findUnderMouse() found ddview") + if n.Visible() { + log.Log(NOW, "findUnderMouse() and ddview is visable. hide it here. TODO: find highlighted row") + found = n + // find the actual value here and set the dropdown widget + me.baseGui.DeleteView("ddview") + } else { + log.Log(NOW, "findUnderMouse() I was lying, actually it's not found") + } + } + + for _, child := range n.children { + f(child) + } + } + f(me.rootNode) + // widgets has everything that matches + // TODO: pop up menu with a list of them + for _, n := range widgets { + //log(logNow, "ctrlDown() FOUND widget", widget.id, widget.name) + n.showWidgetPlacement(true, "findUnderMouse() FOUND") + } + return found +} + +// find the widget under the mouse click +func ctrlDown(g *gocui.Gui, v *gocui.View) error { + var found *node + // var widgets []*node + // var f func (n *node) + found = findUnderMouse() + if me.ctrlDown == nil { + setupCtrlDownWidget() + me.ctrlDown.label = found.progname + me.ctrlDown.tk.cuiName = "ctrlDown" + // me.ctrlDown.parent = me.rootNode + } + cd := me.ctrlDown.tk + if found == nil { + found = me.rootNode + } + me.ctrlDown.label = found.progname + newR := found.realGocuiSize() + cd.gocuiSize.w0 = newR.w0 + cd.gocuiSize.h0 = newR.h0 + cd.gocuiSize.w1 = newR.w1 + cd.gocuiSize.h1 = newR.h1 + if me.ctrlDown.Visible() { + me.ctrlDown.hideView() + } else { + me.ctrlDown.showView() + } + me.ctrlDown.showWidgetPlacement(true, "ctrlDown:") + return nil +} diff --git a/color.go b/color.go new file mode 100644 index 0000000..45600c3 --- /dev/null +++ b/color.go @@ -0,0 +1,120 @@ +package main + +import ( + "github.com/awesome-gocui/gocui" + "math/rand" + + "go.wit.com/log" +) + +//w.v.SelBgColor = gocui.ColorCyan +//color.go: w.v.SelFgColor = gocui.ColorBlack +//color.go: w.v.BgColor = gocui.ColorGreen + +type colorT struct { + frame gocui.Attribute + fg gocui.Attribute + bg gocui.Attribute + selFg gocui.Attribute + selBg gocui.Attribute + name string +} + +var none gocui.Attribute = gocui.AttrNone +var lightPurple gocui.Attribute = gocui.GetColor("#DDDDDD") // light purple +var darkPurple gocui.Attribute = gocui.GetColor("#FFAA55") // Dark Purple +var heavyPurple gocui.Attribute = gocui.GetColor("#88AA55") // heavy purple +var powdererBlue gocui.Attribute = gocui.GetColor("#B0E0E6") // w3c 'powerder blue' +var superLightGrey gocui.Attribute = gocui.GetColor("#55AAFF") // super light grey + +// Standard defined colors from gocui: +// ColorBlack ColorRed ColorGreen ColorYellow ColorBlue ColorMagenta ColorCyan ColorWhite + +// v.BgColor = gocui.GetColor("#111111") // crazy red +// v.BgColor = gocui.GetColor("#FF9911") // heavy red +// v.SelBgColor = gocui.GetColor("#FFEE11") // blood red + +// v.BgColor = gocui.GetColor("#55AAFF") // super light grey +// v.BgColor = gocui.GetColor("#FFC0CB") // 'w3c pink' yellow + +// Normal Text On mouseover +// +// Widget Frame Text background Text background +var colorWindow colorT = colorT{none, gocui.ColorBlue, none, none, powdererBlue, "normal window"} +var colorActiveW colorT = colorT{none, none, powdererBlue, none, powdererBlue, "active window"} + +var colorTab colorT = colorT{gocui.ColorBlue, gocui.ColorBlue, none, none, powdererBlue, "normal tab"} +var colorActiveT colorT = colorT{gocui.ColorBlue, none, powdererBlue, none, powdererBlue, "active tab"} + +var colorButton colorT = colorT{gocui.ColorGreen, none, gocui.ColorWhite, gocui.ColorGreen, gocui.ColorBlack, "normal button"} +var colorLabel colorT = colorT{none, none, superLightGrey, none, superLightGrey, "normal label"} +var colorGroup colorT = colorT{none, none, superLightGrey, none, superLightGrey, "normal group"} + +// widget debugging colors. these widgets aren't displayed unless you are debugging +var colorRoot colorT = colorT{gocui.ColorRed, none, powdererBlue, none, gocui.ColorBlue, "debug root"} +var colorFlag colorT = colorT{gocui.ColorRed, none, powdererBlue, none, gocui.ColorGreen, "debug flag"} +var colorBox colorT = colorT{gocui.ColorRed, none, lightPurple, none, gocui.ColorCyan, "debug box"} +var colorGrid colorT = colorT{gocui.ColorRed, none, lightPurple, none, gocui.ColorRed, "debug grid"} +var colorNone colorT = colorT{none, none, none, none, none, "debug none"} + +// actually sets the colors for the gocui element +// the user will see the colors change when this runs +// TODO: add black/white only flag for ttyS0 +// TODO: or fix kvm/qemu serial console & SIGWINCH. +// TODO: and minicom and uboot and 5 million other things. +// TODO: maybe enough of us could actually do that if we made it a goal. +// TODO: start with riscv boards and fix it universally there +// TODO: so just a small little 'todo' item here +func (n *node) setColor(newColor *colorT) { + tk := n.tk + if tk.color == newColor { + // nothing to do since the colors have nto changed + return + } + tk.color = newColor + if tk.v == nil { + return + } + if tk.color == nil { + log.Log(NOW, "Set the node to color = nil") + tk.color = &colorNone + } + log.Log(NOW, "Set the node to color =", tk.color.name) + n.recreateView() +} + +func (n *node) setDefaultWidgetColor() { + n.showView() +} + +func (n *node) setDefaultHighlight() { + w := n.tk + if w.v == nil { + log.Log(ERROR, "SetColor() failed on view == nil") + return + } + w.v.SelBgColor = gocui.ColorGreen + w.v.SelFgColor = gocui.ColorBlack +} + +func randColor() gocui.Attribute { + colors := []string{"Green", "#FFAA55", "Yellow", "Blue", "Red", "Black", "White"} + i := rand.Intn(len(colors)) + log.Log(NOW, "randColor() i =", i) + return gocui.GetColor(colors[i]) +} + +func (n *node) redoColor(draw bool) { + w := n.tk + if w == nil { + return + } + + log.Sleep(.05) + n.setDefaultHighlight() + n.setDefaultWidgetColor() + + for _, child := range n.children { + child.redoColor(draw) + } +} diff --git a/common.go b/common.go new file mode 100644 index 0000000..267277f --- /dev/null +++ b/common.go @@ -0,0 +1,218 @@ +package main + +/* + These code should be common to all gui plugins + + There are some helper functions that are probably going to be + the same everywhere. Mostly due to handling the binary tree structure + and the channel communication + + For now, it's just a symlink to the 'master' version in + ./toolkit/nocui/common.go +*/ + +import ( + "go.wit.com/lib/widget" + "go.wit.com/log" +) + +// this is the channel we send user events like +// mouse clicks or keyboard events back to the program +var callback chan widget.Action + +// this is the channel we get requests to make widgets +var pluginChan chan widget.Action + +type node struct { + parent *node + children []*node + + WidgetId int // widget ID + WidgetType widget.WidgetType + ParentId int // parent ID + + state widget.State + + // a reference name for programming and debuggign. Must be unique + progname string + + // the text used for button labesl, window titles, checkbox names, etc + label string + + // horizontal means layout widgets like books on a bookshelf + // vertical means layout widgets like books in a stack + // direction widget.Orientation + direction widget.Orientation + + // This is how the values are passed back and forth + // values from things like checkboxes & dropdown's + value any + + strings []string + + // This is used for things like a slider(0,100) + X int + Y int + + // This is for the grid size & widget position + W int + H int + AtW int + AtH int + + vals []string // dropdown menu items + + // horizontal bool `default:false` + + hasTabs bool // does the window have tabs? + currentTab bool // the visible tab + + // the internal plugin toolkit structure + // in the gtk plugin, it has gtk things like margin & border settings + // in the text console one, it has text console things like colors for menus & buttons + tk *guiWidget +} + +// searches the binary tree for a WidgetId +func (n *node) findWidgetId(id int) *node { + if n == nil { + return nil + } + + if n.WidgetId == id { + return n + } + + for _, child := range n.children { + newN := child.findWidgetId(id) + if newN != nil { + return newN + } + } + return nil +} + +func (n *node) doUserEvent() { + if callback == nil { + log.Log(ERROR, "doUserEvent() callback == nil", n.WidgetId) + return + } + var a widget.Action + a.WidgetId = n.WidgetId + a.Value = n.value + a.ActionType = widget.User + log.Log(INFO, "doUserEvent() START: send a user event to the callback channel") + callback <- a + log.Log(INFO, "doUserEvent() END: sent a user event to the callback channel") + return +} + +// Other goroutines must use this to access the GUI +// +// You can not acess / process the GUI thread directly from +// other goroutines. This is due to the nature of how +// Linux, MacOS and Windows work (they all work differently. suprise. surprise.) +// +// this sets the channel to send user events back from the plugin +func Callback(guiCallback chan widget.Action) { + callback = guiCallback +} + +func PluginChannel() chan widget.Action { + return pluginChan +} + +/* +func convertString(val any) string { + switch v := val.(type) { + case bool: + n.B = val.(bool) + case string: + n.label = val.(string) + n.S = val.(string) + case int: + n.I = val.(int) + default: + log.Error(errors.New("Set() unknown type"), "v =", v) + } +} +*/ + +/* +// this is in common.go, do not move it +func getString(A any) string { + if A == nil { + log.Warn("getString() got nil") + return "" + } + var k reflect.Kind + k = reflect.TypeOf(A).Kind() + + switch k { + case reflect.Int: + var i int + i = A.(int) + return string(i) + case reflect.String: + return A.(string) + case reflect.Bool: + if A.(bool) == true { + return "true" + } else { + return "false" + } + default: + log.Warn("getString uknown kind", k, "value =", A) + return "" + } + return "" +} +*/ + +// this is in common.go, do not move it +func addNode(a *widget.Action) *node { + n := new(node) + n.WidgetType = a.WidgetType + n.WidgetId = a.WidgetId + n.ParentId = a.ParentId + + n.state = a.State + + // copy the data from the action message + n.progname = a.ProgName + n.value = a.Value + n.direction = a.Direction + n.strings = a.Strings + + // TODO: these need to be rethought + n.X = a.X + n.Y = a.Y + n.W = a.W + n.H = a.H + n.AtW = a.AtW + n.AtH = a.AtH + + // store the internal toolkit information + n.tk = initWidget(n) + // n.tk = new(guiWidget) + + if a.WidgetType == widget.Root { + log.Log(INFO, "addNode() Root") + return n + } + + if me.rootNode.findWidgetId(a.WidgetId) != nil { + log.Log(ERROR, "addNode() WidgetId already exists", a.WidgetId) + return me.rootNode.findWidgetId(a.WidgetId) + } + + // add this new widget on the binary tree + n.parent = me.rootNode.findWidgetId(a.ParentId) + if n.parent != nil { + n.parent.children = append(n.parent.children, n) + //w := n.tk + //w.parent = n.parent.tk + //w.parent.children = append(w.parent.children, w) + } + return n +} diff --git a/debug.go b/debug.go new file mode 100644 index 0000000..ee58727 --- /dev/null +++ b/debug.go @@ -0,0 +1,74 @@ +package main + +import ( + "fmt" + "go.wit.com/lib/widget" + "go.wit.com/log" +) + +func (n *node) dumpTree(draw bool) { + w := n.tk + if w == nil { + return + } + n.showWidgetPlacement(true, "dumpTree()") + + for _, child := range n.children { + child.dumpTree(draw) + } +} + +func (n *node) showWidgetPlacement(b bool, s string) { + if n == nil { + log.Log(ERROR, "WTF w == nil") + return + } + w := n.tk + + var s1 string + var pId int + if n.parent == nil { + log.Log(INFO, "showWidgetPlacement() parent == nil", n.WidgetId, w.cuiName) + pId = 0 + } else { + pId = n.parent.WidgetId + } + s1 = fmt.Sprintf("(wId,pId)=(%2d,%2d) ", n.WidgetId, pId) + if n.Visible() { + s1 += fmt.Sprintf("gocui=(%2d,%2d)(%2d,%2d,%2d,%2d)", + w.gocuiSize.Width(), w.gocuiSize.Height(), + w.gocuiSize.w0, w.gocuiSize.h0, w.gocuiSize.w1, w.gocuiSize.h1) + } else { + s1 += fmt.Sprintf(" ") + } + if n.parent != nil { + if n.parent.WidgetType == widget.Grid { + s1 += fmt.Sprintf("At(%2d,%2d) ", n.AtW, n.AtH) + } + } + tmp := "." + n.progname + "." + log.Log(INFO, s1, s, n.WidgetType, ",", tmp) // , "text=", w.text) +} + +func (n *node) dumpWidget(pad string) { + log.Log(NOW, "node:", pad, n.WidgetId, "At(", n.AtW, n.AtH, ") ,", n.WidgetType, ", n.progname =", n.progname, ", n.label =", n.label) +} + +func (n *node) listWidgets() { + if n == nil { + return + } + + var pad string + for i := 0; i < me.depth; i++ { + pad = pad + " " + } + n.dumpWidget(pad) + + for _, child := range n.children { + me.depth += 1 + child.listWidgets() + me.depth -= 1 + } + return +} diff --git a/fakefile.go b/fakefile.go new file mode 100644 index 0000000..47ef529 --- /dev/null +++ b/fakefile.go @@ -0,0 +1,58 @@ +package main + +import ( + "bytes" + "errors" + "io" +) + +type FakeFile struct { + reader *bytes.Reader + buffer *bytes.Buffer + offset int64 +} + +func (f *FakeFile) Read(p []byte) (n int, err error) { + n, err = f.reader.ReadAt(p, f.offset) + f.offset += int64(n) + return n, err +} + +func (f *FakeFile) Write(p []byte) (n int, err error) { + n, err = f.buffer.Write(p) + f.offset += int64(n) + f.reader.Reset(f.buffer.Bytes()) + return n, err +} + +func (f *FakeFile) Seek(offset int64, whence int) (int64, error) { + newOffset := f.offset + + switch whence { + case io.SeekStart: + newOffset = offset + case io.SeekCurrent: + newOffset += offset + case io.SeekEnd: + newOffset = int64(f.buffer.Len()) + offset + default: + return 0, errors.New("Seek: whence not at start,current or end") + } + // never can get here right? + + if newOffset < 0 { + return 0, errors.New("Seek: offset < 0") + } + + f.offset = newOffset + return f.offset, nil +} + +func NewFakeFile() *FakeFile { + buf := &bytes.Buffer{} + return &FakeFile{ + reader: bytes.NewReader(buf.Bytes()), + buffer: buf, + offset: 0, + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..db7d13f --- /dev/null +++ b/go.mod @@ -0,0 +1,21 @@ +module go.wit.com/gui/toolkits/gocui + +go 1.21.4 + +require ( + github.com/awesome-gocui/gocui v1.1.0 + go.wit.com/gui/widget v1.1.3 + go.wit.com/log v0.5.4 +) + +require ( + github.com/gdamore/encoding v1.0.0 // indirect + github.com/gdamore/tcell/v2 v2.4.0 // indirect + github.com/lucasb-eyer/go-colorful v1.0.3 // indirect + github.com/mattn/go-runewidth v0.0.10 // indirect + github.com/rivo/uniseg v0.1.0 // indirect + go.wit.com/dev/davecgh/spew v1.1.4 // indirect + golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 // indirect + golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf // indirect + golang.org/x/text v0.3.3 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..e0bce49 --- /dev/null +++ b/go.sum @@ -0,0 +1,30 @@ +github.com/awesome-gocui/gocui v1.1.0 h1:db2j7yFEoHZjpQFeE2xqiatS8bm1lO3THeLwE6MzOII= +github.com/awesome-gocui/gocui v1.1.0/go.mod h1:M2BXkrp7PR97CKnPRT7Rk0+rtswChPtksw/vRAESGpg= +github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko= +github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg= +github.com/gdamore/tcell/v2 v2.4.0 h1:W6dxJEmaxYvhICFoTY3WrLLEXsQ11SaFnKGVEXW57KM= +github.com/gdamore/tcell/v2 v2.4.0/go.mod h1:cTTuF84Dlj/RqmaCIV5p4w8uG1zWdk0SF6oBpwHp4fU= +github.com/lucasb-eyer/go-colorful v1.0.3 h1:QIbQXiugsb+q10B+MI+7DI1oQLdmnep86tWFlaaUAac= +github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/mattn/go-runewidth v0.0.10 h1:CoZ3S2P7pvtP45xOtBw+/mDL2z0RKI576gSkzRRpdGg= +github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= +github.com/rivo/uniseg v0.1.0 h1:+2KBaVoUmb9XzDsrx/Ct0W/EYOSFf/nWTauy++DprtY= +github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +go.wit.com/dev/davecgh/spew v1.1.3 h1:hqnB5qsPgC2cLZaJXqQJspQ5n/Ugry9kyL3tLk0hVzQ= +go.wit.com/dev/davecgh/spew v1.1.3/go.mod h1:sihvWmnQ/09FWplnEmozt90CCVqBtGuPXM811tgfhFA= +go.wit.com/dev/davecgh/spew v1.1.4 h1:C9hj/rjlUpdK+E6aroyLjCbS5MFcyNUOuP1ICLWdNek= +go.wit.com/dev/davecgh/spew v1.1.4/go.mod h1:sihvWmnQ/09FWplnEmozt90CCVqBtGuPXM811tgfhFA= +go.wit.com/gui/widget v1.1.3 h1:GvLzGSOF9tfmoh6HNbFdN+NSlBo2qeS/Ba2TnQQ1A1U= +go.wit.com/gui/widget v1.1.3/go.mod h1:A6/FaiFQtAHTjgo7c4FrokXe6bXX1Cowo35b2Lgi31E= +go.wit.com/log v0.5.3 h1:/zHkniOPusPEuX1R401rMny9uwSO/nSU/QOMx6qoEnE= +go.wit.com/log v0.5.3/go.mod h1:LzIzVxc2xJQxWQBtV9VbV605P4TOxmYDCl+BZF38yGE= +go.wit.com/log v0.5.4 h1:vijLRPTUgChb8J5tx/7Uma/lGTUxeSXosFbheAmL914= +go.wit.com/log v0.5.4/go.mod h1:BaJBfHFqcJSJLXGQ9RHi3XVhPgsStxSMZRlaRxW4kAo= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 h1:nxC68pudNYkKU6jWhgrqdreuFiOQWj1Fs7T3VrH4Pjw= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf h1:MZ2shdL+ZM/XzY3ZGOnh4Nlpnxz5GSOhOmtHo3iPU6M= +golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/gocui.go b/gocui.go new file mode 100644 index 0000000..6d924f8 --- /dev/null +++ b/gocui.go @@ -0,0 +1,102 @@ +// Copyright 2014 The gocui Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package main + +import ( + "errors" + "github.com/awesome-gocui/gocui" + + "go.wit.com/log" +) + +// This initializes the gocui package +// it runs SetManagerFunc which passes every input +// event (keyboard, mouse, etc) to the function "gocuiEvent()" +func gocuiMain() { + g, err := gocui.NewGui(gocui.OutputNormal, true) + if err != nil { + panic(err) + } + defer g.Close() + + me.baseGui = g + + g.Cursor = true + g.Mouse = true + + // this sets the function that is run on every event. For example: + // When you click the mouse, move the mouse, or press a key on the keyboard + // This is equivalent to xev or similar to cat /dev/input on linux + g.SetManagerFunc(gocuiEvent) + + if err := defaultKeybindings(g); err != nil { + panic(err) + } + + if err := g.MainLoop(); err != nil && !errors.Is(err, gocui.ErrQuit) { + panic(err) + } +} + +// Thanks to the gocui developers -- your package kicks ass +// This function is called on every event. It is a callback function from the gocui package +// which has an excellent implementation. While gocui handles things like text highlighting +// and the layout of the text areas -- also things like handling SIGWINCH and lots of really +// complicated console handling, it sends events here in a clean way. +// This is equivalent to the linux command xev (apt install x11-utils) +func gocuiEvent(g *gocui.Gui) error { + maxX, maxY := g.Size() + mx, my := g.MousePosition() + log.Log(NOW, "handleEvent() START", maxX, maxY, mx, my, msgMouseDown) + if _, err := g.View("msg"); msgMouseDown && err == nil { + moveMsg(g) + } + if widgetView, _ := g.View("msg"); widgetView == nil { + log.Log(NOW, "handleEvent() create output widget now", maxX, maxY, mx, my) + makeOutputWidget(g, "this is a create before a mouse click") + if me.logStdout != nil { + // setOutput(me.logStdout) + } + } else { + log.Log(INFO, "output widget already exists", maxX, maxY, mx, my) + } + mouseMove(g) + log.Log(INFO, "handleEvent() END ", maxX, maxY, mx, my, msgMouseDown) + return nil +} + +func dragOutputWindow() { +} + +// turns off the frame on the global window +func setFrame(b bool) { + // TODO: figure out what this might be useful for + // what is this do? I made it just 2 lines for now. Is this useful for something? + v := SetView("global", 5, 10, 5, 10, 0) // x0, x1, y1, y2, overlap + if v == nil { + log.Log(ERROR, "setFrame() global failed") + } + v.Frame = b +} + +func quit(g *gocui.Gui, v *gocui.View) error { + return gocui.ErrQuit +} + +func SetView(name string, x0, y0, x1, y1 int, overlaps byte) *gocui.View { + if me.baseGui == nil { + log.Log(ERROR, "SetView() ERROR: me.baseGui == nil") + return nil + } + + v, err := me.baseGui.SetView(name, x0, y0, x1, y1, overlaps) + if err != nil { + if !errors.Is(err, gocui.ErrUnknownView) { + log.Log(ERROR, "SetView() global failed on name =", name) + } + return nil + } + return v +} diff --git a/help.go b/help.go new file mode 100644 index 0000000..832b397 --- /dev/null +++ b/help.go @@ -0,0 +1,71 @@ +// Copyright 2014 The gocui Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package main + +import ( + "errors" + "fmt" + "strings" + + "github.com/awesome-gocui/gocui" +) + +var helpText []string = []string{"KEYBINDINGS", + "", + "?: toggle help", + "d: toggle debugging", + "r: redraw widgets", + "s/h: show/hide all widgets", + "L: list all widgets", + "M: list all widgets positions", + "q: quit()", + "p: panic()", + "o: show Stdout", + "l: log to /tmp/witgui.log", + "Ctrl-D: Toggle Debugging", + "Ctrl-V: Toggle Verbose Debugging", + "Ctrl-C: Exit", + "", +} + +func hidehelplayout() { + me.baseGui.DeleteView("help") + // n.deleteView() + // child.hideFake() +} + +func helplayout() error { + g := me.baseGui + var err error + maxX, _ := g.Size() + + var newW int = 8 + for _, s := range helpText { + if newW < len(s) { + newW = len(s) + } + } + + help, err := g.SetView("help", maxX-(newW+me.FramePadW), 0, maxX-1, len(helpText)+me.FramePadH, 0) + if err != nil { + if !errors.Is(err, gocui.ErrUnknownView) { + return err + } + help.SelBgColor = gocui.ColorGreen + help.SelFgColor = gocui.ColorBlack + // fmt.Fprintln(help, "Enter: Click Button") + // fmt.Fprintln(help, "Tab/Space: Switch Buttons") + // fmt.Fprintln(help, "Backspace: Delete Button") + // fmt.Fprintln(help, "Arrow keys: Move Button") + + fmt.Fprintln(help, strings.Join(helpText, "\n")) + + if _, err := g.SetCurrentView("help"); err != nil { + return err + } + } + me.helpLabel = help + return nil +} diff --git a/keybindings.go b/keybindings.go new file mode 100644 index 0000000..79666fa --- /dev/null +++ b/keybindings.go @@ -0,0 +1,165 @@ +// Copyright 2014 The gocui Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package main + +import ( + "github.com/awesome-gocui/gocui" + + "go.wit.com/lib/widget" + "go.wit.com/log" +) + +func defaultKeybindings(g *gocui.Gui) error { + if err := g.SetKeybinding("", gocui.KeyCtrlC, gocui.ModNone, quit); err != nil { + return err + } + for _, n := range []string{"but1", "but2", "help", "but3"} { + if err := g.SetKeybinding(n, gocui.MouseLeft, gocui.ModNone, showMsg); err != nil { + return err + } + } + if err := g.SetKeybinding("", gocui.MouseRelease, gocui.ModNone, mouseUp); err != nil { + return err + } + // mouseDown() runs whenever you click on an unknown view (?) + if err := g.SetKeybinding("", gocui.MouseLeft, gocui.ModNone, mouseDown); err != nil { + return err + } + if err := g.SetKeybinding("", gocui.MouseLeft, gocui.ModMouseCtrl, ctrlDown); err != nil { + return err + } + // if err := g.SetKeybinding(w.v.Name(), gocui.MouseLeft, gocui.ModNone, click); err != nil { + // return err + // } + /* + if err := g.SetKeybinding("", gocui.MouseLeft, gocui.ModNone, globalDown); err != nil { + return err + } + */ + if err := g.SetKeybinding("msg", gocui.MouseLeft, gocui.ModNone, msgDown); err != nil { + return err + } + addDebugKeys(g) + return nil +} + +func addDebugKeys(g *gocui.Gui) { + // show debugging buttons + g.SetKeybinding("", 'd', gocui.ModNone, + func(g *gocui.Gui, v *gocui.View) error { + fakeStartWidth = me.FakeW + fakeStartHeight = me.TabH + me.FramePadH + if showDebug { + me.rootNode.showFake() + showDebug = false + } else { + me.rootNode.hideFake() + showDebug = true + } + return nil + }) + + // display the help menu + g.SetKeybinding("", '?', gocui.ModNone, + func(g *gocui.Gui, v *gocui.View) error { + if showHelp { + helplayout() + showHelp = false + } else { + me.baseGui.DeleteView("help") + showHelp = true + } + return nil + }) + + // redraw all the widgets + g.SetKeybinding("", 'r', gocui.ModNone, + func(g *gocui.Gui, v *gocui.View) error { + if redoWidgets { + redoWindows(0, 0) + redoWidgets = false + } else { + me.rootNode.hideWidgets() + redoWidgets = true + } + return nil + }) + + // hide all widgets + g.SetKeybinding("", 'h', gocui.ModNone, + func(g *gocui.Gui, v *gocui.View) error { + me.rootNode.hideWidgets() + return nil + }) + + // show all widgets + g.SetKeybinding("", 's', gocui.ModNone, + func(g *gocui.Gui, v *gocui.View) error { + me.rootNode.showWidgets() + return nil + }) + + // list all widgets + g.SetKeybinding("", 'L', gocui.ModNone, + func(g *gocui.Gui, v *gocui.View) error { + me.rootNode.listWidgets() + return nil + }) + + // list all widgets with positions + g.SetKeybinding("", 'M', gocui.ModNone, + func(g *gocui.Gui, v *gocui.View) error { + me.rootNode.dumpTree(true) + return nil + }) + + // log to output window + g.SetKeybinding("", 'o', gocui.ModNone, + func(g *gocui.Gui, v *gocui.View) error { + log.Log(ERROR, "TODO: re-implement this") + if me.logStdout.Visible() { + me.logStdout.SetVisible(false) + // setOutput(os.Stdout) + } else { + me.logStdout.SetVisible(true) + // setOutput(me.logStdout.tk) + } + return nil + }) + + // exit + g.SetKeybinding("", 'q', gocui.ModNone, + func(g *gocui.Gui, v *gocui.View) error { + standardExit() + return nil + }) + g.SetKeybinding("", gocui.KeyCtrlC, gocui.ModNone, + func(g *gocui.Gui, v *gocui.View) error { + standardExit() + return nil + }) + g.SetKeybinding("", gocui.KeyCtrlD, gocui.ModNone, + func(g *gocui.Gui, v *gocui.View) error { + if showDebug { + var a widget.Action + a.Value = true + a.ActionType = widget.EnableDebug + callback <- a + } + return nil + }) + g.SetKeybinding("", gocui.KeyCtrlV, gocui.ModNone, + func(g *gocui.Gui, v *gocui.View) error { + return nil + }) + + // panic + g.SetKeybinding("", 'p', gocui.ModNone, + func(g *gocui.Gui, v *gocui.View) error { + standardExit() + panic("forced panic in gocui") + return nil + }) +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..37eed29 --- /dev/null +++ b/main.go @@ -0,0 +1,104 @@ +// Copyright 2014 The gocui Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package main + +import ( + "os" + + "go.wit.com/lib/widget" + "go.wit.com/log" +) + +// sets defaults and establishes communication +// to this toolkit from the wit/gui golang package +func init() { + log.Log(INFO, "Init() of awesome-gocui") + + // init the config struct default values + Set(&me, "default") + + pluginChan = make(chan widget.Action) + + log.Log(NOW, "Init() start pluginChan") + go catchActionChannel() + log.Sleep(.1) // probably not needed, but in here for now under development + go main() + log.Sleep(.1) // probably not needed, but in here for now under development +} + +/* +recieves requests from the program to do things like: +* add new widgets +* change the text of a label +* etc.. +*/ +func catchActionChannel() { + log.Log(INFO, "catchActionChannel() START") + for { + log.Log(INFO, "catchActionChannel() infinite for() loop restarted select on channel") + select { + case a := <-pluginChan: + if me.baseGui == nil { + // something went wrong initializing the gocui + log.Log(ERROR, "ERROR: console did not initialize") + continue + } + log.Log(INFO, "catchActionChannel()", a.WidgetId, a.ActionType, a.WidgetType, a.ProgName) + action(&a) + } + } +} + +func Exit() { + // TODO: what should actually happen here? + log.Log(NOW, "Exit() here. doing standardExit()") + standardExit() +} + +func standardExit() { + log.Log(NOW, "standardExit() doing baseGui.Close()") + me.baseGui.Close() + log.Log(NOW, "standardExit() doing outf.Close()") + outf.Close() + // log(true, "standardExit() setOutput(os.Stdout)") + // setOutput(os.Stdout) + log.Log(NOW, "standardExit() send back Quit()") + go sendBackQuit() // don't stall here in case the + // induces a delay in case the callback channel is broken + log.Sleep(1) + log.Log(NOW, "standardExit() exit()") + os.Exit(0) +} +func sendBackQuit() { + // send 'Quit' back to the program (?) + var a widget.Action + a.ActionType = widget.UserQuit + callback <- a +} + +var outf *os.File + +func main() { + var err error + log.Log(INFO, "main() start Init()") + + outf, err = os.OpenFile("/tmp/witgui.log", os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666) + if err != nil { + log.Error(err, "error opening file: %v") + os.Exit(0) + } + os.Stdout = outf + defer outf.Close() + + // setOutput(outf) + // log("This is a test log entry") + + ferr, _ := os.OpenFile("/tmp/witgui.err", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0664) + os.Stderr = ferr + gocuiMain() + + log.Log(NOW, "MouseMain() closed") + standardExit() +} diff --git a/mouse.go b/mouse.go new file mode 100644 index 0000000..02efc0d --- /dev/null +++ b/mouse.go @@ -0,0 +1,151 @@ +// Copyright 2014 The gocui Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package main + +import ( + "errors" + "fmt" + "github.com/awesome-gocui/gocui" + + "go.wit.com/log" +) + +// this function uses the mouse position to highlight & unhighlight things +// this is run every time the user moves the mouse over the terminal window +func mouseMove(g *gocui.Gui) { + mx, my := g.MousePosition() + for _, view := range g.Views() { + view.Highlight = false + } + if v, err := g.ViewByPosition(mx, my); err == nil { + v.Highlight = true + } +} + +func msgDown(g *gocui.Gui, v *gocui.View) error { + initialMouseX, initialMouseY = g.MousePosition() + log.Log(NOW, "msgDown() X,Y", initialMouseX, initialMouseY) + if vx, vy, _, _, err := g.ViewPosition("msg"); err == nil { + xOffset = initialMouseX - vx + yOffset = initialMouseY - vy + msgMouseDown = true + } + return nil +} + +func hideDDview() error { + w, h := me.baseGui.MousePosition() + log.Log(NOW, "hide dropdown menu() view msgMouseDown (w,h) =", w, h) + if me.ddview == nil { + return gocui.ErrUnknownView + } + if me.ddview.tk.v == nil { + return gocui.ErrUnknownView + } + me.ddview.SetVisible(false) + return nil +} + +func showDDview() error { + w, h := me.baseGui.MousePosition() + log.Log(NOW, "show dropdown menu() view msgMouseDown (w,h) =", w, h) + if me.ddview == nil { + return gocui.ErrUnknownView + } + if me.ddview.tk.v == nil { + return gocui.ErrUnknownView + } + me.ddview.SetVisible(true) + return nil +} + +func mouseUp(g *gocui.Gui, v *gocui.View) error { + w, h := g.MousePosition() + log.Log(NOW, "mouseUp() view msgMouseDown (check here for dropdown menu click) (w,h) =", w, h) + if me.ddClicked { + me.ddClicked = false + log.Log(NOW, "mouseUp() ddview is the thing that was clicked", w, h) + log.Log(NOW, "mouseUp() find out what the string is here", w, h, me.ddview.tk.gocuiSize.h1) + + var newZone string = "" + if me.ddNode != nil { + value := h - me.ddview.tk.gocuiSize.h0 - 1 + log.Log(NOW, "mouseUp() me.ddview.tk.gocuiSize.h1 =", me.ddview.tk.gocuiSize.h1) + log.Log(NOW, "mouseUp() me.ddNode.vals =", me.ddNode.vals) + valsLen := len(me.ddNode.vals) + log.Log(NOW, "mouseUp() value =", value, "valsLen =", valsLen) + log.Log(NOW, "mouseUp() me.ddNode.vals =", me.ddNode.vals) + if (value >= 0) && (value < valsLen) { + newZone = me.ddNode.vals[value] + log.Log(NOW, "mouseUp() value =", value, "newZone =", newZone) + } + } + hideDDview() + if newZone != "" { + if me.ddNode != nil { + me.ddNode.SetText(newZone) + me.ddNode.value = newZone + me.ddNode.doUserEvent() + } + } + return nil + } + /* + // if there is a drop down view active, treat it like a dialog box and close it + if (hideDDview() == nil) { + return nil + } + */ + if msgMouseDown { + msgMouseDown = false + if movingMsg { + movingMsg = false + return nil + } else { + g.DeleteView("msg") + } + } else if globalMouseDown { + globalMouseDown = false + g.DeleteView("globalDown") + } + return nil +} + +func mouseDown(g *gocui.Gui, v *gocui.View) error { + mx, my := g.MousePosition() + if vx0, vy0, vx1, vy1, err := g.ViewPosition("msg"); err == nil { + if mx >= vx0 && mx <= vx1 && my >= vy0 && my <= vy1 { + return msgDown(g, v) + } + } + globalMouseDown = true + maxX, _ := g.Size() + test := findUnderMouse() + msg := fmt.Sprintf("Mouse really down at: %d,%d", mx, my) + "foobar" + if test == me.ddview { + if me.ddview.Visible() { + log.Log(NOW, "hide DDview() Mouse really down at:", mx, my) + hideDDview() + } else { + log.Log(NOW, "show DDview() Mouse really down at:", mx, my) + showDDview() + } + return nil + } + x := mx - len(msg)/2 + if x < 0 { + x = 0 + } else if x+len(msg)+1 > maxX-1 { + x = maxX - 1 - len(msg) - 1 + } + log.Log(NOW, "mouseDown() about to write out message to 'globalDown' view. msg =", msg) + if v, err := g.SetView("globalDown", x, my-1, x+len(msg)+1, my+1, 0); err != nil { + if !errors.Is(err, gocui.ErrUnknownView) { + return err + } + v.WriteString(msg) + } + return nil +} diff --git a/place.go b/place.go new file mode 100644 index 0000000..4bdcd42 --- /dev/null +++ b/place.go @@ -0,0 +1,188 @@ +package main + +import ( + "strings" + + "go.wit.com/lib/widget" + "go.wit.com/log" +) + +func (n *node) placeBox(startW int, startH int) { + if n.WidgetType != widget.Box { + return + } + n.showWidgetPlacement(true, "boxS()") + + newW := startW + newH := startH + for _, child := range n.children { + child.placeWidgets(newW, newH) + // n.showWidgetPlacement(logNow, "boxS()") + newR := child.realGocuiSize() + w := newR.w1 - newR.w0 + h := newR.h1 - newR.h0 + if n.direction == widget.Horizontal { + log.Log(NOW, "BOX IS HORIZONTAL", n.progname, "newWH()", newW, newH, "child()", w, h, child.progname) + // expand based on the child width + newW += w + } else { + log.Log(NOW, "BOX IS VERTICAL ", n.progname, "newWH()", newW, newH, "child()", w, h, child.progname) + // expand based on the child height + newH += h + } + } + + // just compute this every time? + // newR := n.realGocuiSize() + + n.showWidgetPlacement(true, "boxE()") +} + +func (n *node) placeWidgets(startW int, startH int) { + if n == nil { + return + } + if me.rootNode == nil { + return + } + + switch n.WidgetType { + case widget.Window: + for _, child := range n.children { + child.placeWidgets(me.RawW, me.RawH) + return + } + case widget.Tab: + for _, child := range n.children { + child.placeWidgets(me.RawW, me.RawH) + return + } + case widget.Grid: + n.placeGrid(startW, startH) + case widget.Box: + n.placeBox(startW, startH) + case widget.Group: + // move the group to the parent's next location + n.gocuiSetWH(startW, startH) + n.showWidgetPlacement(true, "group()") + + newW := startW + me.GroupPadW + newH := startH + 3 // normal hight of the group label + // now move all the children aka: run place() on them + for _, child := range n.children { + child.placeWidgets(newW, newH) + newR := child.realGocuiSize() + // w := newR.w1 - newR.w0 + h := newR.h1 - newR.h0 + + // increment straight down + newH += h + } + default: + n.gocuiSetWH(startW, startH) + // n.moveTo(startW, startH) + } +} + +func (n *node) placeGrid(startW int, startH int) { + w := n.tk + n.showWidgetPlacement(true, "grid0:") + if n.WidgetType != widget.Grid { + return + } + + // first compute the max sizes of the rows and columns + for _, child := range n.children { + newR := child.realGocuiSize() + childW := newR.w1 - newR.w0 + childH := newR.h1 - newR.h0 + + // set the child's realWidth, and grid offset + if w.widths[child.AtW] < childW { + w.widths[child.AtW] = childW + } + if w.heights[child.AtH] < childH { + w.heights[child.AtH] = childH + } + // child.showWidgetPlacement(logInfo, "grid: ") + log.Log(INFO, "placeGrid:", child.progname, "child()", childW, childH, "At()", child.AtW, child.AtH) + } + + // find the width and height offset of the grid for AtW,AtH + for _, child := range n.children { + child.showWidgetPlacement(true, "grid1:") + + var totalW, totalH int + for i, w := range w.widths { + if i < child.AtW { + totalW += w + } + } + for i, h := range w.heights { + if i < child.AtH { + totalH += h + } + } + + // the new corner to move the child to + newW := startW + totalW + newH := startH + totalH + + log.Log(INFO, "placeGrid:", child.progname, "new()", newW, newH, "At()", child.AtW, child.AtH) + child.placeWidgets(newW, newH) + child.showWidgetPlacement(true, "grid2:") + } + n.showWidgetPlacement(true, "grid3:") +} + +// computes the real, actual size of all the gocli objects in a widget +func (n *node) realGocuiSize() *rectType { + var f func(n *node, r *rectType) + newR := new(rectType) + // initialize the values to opposite + newR.w0 = 80 + newR.h0 = 24 + if me.baseGui != nil { + maxW, maxH := me.baseGui.Size() + newR.w0 = maxW + newR.h0 = maxH + } + newR.w1 = 0 + newR.h1 = 0 + + // expand the rectangle to the biggest thing displayed + f = func(n *node, r *rectType) { + newR := n.tk.gocuiSize + if !n.tk.isFake { + if r.w0 > newR.w0 { + r.w0 = newR.w0 + } + if r.h0 > newR.h0 { + r.h0 = newR.h0 + } + if r.w1 < newR.w1 { + r.w1 = newR.w1 + } + if r.h1 < newR.h1 { + r.h1 = newR.h1 + } + } + for _, child := range n.children { + f(child, r) + } + } + f(n, newR) + return newR +} + +func (n *node) textSize() (int, int) { + var width, height int + + for _, s := range strings.Split(widget.GetString(n.value), "\n") { + if width < len(s) { + width = len(s) + } + height += 1 + } + return width, height +} diff --git a/plugin.go b/plugin.go new file mode 100644 index 0000000..c0a925f --- /dev/null +++ b/plugin.go @@ -0,0 +1,117 @@ +package main + +import ( + // if you include more than just this import + // then your plugin might be doing something un-ideal (just a guess from 2023/02/27) + "go.wit.com/lib/widget" + "go.wit.com/log" +) + +func action(a *widget.Action) { + log.Log(INFO, "action() START", a.WidgetId, a.ActionType, a.WidgetType, a.ProgName) + n := me.rootNode.findWidgetId(a.WidgetId) + var w *guiWidget + if n != nil { + w = n.tk + } + switch a.ActionType { + case widget.Add: + if w == nil { + n := addNode(a) + // w = n.tk + n.addWidget() + } else { + // this is done to protect the plugin being 'refreshed' with the + // widget binary tree. TODO: find a way to keep them in sync + log.Log(ERROR, "action() Add ignored for already defined widget", + a.WidgetId, a.ActionType, a.WidgetType, a.ProgName) + } + case widget.Show: + if widget.GetBool(a.Value) { + n.showView() + } else { + n.hideWidgets() + } + case widget.Set: + if a.WidgetType == widget.Flag { + log.Log(NOW, "TODO: set flag here", a.ActionType, a.WidgetType, a.ProgName) + log.Log(NOW, "TODO: n.WidgetType =", n.WidgetType, "n.progname =", a.ProgName) + } else { + if a.Value == nil { + log.Log(ERROR, "TODO: Set here. a == nil id =", a.WidgetId, "type =", a.WidgetType, "Name =", a.ProgName) + log.Log(ERROR, "TODO: Set here. id =", a.WidgetId, "n.progname =", n.progname) + } else { + n.Set(a.Value) + } + } + case widget.SetText: + n.SetText(widget.GetString(a.Value)) + case widget.AddText: + n.AddText(widget.GetString(a.Value)) + case widget.Move: + log.Log(NOW, "attempt to move() =", a.ActionType, a.WidgetType, a.ProgName) + case widget.ToolkitClose: + log.Log(NOW, "attempting to close the plugin and release stdout and stderr") + standardExit() + case widget.Enable: + if n.Visible() { + // widget was already shown + } else { + log.Log(INFO, "Setting Visable to true", a.ProgName) + n.SetVisible(true) + } + case widget.Disable: + if n.Visible() { + log.Log(INFO, "Setting Visable to false", a.ProgName) + n.SetVisible(false) + } else { + // widget was already hidden + } + default: + log.Log(ERROR, "action() ActionType =", a.ActionType, "WidgetType =", a.WidgetType, "Name =", a.ProgName) + } + log.Log(INFO, "action() END") +} + +func (n *node) AddText(text string) { + if n == nil { + log.Log(NOW, "widget is nil") + return + } + n.vals = append(n.vals, text) + for i, s := range n.vals { + log.Log(NOW, "AddText()", n.progname, i, s) + } + n.SetText(text) +} + +func (n *node) SetText(text string) { + var changed bool = false + if n == nil { + log.Log(NOW, "widget is nil") + return + } + if widget.GetString(n.value) != text { + n.value = text + changed = true + } + if !changed { + return + } + + if n.Visible() { + n.textResize() + n.deleteView() + n.showView() + } +} + +func (n *node) Set(val any) { + // w := n.tk + log.Log(INFO, "Set() value =", val) + + n.value = val + if n.WidgetType != widget.Checkbox { + n.setCheckbox(val) + } +} diff --git a/showStdout.go b/showStdout.go new file mode 100644 index 0000000..c628f4a --- /dev/null +++ b/showStdout.go @@ -0,0 +1,98 @@ +package main + +import ( + "errors" + "fmt" + + "github.com/awesome-gocui/gocui" + + "go.wit.com/lib/widget" + "go.wit.com/log" +) + +var outputW int = 180 +var outputH int = 24 + +func moveMsg(g *gocui.Gui) { + mx, my := g.MousePosition() + if !movingMsg && (mx != initialMouseX || my != initialMouseY) { + movingMsg = true + } + g.SetView("msg", mx-xOffset, my-yOffset, mx-xOffset+outputW, my-yOffset+outputH+me.FramePadH, 0) + g.SetViewOnBottom("msg") +} + +func showMsg(g *gocui.Gui, v *gocui.View) error { + var l string + var err error + + log.Log(NOW, "showMsg() v.name =", v.Name()) + if _, err := g.SetCurrentView(v.Name()); err != nil { + return err + } + + _, cy := v.Cursor() + if l, err = v.Line(cy); err != nil { + l = "" + } + + makeOutputWidget(g, l) + return nil +} + +func makeOutputWidget(g *gocui.Gui, stringFromMouseClick string) *gocui.View { + maxX, maxY := g.Size() + + if me.rootNode == nil { + // keep skipping this until the binary tree is initialized + return nil + } + + if me.logStdout == nil { + a := new(widget.Action) + a.ProgName = "stdout" + a.WidgetType = widget.Stdout + a.WidgetId = -3 + a.ParentId = 0 + n := addNode(a) + me.logStdout = n + me.logStdout.tk.gocuiSize.w0 = maxX - 32 + me.logStdout.tk.gocuiSize.h0 = maxY / 2 + me.logStdout.tk.gocuiSize.w1 = me.logStdout.tk.gocuiSize.w0 + outputW + me.logStdout.tk.gocuiSize.h1 = me.logStdout.tk.gocuiSize.h0 + outputH + } + v, err := g.View("msg") + if v == nil { + log.Log(NOW, "makeoutputwindow() this is supposed to happen. v == nil", err) + } else { + log.Log(NOW, "makeoutputwindow() msg != nil. WTF now? err =", err) + } + + // help, err := g.SetView("help", maxX-32, 0, maxX-1, 13, 0) + // v, err = g.SetView("msg", 3, 3, 30, 30, 0) + + v, err = g.SetView("msg", maxX-32, maxY/2, maxX/2+outputW, maxY/2+outputH, 0) + if errors.Is(err, gocui.ErrUnknownView) { + log.Log(NOW, "makeoutputwindow() this is supposed to happen?", err) + } + + if err != nil { + log.Log(NOW, "makeoutputwindow() create output window failed", err) + return nil + } + + if v == nil { + log.Log(NOW, "makeoutputwindow() msg == nil. WTF now? err =", err) + return nil + } else { + me.logStdout.tk.v = v + } + + v.Clear() + v.SelBgColor = gocui.ColorCyan + v.SelFgColor = gocui.ColorBlack + fmt.Fprintln(v, "figure out how to capture STDOUT to here\n"+stringFromMouseClick) + g.SetViewOnBottom("msg") + // g.SetViewOnBottom(v.Name()) + return v +} diff --git a/structs.go b/structs.go new file mode 100644 index 0000000..e899f78 --- /dev/null +++ b/structs.go @@ -0,0 +1,229 @@ +// LICENSE: same as the go language itself +// Copyright 2023 WIT.COM + +// all structures and variables are local (aka lowercase) +// since the plugin should be isolated to access only +// by functions() to insure everything here is run +// inside a dedicated goroutine + +package main + +import ( + "fmt" + "github.com/awesome-gocui/gocui" + "reflect" + "strconv" + "strings" + "sync" + + "go.wit.com/log" +) + +// It's probably a terrible idea to call this 'me' +var me config + +var showDebug bool = true +var showHelp bool = true +var redoWidgets bool = true + +// This is the window that is currently active +var currentWindow *node + +type config struct { + baseGui *gocui.Gui // the main gocui handle + rootNode *node // the base of the binary tree. it should have id == 0 + + ctrlDown *node // shown if you click the mouse when the ctrl key is pressed + currentWindow *node // this is the current tab or window to show + logStdout *node // where to show STDOUT + helpLabel *gocui.View + ddview *node // the gocui view to select dropdrown lists + ddClicked bool // the dropdown menu view was clicked + ddNode *node // the dropdown menu is for this widget + + /* + // this is the channel we send user events like + // mouse clicks or keyboard events back to the program + callback chan toolkit.Action + + // this is the channel we get requests to make widgets + pluginChan chan toolkit.Action + */ + + // When the widget has a frame, like a button, it adds 2 lines runes on each side + // so you need 3 char spacing in each direction to not have them overlap + // the amount of padding when there is a frame + FramePadW int `default:"1" dense:"0"` + FramePadH int `default:"1" dense:"0"` + + PadW int `default:"1" dense:"0"` + PadH int `default:"1" dense:"0"` + + // how far down to start Window or Tab headings + WindowW int `default:"8" dense:"0"` + WindowH int `default:"-1"` + TabW int `default:"5" dense:"0"` + TabH int `default:"1" dense:"0"` + + // additional amount of space to put between window & tab widgets + WindowPadW int `default:"8" dense:"0"` + TabPadW int `default:"4" dense:"0"` + + // additional amount of space to indent on a group + GroupPadW int `default:"6" dense:"2"` + + // the raw beginning of each window (or tab) + RawW int `default:"1"` + RawH int `default:"5"` + + // offset for the hidden widgets + FakeW int `default:"20"` + + padded bool // add space between things like buttons + bookshelf bool // do you want things arranged in the box like a bookshelf or a stack? + canvas bool // if set to true, the windows are a raw canvas + menubar bool // for windows + stretchy bool // expand things like buttons to the maximum size + margin bool // add space around the frames of windows + + // writeMutex protects locks the write process + writeMutex sync.Mutex + + // used for listWidgets() debugging + depth int +} + +// deprecate these +var ( + initialMouseX, initialMouseY, xOffset, yOffset int + globalMouseDown, msgMouseDown, movingMsg bool +) + +// this is the gocui way +// corner starts at in the upper left corner +type rectType struct { + w0, h0, w1, h1 int // left top right bottom +} + +func (r *rectType) Width() int { + return r.w1 - r.w0 +} + +func (r *rectType) Height() int { + return r.h1 - r.h0 +} + +type guiWidget struct { + // the gocui package variables + v *gocui.View // this is nil if the widget is not displayed + cuiName string // what gocui uses to reference the widget + + // the actual text to display in the console + label string + + // the logical size of the widget + // For example, 40x12 would be the center of a normal terminal + // size rectType + + // the actual gocui display view of this widget + // sometimes this isn't visible like with a Box or Grid + gocuiSize rectType + + isCurrent bool // is this the currently displayed Window or Tab? + isFake bool // widget types like 'box' are 'false' + + // used to track the size of grids + widths map[int]int // how tall each row in the grid is + heights map[int]int // how wide each column in the grid is + + tainted bool + frame bool + + // for a window, this is currently selected tab + selectedTab *node + + // what color to use + color *colorT +} + +// from the gocui devs: +// Write appends a byte slice into the view's internal buffer. Because +// View implements the io.Writer interface, it can be passed as parameter +// of functions like fmt.Fprintf, fmt.Fprintln, io.Copy, etc. Clear must +// be called to clear the view's buffer. + +func (w *guiWidget) Write(p []byte) (n int, err error) { + w.tainted = true + me.writeMutex.Lock() + defer me.writeMutex.Unlock() + if me.logStdout.tk.v == nil { + // optionally write the output to /tmp + s := fmt.Sprint(string(p)) + s = strings.TrimSuffix(s, "\n") + fmt.Fprintln(outf, s) + v, _ := me.baseGui.View("msg") + if v != nil { + // fmt.Fprintln(outf, "found msg") + me.logStdout.tk.v = v + } + } else { + // display the output in the gocui window + me.logStdout.tk.v.Clear() + + s := fmt.Sprint(string(p)) + s = strings.TrimSuffix(s, "\n") + tmp := strings.Split(s, "\n") + outputS = append(outputS, tmp...) + if len(outputS) > outputH { + l := len(outputS) - outputH + outputS = outputS[l:] + } + fmt.Fprintln(me.logStdout.tk.v, strings.Join(outputS, "\n")) + } + + return len(p), nil +} + +func Set(ptr interface{}, tag string) error { + if reflect.TypeOf(ptr).Kind() != reflect.Ptr { + log.Log(ERROR, "Set() Not a pointer", ptr, "with tag =", tag) + return fmt.Errorf("Not a pointer") + } + + v := reflect.ValueOf(ptr).Elem() + t := v.Type() + + for i := 0; i < t.NumField(); i++ { + defaultVal := t.Field(i).Tag.Get(tag) + name := t.Field(i).Name + // log("Set() try name =", name, "defaultVal =", defaultVal) + setField(v.Field(i), defaultVal, name) + } + return nil +} + +func setField(field reflect.Value, defaultVal string, name string) error { + + if !field.CanSet() { + // log("setField() Can't set value", field, defaultVal) + return fmt.Errorf("Can't set value\n") + } else { + log.Log(NOW, "setField() Can set value", name, defaultVal) + } + + switch field.Kind() { + case reflect.Int: + val, _ := strconv.Atoi(defaultVal) + field.Set(reflect.ValueOf(int(val)).Convert(field.Type())) + case reflect.String: + field.Set(reflect.ValueOf(defaultVal).Convert(field.Type())) + case reflect.Bool: + if defaultVal == "true" { + field.Set(reflect.ValueOf(true)) + } else { + field.Set(reflect.ValueOf(false)) + } + } + + return nil +} diff --git a/tab.go b/tab.go new file mode 100644 index 0000000..f0fc563 --- /dev/null +++ b/tab.go @@ -0,0 +1,111 @@ +package main + +// implements widgets 'Window' and 'Tab' + +import ( + "strings" + + "go.wit.com/lib/widget" + "go.wit.com/log" +) + +func (w *guiWidget) Width() int { + if w.frame { + return w.gocuiSize.w1 - w.gocuiSize.w0 + } + return w.gocuiSize.w1 - w.gocuiSize.w0 - 1 +} + +func (w *guiWidget) Height() int { + if w.frame { + return w.gocuiSize.h1 - w.gocuiSize.h0 + } + return w.gocuiSize.h1 - w.gocuiSize.h0 - 1 +} + +func (n *node) gocuiSetWH(sizeW, sizeH int) { + w := len(widget.GetString(n.value)) + lines := strings.Split(widget.GetString(n.value), "\n") + h := len(lines) + + tk := n.tk + if tk.isFake { + tk.gocuiSize.w0 = sizeW + tk.gocuiSize.h0 = sizeH + tk.gocuiSize.w1 = tk.gocuiSize.w0 + w + me.FramePadW + tk.gocuiSize.h1 = tk.gocuiSize.h0 + h + me.FramePadH + return + } + + if tk.frame { + tk.gocuiSize.w0 = sizeW + tk.gocuiSize.h0 = sizeH + tk.gocuiSize.w1 = tk.gocuiSize.w0 + w + me.FramePadW + tk.gocuiSize.h1 = tk.gocuiSize.h0 + h + me.FramePadH + } else { + tk.gocuiSize.w0 = sizeW - 1 + tk.gocuiSize.h0 = sizeH - 1 + tk.gocuiSize.w1 = tk.gocuiSize.w0 + w + 1 + tk.gocuiSize.h1 = tk.gocuiSize.h0 + h + 1 + } +} + +func redoWindows(nextW int, nextH int) { + for _, n := range me.rootNode.children { + if n.WidgetType != widget.Window { + continue + } + w := n.tk + var tabs bool + for _, child := range n.children { + if child.WidgetType == widget.Tab { + tabs = true + } + } + if tabs { + // window is tabs. Don't show it as a standard button + w.frame = false + n.hasTabs = true + } else { + w.frame = false + n.hasTabs = false + } + + n.gocuiSetWH(nextW, nextH) + n.deleteView() + n.showView() + + sizeW := w.Width() + me.WindowPadW + sizeH := w.Height() + nextW += sizeW + log.Log(NOW, "redoWindows() start nextW,H =", nextW, nextH, "gocuiSize.W,H =", sizeW, sizeH, n.progname) + + if n.hasTabs { + n.redoTabs(me.TabW, me.TabH) + } + } +} + +func (p *node) redoTabs(nextW int, nextH int) { + for _, n := range p.children { + if n.WidgetType != widget.Tab { + continue + } + w := n.tk + w.frame = true + + n.gocuiSetWH(nextW, nextH) + n.deleteView() + // setCurrentTab(n) + // if (len(w.cuiName) < 4) { + // w.cuiName = "abcd" + // } + + n.showView() + + sizeW := w.Width() + me.TabPadW + sizeH := w.Height() + log.Log(NOW, "redoTabs() start nextW,H =", nextW, nextH, "gocuiSize.W,H =", sizeW, sizeH, n.progname) + nextW += sizeW + } +} diff --git a/view.go b/view.go new file mode 100644 index 0000000..b2bc81b --- /dev/null +++ b/view.go @@ -0,0 +1,233 @@ +package main + +import ( + "bufio" + "errors" + "fmt" + "strings" + + "github.com/awesome-gocui/gocui" + + "go.wit.com/lib/widget" + "go.wit.com/log" +) + +func splitLines(s string) []string { + var lines []string + sc := bufio.NewScanner(strings.NewReader(s)) + for sc.Scan() { + lines = append(lines, sc.Text()) + } + return lines +} + +func (n *node) textResize() bool { + w := n.tk + var width, height int = 0, 0 + var changed bool = false + + for i, s := range splitLines(n.tk.label) { + log.Log(INFO, "textResize() len =", len(s), i, s) + if width < len(s) { + width = len(s) + } + height += 1 + } + if w.gocuiSize.w1 != w.gocuiSize.w0+width+me.FramePadW { + w.gocuiSize.w1 = w.gocuiSize.w0 + width + me.FramePadW + changed = true + } + if w.gocuiSize.h1 != w.gocuiSize.h0+height+me.FramePadH { + w.gocuiSize.h1 = w.gocuiSize.h0 + height + me.FramePadH + changed = true + } + if changed { + n.showWidgetPlacement(true, "textResize() changed") + } + return changed +} + +func (n *node) hideView() { + n.SetVisible(false) +} + +// display's the text of the widget in gocui +// will create a new gocui view if there isn't one or if it has been moved +func (n *node) showView() { + var err error + w := n.tk + + if w.cuiName == "" { + log.Log(ERROR, "showView() w.cuiName was not set for widget", w) + w.cuiName = string(n.WidgetId) + } + + // if the gocui element doesn't exist, create it + if w.v == nil { + n.recreateView() + } + x0, y0, x1, y1, err := me.baseGui.ViewPosition(w.cuiName) + log.Log(INFO, "showView() w.v already defined for widget", n.progname, err) + + // n.smartGocuiSize() + changed := n.textResize() + + if changed { + log.Log(NOW, "showView() textResize() changed. Should recreateView here wId =", w.cuiName) + } else { + log.Log(NOW, "showView() Clear() and Fprint() here wId =", w.cuiName) + w.v.Clear() + fmt.Fprint(w.v, n.tk.label) + n.SetVisible(false) + n.SetVisible(true) + return + } + + // if the gocui element has changed where it is supposed to be on the screen + // recreate it + if x0 != w.gocuiSize.w0 { + n.recreateView() + return + } + if y0 != w.gocuiSize.h0 { + log.Log(ERROR, "showView() start hight mismatch id=", w.cuiName, "gocui h vs computed h =", w.gocuiSize.h0, y0) + n.recreateView() + return + } + if x1 != w.gocuiSize.w1 { + log.Log(ERROR, "showView() too wide", w.cuiName, "w,w", w.gocuiSize.w1, x1) + n.recreateView() + return + } + if y1 != w.gocuiSize.h1 { + log.Log(ERROR, "showView() too high", w.cuiName, "h,h", w.gocuiSize.h1, y1) + n.recreateView() + return + } + + n.SetVisible(true) +} + +// create or recreate the gocui widget visible +// deletes the old view if it exists and recreates it +func (n *node) recreateView() { + var err error + w := n.tk + log.Log(ERROR, "recreateView() START", n.WidgetType, n.progname) + if me.baseGui == nil { + log.Log(ERROR, "recreateView() ERROR: me.baseGui == nil", w) + return + } + + // this deletes the button from gocui + me.baseGui.DeleteView(w.cuiName) + w.v = nil + + if n.progname == "CLOUDFLARE_EMAIL" { + n.showWidgetPlacement(true, "n.progname="+n.progname+" n.tk.label="+n.tk.label+" "+w.cuiName) + n.dumpWidget("jwc") + n.textResize() + n.showWidgetPlacement(true, "n.progname="+n.progname+" n.tk.label="+n.tk.label+" "+w.cuiName) + } + + a := w.gocuiSize.w0 + b := w.gocuiSize.h0 + c := w.gocuiSize.w1 + d := w.gocuiSize.h1 + + w.v, err = me.baseGui.SetView(w.cuiName, a, b, c, d, 0) + if err == nil { + n.showWidgetPlacement(true, "recreateView()") + log.Log(ERROR, "recreateView() internal plugin error err = nil") + return + } + if !errors.Is(err, gocui.ErrUnknownView) { + n.showWidgetPlacement(true, "recreateView()") + log.Log(ERROR, "recreateView() internal plugin error error.IS()", err) + return + } + + // this sets up the keybinding for the name of the window + // does this really need to be done? I think we probably already + // know everything about where all the widgets are so we could bypass + // the gocui package and just handle all the mouse events internally here (?) + // for now, the w.v.Name is a string "1", "2", "3", etc from the widgetId + + // set the binding for this gocui view now that it has been created + // gocui handles overlaps of views so it will run on the view that is clicked on + me.baseGui.SetKeybinding(w.v.Name(), gocui.MouseLeft, gocui.ModNone, click) + + // this actually sends the text to display to gocui + w.v.Wrap = true + w.v.Frame = w.frame + w.v.Clear() + fmt.Fprint(w.v, n.tk.label) + // n.showWidgetPlacement(true, "n.progname=" + n.progname + " n.tk.label=" + n.tk.label + " " + w.cuiName) + // n.dumpWidget("jwc 2") + + // if you don't do this here, it will be black & white only + if w.color != nil { + w.v.FrameColor = w.color.frame + w.v.FgColor = w.color.fg + w.v.BgColor = w.color.bg + w.v.SelFgColor = w.color.selFg + w.v.SelBgColor = w.color.selBg + } + if n.progname == "CLOUDFLARE_EMAIL" { + n.showWidgetPlacement(true, "n.progname="+n.progname+" n.tk.label="+n.tk.label+" "+w.cuiName) + n.dumpTree(true) + } + log.Log(ERROR, "recreateView() END") +} + +func (n *node) hideWidgets() { + w := n.tk + w.isCurrent = false + switch n.WidgetType { + case widget.Root: + case widget.Flag: + case widget.Window: + case widget.Box: + case widget.Grid: + default: + n.hideView() + } + for _, child := range n.children { + child.hideWidgets() + } +} + +func (n *node) hideFake() { + w := n.tk + if w.isFake { + n.hideView() + } + for _, child := range n.children { + child.hideFake() + } +} + +func (n *node) showFake() { + w := n.tk + if w.isFake { + n.setFake() + n.showWidgetPlacement(true, "showFake:") + n.showView() + } + for _, child := range n.children { + child.showFake() + } +} + +func (n *node) showWidgets() { + w := n.tk + if w.isFake { + // don't display by default + } else { + n.showWidgetPlacement(true, "current:") + n.showView() + } + for _, child := range n.children { + child.showWidgets() + } +} diff --git a/widget.go b/widget.go new file mode 100644 index 0000000..ff8df1b --- /dev/null +++ b/widget.go @@ -0,0 +1,139 @@ +package main + +import ( + "go.wit.com/lib/widget" + "go.wit.com/log" +) + +func initWidget(n *node) *guiWidget { + var w *guiWidget + w = new(guiWidget) + // Set(w, "default") + + w.frame = true + + // set the name used by gocui to the id + w.cuiName = string(n.WidgetId) + + if n.WidgetType == widget.Root { + log.Log(INFO, "setupWidget() FOUND ROOT w.id =", n.WidgetId) + n.WidgetId = 0 + me.rootNode = n + return w + } + + if n.WidgetType == widget.Grid { + w.widths = make(map[int]int) // how tall each row in the grid is + w.heights = make(map[int]int) // how wide each column in the grid is + } + + return w +} + +func setupCtrlDownWidget() { + a := new(widget.Action) + a.ProgName = "ctrlDown" + a.WidgetType = widget.Dialog + a.WidgetId = -1 + a.ParentId = 0 + n := addNode(a) + + me.ctrlDown = n +} + +func (n *node) deleteView() { + w := n.tk + if w.v != nil { + w.v.Visible = false + return + } + // make sure the view isn't really there + me.baseGui.DeleteView(w.cuiName) + w.v = nil +} + +// searches the binary tree for a WidgetId +func (n *node) findWidgetName(name string) *node { + if n == nil { + return nil + } + + if n.tk.cuiName == name { + return n + } + + for _, child := range n.children { + newN := child.findWidgetName(name) + if newN != nil { + return newN + } + } + return nil +} + +func (n *node) IsCurrent() bool { + w := n.tk + if n.WidgetType == widget.Tab { + return w.isCurrent + } + if n.WidgetType == widget.Window { + return w.isCurrent + } + if n.WidgetType == widget.Root { + return false + } + return n.parent.IsCurrent() +} + +func (n *node) Visible() bool { + if n == nil { + return false + } + if n.tk == nil { + return false + } + if n.tk.v == nil { + return false + } + return n.tk.v.Visible +} + +func (n *node) SetVisible(b bool) { + if n == nil { + return + } + if n.tk == nil { + return + } + if n.tk.v == nil { + return + } + n.tk.v.Visible = b +} + +func addDropdown() *node { + n := new(node) + n.WidgetType = widget.Flag + n.WidgetId = -2 + n.ParentId = 0 + + // copy the data from the action message + n.progname = "DropBox" + n.tk.label = "DropBox text" + + // store the internal toolkit information + n.tk = new(guiWidget) + n.tk.frame = true + + // set the name used by gocui to the id + n.tk.cuiName = "-1 dropbox" + + n.tk.color = &colorFlag + + // add this new widget on the binary tree + n.parent = me.rootNode + if n.parent != nil { + n.parent.children = append(n.parent.children, n) + } + return n +}