This tutorial closely follows the structure of the urwid tutorial. When a type is named without an explicit Go package specifier for brevity, then the package is gowid.
Here is the traditional Hello World program, written for gowid. It displays "hello world" in the top left-hand corner of the terminal and will run until terminated with one of a few keypresses - Escape, Ctrl-c, q or Q. You can find this example at github.com/gcla/gowid/examples/gowid-tutorial1 and run it via gowid-tutorial1.
package main
import (
"github.com/gcla/gowid"
"github.com/gcla/gowid/widgets/text"
)
func main() {
txt := text.New("hello world")
app, _ := gowid.NewApp(gowid.AppArgs{View: txt})
app.SimpleMainLoop()
}- txt is a
text.Widgetand renders strings to its canvas. This widget also supports rendering collections of text with style and color attributes attached, called markup. Atext.Widgetcan render in urwid's "flow-mode", meaning itsRender()function is provided with a number of columns, but with no specified number of rows. The widget will create a canvas with as many rows as it needs to render suitably. A widget that renders in urwid's "box-mode" will be given both a number of columns and a number of rows, and must create a canvas of that size. -
- The second value returned from
NewAppis an error, which you should check - though there's not much to do except exit gracefully.
- The second value returned from
- The
app'sSimpleMainLoop()function will hand control over to gowid. Terminal events will be handled by gowid, and in particular, user input will be processed by the hierarchy of widgets that constitute the user interface. Input is handed to the root widget provided as theViewparameter toNewApp(). It may handle the event and it may also hand the event to its children. In this case,text.Widgetis the root of the hierarchy, and it does not accept user input. Gowid will then hand the input to anIUnhandledInputwhich is provided in this case bySimpleMainLoop(). It checks for Escape, Ctrl-c, q or Q - if any are detected, theapp'sQuit()function is called. After processing input,SimpleMainLoop()will then terminate.
The second example features a function that processes user input. If the user does not press a quit key, the "hello world" message is updated to show what key was pressed. You can find this example at github.com/gcla/gowid/examples/gowid-tutorial2 and run it via gowid-tutorial2.
package main
import (
"fmt"
"github.com/gcla/gowid"
"github.com/gcla/gowid/widgets/text"
"github.com/gdamore/tcell"
)
var txt *text.Widget
func unhandled(app gowid.IApp, ev interface{}) bool {
if evk, ok := ev.(*tcell.EventKey); ok {
switch evk.Rune() {
case 'q', 'Q':
app.Quit()
default:
txt.SetText(fmt.Sprintf("hello world - %c", evk.Rune()), app)
}
}
return true
}
func main() {
txt = text.New("hello world")
app, _ := gowid.NewApp(gowid.AppArgs{View: txt})
app.MainLoop(gowid.UnhandledInputFunc(unhandled))
}- The main loop is now provided an explicit function to process input that is not handled by any widget in the hierarchy. The
app'sMainLoop()function expects a type that implementsIUnhandledInput. The gowid typeUnhandledInputFuncis a simple function adapter that allows use of a regular Go function. - The function
unhandled()is given theappand the user input in the form of atcell.Event. Gowid relies throughout on the Go packagetcelland its representation of terminal input, both from the keyboard and the mouse. If the input provided is from the keyboard and is not one of the quit keys, the roottext.Widgetis updated to display the key that was pressed.
The third example demonstrates the use of color. You can find this example at github.com/gcla/gowid/examples/gowid-tutorial3 and run it via gowid-tutorial3.
package main
import (
"github.com/gcla/gowid"
"github.com/gcla/gowid/widgets/styled"
"github.com/gcla/gowid/widgets/text"
"github.com/gcla/gowid/widgets/vpadding"
)
func main() {
palette := gowid.Palette{
"banner": gowid.MakePaletteEntry(gowid.ColorBlack, gowid.NewUrwidColor("light gray")),
"streak": gowid.MakePaletteEntry(gowid.ColorBlack, gowid.ColorRed),
"bg": gowid.MakePaletteEntry(gowid.ColorBlack, gowid.ColorDarkBlue),
}
txt := text.NewFromContentExt(
text.NewContent([]text.ContentSegment{
text.StyledContent("hello world", gowid.MakePaletteRef("banner")),
}), text.Options{
Align: gowid.HAlignMiddle{},
})
map1 := styled.New(txt, gowid.MakePaletteRef("streak"))
vert := vpadding.New(map1, gowid.VAlignMiddle{}, gowid.RenderFlow{})
map2 := styled.New(vert, gowid.MakePaletteRef("bg"))
app, _ := gowid.NewApp(gowid.AppArgs{
View: map2,
Palette: palette,
})
app.SimpleMainLoop()
}- Display attributes are defined and named in a
Palette. The first argument toMakePaletteEntry()represents a foreground color and the second a background color. A similar gowid API allows for a third argument which represents text "styles" like underline and bold. - Gowid allows colors to be defined in a number of ways. Each color type must implement
IColor, an interface which provides for a conversion totcellcolor primitives (depending on the color mode of the terminal), ready for rendering on the terminal screen.ColorBlackis one of a set of predefinedTCellColors you can use. It trivially implementsIColor.NewUrwidColor()allows you to provide the name of a color that would be accepted by urwid and returns a*UrwidColor. You can read about urwid's color options here.
- You can pass the palette when initializing an
App. Certain gowid widgets that use colors and styles can then refer to palette entries by name when rendering by using theapp'sGetCellStyler()function and providing the name of the palette entry. For example, "hello world" appears in a called totext.StyledContent()which binds the display string together with a "cell styler" that comes from a reference to the palette. When this text widget is rendered, the string hello world is displayed in black text with a light gray background. - You can also give
text.Widgetan alignment parameter. When rendering, the widget will then shift the text left or right depending on how many columns are required. But note that only "hello world" is styled, so the extra space on the left and right is blank. - The text widget is enclosed in a
styled.Widgetand then inside avpadding.Widgetthat is also styled. Thestyled.Widgetwill apply the supplied style "underneath" any styling currently in use for the given widget. This has the effect of applying "streak" in the unstyled areas to the left and right of "hello world". Similarly,map2will apply "bg" in the unstyled areas above and below "hello world". vpadding.New()has a third argument,RenderFlow{}. This determines how the inner widget,map1, is rendered. In this case, it says that whatever size argument is provided when renderingvert, use flow-mode to rendermap.
The screenshots above show how the app reacts to being resized. You can see here that gowid's text widget is less sophisticated than urwid's. When made too narrow to fit on one line, the widget should really break "hello world" on the space in the middle. At the moment it doesn't do that. Room for improvement!
This program is a glitzier "hello world". This example is at github.com/gcla/gowid/examples/helloworld and you can run it via gowid-helloworld.
import (
"github.com/gcla/gowid"
"github.com/gcla/gowid/widgets/divider"
"github.com/gcla/gowid/widgets/pile"
"github.com/gcla/gowid/widgets/styled"
"github.com/gcla/gowid/widgets/text"
"github.com/gcla/gowid/widgets/vpadding"
)
func main() {
palette := gowid.Palette{
"banner": gowid.MakePaletteEntry(gowid.ColorWhite, gowid.MakeRGBColor("#60d")),
"streak": gowid.MakePaletteEntry(gowid.ColorNone, gowid.MakeRGBColor("#60a")),
"inside": gowid.MakePaletteEntry(gowid.ColorNone, gowid.MakeRGBColor("#808")),
"outside": gowid.MakePaletteEntry(gowid.ColorNone, gowid.MakeRGBColor("#a06")),
"bg": gowid.MakePaletteEntry(gowid.ColorNone, gowid.MakeRGBColor("#d06")),
}
div := divider.NewBlank()
outside := styled.New(div, gowid.MakePaletteRef("outside"))
inside := styled.New(div, gowid.MakePaletteRef("inside"))
helloworld := styled.New(
text.NewFromContentExt(
text.NewContent([]text.ContentSegment{
text.StyledContent("Hello World", gowid.MakePaletteRef("banner")),
}),
text.Options{
Align: gowid.HAlignMiddle{},
},
),
gowid.MakePaletteRef("streak"),
)
f := gowid.RenderFlow{}
view := styled.New(
vpadding.New(
pile.New([]gowid.IContainerWidget{
&gowid.ContainerWidget{IWidget: outside, D: f},
&gowid.ContainerWidget{IWidget: inside, D: f},
&gowid.ContainerWidget{IWidget: helloworld, D: f},
&gowid.ContainerWidget{IWidget: inside, D: f},
&gowid.ContainerWidget{IWidget: outside, D: f},
}),
gowid.VAlignMiddle{},
f),
gowid.MakePaletteRef("bg"),
)
app, _ := gowid.NewApp(gowid.AppArgs{
View: view,
Palette: &palette,
})
app.SimpleMainLoop()
}- To create the vertical effect, a
pile.Widgetis used. The blank lines are made with adivider.Widget, whereoutsideandinsideare styled with different colors. The widget pile is centered with avpadding.WidgetandVAlignMiddle{}, and the rest of the blank space is styled with "bg". - This example uses a new
IColor-creating function,MakeRGBColor(). You can provide hex values for red, green and blue, where each value should range from 0x0 to 0xF. If the terminal is in a mode with fewer color combinations, such as 256-color mode, the chosen RGB value is interpolated into an 8x8x8 color cube to find the closest match - in exactly the same fashion as urwid.
The next example asks for the user's name. When the user presses enter, it displays a friendly personalized message. The q or Q key will terminate the app. You can find this example at github.com/gcla/gowid/examples/gowid-tutorial4 and run it via gowid-tutorial4.
package main
import (
"fmt"
"github.com/gcla/gowid"
"github.com/gcla/gowid/widgets/edit"
"github.com/gcla/gowid/widgets/text"
"github.com/gdamore/tcell"
)
//======================================================================
type QuestionBox struct {
gowid.IWidget
}
func (w *QuestionBox) UserInput(ev interface{}, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) bool {
res := true
if evk, ok := ev.(*tcell.EventKey); ok {
switch evk.Key() {
case tcell.KeyEnter:
w.IWidget = text.New(fmt.Sprintf("Nice to meet you, %s.\n\nPress Q to exit.", w.IWidget.(*edit.Widget).Text()))
default:
res = w.IWidget.UserInput(w.IWidget, ev, size, focus, app)
}
}
return res
}
func main() {
edit := edit.New(edit.Options{Caption: "What is your name?\n"})
qb := &QuestionBox{edit}
app, _ := gowid.NewApp(gowid.AppArgs{View: qb})
app.MainLoop(gowid.UnhandledInputFunc(gowid.HandleQuitKeys))
}- This example shows how you can extend a widget.
QuestionBoxembeds anIWidgetmeaning that it itself implementsIWidget. Themain()function sets up aQuestionBoxwidget that extends anedit.Widget. That meansQuestionBoxwill render likeedit.Widget. ButQuestionBoxprovides a new implementation ofUserInput(), one of the requirements ofIWidget. If the key pressed is not "enter" then it defers to its embeddedIWidget's implementation ofUserInput(). That means the embeddededit.Widgetwill process it, and it will accumulate the user's typed input and display that when rendered. But if the user presses "enter",QuestionBoxreplaces its embedded widget with a newtext.Widgetthat displays a message to the name the user has typed in. - When constructing the "Nice to meet you" message, the embedded
IWidgetis cast to an*edit.Widget. That's safe because we control the embedded widget, so we know its type. Note that the concrete type is a pointer - gowid widgets have pointer-receiver functions, for the most part, including all methods used to implementIWidget. - There are pitfalls if your mindset is "object-oriented" like Java or older-style C++. My first instinct was to view
UserInput()as "overriding" the embedded widget'sUserInput(). And it's true that our new implementation will be called from anIWidgetif the interface's type is aQuestionBoxpointer. But let's say you also provide a specialized implementation forRenderSize()anotherIWidgetrequirement. And let's sayUserInput()calls a method which is not "overridden" inedit.Widget, and that in turn callsRenderSize(); then your new version will not be called. The receiver will be theedit.Widgetpointer. Go does not support dynamic dispatch except for calls through an interface. I certainly misunderstood that when getting going. More details here: https://golang.org/doc/faq#How_do_I_get_dynamic_dispatch_of_methods.
This example shows how you can respond to widget actions, like a button click. See this example at github.com/gcla/gowid/examples/gowid-tutorial5 and run it via gowid-tutorial5.
package main
import (
"fmt"
"github.com/gcla/gowid"
"github.com/gcla/gowid/widgets/button"
"github.com/gcla/gowid/widgets/divider"
"github.com/gcla/gowid/widgets/edit"
"github.com/gcla/gowid/widgets/pile"
"github.com/gcla/gowid/widgets/styled"
"github.com/gcla/gowid/widgets/text"
)
//======================================================================
func main() {
ask := edit.New(edit.Options{Caption: "What is your name?\n"})
reply := text.New("")
btn := button.New(text.New("Exit"))
sbtn := styled.New(btn, gowid.MakeStyledAs(gowid.StyleReverse))
div := divider.NewBlank()
btn.OnClick(gowid.WidgetCallback{"cb", func(app gowid.IApp, w gowid.IWidget) {
app.Quit()
}})
ask.OnTextSet(gowid.WidgetCallback{"cb", func(app gowid.IApp, w gowid.IWidget) {
if ask.Text() == "" {
reply.SetText("", app)
} else {
reply.SetText(fmt.Sprintf("Nice to meet you, %s", ask.Text()), app)
}
}})
f := gowid.RenderFlow{}
view := pile.New([]gowid.IContainerWidget{
&gowid.ContainerWidget{IWidget: ask, D: f},
&gowid.ContainerWidget{IWidget: div, D: f},
&gowid.ContainerWidget{IWidget: reply, D: f},
&gowid.ContainerWidget{IWidget: div, D: f},
&gowid.ContainerWidget{IWidget: sbtn, D: f},
})
app, _ := gowid.NewApp(gowid.AppArgs{View: view})
app.SimpleMainLoop()
}- The bottom-most widget in the pile is a
button.Widget. It itself wraps an inner widget, and when rendered will add characters on the left and right of the inner widget to create a button effect. button.Widgetcan call an interface method when it's clicked.OnClick()expects anIWidgetChangedCallback. You can use theWidgetCallback()adapter to pass a simple function.- The first parameter of
WidgetCallbackis aninterface{}. It's meant to uniquely identify this callback instance so that if you later need to remove the callback, you can by passing the sameinterface{}. Here I've used a simple string, "cb". The callbacks are scoped to the widget, so you can use the same callback identifier when registering callbacks for other widgets. edit.Widgetcan call an interface method when its text changes. In this example, every time the user enters a character,askwill update thereplywidget so that it displays a message.- The callback will be called with two arguments - the application
appand the widget issuing the callback. But if it's more convenient, you can rely on Go's scope rules to capture the widgets that you need to modify in the callback.ask's callback refers toreplyand not the callback parameterw. - The
<exit>button is styled usingMakeStyleAs(), which applies a text style like underline, bold or reverse-video. No colors are given, so the button will use the terminal's default colors.
The final example asks the same question over and over, and collects the results. You can go back and edit previous answers and the program will update its response. It demonstrates the use of a gowid listbox. This example is available at github.com/gcla/gowid/examples/gowid-tutorial6 and you can run it via gowid-tutorial6.
package main
import (
"fmt"
"github.com/gcla/gowid"
"github.com/gcla/gowid/widgets/edit"
"github.com/gcla/gowid/widgets/list"
"github.com/gcla/gowid/widgets/pile"
"github.com/gcla/gowid/widgets/text"
"github.com/gdamore/tcell"
)
//======================================================================
func question() *pile.Widget {
return pile.New([]gowid.IContainerWidget{
&gowid.ContainerWidget{
IWidget: edit.New(edit.Options{Caption: "What is your name?\n"}),
D: gowid.RenderFlow{},
},
})
}
func answer(name string) *gowid.ContainerWidget {
return &gowid.ContainerWidget{
IWidget: text.New(fmt.Sprintf("Nice to meet you, %s", name)),
D: gowid.RenderFlow{},
}
}
type ConversationWidget struct {
*list.Widget
}
func NewConversationWidget() *ConversationWidget {
widgets := make([]gowid.IWidget, 1)
widgets[0] = question()
lb := list.New(list.NewSimpleListWalker(widgets))
return &ConversationWidget{lb}
}
func (w *ConversationWidget) UserInput(ev interface{}, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) bool {
res := false
if evk, ok := ev.(*tcell.EventKey); ok && evk.Key() == tcell.KeyEnter {
res = true
focus := w.Walker().Focus()
focusPile := focus.Widget.(*pile.Widget)
pileChildren := focusPile.SubWidgets()
ed := pileChildren[0].(*gowid.ContainerWidget).SubWidget().(*edit.Widget)
focusPile.SetSubWidgets(append(pileChildren[0:1], answer(ed.Text())), app)
walker := w.Widget.Walker().(*list.SimpleListWalker)
walker.Widgets = append(walker.Widgets, question())
nextPos := walker.Next(focus.Pos).Pos
walker.SetFocus(nextPos)
w.Widget.GoToBottom(app)
} else {
res = gowid.UserInput(w.Widget, ev, size, focus, app)
}
return res
}
func main() {
app, _ := gowid.NewApp(gowid.AppArgs{View: NewConversationWidget()})
app.SimpleMainLoop()
}- In this example I've created a new widget called
ConversationWidget. It embeds a*list.Widgetand renders like one, but its input is handled specially. Alist.Widgetis a more general form ofpile.Widget. You provide alist.Widgetwith alist.IListWalkerwhich is like a widget iterator. It can return the current "focus" widget, move to the next widget and move to the previous widget. This allows it, potentially, to be unbounded. For an example of that in action, seegithub.com/gcla/gowid/examples/gowid-fibwhich is plagiarized heavily from urwid'sfib.pyexample. - The list walker in this example is a wrapper around a Go array of widgets. Each widget in the list is a
pile.Widgetcontaining either- A single
edit.Widgetasking for a name, or - An
edit.Widgetasking for a name and the user's response as atext.Widget.
- A single
- When the user presses "enter" in an
edit.Widget, the current focuspile.Widgetis manipulated. Any previous answer is eliminated, and a new answer is appended. The walker is advanced one position, and finally, thelist.Widgetis told to render so the focus widget is at the bottom of the canvas. There is a good deal of type-casting here, but again it's safe because we control the concrete types involved in the construction of this widget hierarchy. - When the user presses the up and down cursor keys in the context of a
list.Widget, the widget's walker adjusts its focus widget. In this example, focus will move from onepile.Widgetto another. Within thatpile.Widgetthere are at most two widgets - oneedit.Widgetand onetext.Widget. Anedit.Widgetis "selectable", which means it is useful for it to be given the focus. Atext.Widgetis not selectable, which means there's no point in it being given the focus. That means that as the user moves up and down, focus will always be given to anedit.Widget. Do note though that just like in urwid, a non-selectable widget can still be given focus e.g. if there is no other selectable widget in the current widget scope. - If you're following along with the urwid tutorial, you'll noticed that this example is a little longer than the corresponding urwid program. I attribute that to Go having fewer short-cuts than python, and forcing the programmer to be more explicit. I like that, personally.