Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Seeking guidance: "Status" view not updating #122

Open
psparago opened this issue Jun 15, 2022 · 7 comments
Open

Seeking guidance: "Status" view not updating #122

psparago opened this issue Jun 15, 2022 · 7 comments

Comments

@psparago
Copy link

psparago commented Jun 15, 2022

If this is not the proper place for this question, my apologies (and sorry for the length of this post).

I am working on a shell UI that is legacy code (that I didn't author). It has been working for many years, but It was based on a hacked version of jroimartin/gocui and a hacked version of a widget package the author found (4 years ago, he just disappeared).

We recently moved all of our Go code to 1.18 (from 1.11) and, with that, we revised all our packages including moving our terminal GUI utility from the hacked jroimartin/gocui code to the latest release V1.1.0 of awesome-gocui (Thanks folks!).

The terminal UI app is a utility that has a main menu "page" that transfers to several "pages" of "forms". All of these "pages" are comprised of widgets from the still hacked widgets package (they seem to work OK).

Every "page" has a status view (not a widget) at the top of the page. For some reason, the status view no longer shows any text.

Relevant code:

"Pages" are represented by an unfortunately named "Window" struct. In the Layout function on the Window struct, two VIews are created: a View to hold the "form" widgets (gathered as children), and a View to show status messages:

func (w *Window) Layout(g *gocui.Gui) (err error) {

      // ... gather child widgets and create the "form" View (not shown)
      w.view, _ = g.SetView(w.name, w.x, w.y, w.x+w.width, w.y+totalHeight, 0)

	// Always creating a one line status view at the top of the screen
	_ = g.DeleteView("status")
	w.status, err = g.SetView("status", 2, 1, maxX-2, 2, 0)
	if err != nil {
		if err != gocui.ErrUnknownView {
			return err
		}
		w.status.Frame = false
		w.status.Editable = false
	}

The status text is set using the SetStatus function on the "Window" struct :

func (w *Window) SetStatus(statusType int, text string) {
	if w.status != nil {
		w.gui.Update(func(g *gocui.Gui) error {
			switch statusType {
			case StatusAlert:
				w.status.BgColor = gocui.ColorRed
				w.status.FgColor = gocui.ColorBlack
			case StatusInformation:
				w.status.BgColor = gocui.ColorBlue
				w.status.FgColor = gocui.ColorWhite
			case StatusSuccess:
				w.status.BgColor = gocui.ColorGreen
				w.status.FgColor = gocui.ColorWhite
			}
			w.status.Clear()
			_, _ = fmt.Fprint(w.status, text)
			fmt.Printf(w.status.Buffer())
			return nil
		})
	}
}

Here's an example use from a test page I created:

var (
	mmWindow  *widgets.Window
	mmBtnTest widgets.WidgetInterface
)

func mainMenu(g *gocui.Gui) error {
	if mmWindow == nil {
		mmWindow = widgets.NewWindow(g, "mainMenu", 40)
		mmBtnTest = widgets.NewButtonWidget(g, mmWindow, "mmBtnTest", 0, 0, 0, "Test",
			func(g *gocui.Gui, v *gocui.View) error {
				mmWindow.SetStatus(widgets.StatusAlert, "Say Something!!!")
				return nil
			})
		mmBtnTest.SetPadding(1)
		mmWindow.AddWidget(&mmBtnTest)
	}
	_ = mmWindow.Layout(g)
	return nil
}

Note the call to SetStatus in the example code.

If you look closely at the SetStatus function you see that I am printing the View buffer to the screen after writing the status text to the buffer. The text "Say Something!!!" does indeed get printed to the screen when the Test "button" is pressed.

Any guidance would be greatly appreciated. Thank you.
gocui-test.tar.gz

@dankox
Copy link

dankox commented Jun 15, 2022

Hmmm... your problem made me think that we probably introduced some bug with the size of View when we rewrote that functionality. But I tried the original jroimarting/gocui implementation and it has the same problem.

So just to explain why it doesn't show anything, it's because the size of the View is too small. Even without Frame, it just sees it as empty, because the x0, y0 and x1, y1 are corners of the frame even if the frame is not there.
If you increase the y1 to 3, it would increase the size of the View to 1 (top border is on position 1 and bottom border on position 3, making position 2 available to write and giving the size of the inner view as 1 line):

	w.status, err = g.SetView("status", 2, 1, maxX-2, 3, 0) // changed the 2, 0) into 3, 0)

I tried it on hello.go example, where you can just lower the y1 parameter by 1 and it wouldn't display anything (not even in the original implementation).

Let us know if that helps. I'm not sure if the View size which you have there ever worked. So I hope by increasing it you will resolve your problem and won't introduce any new one :)

@psparago
Copy link
Author

Thank you very much for the speedy response. I truly appreciate it.

Unfortunately I get the same behavior after make the change suggested. I've even expanded the y1 to 5 and draw a frame around the view and it is still empty.

I'm wondering if I'm handling the actual write to the status view properly. In the hello.go example, the write is done in layout. That wouldn't be the case in my implementation. I am using the gui update event to update the view. I did set breakpoints and I see the event being consumed but still no output in the status view.

Do you have any ideas as to why my code isn't producing the output even with the expanded vertical size?

I've uploaded an updated copy of my test application.

Thank you again.

gocui-test.tar.gz

@dankox
Copy link

dankox commented Jun 16, 2022

Oh I see now. I didn't download the app before as I just saw the View size in your comment and assumed that's the reason.

Now I downloaded and checked your code and the reason why it's not working for you is that you delete the status view every call of (*Window)Layout(*gocui.Gui) function in widgets/window.go:188.

	_ = g.DeleteView("status") // <======= This line is the problem!
	w.status, err = g.SetView("status", 2, 0, maxX-2, 3, 0)
	if err != nil {
		if err != gocui.ErrUnknownView {
			return err
		}
		w.status.Frame = true
		w.status.Editable = false
	}

To make it more clear on how gocui works:

  1. waits for any event (user even like gocui.Update, keybing, etc.) or system event (change of terminal size, etc.)
  2. consume that event and any other event which was queued. For user event it means it will execute the function (in your case update the status window)
  3. "flush" the screen to the terminal, which draws the whole layout. What this means it will run thru all the Layout functions and call them and then after the buffer representing the screen is created, it will display it in terminal

From this you can see, that when your button press update the status it happens in the 2nd step. However, the window Layout function is called in the 3rd step and the status view which currently has the message in it would be deleted and recreated again without any content.

There is two ways you can do here, either not delete the view if it exist and keep it around. Or have a status message string field in the Window instead and in the window Layout just create the status view and populate it with that message if necessary.

@dankox
Copy link

dankox commented Jun 16, 2022

Maybe one more thing about it. The gocui.ErrUnknownView is probably the design decision here, where you can always run SetView and just check and decide accordingly what should be done.

  1. err == gocui.ErrUnknownView - newly created View, you can populate it with the "default" content
  2. err == nil - this is existing view and was updated with new dimension (or the same dimension), it may contain some content already
  3. err != nil - something is wrong, maybe incorrectly specified dimension or name

@psparago
Copy link
Author

Thank you SO much, especially for taking the time to explain how this works. I removed the delete of the view in the test app and I now have output! I will run through the logic and decide what best works in the actual application.

I should note that the author did modify (and Expose) flush() in his hacked version. I must have missed a nuance that made his version work.

Once again I truly appreciate the time you took to not only solve my issue, but more importantly (to me) to explain how it works so I can make a proper design decision.

Thank you!!!

@psparago
Copy link
Author

psparago commented Jun 16, 2022

Hello again, I really hesitant to bother you again and I realize you're not my personal consultant :-), but, while I have statuses mostly working now, I've run into a roadblock I spent today trying to work around.

Essentially, status view output is not shown while the code invoked by the widgets we're using is processing. That is to say, if the action of the keypress attempts to display a status message, the view is not drawn until the action completes.

The user event is queued immediately but it looks to me that the reason the status view is not drawn is the event loop is executing the keypress synchronously thus the event loop is blocked waiting for the code invoked by a keypress to complete before processing user events (as documented). Once the action is complete the status message does, is fact, drawn.

I think my understanding of why this is happening is accurate and I understand the reasoning for this, but Is there any way in the framework to get around this behavior? (It doesn't appear so)

However, to get around this behavior, I tried creating an independent gocui instance with its own MainLoop dedicated to the status view, but, any attempt to invoke actions just hangs the UI. Would something like this even work?

I have uploaded a version of my test application (that does not have the attempt at a second gocui instance) that demonstrate that the status message is not displayed while the action invoked by the widget keypress is running.

Once again, I do not want to take advantage of your kind nature. However, if you can direct me to a solution or advise that this is simply not possible in our current design, it would be much appreciated.

gocui-test.tar.gz

@dankox
Copy link

dankox commented Jun 16, 2022

No problem at all, just happend to be around so happy to help ;)

This is quite straight forward in gocui, as you observed the user functions (executed by gui.Update()) are executed sequentially in main gocui loop described above (step 2). That's the reason it's hanging.

You shouldn't really have a logic in those functions, and rather use them only to update the UI.
Your logic should go into separate go routine. You can start the go routine before executing MainLoop or you can start it from any gui.Update or keybind function, but it should be started as a go routine and not called as a regular function.
When you have the output, you just issue gui.Update where you populate View you want with data you've got. You can do it directly in that go routine or send the data back thru channel into some separate go routine which handles UI updates. That depends on you.

I would suggest to look into goroutine example which has some basic code for this kind of situation:
https://github.com/awesome-gocui/gocui/blob/master/_examples/goroutine.go

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants