// Copyright 2017-2025 WIT.COM Inc. All rights reserved. // Use of this source code is governed by the GPL 3.0 // 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" "os" "runtime" "runtime/debug" "runtime/pprof" "time" "github.com/awesome-gocui/gocui" "go.wit.com/log" ) // sent via -ldflags var VERSION string var BUILDTIME string var PLUGIN string = "gocui" // this is called at the very initial connection // between the app and this gocui plugin // this is a good place to initialize gocui's default behavior func toolkitInit() { log.Info("gocui toolkitInit() me.ok =", me.ok) if me.baseGui == nil { log.Info("gocui baseGui is still nil") standardExit() } if me.treeRoot == nil { log.Info("gocui treeRoot is still nil") standardExit() } w := me.treeRoot.TK.(*guiWidget) w.dumpTree("MM") w.dumpWindows("WW") // SETUP HELP START me.baseGui.Update(testRefresh) log.Info("gocui toolkitInit() trying showHelp() me.ok =", me.ok) showHelp() hideHelp() showHelp() // SETUP HELP END // SETUP STDOUT START time.Sleep(300 * time.Millisecond) log.Info("gocui toolkitInit() me.ok =", me.ok) me.baseGui.Update(testRefresh) if !me.stdout.init { log.Info("gocui toolkitInit() stdout.Init me.ok =", me.ok) me.stdout.init = true relocateStdoutOffscreen() } time.Sleep(1 * time.Second) me.stdout.outputOnTop = false setThingsOnTop() // SETUP STDOUT END // SETUP BG if me.BG.tk == nil { me.BG.tk = makeNewInternalWidget(me.BG.wId) } // PUT INIT DEBUG COOE HERE var toggle bool for i := 0; i < 6; i++ { w := me.treeRoot.TK.(*guiWidget) w.dumpTree("MM") w.dumpWindows("WW") time.Sleep(1000 * time.Millisecond) if toggle { toggle = false log.Info("gocui toolkitInit() put testing true stuff here") } else { toggle = true log.Info("gocui toolkitInit() put testing false stuff here") } setBottomBG() } // PUT INIT DEBUG COOE HERE END // TEST TEXTBOX START time.Sleep(1 * time.Second) log.Info("gocui toolkitInit() me.ok =", me.ok) me.baseGui.Update(testRefresh) if me.textbox.tk == nil { log.Info("gocui toolkitInit() initTextbox me.ok =", me.ok) initTextbox() me.textbox.tk.prepTextbox() } // TEST TEXTBOX END } func toolkitClose() { me.baseGui.Close() } // a GO GUI plugin should initTree in init() // this should be done before the application // starts trying to open up a channel func init() { me.myTree = initTree() } // sets defaults and establishes communication // to this toolkit from the wit/gui golang package func initPlugin() { defer func() { if r := recover(); r != nil { fmt.Fprintf(me.outf, "PANIC: initPlugin() recovered %v\n", r) return } }() var err error // read in defaults from config protobuf if val, err := me.myTree.ConfigFind("stdout"); err == nil { if val == "true" { me.stdout.startOnscreen = true // me.stdout.Write([]byte("starting with stdout onscreen\n")) } if val == "disable" { log.Info("COMPLETELY DISABLE LOG CRAP") me.stdout.disable = true } } if val, err := me.myTree.ConfigFind("dark"); err == nil { if val == "true" { me.dark = true } } else { // macos iterm2 really only works with dark mode right now if runtime.GOOS == "macos" { me.dark = true } } // todo: make this a tmp file that goes away if !me.stdout.disable { log.Info("USING STDOUT") me.outf, err = os.OpenFile("/tmp/captureMode.log", os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666) if err != nil { log.Info("error opening file:", err) os.Exit(0) } // todo: some early output still goes to the /tmp/ file //time.Sleep(200 * time.Millisecond) log.CaptureMode(me.stdout) } me.starttime = time.Now() log.Log(INFO, "Init() of awesome-gocui") // init the config struct default values Set(&me, "default") // initial app window settings // initial stdout window settings me.stdout.w = 180 me.stdout.h = 40 me.stdout.lastW = 4 me.stdout.lastH = 20 // just make up unique values for these me.dropdown.wId = -77 me.textbox.wId = -55 me.stdout.wId = -4 me.BG.wId = -22 // the clock widget id and offset me.notify.clock.wId = -5 me.notify.clock.offsetW = 13 me.notify.clock.offsetH = 1 me.notify.menu.wId = -5 Set(&me.dropdown, "default") // s := fmt.Sprintln("fake default check =", me.FakeW, "dropdown.Id", me.dropdown.Id) // me.stdout.Write([]byte(s)) me.mouse.mouseUp = true me.mouse.clicktime = time.Millisecond * 200 me.mouse.doubletime = time.Millisecond * 400 me.newWindowTrigger = make(chan *guiWidget, 1) go newWindowTrigger() go refreshGocui() log.Log(NOW, "Init() start pluginChan") if me.stdout.disable { log.Info("Using STDOUT") } else { log.Info("Using gocui STDOUT") os.Stdout = me.outf log.CaptureMode(me.outf) } // init gocui g, err := gocui.NewGui(gocui.OutputNormal, true) if err != nil { os.Exit(-1) return } 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) // register how the 'gocui' will work as a GO toolkit plugin // all applications will use these keys. they are universal. // registered event handlers still have the events sent to gocuiEvent() above registerHandlers(g) time.Sleep(100 * time.Millisecond) if me.outf != nil { fmt.Fprintln(me.outf, "hello world", time.Since(me.starttime)) } // coreStdout() // createStdout(g) // tell 'tree' that we are okay to start talking to me.myTree.InitOK() me.ok = true // this tells init() it's okay to work with gocui go gocuiMain() } // This goroutine sits in gocui's MainLoop() func gocuiMain() { defer func() { if r := recover(); r != nil { log.Warn("PANIC ecovered in gocuiMain()", r) if me.outf == nil { debug.PrintStack() pprof.Lookup("goroutine").WriteTo(os.Stdout, 1) panic(os.Stdout) } else { fmt.Fprintf(me.outf, "PANIC recovered in r = %v", r) os.Stderr = me.outf os.Stdout = me.outf debug.PrintStack() pprof.Lookup("goroutine").WriteTo(me.outf, 1) panic(me.outf) } } }() // me.stdout.Write([]byte("begin gogui.MainLoop()\n")) if err := me.baseGui.MainLoop(); err != nil && !errors.Is(err, gocui.ErrQuit) { log.Log(NOW, "g.MainLoop() panic err =", err) // normally panic here panic("gocuiTKmainloop OOPS") } } func standardExit() { log.Log(NOW, "standardExit() doing baseGui.Close()") me.baseGui.Close() if me.outf != nil { log.Log(NOW, "standardExit() doing outf.Close()") me.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 time.Sleep(200 * time.Millisecond) log.Log(NOW, "standardExit() exit()") os.Exit(0) } func standardClose() { log.Log(NOW, "standardExit() doing baseGui.Close()") me.baseGui.Close() log.Log(NOW, "standardExit() doing outf.Close()") me.outf.Close() // os.Stdin = os.Stdin // os.Stdout = os.Stdout // os.Stderr = os.Stderr log.Log(NOW, "standardExit() send back Quit()") } func main() { } // this hack is to wait for the application to send something // before trying to do anything. todo: rethink this someday func waitOK() { for { if me.ok { return } time.Sleep(10 * time.Millisecond) } } // this hack is to wait for the application to send something // before trying to do anything. todo: rethink this someday func waitFirstWindow() { for { if me.firstWindowOk { return } time.Sleep(10 * time.Millisecond) } } // empty function. this triggers gocui to refresh the screen func testRefresh(*gocui.Gui) error { // log.Info("in testRefresh") return nil } // refresh the screen 10 times a second func refreshGocui() { defer func() { if r := recover(); r != nil { if me.outf == nil { log.Info("INIT PLUGIN recovered in r", r) } else { fmt.Fprintln(me.outf, "INIT PLUGIN recovered in r", r) } return } }() var lastRefresh time.Time lastRefresh = time.Now() me.refresh = false for { time.Sleep(100 * time.Millisecond) // log.Info("refresh checking ok") if !me.ok { continue } // redraw the windows if something has changed if time.Since(lastRefresh) > 1000*time.Millisecond { if me.refresh { log.Info("refresh triggered") me.newWindowTrigger <- me.treeRoot.TK.(*guiWidget) me.refresh = false } } // this code updates the clock if time.Since(lastRefresh) > 1000*time.Millisecond { // artificially pause clock while dragging. // this is a reminder to make this refresh code smarter // after the switch to protocol buffers if me.mouse.mouseUp { // log.Info("refresh now on mouseUp") // todo: add logic here to see if the application has changed anything libNotifyUpdate() lastRefresh = time.Now() } else { if time.Since(lastRefresh) > 3*time.Second { libNotifyUpdate() lastRefresh = time.Now() } } } } } // set the widget start width & height func newWindowTrigger() { // log.Log(NOW, "newWindowTriggerl() START") for { // log.Log(NOW, "GO plugin toolkit made a new window") select { case tk := <-me.newWindowTrigger: // log.Log(NOW, "newWindowTrigger() got new window", tk.cuiName) // time.Sleep(200 * time.Millisecond) waitOK() // time.Sleep(200 * time.Millisecond) redoWindows(me.FirstWindowW, me.FirstWindowH) me.firstWindowOk = true if !me.stdout.init { me.stdout.init = true relocateStdoutOffscreen() } if me.textbox.tk == nil { initTextbox() me.textbox.tk.prepTextbox() } tk.makeWindowActive() } } }