[GH-ISSUE #662] Example of popup Modal by shortcut #487

Closed
opened 2026-03-04 01:05:25 +03:00 by kerem · 17 comments
Owner

Originally created by @abitrolly on GitHub (Oct 11, 2021).
Original GitHub issue: https://github.com/rivo/tview/issues/662

https://github.com/rivo/tview/wiki/Modal example list modal hardwired to root or Pages elements. But I already have List as my root element, and I want to show modal (without buttons) when I press i and close it on Esc or clicking outside. Would be nice to have an example that demonstrates how that's possible.

The straightforward approach I tried that doesn't work.

        // Create UI elements
        list := tview.NewList()
        // Set Box properties on the List component
        list.SetBorder(true).SetTitle(" package updates ")
        // Set List properties
        list.ShowSecondaryText(false)

        infobox := tview.NewModal().
                AddButtons([]string{"Quit", "Cancel"}).
                SetText("Lorem Ipsum Is A Pain")

        // Attach elements and start UI app
        app := tview.NewApplication()
        app.SetRoot(list, true) // SetRoot(root Primitive, fullscreen bool)
        app.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
                if event.Key() == tcell.KeyESC {
                        app.Stop()
                }
                // Key is a character
                if event.Key() == tcell.KeyRune {
                        if event.Rune() == 'q' {
                                app.Stop()
                        }
                        if event.Rune() == 'i' {
                                app.SetFocus(infobox)
                        }
                }
                return event
        })

After pressing i the List border becomes single line, and it stops responding to events. Nothing else is shown. Pressing Esc doesn't revert to previous state, but exits the application.

Originally created by @abitrolly on GitHub (Oct 11, 2021). Original GitHub issue: https://github.com/rivo/tview/issues/662 https://github.com/rivo/tview/wiki/Modal example list modal hardwired to root or Pages elements. But I already have List as my root element, and I want to show modal (without buttons) when I press `i` and close it on `Esc` or clicking outside. Would be nice to have an example that demonstrates how that's possible. The straightforward approach I tried that doesn't work. ```go // Create UI elements list := tview.NewList() // Set Box properties on the List component list.SetBorder(true).SetTitle(" package updates ") // Set List properties list.ShowSecondaryText(false) infobox := tview.NewModal(). AddButtons([]string{"Quit", "Cancel"}). SetText("Lorem Ipsum Is A Pain") // Attach elements and start UI app app := tview.NewApplication() app.SetRoot(list, true) // SetRoot(root Primitive, fullscreen bool) app.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { if event.Key() == tcell.KeyESC { app.Stop() } // Key is a character if event.Key() == tcell.KeyRune { if event.Rune() == 'q' { app.Stop() } if event.Rune() == 'i' { app.SetFocus(infobox) } } return event }) ``` After pressing `i` the List border becomes single line, and it stops responding to events. Nothing else is shown. Pressing `Esc` doesn't revert to previous state, but exits the application.
kerem closed this issue 2026-03-04 01:05:25 +03:00
Author
Owner

@rivo commented on GitHub (Oct 29, 2021):

If you put your list at the root, you won't be able to show anything other than that list. (SetFocus(infobox) will just shift the focus to a widget that is not part of the widget tree and thus not drawn anywhere.)

So to show more widgets, in your case a Modal, you need some kind of layout widget at the root, e.g. Grid, Flex, or Pages. From what you're describing, I think Pages is what you want.

<!-- gh-comment-id:954804072 --> @rivo commented on GitHub (Oct 29, 2021): If you put your list at the root, you won't be able to show anything other than that list. (`SetFocus(infobox)` will just shift the focus to a widget that is not part of the widget tree and thus not drawn anywhere.) So to show more widgets, in your case a `Modal`, you need some kind of layout widget at the root, e.g. `Grid`, `Flex`, or `Pages`. From what you're describing, I think `Pages` is what you want.
Author
Owner

@abitrolly commented on GitHub (Oct 30, 2021):

@rivo why it is not possible to attach the Modal dynamically to the root element?

<!-- gh-comment-id:955192160 --> @abitrolly commented on GitHub (Oct 30, 2021): @rivo why it is not possible to attach the Modal dynamically to the root element?
Author
Owner

@rivo commented on GitHub (Oct 30, 2021):

Sure, it's possible. Just use app.SetRoot():

package main

import (
	"github.com/gdamore/tcell/v2"
	"github.com/rivo/tview"
)

