[GH-ISSUE #781] Unable to "refresh" the form primitive on a page based on state changes #572

Closed
opened 2026-03-04 01:06:08 +03:00 by kerem · 5 comments
Owner

Originally created by @psparago on GitHub (Dec 16, 2022).
Original GitHub issue: https://github.com/rivo/tview/issues/781

Hello. New to tview and very thankful for the effort in producing a great TUI - with forms :-)! Sorry for the long post.

I have a page based tview application. There is a single main menu page with a List primitive, and many other pages that use form primitives. As you may guess, the list items in the menu page switch to the form pages.

I have one use case I'm struggling with where the editing of a form could require rebuilding the form (e.g. user presses a button and after the action happens the form should change to reflect the results of the action). I can't find a way to do this within the tview framework.

I should note that as a solution to resetting the form item buffers in the event of a "cancel", I implemented a simple Navigator with Activate() (and Deactivate()) operations. The Activate() is called before the page is switched to. (Perhaps there is a better way to do this?)

// GotoPage manages the active shell page state
// the OnDeactivate callback is called for the page becoming inactive
// the Activate callback is called for the page becoming active
//
// if the onShow callback returns an error, an error is displayed and
// the active page is set to the main menu page
func (n *NavigatorImpl) GotoPage(name string) {
    page := n.FindShellPage(name)
    if page != nil {
        if n.activePage != nil {
            n.activePage.Deactivate(n)
            n.activePage = nil
        }
        if err := page.Activate(n); err == nil {
            n.ClearStatus()
            n.activePage = page
             n.pages.SwitchToPage(name)
        } else {
            n.SetStatusError(err.Error())
            n.activePage = n.FindShellPage(MainMenuPage)
            n.pages.SwitchToPage(MainMenuPage)
        }
    }
}

The implementation of Activate() in the page is to do a form.Clear() and rebuild the form. Below is the code for this.

func (sp *somePage) Activate(n Navigator) (err error) {
    // sp.data is the true state of the form's model
    // sp.buffer is where the form items get the values
    // sb.buffer always gets reset to the model values before the form is rebuilt and 
    // the page is displayed
    *sp.buffer = *sp.data

    sp.createFormFields(n)
    return nil
}

func (sp *somePage) RefreshPage(n Navigator) {
	rsp.createFormFields(n)
}

func (sp *somePage) createFormFields(n Navigator) {
    form := sp.primitive.(*tview.Form)
    form.Clear(true)
   form.AddTextView( ...)
   ...
}

I have tried several approaches to solve the need to rebuild (redraw) the page's form when the state changes (without switching to another page first) but cannot get this fully working.

func (n *NavigatorImpl) RefreshPage() {
	if n.activePage != nil {
		go func() {
			n.app.QueueUpdate(func() {
				n.activePage.RefreshPage(n)
			})
		}()
	}
}

Setting breakpoints, I see that the RefreshPage() does get called and presumably the form is cleared and rebuilt (so the Update is not deadlocked?), but the form is not redrawn and the keyboard no longer responds.

So, if you've read this far, thank you. Any help or advice would be greatly appreciated.

Thank you.
Peter

Originally created by @psparago on GitHub (Dec 16, 2022). Original GitHub issue: https://github.com/rivo/tview/issues/781 Hello. New to tview and very thankful for the effort in producing a great TUI - with forms :-)! Sorry for the long post. I have a page based tview application. There is a single main menu page with a List primitive, and many other pages that use form primitives. As you may guess, the list items in the menu page switch to the form pages. I have one use case I'm struggling with where the editing of a form could require rebuilding the form (e.g. user presses a button and after the action happens the form should change to reflect the results of the action). I can't find a way to do this within the tview framework. I should note that as a solution to resetting the form item buffers in the event of a "cancel", I implemented a simple Navigator with Activate() (and Deactivate()) operations. The Activate() is called before the page is switched to. (Perhaps there is a better way to do this?) ``` // GotoPage manages the active shell page state // the OnDeactivate callback is called for the page becoming inactive // the Activate callback is called for the page becoming active // // if the onShow callback returns an error, an error is displayed and // the active page is set to the main menu page func (n *NavigatorImpl) GotoPage(name string) { page := n.FindShellPage(name) if page != nil { if n.activePage != nil { n.activePage.Deactivate(n) n.activePage = nil } if err := page.Activate(n); err == nil { n.ClearStatus() n.activePage = page n.pages.SwitchToPage(name) } else { n.SetStatusError(err.Error()) n.activePage = n.FindShellPage(MainMenuPage) n.pages.SwitchToPage(MainMenuPage) } } } ``` The implementation of Activate() in the page is to do a form.Clear() and rebuild the form. Below is the code for this. ``` func (sp *somePage) Activate(n Navigator) (err error) { // sp.data is the true state of the form's model // sp.buffer is where the form items get the values // sb.buffer always gets reset to the model values before the form is rebuilt and // the page is displayed *sp.buffer = *sp.data sp.createFormFields(n) return nil } func (sp *somePage) RefreshPage(n Navigator) { rsp.createFormFields(n) } func (sp *somePage) createFormFields(n Navigator) { form := sp.primitive.(*tview.Form) form.Clear(true) form.AddTextView( ...) ... } ``` I have tried several approaches to solve the need to rebuild (redraw) the page's form when the state changes (without switching to another page first) but cannot get this fully working. ``` func (n *NavigatorImpl) RefreshPage() { if n.activePage != nil { go func() { n.app.QueueUpdate(func() { n.activePage.RefreshPage(n) }) }() } } ``` Setting breakpoints, I see that the RefreshPage() does get called and presumably the form is cleared and rebuilt (so the Update is not deadlocked?), but the form is not redrawn and the keyboard no longer responds. So, if you've read this far, thank you. Any help or advice would be greatly appreciated. Thank you. Peter
kerem closed this issue 2026-03-04 01:06:09 +03:00
Author
Owner