func main() {
	// Create UI elements
	list := tview.NewList()
	// Set Box properties on the List component
	list.SetBorder(true).SetTitle(" package updates ")
	// Set List properties
	list.ShowSecondaryText(false)

	infobox := tview.NewModal().
		AddButtons([]string{"Quit", "Cancel"}).
		SetText("Lorem Ipsum Is A Pain")

	// Attach elements and start UI app
	app := tview.NewApplication()
	app.SetRoot(list, true) // SetRoot(root Primitive, fullscreen bool)
	app.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
		if event.Key() == tcell.KeyESC {
			app.Stop()
		}
		// Key is a character
		if event.Key() == tcell.KeyRune {
			if event.Rune() == 'q' {
				app.Stop()
			}
			if event.Rune() == 'i' {
				app.SetRoot(infobox, false)
			}
		}
		return event
	})
	app.Run()
}

Now (after pressing i) your list is not attached anymore, though.

<!-- gh-comment-id:955203604 --> @rivo commented on GitHub (Oct 30, 2021): Sure, it's possible. Just use `app.SetRoot()`: ```go package main import ( "github.com/gdamore/tcell/v2" "github.com/rivo/tview" ) func main() { // Create UI elements list := tview.NewList() // Set Box properties on the List component list.SetBorder(true).SetTitle(" package updates ") // Set List properties list.ShowSecondaryText(false) infobox := tview.NewModal(). AddButtons([]string{"Quit", "Cancel"}). SetText("Lorem Ipsum Is A Pain") // Attach elements and start UI app app := tview.NewApplication() app.SetRoot(list, true) // SetRoot(root Primitive, fullscreen bool) app.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { if event.Key() == tcell.KeyESC { app.Stop() } // Key is a character if event.Key() == tcell.KeyRune { if event.Rune() == 'q' { app.Stop() } if event.Rune() == 'i' { app.SetRoot(infobox, false) } } return event }) app.Run() } ``` Now (after pressing <kbd>i</kbd>) your list is not attached anymore, though.
Author
Owner

@abitrolly commented on GitHub (Nov 9, 2021):

Nice. I think this high level overview is not described anywhere. Namely that for building a UI with tview you first define a tree of elements, and then attach that tree to the app screen with app.Root() call. If you define an element that is not in a tree or not attached, then it won't be visible.

Now for the example - it works, but the list is gone from the background when switching root to infobox, because as you said it is not attached anymore. So is it possible to attach the infobox as a child modal to the list?

<!-- gh-comment-id:964168635 --> @abitrolly commented on GitHub (Nov 9, 2021): Nice. I think this high level overview is not described anywhere. Namely that for building a UI with `tview` you first define a tree of elements, and then attach that tree to the app screen with `app.Root()` call. If you define an element that is not in a tree or not attached, then it won't be visible. Now for the example - it works, but the list is gone from the background when switching root to infobox, because as you said it is not attached anymore. So is it possible to attach the infobox as a child modal to the list?
Author
Owner

@rivo commented on GitHub (Nov 9, 2021):

I think this high level overview is not described anywhere.

I realize that. I have yet to add more examples and tutorials. It's just due to lack of time that this doesn't exist yet.

So is it possible to attach the infobox as a child modal to the list?

As mentioned in my first reply, to attach multiple primitives to the application, you need a container primitive (Flex, Grid, or Pages) at the root. In your example, I would suggest Pages, to which you add your list and your modal. Then use SwitchToPage() to switch between the two.

<!-- gh-comment-id:964266977 --> @rivo commented on GitHub (Nov 9, 2021): > I think this high level overview is not described anywhere. I realize that. I have yet to add more examples and tutorials. It's just due to lack of time that this doesn't exist yet. > So is it possible to attach the infobox as a child modal to the list? As mentioned in my first reply, to attach multiple primitives to the application, you need a container primitive (`Flex`, `Grid`, or `Pages`) at the root. In your example, I would suggest `Pages`, to which you add your list and your modal. Then use [`SwitchToPage()`](https://pkg.go.dev/github.com/rivo/tview#Pages.SwitchToPage) to switch between the two.
Author
Owner

@abitrolly commented on GitHub (Nov 10, 2021):

In Qt/PySide2 any Widget can be root, much like in tview, but also any widget can have a parent and children - https://wiki.qt.io/Qt_for_Beginners#parenting_system - so I kind of expected something similar. In tview the only widgets that allow children are "layout" widget with its own APIs:

  • Pages - .addPage()
  • Flex - .addItem()
  • Grid - .addItem()
<!-- gh-comment-id:965029105 --> @abitrolly commented on GitHub (Nov 10, 2021): In `Qt/PySide2` any `Widget` can be root, much like in `tview`, but also any widget can have a parent and children - https://wiki.qt.io/Qt_for_Beginners#parenting_system - so I kind of expected something similar. In `tview` the only widgets that allow children are "layout" widget with its own APIs: * Pages - `.addPage()` * Flex - `.addItem()` * Grid - `.addItem()`
Author
Owner

@rivo commented on GitHub (Nov 11, 2021):

Yes, that's correct.

<!-- gh-comment-id:966024193 --> @rivo commented on GitHub (Nov 11, 2021): Yes, that's correct.
Author
Owner

@abitrolly commented on GitHub (Nov 11, 2021):

@rivo what do you think about adding bit flags to .AddPage() API. Right now the code like this is not really readable.

		AddPage("background", background, true, true).

I am thinking about something line this.

		AddPage("background", background, Resize | Visible).

Or maybe a backward compatible solution.

		AddPage("background", background, Resize, Visible).
<!-- gh-comment-id:966166107 --> @abitrolly commented on GitHub (Nov 11, 2021): @rivo what do you think about adding bit flags to `.AddPage()` API. Right now the code like this is not really readable. ```go AddPage("background", background, true, true). ``` I am thinking about something line this. ```go AddPage("background", background, Resize | Visible). ``` Or maybe a backward compatible solution. ```go AddPage("background", background, Resize, Visible). ```
Author
Owner

@abitrolly commented on GitHub (Nov 11, 2021):

@rivo SwitchToPage() hides the list too instead of sending it to the background. The example at https://github.com/rivo/tview/wiki/Modal is not using switching, so it is not affected.

<!-- gh-comment-id:966238539 --> @abitrolly commented on GitHub (Nov 11, 2021): @rivo `SwitchToPage()` hides the `list` too instead of sending it to the background. The example at https://github.com/rivo/tview/wiki/Modal is not using switching, so it is not affected.
Author
Owner

@abitrolly commented on GitHub (Nov 11, 2021):

I have to use .ShowPage("infobox") to bring up modal on a background and then use .SwitchToPage("list") to turn it off.

<!-- gh-comment-id:966256656 --> @abitrolly commented on GitHub (Nov 11, 2021): I have to use `.ShowPage("infobox")` to bring up modal on a background and then use `.SwitchToPage("list")` to turn it off.
Author
Owner

@abitrolly commented on GitHub (Nov 11, 2021):

Now I need to figure out how to deal with modal specific shortcuts, so that Esc closed modal and not the whole app.

<!-- gh-comment-id:966256943 --> @abitrolly commented on GitHub (Nov 11, 2021): Now I need to figure out how to deal with modal specific shortcuts, so that `Esc` closed modal and not the whole app.
Author
Owner

@rivo commented on GitHub (Nov 11, 2021):

Box.SetInputCapture() is your friend.

<!-- gh-comment-id:966494375 --> @rivo commented on GitHub (Nov 11, 2021): [`Box.SetInputCapture()`](https://pkg.go.dev/github.com/rivo/tview#Box.SetInputCapture) is your friend.
Author
Owner

@abitrolly commented on GitHub (Nov 11, 2021):

Is there a way for a Modal to "quit" itself without holding a reference to global Page?

          infobox := tview.NewModal().
                  AddButtons([]string{"Quit", "Cancel"}).
                  SetText("Lorem Ipsum Is A Pain").
                  SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
                          if event.Key() == tcell.KeyESC {
>>                                pager.HidePage("infobox")
                          }
                          return event
                  })
<!-- gh-comment-id:966690290 --> @abitrolly commented on GitHub (Nov 11, 2021): Is there a way for a Modal to "quit" itself without holding a reference to global `Page`? ```go infobox := tview.NewModal(). AddButtons([]string{"Quit", "Cancel"}). SetText("Lorem Ipsum Is A Pain"). SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { if event.Key() == tcell.KeyESC { >> pager.HidePage("infobox") } return event }) ```
Author
Owner

@abitrolly commented on GitHub (Nov 12, 2021):