@rivo commented on GitHub (Dec 17, 2022):

In your last snippet, you don't need to wrap the QueueUpdate() in a goroutine. Actually, unless RefreshPage() is called from a non-main goroutine, you don't even need QueueUpdate() at all. If RefreshPage() is called in direct response to a key or mouse event, you can (or probably even should) call RefreshPage() without queuing it.

But if RefreshPage() was called from a separate goroutine, maybe due to some external event (I don't know enough about your application so I don't know if this is the case), yes, you need to queue it. In that case, you'll want to use QueueUpdateDraw() instead of QueueUpdate(), to ensure the new form is also drawn afterwards. Otherwise, the form will get modified but the screen will not be redrawn.

The following Wiki page contains more information about concurrency:

https://github.com/rivo/tview/wiki/Concurrency

<!-- gh-comment-id:1356491139 --> @rivo commented on GitHub (Dec 17, 2022): In your last snippet, you don't need to wrap the `QueueUpdate()` in a goroutine. Actually, unless `RefreshPage()` is called from a non-main goroutine, you don't even need `QueueUpdate()` at all. If `RefreshPage()` is called in direct response to a key or mouse event, you can (or probably even _should_) call `RefreshPage()` without queuing it. But if `RefreshPage()` was called from a separate goroutine, maybe due to some external event (I don't know enough about your application so I don't know if this is the case), yes, you need to queue it. In that case, you'll want to use [`QueueUpdateDraw()`](https://pkg.go.dev/github.com/rivo/tview#Application.QueueUpdateDraw) instead of `QueueUpdate()`, to ensure the new form is also drawn afterwards. Otherwise, the form will get modified but the screen will not be redrawn. The following Wiki page contains more information about concurrency: https://github.com/rivo/tview/wiki/Concurrency
Author
Owner

@psparago commented on GitHub (Jan 1, 2023):

Hello again, I want to apologize for not getting back to you to thank you for your speedy response. With the holidays and a some work priorities and touch of COVID, I haven't been able to get back to this yet and I am eager to do so.

Once again, thank you so much for your hard work on, and support of, this great and much needed package.

<!-- gh-comment-id:1368448889 --> @psparago commented on GitHub (Jan 1, 2023): Hello again, I want to apologize for not getting back to you to thank you for your speedy response. With the holidays and a some work priorities and touch of COVID, I haven't been able to get back to this yet and I am eager to do so. Once again, thank you so much for your hard work on, and support of, this great and much needed package.
Author
Owner

@rivo commented on GitHub (Jan 1, 2023):

Hi, no problem at all. Some of the issues here have been open for months because I sometimes don't have the time to reply. (I was fast this time because I had some time on my hands over the holidays.)

If you find that this issue is resolved, please consider closing it.

<!-- gh-comment-id:1368454259 --> @rivo commented on GitHub (Jan 1, 2023): Hi, no problem at all. Some of the issues here have been open for months because I sometimes don't have the time to reply. (I was fast this time because I had some time on my hands over the holidays.) If you find that this issue is resolved, please consider closing it.
Author
Owner

@psparago commented on GitHub (Jan 2, 2023):

I guess I'm a bit confused and my apologies for that in advance :).

So, to recap:

The app has many pages. In each page, there is a private function that builds up a form with buttons. On some of the pages the state of what is being edited changes such that on a button press, the form should be rebuilt to reflect the change.

In one of the pages I have something like:

func (rsp *rcSetupPage) createFormFields(navigator common.Navigator) {
	form := rsp.primitive.(*tview.Form)
	form.Clear(true)

       // populate the form with several primitives ...

	if rsp.isRegistered {
		form.AddButton("Reset",
			func() {
				navigator.SetStatus(fmt.Sprintf("Resetting the %s configuration", rsp.displayName))
				if err := rsp.reset(); err != nil {
					navigator.SetStatusError(fmt.Sprintf("Error resetting the %s configuration: %s",
						rsp.displayName, err.Error()))
				} else {
					navigator.SetStatusSuccess(fmt.Sprintf("The %s configuration has been reset",
						rsp.displayName))
				}

				// reload the page in the unregistered state
                                 // this next line will cause a deadlock
                                 rsp.createFormFields(navigator)
			})
         }

}

Here's where I'm confused. Based on your response and the reading of the concurrency article, it seemed the correct action was to simply call createFormFields() on the button press. However, when I do this, the UI freezes (the keyboard is unresponsive (e.g. even ctrl-C does not work). Obviously, I am causing a deadlock.

However, my interpretation of your response was that rebuilding the form from a button press on one of the form's primitives is NOT a separate goroutine (or is it?), so therefore I do not need to queue an update. Clearly I'm wrong :-).

I know this is something you do to help the community so, if you have any time to spare, I'd be grateful for further guidance.

Also, just a note, just to be sure, I did try calling QueueUpdate as well and that also deadlocked.

	if rsp.isRegistered {
		form.AddButton("Reset",
			func() {
				navigator.SetStatus(fmt.Sprintf("Resetting the %s configuration", rsp.displayName))
				if err := rsp.reset(); err != nil {
					navigator.SetStatusError(fmt.Sprintf("Error resetting the %s configuration: %s",
						rsp.displayName, err.Error()))
				} else {
					navigator.SetStatusSuccess(fmt.Sprintf("The %s configuration has been reset",
						rsp.displayName))
				}

				// rebuild the form in unregistered mode
                                 // this approach also deadlocks
				navigator.App().QueueUpdate(func() {
					rsp.createFormFields(navigator)
				})
			})

Thank you again for your patience, support and providing this package to the community!

Peter...

<!-- gh-comment-id:1369226815 --> @psparago commented on GitHub (Jan 2, 2023): I guess I'm a bit confused and my apologies for that in advance :). So, to recap: The app has many pages. In each page, there is a private function that builds up a form with buttons. On some of the pages the state of what is being edited changes such that on a button press, the form should be rebuilt to reflect the change. In one of the pages I have something like: ``` func (rsp *rcSetupPage) createFormFields(navigator common.Navigator) { form := rsp.primitive.(*tview.Form) form.Clear(true) // populate the form with several primitives ... if rsp.isRegistered { form.AddButton("Reset", func() { navigator.SetStatus(fmt.Sprintf("Resetting the %s configuration", rsp.displayName)) if err := rsp.reset(); err != nil { navigator.SetStatusError(fmt.Sprintf("Error resetting the %s configuration: %s", rsp.displayName, err.Error())) } else { navigator.SetStatusSuccess(fmt.Sprintf("The %s configuration has been reset", rsp.displayName)) } // reload the page in the unregistered state // this next line will cause a deadlock rsp.createFormFields(navigator) }) } } ``` Here's where I'm confused. Based on your response and the reading of the concurrency article, it seemed the correct action was to simply call createFormFields() on the button press. However, when I do this, the UI freezes (the keyboard is unresponsive (e.g. even ctrl-C does not work). Obviously, I am causing a deadlock. However, my interpretation of your response was that rebuilding the form from a button press on one of the form's primitives is NOT a separate goroutine (or is it?), so therefore I do not need to queue an update. Clearly I'm wrong :-). I know this is something you do to help the community so, if you have any time to spare, I'd be grateful for further guidance. Also, just a note, just to be sure, I did try calling QueueUpdate as well and that also deadlocked. ``` if rsp.isRegistered { form.AddButton("Reset", func() { navigator.SetStatus(fmt.Sprintf("Resetting the %s configuration", rsp.displayName)) if err := rsp.reset(); err != nil { navigator.SetStatusError(fmt.Sprintf("Error resetting the %s configuration: %s", rsp.displayName, err.Error())) } else { navigator.SetStatusSuccess(fmt.Sprintf("The %s configuration has been reset", rsp.displayName)) } // rebuild the form in unregistered mode // this approach also deadlocks navigator.App().QueueUpdate(func() { rsp.createFormFields(navigator) }) }) ``` Thank you again for your patience, support and providing this package to the community! Peter...
Author
Owner

@psparago commented on GitHub (Jan 7, 2023):

I think I'll close this issue. I have worked around this. Thank you for your help.

<!-- gh-comment-id:1374609613 --> @psparago commented on GitHub (Jan 7, 2023): I think I'll close this issue. I have worked around this. Thank you for your help.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
starred/tview#572
No description provided.