I can not stop ESC event from propagating by returning nil. With the following code the app still exits when ESC is pressed in the modal.

          infobox.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
                  if event.Key() == tcell.KeyESC {
                          log.Println("ESC pressed")
                          //pager.HidePage("infobox")
                          return nil
                  }
                  return nil //event
          })
<!-- gh-comment-id:966865073 --> @abitrolly commented on GitHub (Nov 12, 2021): I can not stop ESC event from propagating by returning `nil`. With the following code the app still exits when ESC is pressed in the modal. ```go infobox.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { if event.Key() == tcell.KeyESC { log.Println("ESC pressed") //pager.HidePage("infobox") return nil } return nil //event }) ```
Author
Owner

@rivo commented on GitHub (Nov 13, 2021):

I don't know why your app exits when hitting Esc. That is not built-in. It only exits upon Ctrl-C. If you want to stop it from doing that, you can do that with Application.SetInputCapture() on the application level. So I'm not sure what the issue with Esc is here. If you post some code that I can run, I might be able to tell you.

Is there a way for a Modal to "quit" itself without holding a reference to global Page?

The package doesn't provide a way for primitives to make themselves "disappear" and have container primitives handle that automatically. You can probably code up your own dependency injection by creating a custom constructor for your modal. Or you handle Modal.SetDoneFunc() somewhere outside. But eventually, the Pages class needs to switch the order of the primitives displayed to make that visible to the user.

<!-- gh-comment-id:967858247 --> @rivo commented on GitHub (Nov 13, 2021): I don't know why your app exits when hitting <kbd>Esc</kbd>. That is not built-in. It only exits upon <kbd>Ctrl-C</kbd>. If you want to stop it from doing that, you can do that with [`Application.SetInputCapture()`](https://pkg.go.dev/github.com/rivo/tview#Application.SetInputCapture) on the application level. So I'm not sure what the issue with <kbd>Esc</kbd> is here. If you post some code that I can run, I might be able to tell you. > Is there a way for a Modal to "quit" itself without holding a reference to global `Page`? The package doesn't provide a way for primitives to make themselves "disappear" and have container primitives handle that automatically. You can probably code up your own dependency injection by creating a custom constructor for your modal. Or you handle [`Modal.SetDoneFunc()`](https://pkg.go.dev/github.com/rivo/tview#Modal.SetDoneFunc) somewhere outside. But eventually, the `Pages` class needs to switch the order of the primitives displayed to make that visible to the user.
Author
Owner

@abitrolly commented on GitHub (Nov 13, 2021):

@rivo here is the code.

package main

import (
	"fmt"
	"log"

	"github.com/gdamore/tcell/v2"
	"github.com/rivo/tview"
)

// Define some constants for readability
const Fullscreen = true
const Resize = true
const Visible = true
const Hidden = false

type PackageUpdate struct {
	name    string
	oldver  string
	newver  string
	summary string
}

// UpdateList runs and parses `dnf repoquery` command that returns the list
// of upcoming updates
func UpdateList() []PackageUpdate {
	var result []PackageUpdate
	result = append(result, PackageUpdate{"pkg1", "", "0.1", "package 1"})
	result = append(result, PackageUpdate{"pkg2", "", "0.1", "package 2"})
	result = append(result, PackageUpdate{"pkg3", "", "0.3", "package 3"})
	result = append(result, PackageUpdate{"pkg4", "", "0.1", "package 4"})
	return result
}

func main() {
	// Setup logging
	log.SetFlags(0)

	// Read `dnf` data
	log.Println("[INFO] Getting `dnf` data ...")
	updates := UpdateList()

	// Create UI elements
	// 1. list of updates
	list := tview.NewList()
	// Set Box properties on the List component
	list.SetBorder(true).SetTitle(" package updates ")
	// Set List properties
	list.SetWrapAround(false)
	list.ShowSecondaryText(false)
	for _, u := range updates {
		verstr := u.newver
		if u.newver != u.oldver {
			verstr += " (" + u.oldver + ")"
		}
		update := fmt.Sprintf("%-30s  %-20s %s", u.name, verstr, u.summary)
		list.AddItem(update, "", 0, nil)
	}
	// 2. modal with detailed info
	infobox := tview.NewModal().
		AddButtons([]string{"Quit", "Cancel"}).
		SetText("Lorem Ipsum Is A Pain")
	// 3. layout with two pages (second page is needed to show modal)
	pager := tview.NewPages().
		AddPage("list", list, Resize, Visible).
		AddPage("infobox", infobox, Resize, Hidden)

	// Attach UI elements tree to app and start it
	app := tview.NewApplication()
	app.SetRoot(pager, Fullscreen)
	// Set event handling
	infobox.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
		if event.Key() == tcell.KeyESC {
			log.Println("ESC pressed")
			//pager.HidePage("infobox")
			return nil
		}
		return nil //event
	})
	app.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
		if event.Key() == tcell.KeyESC {
			app.Stop()
		}
		// Key is a character
		if event.Key() == tcell.KeyRune {
			if event.Rune() == 'q' {
				app.Stop()
			}
			if event.Rune() == 'i' {
				pager.ShowPage("infobox")
			}
			if event.Rune() == 'o' {
				pager.SwitchToPage("list")
			}
		}
		return event
	})
	if err := app.Run(); err != nil {
		log.Panicln(err)
	}
}
<!-- gh-comment-id:968157127 --> @abitrolly commented on GitHub (Nov 13, 2021): @rivo here is the code. ```go package main import ( "fmt" "log" "github.com/gdamore/tcell/v2" "github.com/rivo/tview" ) // Define some constants for readability const Fullscreen = true const Resize = true const Visible = true const Hidden = false type PackageUpdate struct { name string oldver string newver string summary string } // UpdateList runs and parses `dnf repoquery` command that returns the list // of upcoming updates func UpdateList() []PackageUpdate { var result []PackageUpdate result = append(result, PackageUpdate{"pkg1", "", "0.1", "package 1"}) result = append(result, PackageUpdate{"pkg2", "", "0.1", "package 2"}) result = append(result, PackageUpdate{"pkg3", "", "0.3", "package 3"}) result = append(result, PackageUpdate{"pkg4", "", "0.1", "package 4"}) return result } func main() { // Setup logging log.SetFlags(0) // Read `dnf` data log.Println("[INFO] Getting `dnf` data ...") updates := UpdateList() // Create UI elements // 1. list of updates list := tview.NewList() // Set Box properties on the List component list.SetBorder(true).SetTitle(" package updates ") // Set List properties list.SetWrapAround(false) list.ShowSecondaryText(false) for _, u := range updates { verstr := u.newver if u.newver != u.oldver { verstr += " (" + u.oldver + ")" } update := fmt.Sprintf("%-30s %-20s %s", u.name, verstr, u.summary) list.AddItem(update, "", 0, nil) } // 2. modal with detailed info infobox := tview.NewModal(). AddButtons([]string{"Quit", "Cancel"}). SetText("Lorem Ipsum Is A Pain") // 3. layout with two pages (second page is needed to show modal) pager := tview.NewPages(). AddPage("list", list, Resize, Visible). AddPage("infobox", infobox, Resize, Hidden) // Attach UI elements tree to app and start it app := tview.NewApplication() app.SetRoot(pager, Fullscreen) // Set event handling infobox.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { if event.Key() == tcell.KeyESC { log.Println("ESC pressed") //pager.HidePage("infobox") return nil } return nil //event }) app.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { if event.Key() == tcell.KeyESC { app.Stop() } // Key is a character if event.Key() == tcell.KeyRune { if event.Rune() == 'q' { app.Stop() } if event.Rune() == 'i' { pager.ShowPage("infobox") } if event.Rune() == 'o' { pager.SwitchToPage("list") } } return event }) if err := app.Run(); err != nil { log.Panicln(err) } } ```
Author
Owner

@rivo commented on GitHub (Nov 13, 2021):

Ok, well, that makes sense. The Application.SetInputCapture() call is evaluated first before any other overrides in primitives. So if you globally stop the application upon hitting Esc, that will always happen, regardless of what you do in your Modal. Same with q.

It seems to me that you only want to close the application with Esc when the user is in the List. So you'll want to capture the Esc event in your list instead of globally in the application.

<!-- gh-comment-id:968158591 --> @rivo commented on GitHub (Nov 13, 2021): Ok, well, that makes sense. The `Application.SetInputCapture()` call is evaluated first before any other overrides in primitives. So if you globally stop the application upon hitting <kbd>Esc</kbd>, that will always happen, regardless of what you do in your `Modal`. Same with <kbd>q</kbd>. It seems to me that you only want to close the application with <kbd>Esc</kbd> when the user is in the `List`. So you'll want to capture the <kbd>Esc</kbd> event in your list instead of globally in the application.
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#487
No description provided.