pax_global_header00006660000000000000000000000064142623445400014516gustar00rootroot0000000000000052 comment=fe6367ab360345bffb6ba27a681227a3a9366a3b gowid-1.4.0/000077500000000000000000000000001426234454000126315ustar00rootroot00000000000000gowid-1.4.0/.github/000077500000000000000000000000001426234454000141715ustar00rootroot00000000000000gowid-1.4.0/.github/workflows/000077500000000000000000000000001426234454000162265ustar00rootroot00000000000000gowid-1.4.0/.github/workflows/go.yml000066400000000000000000000011421426234454000173540ustar00rootroot00000000000000name: Go on: [push] jobs: build: name: Build runs-on: ubuntu-latest steps: - name: Set up Go 1.13 uses: actions/setup-go@v1 with: go-version: 1.13 id: go - name: Check out code into the Go module directory uses: actions/checkout@v1 - name: Get dependencies run: | go get -v -t -d ./... if [ -f Gopkg.toml ]; then curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh dep ensure fi - name: Build run: go build -v ./... - name: Test run: go test -v ./... gowid-1.4.0/.gitignore000066400000000000000000000000341426234454000146160ustar00rootroot00000000000000*.log /go.work /go.work.sum gowid-1.4.0/.travis.yml000066400000000000000000000002621426234454000147420ustar00rootroot00000000000000language: go env: - GO111MODULE=on arch: - amd64 - ppc64le go: - 1.13.x - 1.14.x - 1.15.x notifications: email: true script: - go test -v -race ./... gowid-1.4.0/LICENSE000066400000000000000000000020551426234454000136400ustar00rootroot00000000000000MIT License Copyright (c) 2019 Graham Clark Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. gowid-1.4.0/README.md000066400000000000000000000154711426234454000141200ustar00rootroot00000000000000# Terminal User Interface Widgets in Go Gowid provides widgets and a framework for making terminal user interfaces. It's written in Go and inspired by [urwid](http://urwid.org). Widgets out-of-the-box include: - input components like button, checkbox and an editable text field with support for passwords - layout components for arranging widgets in columns, rows and a grid - structured components - a tree, an infinite list and a table - pre-canned widgets - a progress bar, a modal dialog, a bar graph and a menu - a VT220-compatible terminal widget, heavily cribbed from urwid :smiley: All widgets support interaction with the mouse when the terminal allows. Gowid is built on top of the fantastic [tcell](https://github.com/gdamore/tcell) package. There are many alternatives to gowid - see [Similar Projects](#similar-projects) The most developed gowid application is currently [termshark](https://termshark.io), a terminal UI for tshark. ## Installation ```bash go get github.com/gcla/gowid/... ``` ## Examples Make sure `$GOPATH/bin` is in your PATH (or `~/go/bin` if `GOPATH` isn't set), then tab complete "gowid-" e.g. ```bash gowid-fib ``` Here is a port of urwid's [palette](https://github.com/urwid/urwid/blob/master/examples/palette_test.py) example: Here is urwid's [graph](https://github.com/urwid/urwid/blob/master/examples/graph.py) example: And urwid's [fibonacci](https://github.com/urwid/urwid/blob/master/examples/fib.py) example: A demonstration of gowid's terminal widget, a port of urwid's [terminal widget](https://github.com/urwid/urwid/blob/master/examples/terminal.py): Finally, here is an animation of termshark in action: ## Hello World This example is an attempt to mimic urwid's ["Hello World"](http://urwid.org/tutorial/index.html) example. ```go package main 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() } ``` Running the example above displays this: ## Documentation - The beginnings of a [tutorial](docs/Tutorial.md) - A list of most of the [widgets](docs/Widgets.md) - Some [FAQs](docs/FAQ.md) (which I guessed at...) - Some gowid [programming tricks](docs/Debugging.md) ## Similar Projects Gowid is late to the TUI party. There are many options from which to choose - please read https://appliedgo.net/tui/ for a nice summary for the Go language. Here is a selection: - [urwid](http://urwid.org/) - one of the oldest, for those working in python - [tview](https://github.com/rivo/tview) - active, polished, concise, lots of widgets, Go - [termui](https://github.com/gizak/termui) - focus on graphing and dataviz, Go - [gocui](https://github.com/jroimartin/gocui) - focus on layout, good input options, mouse support, Go - [clui](https://github.com/VladimirMarkelov/clui) - active, many widgets, mouse support, Go - [tui-go](https://github.com/marcusolsson/tui-go) - QT-inspired, experimental, nice examples, Go ## Dependencies Gowid depends on these great open-source packages: - [urwid](http://urwid.org) - not a Go-dependency, but the model for most of gowid's design - [tcell](https://github.com/gdamore/tcell) - a cell based view for text terminals, like xterm, inspired by termbox - [asciigraph](https://github.com/guptarohit/asciigraph) - lightweight ASCII line-graphs for Go - [logrus](https://github.com/sirupsen/logrus) - structured pluggable logging for Go - [testify](https://github.com/stretchr/testify) - tools for testifying that your code will behave as you intend ## Contact - The author - Graham Clark (grclark@gmail.com) ## License [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) gowid-1.4.0/app.go000066400000000000000000000645671426234454000137620ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. package gowid import ( "fmt" "os" "path/filepath" "runtime/debug" "strings" "sync" "time" tcell "github.com/gdamore/tcell/v2" log "github.com/sirupsen/logrus" ) //====================================================================== // IGetScreen provides access to a tcell.Screen object e.g. for rendering // a canvas to the terminal. type IGetScreen interface { GetScreen() tcell.Screen } // IColorMode provides access to a ColorMode value which represents the current // mode of the terminal e.g. 24-bit color, 256-color, monochrome. type IColorMode interface { GetColorMode() ColorMode } // IPalette provides application "palette" information - it can look up a // Cell styling interface by name (e.g. "main text" -> (black, white, underline)) // and it can let clients apply a function to each member of the palette (e.g. // in order to construct a new modified palette). type IPalette interface { CellStyler(name string) (ICellStyler, bool) RangeOverPalette(f func(key string, value ICellStyler) bool) } // IRenderContext proviees palette and color mode information. type IRenderContext interface { IPalette IColorMode } // IApp is the interface of the application passed to every widget during Render or UserInput. // It provides several features: // - a function to terminate the application // - access to the state of the mouse // - access to the underlying tcell screen // - access to an application-specific logger // - functions to get and set the root widget of the widget hierarchy // - a method to keep track of which widgets were last "clicked" // type IApp interface { IRenderContext IGetScreen ISettableComposite Quit() // Terminate the running gowid app + main loop soon Redraw() // Issue a redraw of the terminal soon Sync() // From tcell's screen - refresh every screen cell e.g. if screen becomes corrupted SetColorMode(mode ColorMode) // Change the terminal's color mode - 256, 16, mono, etc Run(f IAfterRenderEvent) error // Send a function to run on the widget rendering goroutine SetClickTarget(k tcell.ButtonMask, w IIdentityWidget) bool // When a mouse is clicked on a widget, track that widget. So... ClickTarget(func(tcell.ButtonMask, IIdentityWidget)) // when the button is released, we can activate the widget if we are still "over" it GetMouseState() MouseState // Which buttons are currently clicked GetLastMouseState() MouseState // Which buttons were clicked before current event RegisterMenu(menu IMenuCompatible) // Required for an app to display an overlaying menu UnregisterMenu(menu IMenuCompatible) bool // Returns false if the menu is not found in the hierarchy InCopyMode(...bool) bool // A getter/setter - to set the app into copy mode. Widgets might render differently as a result CopyModeClaimedAt(...int) int // the level that claims copy, 0 means deepest should claim CopyModeClaimedBy(...IIdentity) IIdentity // the level that claims copy, 0 means deepest should claim RefreshCopyMode() // Give widgets another chance to display copy options (after the user perhaps adjusted the scope of a copy selection) Clips() []ICopyResult // If in copy-mode, the app will descend the widget hierarchy with a special user input, gathering options for copying data CopyLevel(...int) int // level we're at as we descend } // App is an implementation of IApp. The App struct conforms to IApp and // provides services to a running gowid application, such as access to the // palette, the screen and the state of the mouse. type App struct { IPalette // App holds an IPalette and provides it to each widget when rendering screen tcell.Screen // Each app has one screen TCellEvents chan tcell.Event // Events from tcell e.g. resize AfterRenderEvents chan IAfterRenderEvent // Functions intended to run on the widget goroutine closing bool // If true then app is in process of closing - it may be draining AfterRenderEvents. closingMtx sync.Mutex // Make sure an AfterRenderEvent and closing don't race. viewPlusMenus IWidget // The base widget that is displayed - includes registered menus view IWidget // The base widget that is displayed under registered menus colorMode ColorMode // The current color mode of the terminal - 256, 16, mono, etc inCopyMode bool // True if the app has been switched into "copy mode", for the user to copy a widget value copyClaimed int // True if a widget has "claimed" copy mode during this Render pass copyClaimedBy IIdentity copyLevel int refreshCopy bool prevWasMouseMove bool // True if we last processed simple mouse movement. We can optimize on slow enableMouseMotion bool enableBracketedPaste bool screenInited bool dontOwnScreen bool tty string lastMouse MouseState // So I can tell if a button was previously clicked MouseState // Track which mouse buttons are currently down ClickTargets // When mouse is clicked, track potential interaction here log log.StdLogger // For any application logging } var _ IApp = (*App)(nil) // AppArgs is a helper struct, providing arguments for the initialization of App. type AppArgs struct { Screen tcell.Screen View IWidget Palette IPalette EnableMouseMotion bool EnableBracketedPaste bool Log log.StdLogger DontActivate bool Tty string } // IUnhandledInput is used as a handler for application user input that is not handled by any // widget in the widget hierarchy. type IUnhandledInput interface { UnhandledInput(app IApp, ev interface{}) bool } // UnhandledInputFunc satisfies IUnhandledInput, allowing use of a simple function for // handling input not claimed by any widget. type UnhandledInputFunc func(app IApp, ev interface{}) bool func (f UnhandledInputFunc) UnhandledInput(app IApp, ev interface{}) bool { return f(app, ev) } // IgnoreUnhandledInput is a helper function for main loops that don't need to deal // with hanlding input that the widgets haven't claimed. var IgnoreUnhandledInput UnhandledInputFunc = func(app IApp, ev interface{}) bool { return false } //====================================================================== // ClickTargets is used by the App to keep track of which widgets have been // clicked. This allows the application to determine if a widget has been // "selected" which may be best determined across two calls to UserInput - click // and release. type ClickTargets struct { click map[tcell.ButtonMask][]IIdentityWidget // When mouse is clicked, track potential interaction here } func MakeClickTargets() ClickTargets { return ClickTargets{ click: make(map[tcell.ButtonMask][]IIdentityWidget), } } // SetClickTarget expects a Widget that provides an ID() function. Most // widgets that can be clicked on can just use the default (&w). But if a // widget might be recreated between the click down and release, and the // widget under focus at the time of the release provides the same ID() // (even if not the same object), then it can be given the click. // func (t ClickTargets) SetClickTarget(k tcell.ButtonMask, w IIdentityWidget) bool { targets, ok := t.click[k] if !ok { targets = make([]IIdentityWidget, 1) targets[0] = w } else { targets = append(targets, w) } t.click[k] = targets return !ok } func (t ClickTargets) ClickTarget(f func(tcell.ButtonMask, IIdentityWidget)) { for k, v := range t.click { for _, t := range v { f(k, t) } } } func (t ClickTargets) DeleteClickTargets(k tcell.ButtonMask) { if ws, ok := t.click[k]; ok { for _, w := range ws { if w2, ok := w.(IClickTracker); ok { w2.SetClickPending(false) } } delete(t.click, k) } } //====================================================================== type MouseState struct { MouseLeftClicked bool MouseMiddleClicked bool MouseRightClicked bool } func (m MouseState) String() string { return fmt.Sprintf("LeftClicked: %v, MiddleClicked: %v, RightClicked: %v", m.MouseLeftClicked, m.MouseMiddleClicked, m.MouseRightClicked, ) } func (m MouseState) NoButtonClicked() bool { return !m.LeftIsClicked() && !m.MiddleIsClicked() && !m.RightIsClicked() } func (m MouseState) LeftIsClicked() bool { return m.MouseLeftClicked } func (m MouseState) MiddleIsClicked() bool { return m.MouseMiddleClicked } func (m MouseState) RightIsClicked() bool { return m.MouseRightClicked } //====================================================================== func NewApp(args AppArgs) (rapp *App, rerr error) { app, err := newApp(args) if err != nil { return nil, err } return app, nil } // NewAppSafe returns an initialized App struct, or an error on failure. It will // initialize a tcell.Screen object and enable mouse support if its not provided, // meaning that tcell will receive mouse events if the terminal supports them. func newApp(args AppArgs) (rapp *App, rerr error) { screen := args.Screen if screen == nil { var err error screen, err = tcellScreen(args.Tty) if err != nil { rerr = WithKVs(err, map[string]interface{}{"TERM": os.Getenv("TERM")}) return } } var palette IPalette = args.Palette if palette == nil { palette = make(Palette) } tch := make(chan tcell.Event, 1000) wch := make(chan IAfterRenderEvent, 1000) clicks := MakeClickTargets() if args.Log == nil { logname := filepath.Base(os.Args[0]) logname = fmt.Sprintf("%s.log", strings.TrimSuffix(logname, filepath.Ext(logname))) logfile, err := os.Create(logname) if err != nil { return nil, err } logger := log.New() logger.Out = logfile args.Log = logger } res := &App{ IPalette: palette, screen: screen, TCellEvents: tch, AfterRenderEvents: wch, closing: false, view: args.View, viewPlusMenus: args.View, colorMode: Mode256Colors, ClickTargets: clicks, log: args.Log, enableMouseMotion: args.EnableMouseMotion, enableBracketedPaste: args.EnableBracketedPaste, dontOwnScreen: args.Screen != nil, tty: args.Tty, } if !res.dontOwnScreen && !args.DontActivate { if err := res.initScreen(); err != nil { return nil, err } res.initColorMode() if res.enableBracketedPaste { screen.EnablePaste() } } screen.Clear() rapp = res return } func (a *App) initColorMode() { cols := a.screen.Colors() switch { case cols > 256: a.SetColorMode(Mode24BitColors) case cols == 256: a.SetColorMode(Mode256Colors) case cols == 88: a.SetColorMode(Mode88Colors) case cols == 16: a.SetColorMode(Mode16Colors) case cols < 0: a.SetColorMode(ModeMonochrome) default: a.SetColorMode(Mode8Colors) } } func (a *App) GetScreen() tcell.Screen { return a.screen } func (a *App) RefreshCopyMode() { a.refreshCopy = true } func (a *App) CopyLevel(lvl ...int) int { if len(lvl) > 0 { a.copyLevel = lvl[0] } return a.copyLevel } func (a *App) InCopyMode(on ...bool) bool { if len(on) > 0 { a.inCopyMode = on[0] } return a.inCopyMode } func (a *App) CopyModeClaimedAt(lvl ...int) int { if len(lvl) > 0 { a.copyClaimed = lvl[0] } return a.copyClaimed } func (a *App) CopyModeClaimedBy(id ...IIdentity) IIdentity { if len(id) > 0 { a.copyClaimedBy = id[0] } return a.copyClaimedBy } func (a *App) SetSubWidget(widget IWidget, app IApp) { a.view = widget if a.viewPlusMenus == nil { a.viewPlusMenus = widget } } func (a *App) SubWidget() IWidget { return a.view } func (a *App) SetPalette(palette IPalette) { a.IPalette = palette } func (a *App) GetPalette() IPalette { return a.IPalette } func (a *App) GetMouseState() MouseState { return a.MouseState } func (a *App) GetLastMouseState() MouseState { return a.lastMouse } func (a *App) SetColorMode(mode ColorMode) { a.colorMode = mode } func (a *App) GetColorMode() ColorMode { return a.colorMode } // TerminalSize returns the terminal's size. func (a *App) TerminalSize() (x, y int) { x, y = a.screen.Size() if x == 0 && y == 0 { // vim uses the following rules (https://github.com/vim/vim/blob/master/runtime/doc/term.txt#L629) // - an ioctl call (TIOCGSIZE or TIOCGWINSZ, depends on your system) // - the environment variables "LINES" and "COLUMNS" // - from the termcap entries "li" and "co" // // If tcell still reports (0,0) after following these rules, fall // back to a default like vim does: // // https://github.com/vim/vim/blob/master/runtime/doc/term.txt#L642 // "If everything fails a default size of 24 lines and 80 columns is assumed." // x = 80 y = 24 } return } type LogField struct { Name string Val interface{} } type CopyModeEvent struct{} func (c CopyModeEvent) When() time.Time { return time.Time{} } type ICopyModeClips interface { Collect([]ICopyResult) } type CopyModeClipsFn func([]ICopyResult) func (f CopyModeClipsFn) Collect(clips []ICopyResult) { f(clips) } type CopyModeClipsEvent struct { Action ICopyModeClips } func (c CopyModeClipsEvent) When() time.Time { return time.Time{} } type privateId struct{} func (n privateId) ID() interface{} { return n } func (a *App) Clips() []ICopyResult { res := make([]ICopyResult, 0) cb := CopyModeClipsFn(func(clips []ICopyResult) { res = append(res, clips...) }) unh := UnhandledInputFunc(func(app IApp, ev interface{}) bool { return true }) a.handleInputEvent( CopyModeClipsEvent{ Action: cb, }, unh, ) return res } // HandleTCellEvent handles an event from the underlying TCell library, // based on its type (key-press, error, etc.) User input events are sent // to onInputEvent, which will check the widget hierarchy to see if the // input can be processed; other events might result in gowid updating its // internal state, like the size of the underlying terminal. func (a *App) HandleTCellEvent(ev interface{}, unhandled IUnhandledInput) { switch ev := ev.(type) { case *tcell.EventKey, *tcell.EventPaste: // This makes for a better experience on limited hardware like raspberry pi debug.SetGCPercent(-1) defer debug.SetGCPercent(100) cm := a.InCopyMode() a.handleInputEvent(ev, unhandled) newCopyMode := (!cm && a.InCopyMode()) if newCopyMode || a.refreshCopy { // Now need to work out which widget claims the copy - choose deepest a.copyLevel = 0 // current level as we traverse - start at highest if newCopyMode { // newly entered a.copyClaimed = 100000 // won't ever nest this deep - widget claims beyond this point or at leaf a.copyClaimedBy = privateId{} } a.handleInputEvent(CopyModeEvent{}, unhandled) a.refreshCopy = false } a.RedrawTerminal() //case *tcell.EventPaste: //log.Infof("GCLA: app.go tcell paste") case *tcell.EventMouse: if !a.prevWasMouseMove || a.enableMouseMotion || ev.Modifiers() != 0 || ev.Buttons() != 0 { switch ev.Buttons() { case tcell.Button1: a.MouseLeftClicked = true case tcell.Button2: a.MouseMiddleClicked = true case tcell.Button3: a.MouseRightClicked = true default: } debug.SetGCPercent(-1) defer debug.SetGCPercent(100) a.handleInputEvent(ev, unhandled) // Make sure we don't hold on to references longer than we need to if ev.Buttons() == tcell.ButtonNone { a.ClickTargets.DeleteClickTargets(tcell.Button1) a.ClickTargets.DeleteClickTargets(tcell.Button2) a.ClickTargets.DeleteClickTargets(tcell.Button3) } a.lastMouse = a.MouseState a.MouseState = MouseState{} a.RedrawTerminal() } case *tcell.EventResize: if flog, ok := a.log.(log.FieldLogger); ok { flog.WithField("event", ev).Infof("Terminal was resized") } else { a.log.Printf("Terminal was resized\n") } a.RedrawTerminal() case *tcell.EventInterrupt: if flog, ok := a.log.(log.FieldLogger); ok { flog.WithField("event", ev).Infof("Interrupt event from tcell") } else { a.log.Printf("Interrupt event from tcell: %v\n", ev) } case *tcell.EventError: if flog, ok := a.log.(log.FieldLogger); ok { flog.WithField("event", ev).WithField("error", ev.Error()).Errorf("Error event from tcell") } else { a.log.Printf("Error event from tcell: %v, %v\n", ev, ev.Error()) } default: if flog, ok := a.log.(log.FieldLogger); ok { flog.WithField("event", ev).Infof("Unanticipated event from tcell") } else { a.log.Printf("Unanticipated event from tcell: %v\n", ev) } } if ev, ok := ev.(*tcell.EventMouse); ok && ev.Buttons() == 0 && ev.Modifiers() == 0 { a.prevWasMouseMove = true } else { a.prevWasMouseMove = false } } // Close should be called by a gowid application after the user terminates the application. // It will cleanup tcell's screen object. func (a *App) Close() { a.screen.Fini() } // StartTCellEvents starts a goroutine that listens for events from TCell. The // PollEvent function will block until TCell has something to report - when // something arrives, it is written to the tcellEvents channel. The function // is provided with a quit channel which is consulted for an event that will // terminate this goroutine. func (a *App) StartTCellEvents(quit <-chan Unit, wg *sync.WaitGroup) { wg.Add(1) go func(quit <-chan Unit) { defer wg.Done() Loop: for { a.TCellEvents <- a.screen.PollEvent() select { case <-quit: break Loop default: } } }(quit) } // StopTCellEvents will cause TCell to generate an interrupt event; an event is posted // to the quit channel first to stop the TCell event goroutine. func (a *App) StopTCellEvents(quit chan<- Unit, wg *sync.WaitGroup) { quit <- Unit{} a.screen.PostEventWait(tcell.NewEventInterrupt(nil)) wg.Wait() } // SimpleMainLoop will run your application using a default unhandled input function // that will terminate your application on q/Q, ctrl-c and escape. func (a *App) SimpleMainLoop() { a.MainLoop(UnhandledInputFunc(HandleQuitKeys)) } // HandleQuitKeys is provided as a simple way to terminate your application using typical // "quit" keys - q/Q, ctrl-c, escape. func HandleQuitKeys(app IApp, event interface{}) bool { handled := false if ev, ok := event.(*tcell.EventKey); ok { if ev.Key() == tcell.KeyCtrlC || ev.Key() == tcell.KeyEsc || ev.Rune() == 'q' || ev.Rune() == 'Q' { app.Quit() handled = true } } return handled } type AppRunner struct { app *App wg sync.WaitGroup started bool quitCh chan Unit } func (a *App) Runner() *AppRunner { res := &AppRunner{ app: a, quitCh: make(chan Unit, 100), } return res } func (st *AppRunner) Start() { st.app.StartTCellEvents(st.quitCh, &st.wg) st.started = true } func (st *AppRunner) Stop() { if st.started { st.app.StopTCellEvents(st.quitCh, &st.wg) st.started = false } } // MainLoop is the intended gowid entry point for typical applications. After the App // is instantiated and the widget hierarchy set up, the application should call MainLoop // with a handler for processing input that is not consumed by any widget. func (a *App) MainLoop(unhandled IUnhandledInput) { defer a.Close() st := a.Runner() st.Start() defer st.Stop() a.handleEvents(unhandled) } // RunThenRenderEvent dispatches the event by calling it with the // app as an argument - then it will force the application to re-render // itself. func (a *App) RunThenRenderEvent(ev IAfterRenderEvent) { ev.RunThenRenderEvent(a) a.RedrawTerminal() } // handleEvents processes all gowid events. These can be either app-generated events // like a function which must be executed on the render goroutine, or events from // the underlying TCell library like user input or terminal resize. func (a *App) handleEvents(unhandled IUnhandledInput) { Loop: for { select { case ev := <-a.TCellEvents: a.HandleTCellEvent(ev, unhandled) case ev := <-a.AfterRenderEvents: if ev == nil { break Loop } a.RunThenRenderEvent(ev) } } } // handleInputEvent manages key-press events. A keybinding handler is called when // a key-press or mouse event satisfies a configured keybinding. Furthermore, // currentView's internal buffer is modified if currentView.Editable is true. func (a *App) handleInputEvent(ev interface{}, unhandled IUnhandledInput) { switch ev.(type) { case *tcell.EventKey, *tcell.EventPaste, *tcell.EventMouse: x, y := a.TerminalSize() handled := UserInputIfSelectable(a.viewPlusMenus, ev, RenderBox{C: x, R: y}, Focused, a) if !handled { handled = unhandled.UnhandledInput(a, ev) if !handled { if flog, ok := a.log.(log.FieldLogger); ok { flog.WithField("event", ev).Debugf("Input was not handled") } else { a.log.Printf("Input was not handled: %v\n", ev) } } } default: x, y := a.TerminalSize() UserInputIfSelectable(a.viewPlusMenus, ev, RenderBox{C: x, R: y}, Focused, a) } } // Sync defers immediately to tcell's Screen's Sync() function - it is for updating // every screen cell in the event something corrupts the screen (e.g. ssh -v logging) func (a *App) Sync() { a.screen.Sync() } // RedrawTerminal updates the gui, re-drawing frames and buffers. Call this from // the widget-handling goroutine only. Intended for use by apps that construct their // own main loops and handle gowid events themselves. func (a *App) RedrawTerminal() { RenderRoot(a.viewPlusMenus, a) a.screen.Show() } // RegisterMenu should be called by any widget that wants to display a // menu. The call could be made after initializing the App object. This call // adds the menu above the current root of the widget hierarchy - when the App // renders from the root down, any open menus will be rendered on top of the // original root (using the overlay widget). func (a *App) RegisterMenu(menu IMenuCompatible) { menu.SetSubWidget(a.viewPlusMenus, a) a.viewPlusMenus = menu } type menuView struct { *App } // SetSubWidget will set the real root of the widget hierarchy rather than // the one visible to users of the App. i.e. it allows for a menu to be injected // into the hierarchy. func (a *menuView) SetSubWidget(widget IWidget, app IApp) { a.viewPlusMenus = widget } func (a *App) unregisterMenu(cur ISettableComposite, removeMe IMenuCompatible) bool { res := true for { if sm, ok := cur.SubWidget().(IMenuCompatible); ok { if sm == removeMe { cur.SetSubWidget(sm.SubWidget(), a) break } else { cur = sm } } else { res = false break } } return res } // UnregisterMenu will remove a menu from the widget hierarchy. If it's not found, // false is returned. func (a *App) UnregisterMenu(menu IMenuCompatible) bool { return a.unregisterMenu(&menuView{a}, menu) } //====================================================================== type RunFunction func(IApp) // IAfterRenderEvent is implemented by clients that wish to run a function on the // gowid rendering goroutine, directly after the widget hierarchy is rendered. This // allows the client to be sure that there is no race condition with the // widget rendering code. type IAfterRenderEvent interface { RunThenRenderEvent(IApp) } // RunThenRenderEvent lets the receiver RunOnRenderFunction implement IOnRenderEvent. This // lets a regular function be executed on the same goroutine as the rendering code. func (f RunFunction) RunThenRenderEvent(app IApp) { f(app) } var AppClosingErr = fmt.Errorf("App is closing - no more events accepted.") // Run executes this function on the goroutine that renders // widgets and processes their callbacks. Any function that manipulates // widget state outside of the Render/UserInput chain should be run this // way for thread-safety e.g. a function that changes the UI from a timer // event. func (a *App) Run(f IAfterRenderEvent) error { a.closingMtx.Lock() defer a.closingMtx.Unlock() if !a.closing { a.AfterRenderEvents <- f return nil } return AppClosingErr } // Redraw will re-render the widget hierarchy. func (a *App) Redraw() { a.Run(RunFunction(func(IApp) {})) } // Quit will terminate the gowid main loop. func (a *App) Quit() { a.closingMtx.Lock() defer a.closingMtx.Unlock() a.closing = true close(a.AfterRenderEvents) } // Let screen be taken over by gowid/tcell. A new screen struct is created because // I can't make tcell claim and release the same screen successfully. Clients of // the app struct shouldn't cache the screen object returned via GetScreen(). // // Assumes we own the screen... func (a *App) ActivateScreen() error { screen, err := tcellScreen(a.tty) if err != nil { return WithKVs(err, map[string]interface{}{"TERM": os.Getenv("TERM")}) } a.DeactivateScreen() a.screen = screen if err := a.initScreen(); err != nil { return err } if a.enableBracketedPaste { a.screen.EnablePaste() } return nil } // Assumes we own the screen func (a *App) DeactivateScreen() { if a.screen != nil && a.screenInited { a.screen.Fini() a.screen = nil a.screenInited = false } } func (a *App) initScreen() error { if err := a.screen.Init(); err != nil { return WithKVs(err, map[string]interface{}{"TERM": os.Getenv("TERM")}) } a.screenInited = true a.initColorMode() defFg := ColorDefault defBg := ColorDefault defSt := StyleNone if paletteDefault, ok := a.IPalette.CellStyler("default"); ok { fgCol, bgCol, style := paletteDefault.GetStyle(a) defFg = IColorToTCell(fgCol, defFg, a.GetColorMode()) defBg = IColorToTCell(bgCol, defBg, a.GetColorMode()) defSt = defSt.MergeUnder(style) } defStyle := tcell.Style{}.Attributes(defSt.OnOff).Background(defBg.ToTCell()).Foreground(defFg.ToTCell()) // Ask TCell to set the screen's default style according to the palette's "default" // config, if one is provided. This might make every screen cell underlined, for example, // in the absence of overriding styling from widgets. a.screen.SetStyle(defStyle) a.screen.EnableMouse() if a.enableBracketedPaste { a.screen.EnablePaste() } return nil } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: gowid-1.4.0/callbacks.go000066400000000000000000000065711426234454000151100ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source code is governed by the MIT license // that can be found in the LICENSE file. package gowid import ( "sync" ) //====================================================================== type ClickCB struct{} type KeyPressCB struct{} type SubWidgetCB struct{} type SubWidgetsCB struct{} type DimensionsCB struct{} type FocusCB struct{} type VAlignCB struct{} type HAlignCB struct{} type HeightCB struct{} type WidthCB struct{} // ICallback represents any object that can provide a way to be compared to others, // and that can be called with an arbitrary number of arguments returning no result. // The comparison is expected to be used by having the callback object provide a name // to identify the callback operation e.g. "buttonclicked", so that it can later // be removed. type ICallback interface { IIdentity Call(args ...interface{}) } type CallbackFunction func(args ...interface{}) type CallbackID struct { Name interface{} } // Callback is a simple implementation of ICallback. type Callback struct { Name interface{} CallbackFunction } func (f CallbackFunction) Call(args ...interface{}) { f(args...) } func (f CallbackID) ID() interface{} { return f.Name } func (f Callback) ID() interface{} { return f.Name } type Callbacks struct { sync.Mutex callbacks map[interface{}][]ICallback } type ICallbacks interface { RunCallbacks(name interface{}, args ...interface{}) AddCallback(name interface{}, cb ICallback) RemoveCallback(name interface{}, cb IIdentity) bool } func NewCallbacks() *Callbacks { cb := &Callbacks{} cb.callbacks = make(map[interface{}][]ICallback) var _ ICallbacks = cb return cb } // CopyOfCallbacks is used when callbacks are run - they are copied // so that any callers modifying the callbacks themselves can do so // safely with the modifications taking effect after all callbacks // are run. Can be called with a nil receiver if the widget's callback // object has not been initialized and e.g. RunWidgetCallbacks is called. func (c *Callbacks) CopyOfCallbacks(name interface{}) ([]ICallback, bool) { if c != nil { c.Lock() defer c.Unlock() cbs, ok := c.callbacks[name] if ok { cbscopy := make([]ICallback, len(cbs)) copy(cbscopy, cbs) return cbscopy, true } } return []ICallback{}, false } func (c *Callbacks) RunCallbacks(name interface{}, args ...interface{}) { if cbs, ok := c.CopyOfCallbacks(name); ok { for _, cb := range cbs { if cb != nil { cb.Call(args...) } } } } func (c *Callbacks) AddCallback(name interface{}, cb ICallback) { c.Lock() defer c.Unlock() cbs := c.callbacks[name] cbs = append(cbs, cb) c.callbacks[name] = cbs } func (c *Callbacks) RemoveCallback(name interface{}, cb IIdentity) bool { if c == nil { return false } c.Lock() defer c.Unlock() cbs, ok := c.callbacks[name] if ok { idxs := make([]int, 0) ok = false for i, cb2 := range cbs { if cb.ID() == cb2.ID() { // Append backwards for easier deletion later idxs = append([]int{i}, idxs...) } } if len(idxs) > 0 { ok = true for _, j := range idxs { cbs = append(cbs[:j], cbs[j+1:]...) } if len(cbs) == 0 { delete(c.callbacks, name) } else { c.callbacks[name] = cbs } } } return ok } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: gowid-1.4.0/callbacks_test.go000066400000000000000000000023371426234454000161430ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source code is governed by the MIT license // that can be found in the LICENSE file. package gowid import ( "testing" "github.com/stretchr/testify/assert" ) func Test1(t *testing.T) { cb := NewCallbacks() x := 1 cb.RunCallbacks("test1", 1) assert.Equal(t, 1, x) cb.AddCallback("test2", Callback{"addit", CallbackFunction(func(args ...interface{}) { y := args[0].(int) x = x + y })}) cb.RunCallbacks("test1", 1) assert.Equal(t, 1, x) cb.RunCallbacks("test2", 1) assert.Equal(t, 2, x) cb.RunCallbacks("test2", 2) assert.Equal(t, 4, x) cb.AddCallback("test2", Callback{"addit100", CallbackFunction(func(args ...interface{}) { y := args[0].(int) x = x + (y * 100) })}) cb.RunCallbacks("test2", 3) assert.Equal(t, 307, x) assert.Equal(t, false, cb.RemoveCallback("test2bad", CallbackID{"addit100"})) assert.Equal(t, false, cb.RemoveCallback("test2", CallbackID{"addit100bad"})) assert.Equal(t, true, cb.RemoveCallback("test2", CallbackID{"addit100"})) cb.RunCallbacks("test2", 8) assert.Equal(t, 315, x) } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: gowid-1.4.0/canvas.go000066400000000000000000000571001426234454000144360ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. package gowid import ( "fmt" "io" "strings" "unicode/utf8" "github.com/gcla/gowid/gwutil" tcell "github.com/gdamore/tcell/v2" "github.com/mattn/go-runewidth" "github.com/pkg/errors" ) //====================================================================== // ICanvasLineReader can provide a particular line of Cells, at the specified y // offset. The result may or may not be a copy of the actual Cells, and is determined // by whether the user requested a copy and/or the capability of the ICanvasLineReader // (maybe it has to provide a copy). type ICanvasLineReader interface { Line(int, LineCopy) LineResult } // ICanvasMarkIterator will call the supplied function argument with the name and // position of every mark set on the canvas. If the function returns true, the // loop is terminated early. type ICanvasMarkIterator interface { RangeOverMarks(f func(key string, value CanvasPos) bool) } // ICanvasCellReader can provide a Cell given a row and a column. type ICanvasCellReader interface { CellAt(col, row int) Cell } type IAppendCanvas interface { IRenderBox ICanvasLineReader ICanvasMarkIterator } type IMergeCanvas interface { IRenderBox ICanvasCellReader ICanvasMarkIterator } type IDrawCanvas interface { IRenderBox ICanvasLineReader CursorEnabled() bool CursorCoords() CanvasPos } // ICanvas is the interface of any object which can generate a 2-dimensional // array of Cells that are intended to be rendered on a terminal. This interface is // pretty awful - cluttered and inconsistent and subject to cleanup... Note though // that this interface is not here as the minimum requirement for providing arguments // to a function or module - instead it's supposed to be an API surface for widgets // so includes features that I am trying to guess will be needed, or that widgets // already need. type ICanvas interface { Duplicate() ICanvas MergeUnder(c IMergeCanvas, leftOffset, topOffset int, bottomGetsCursor bool) AppendBelow(c IAppendCanvas, doCursor bool, makeCopy bool) AppendRight(c IMergeCanvas, useCursor bool) SetCellAt(col, row int, c Cell) SetLineAt(row int, line []Cell) Truncate(above, below int) ExtendRight(cells []Cell) ExtendLeft(cells []Cell) TrimRight(cols int) TrimLeft(cols int) SetCursorCoords(col, row int) SetMark(name string, col, row int) GetMark(name string) (CanvasPos, bool) RemoveMark(name string) ICanvasMarkIterator ICanvasCellReader IDrawCanvas fmt.Stringer } // LineResult is returned by some Canvas Line-accessing APIs. If the Canvas // can return a line without copying it, the Copied field will be false, and // the caller is expected to make a copy if necessary (or risk modifying the // original). type LineResult struct { Line []Cell Copied bool } // LineCopy is an argument provided to some Canvas APIs, like Line(). It tells // the function how to allocate the backing array for a line if the line it // returns must be a copy. Typically the API will return a type that indicates // whether the result is a copy or not. Since the caller may receive a copy, // it can help to indicate the allocation details like length and capacity in // case the caller intends to extend the line returned for some other use. type LineCopy struct { Len int Cap int } //====================================================================== // LineCanvas exists to make an array of Cells conform to some interfaces, specifically // IRenderBox (it has a width of len(.) and a height of 1), IAppendCanvas, to allow // an array of Cells to be passed to the canvas function AppendLine(), and ICanvasLineReader // so that an array of Cells can act as a line returned from a canvas. type LineCanvas []Cell // BoxColumns lets LineCanvas conform to IRenderBox func (c LineCanvas) BoxColumns() int { return len(c) } // BoxRows lets LineCanvas conform to IRenderBox func (c LineCanvas) BoxRows() int { return 1 } // BoxRows lets LineCanvas conform to IWidgetDimension func (c LineCanvas) ImplementsWidgetDimension() {} // Line lets LineCanvas conform to ICanvasLineReader func (c LineCanvas) Line(y int, cp LineCopy) LineResult { return LineResult{ Line: c, Copied: false, } } // RangeOverMarks lets LineCanvas conform to ICanvasMarkIterator func (c LineCanvas) RangeOverMarks(f func(key string, value CanvasPos) bool) {} var _ IAppendCanvas = (*LineCanvas)(nil) var _ ICanvasLineReader = (*LineCanvas)(nil) var _ ICanvasMarkIterator = (*LineCanvas)(nil) //====================================================================== var emptyLine [4096]Cell type EmptyLineTooLong struct { Requested int } var _ error = EmptyLineTooLong{} func (e EmptyLineTooLong) Error() string { return fmt.Sprintf("Tried to make an empty line too long - tried %d, max is %d", e.Requested, len(emptyLine)) } // EmptyLine provides a ready-allocated source of empty cells. Of course this is to be // treated as read-only. func EmptyLine(length int) []Cell { if length < 0 { length = 0 } if length > len(emptyLine) { panic(errors.WithStack(EmptyLineTooLong{Requested: length})) } return emptyLine[0:length] } // CanvasPos is a convenience struct to represent the coordinates of a position on a canvas. type CanvasPos struct { X, Y int } func (c CanvasPos) PlusX(n int) CanvasPos { return CanvasPos{X: c.X + n, Y: c.Y} } func (c CanvasPos) PlusY(n int) CanvasPos { return CanvasPos{X: c.X, Y: c.Y + n} } //====================================================================== type CanvasSizeWrong struct { Requested IRenderSize Actual IRenderBox } var _ error = CanvasSizeWrong{} func (e CanvasSizeWrong) Error() string { return fmt.Sprintf("Canvas size %v, %v does not match render size %v", e.Actual.BoxColumns(), e.Actual.BoxRows(), e.Requested) } // PanicIfCanvasNotRightSize is for debugging - it panics if the size of the supplied canvas does // not conform to the size specified by the size argument. For a box argument, columns and rows are // checked; for a flow argument, columns are checked. func PanicIfCanvasNotRightSize(c IRenderBox, size IRenderSize) { switch sz := size.(type) { case IRenderBox: if (c.BoxColumns() != sz.BoxColumns() && c.BoxRows() > 0) || c.BoxRows() != sz.BoxRows() { panic(errors.WithStack(CanvasSizeWrong{Requested: size, Actual: c})) } case IRenderFlowWith: if c.BoxColumns() != sz.FlowColumns() { panic(errors.WithStack(CanvasSizeWrong{Requested: size, Actual: c})) } } } type IRightSizeCanvas interface { IRenderBox ExtendRight(cells []Cell) TrimRight(cols int) Truncate(above, below int) AppendBelow(c IAppendCanvas, doCursor bool, makeCopy bool) } func MakeCanvasRightSize(c IRightSizeCanvas, size IRenderSize) { switch sz := size.(type) { case IRenderBox: rightSizeCanvas(c, sz.BoxColumns(), sz.BoxRows()) case IRenderFlowWith: rightSizeCanvasHorizontally(c, sz.FlowColumns()) } } func rightSizeCanvas(c IRightSizeCanvas, cols int, rows int) { rightSizeCanvasHorizontally(c, cols) rightSizeCanvasVertically(c, rows) } func rightSizeCanvasHorizontally(c IRightSizeCanvas, cols int) { if c.BoxColumns() > cols { c.TrimRight(cols) } else if c.BoxColumns() < cols { c.ExtendRight(EmptyLine(cols - c.BoxColumns())) } } func rightSizeCanvasVertically(c IRightSizeCanvas, rows int) { if c.BoxRows() > rows { c.Truncate(0, c.BoxRows()-rows) } else if c.BoxRows() < rows { AppendBlankLines(c, rows-c.BoxRows()) } } //====================================================================== // Canvas is a simple implementation of ICanvas, and is returned by the Render() function // of all the current widgets. It represents the canvas by a 2-dimensional array of Cells - // no tricks or attempts to optimize this yet! The canvas also stores a map of string // identifiers to positions - for example, the cursor position is tracked this way, and the // menu widget keeps track of where it should render a "dropdown" using canvas marks. Most // Canvas APIs expect that each line has the same length. type Canvas struct { Lines [][]Cell // inner array is a line Marks *map[string]CanvasPos maxCol int } // NewCanvas returns an initialized Canvas struct. Its size is 0 columns and // 0 rows. func NewCanvas() *Canvas { lines := make([][]Cell, 0, 120) res := &Canvas{ Lines: lines[:0], } var _ io.Writer = res return res } // NewCanvasWithLines allocates a canvas struct and sets its contents to the // 2-d array provided as an argument. func NewCanvasWithLines(lines [][]Cell) *Canvas { c := &Canvas{ Lines: lines, } c.AlignRight() c.maxCol = c.ComputeCurrentMaxColumn() var _ io.Writer = c return c } // NewCanvasOfSize returns a canvas struct of size cols x rows, where // each Cell is default-initialized (i.e. empty). func NewCanvasOfSize(cols, rows int) *Canvas { return NewCanvasOfSizeExt(cols, rows, Cell{}) } // NewCanvasOfSize returns a canvas struct of size cols x rows, where // each Cell is initialized by copying the fill argument. func NewCanvasOfSizeExt(cols, rows int, fill Cell) *Canvas { fillArr := make([]Cell, cols) for i := 0; i < cols; i++ { fillArr[i] = fill } res := NewCanvas() if rows > 0 { res.Lines = append(res.Lines, fillArr) for i := 0; i < rows-1; i++ { res.Lines = append(res.Lines, make([]Cell, 0, 120)) } } res.AlignRightWith(fill) res.maxCol = res.ComputeCurrentMaxColumn() var _ io.Writer = res return res } // Duplicate returns a deep copy of the receiver canvas. func (c *Canvas) Duplicate() ICanvas { res := NewCanvasOfSize(c.BoxColumns(), c.BoxRows()) for i := 0; i < c.BoxRows(); i++ { copy(res.Lines[i], c.Lines[i]) } if c.Marks != nil { marks := make(map[string]CanvasPos) res.Marks = &marks for k, v := range *c.Marks { (*res.Marks)[k] = v } } return res } type IRangeOverCanvas interface { IRenderBox ICanvasCellReader SetCellAt(col, row int, c Cell) } // RangeOverCanvas applies the supplied function to each cell, // modifying it in place. func RangeOverCanvas(c IRangeOverCanvas, f ICellProcessor) { for i := 0; i < c.BoxRows(); i++ { for j := 0; j < c.BoxColumns(); j++ { c.SetCellAt(j, i, f.ProcessCell(c.CellAt(j, i))) } } } // Line provides access to the lines of the canvas. LineCopy // determines what the Line() function should allocate if it // needs to make a copy of the Line. Return true if line was // copied. func (c *Canvas) Line(y int, cp LineCopy) LineResult { return LineResult{ Line: c.Lines[y], Copied: false, } } // BoxColumns helps Canvas conform to IRenderBox. func (c *Canvas) BoxColumns() int { return c.maxCol } // BoxRows helps Canvas conform to IRenderBox. func (c *Canvas) BoxRows() int { return len(c.Lines) } // BoxRows helps Canvas conform to IWidgetDimension. func (c *Canvas) ImplementsWidgetDimension() {} // ComputeCurrentMaxColumn walks the 2-d array of Cells to determine // the length of the longest line. This is used by certain APIs that // manipulate the canvas. func (c *Canvas) ComputeCurrentMaxColumn() int { res := 0 for _, line := range c.Lines { res = gwutil.Max(res, len(line)) } return res } // Write lets Canvas conform to io.Writer. Since each Canvas Cell holds a // rune, the byte array argument is interpreted as the UTF-8 encoding of // a sequence of runes. func (c *Canvas) Write(p []byte) (n int, err error) { return WriteToCanvas(c, p) } // WriteToCanvas extracts the logic of implementing io.Writer into a free // function that can be used by any canvas implementing ICanvas. func WriteToCanvas(c IRangeOverCanvas, p []byte) (n int, err error) { done := 0 maxcol := c.BoxColumns() line := 0 col := 0 for i, chr := range string(p) { if c.BoxRows() > line { switch chr { case '\n': for col < maxcol { c.SetCellAt(col, line, Cell{}) col++ } line++ col = 0 default: wid := runewidth.RuneWidth(chr) if col+wid > maxcol { col = 0 line++ } c.SetCellAt(col, line, c.CellAt(col, line).WithRune(chr)) col += wid } done = i + utf8.RuneLen(chr) } else { break } } return done, nil } // CursorEnabled returns true if the cursor is enabled in this canvas, false otherwise. func (c *Canvas) CursorEnabled() bool { ok := false if c.Marks != nil { _, ok = (*c.Marks)["cursor"] } return ok } // CursorCoords returns a pair of ints representing the current cursor coordinates. Note // that the caller must be sure the Canvas's cursor is enabled. func (c *Canvas) CursorCoords() CanvasPos { var pos CanvasPos ok := false if c.Marks != nil { pos, ok = (*c.Marks)["cursor"] } if !ok { // Caller must check first panic(errors.New("Cursor is off!")) } return pos } // SetCursorCoords will set the Canvas's cursor coordinates. The special input of (-1,-1) // will disable the cursor. func (c *Canvas) SetCursorCoords(x, y int) { if x == -1 && y == -1 { c.RemoveMark("cursor") } else { c.SetMark("cursor", x, y) } } // SetMark allows the caller to store a string identifier at a particular position in the // Canvas. The menu widget uses this feature to keep track of where it should "open", acting // as an overlay over the widgets below. func (c *Canvas) SetMark(name string, x, y int) { if c.Marks == nil { marks := make(map[string]CanvasPos) c.Marks = &marks } (*c.Marks)[name] = CanvasPos{X: x, Y: y} } // GetMark returns the position and presence/absence of the specified string identifier // in the Canvas. func (c *Canvas) GetMark(name string) (CanvasPos, bool) { ok := false var i CanvasPos if c.Marks != nil { i, ok = (*c.Marks)[name] } return i, ok } // RemoveMark removes a mark from the Canvas. func (c *Canvas) RemoveMark(name string) { if c.Marks != nil { delete(*c.Marks, name) } } // RangeOverMarks applies the supplied function to each mark and position in the // received Canvas. If the function returns false, the loop is terminated. func (c *Canvas) RangeOverMarks(f func(key string, value CanvasPos) bool) { if c.Marks != nil { for k, v := range *c.Marks { if !f(k, v) { break } } } } // CellAt returns the Cell at the Canvas position provided. Note that the // function assumes the caller has ensured the position is not out of // bounds. func (c *Canvas) CellAt(col, row int) Cell { return c.Lines[row][col] } // SetCellAt sets the Canvas Cell at the position provided. Note that the // function assumes the caller has ensured the position is not out of // bounds. func (c *Canvas) SetCellAt(col, row int, cell Cell) { c.Lines[row][col] = cell } // SetLineAt sets a line of the Canvas at the given y position. The function // assumes a line of the correct width has been provided. func (c *Canvas) SetLineAt(row int, line []Cell) { c.Lines[row] = line } // AppendLine will append the array of Cells provided to the bottom of // the receiver Canvas. If the makeCopy argument is true, a copy is made // of the provided Cell array; otherwise, a slice is taken and used // directly, meaning the Canvas will hold a reference to the underlying // array. func (c *Canvas) AppendLine(line []Cell, makeCopy bool) { newwidth := gwutil.Max(c.BoxColumns(), len(line)) var newline []Cell if cap(line) < newwidth { makeCopy = true } if makeCopy { newline = make([]Cell, newwidth) copy(newline, line) } else if len(line) < newwidth { // extend slice newline = line[0:newwidth] } else { newline = line } c.Lines = append(c.Lines, newline) c.AlignRight() } // String lets Canvas conform to fmt.Stringer. func (c *Canvas) String() string { return CanvasToString(c) } func CanvasToString(c ICanvas) string { lineStrings := make([]string, c.BoxRows()) for i := 0; i < c.BoxRows(); i++ { line := c.Line(i, LineCopy{}).Line curLine := make([]rune, 0) for x := 0; x < len(line); { r := line[x].Rune() curLine = append(curLine, r) x += runewidth.RuneWidth(r) } lineStrings[i] = string(curLine) } return strings.Join(lineStrings, "\n") } // ExtendRight appends to each line of the receiver Canvas the array of // Cells provided as an argument. func (c *Canvas) ExtendRight(cells []Cell) { if len(cells) > 0 { for i := 0; i < len(c.Lines); i++ { if len(c.Lines[i])+len(cells) > cap(c.Lines[i]) { widerLine := make([]Cell, len(c.Lines[i]), len(c.Lines[i])+len(cells)) copy(widerLine, c.Lines[i]) c.Lines[i] = widerLine } c.Lines[i] = append(c.Lines[i], cells...) } c.maxCol += len(cells) } } // ExtendLeft prepends to each line of the receiver Canvas the array of // Cells provided as an argument. func (c *Canvas) ExtendLeft(cells []Cell) { if len(cells) > 0 { for i := 0; i < len(c.Lines); i++ { cellsCopy := make([]Cell, len(cells)+len(c.Lines[i])) copy(cellsCopy, cells) copy(cellsCopy[len(cells):], c.Lines[i]) c.Lines[i] = cellsCopy } if c.Marks != nil { for k, pos := range *c.Marks { (*c.Marks)[k] = pos.PlusX(len(cells)) } } c.maxCol += len(cells) } } // AppendBelow appends the supplied Canvas to the "bottom" of the receiver Canvas. If // doCursor is true and the supplied Canvas has an enabled cursor, it is applied to // the received Canvas, with a suitable Y offset. If makeCopy is true then the supplied // Canvas is copied; if false, and the supplied Canvas is capable of giving up // ownership of its data structures, then they are moved to the receiver Canvas. func (c *Canvas) AppendBelow(c2 IAppendCanvas, doCursor bool, makeCopy bool) { cw := c.BoxColumns() lenc := len(c.Lines) for i := 0; i < c2.BoxRows(); i++ { lr := c2.Line(i, LineCopy{ Len: cw, Cap: cw, }) if makeCopy && !lr.Copied { line := make([]Cell, cw) copy(line, lr.Line) c.Lines = append(c.Lines, line) } else { c.Lines = append(c.Lines, lr.Line) } } c.AlignRight() c2.RangeOverMarks(func(k string, pos CanvasPos) bool { if doCursor || (k != "cursor") { if c.Marks == nil { marks := make(map[string]CanvasPos) c.Marks = &marks } (*c.Marks)[k] = pos.PlusY(lenc) } return true }) } // Truncate removes "above" lines from above the receiver Canvas, and // "below" lines from below. func (c *Canvas) Truncate(above, below int) { if above < 0 { panic(errors.New("Lines to cut above must be >= 0")) } if below < 0 { panic(errors.New("Lines to cut below must be >= 0")) } cutAbove := gwutil.Min(len(c.Lines), above) c.Lines = c.Lines[cutAbove:] cutBelow := len(c.Lines) - gwutil.Min(len(c.Lines), below) c.Lines = c.Lines[:cutBelow] if c.Marks != nil { for k, pos := range *c.Marks { (*c.Marks)[k] = pos.PlusY(-cutAbove) } } } type CellMergeFunc func(lower, upper Cell) Cell // MergeWithFunc merges the supplied Canvas with the receiver canvas, where the receiver canvas // is considered to start at column leftOffset and at row topOffset, therefore translated some // distance from the top-left, and the receiver Canvas is the one modified. A function argument // is supplied which specifies how Cells are merged, one by one e.g. which style takes effect, // which rune, and so on. func (c *Canvas) MergeWithFunc(c2 IMergeCanvas, leftOffset, topOffset int, fn CellMergeFunc, bottomGetsCursor bool) { c2w := c2.BoxColumns() for i := 0; i < c2.BoxRows(); i++ { if i+topOffset < len(c.Lines) { cl := len(c.Lines[i+topOffset]) for j := 0; j < c2w; j++ { if j+leftOffset < cl { c2ij := c2.CellAt(j, i) c.Lines[i+topOffset][j+leftOffset] = fn(c.Lines[i+topOffset][j+leftOffset], c2ij) } else { break } } } } c2.RangeOverMarks(func(k string, v CanvasPos) bool { // Special treatment for the cursor mark - to allow widgets to display the cursor via // a "lower" widget. The terminal will typically support displaying one cursor only. if k != "cursor" || !bottomGetsCursor { if c.Marks == nil { marks := make(map[string]CanvasPos) c.Marks = &marks } (*c.Marks)[k] = v.PlusX(leftOffset).PlusY(topOffset) } return true }) } // MergeUnder merges the supplied Canvas "under" the receiver Canvas, meaning the // receiver Canvas's Cells' settings are given priority. func (c *Canvas) MergeUnder(c2 IMergeCanvas, leftOffset, topOffset int, bottomGetsCursor bool) { c.MergeWithFunc(c2, leftOffset, topOffset, Cell.MergeUnder, bottomGetsCursor) } // AppendRight appends the supplied Canvas to the right of the receiver Canvas. It // assumes both Canvases have the same number of rows. If useCursor is true and the // supplied Canvas has an enabled cursor, then it is applied with a suitable X // offset applied. func (c *Canvas) AppendRight(c2 IMergeCanvas, useCursor bool) { m := c.BoxColumns() c2w := c2.BoxColumns() for y := 0; y < c2.BoxRows(); y++ { if cap(c.Lines[y]) < len(c.Lines[y])+c2w { widerLine := make([]Cell, len(c.Lines[y])+c2w) copy(widerLine, c.Lines[y]) c.Lines[y] = widerLine } else { c.Lines[y] = c.Lines[y][0 : len(c.Lines[y])+c2w] } for x := 0; x < c2w; x++ { c.Lines[y][x+m] = c2.CellAt(x, y) } } c2.RangeOverMarks(func(k string, v CanvasPos) bool { if (k != "cursor") || useCursor { if c.Marks == nil { marks := make(map[string]CanvasPos) c.Marks = &marks } (*c.Marks)[k] = v.PlusX(m) } return true }) c.maxCol = m + c2w } // TrimRight removes columns from the right of the receiver Canvas until there // is the specified number left. func (c *Canvas) TrimRight(colsToHave int) { for i := 0; i < len(c.Lines); i++ { if len(c.Lines[i]) > colsToHave { c.Lines[i] = c.Lines[i][0:colsToHave] } } c.maxCol = colsToHave } // TrimLeft removes columns from the left of the receiver Canvas until there // is the specified number left. func (c *Canvas) TrimLeft(colsToHave int) { colsToTrim := 0 for i := 0; i < len(c.Lines); i++ { colsToTrim = gwutil.Max(colsToTrim, len(c.Lines[i])-colsToHave) } for i := 0; i < len(c.Lines); i++ { if len(c.Lines[i]) >= colsToTrim { c.Lines[i] = c.Lines[i][colsToTrim:] } } if c.Marks != nil { for k, v := range *c.Marks { (*c.Marks)[k] = v.PlusX(-colsToTrim) } } } func appendCell(slice []Cell, data Cell, num int) []Cell { m := len(slice) n := m + num if n > cap(slice) { // if necessary, reallocate newSlice := make([]Cell, (n+1)*2) copy(newSlice, slice) slice = newSlice } slice = slice[0:n] for i := 0; i < num; i++ { slice[m+i] = data } return slice } // AlignRightWith will extend each row of Cells in the receiver Canvas with // the supplied Cell in order to ensure all rows are the same length. Note // that the Canvas will not increase in width as a result. func (c *Canvas) AlignRightWith(cell Cell) { m := c.ComputeCurrentMaxColumn() for j, line := range c.Lines { lineLen := len(line) cols := m - lineLen if len(c.Lines[j])+cols > cap(c.Lines[j]) { tmp := make([]Cell, len(c.Lines[j]), len(c.Lines[j])+cols+32) copy(tmp, c.Lines[j]) c.Lines[j] = tmp[0:len(c.Lines[j])] } c.Lines[j] = appendCell(c.Lines[j], cell, m-lineLen) } c.maxCol = m } // AlignRight will extend each row of Cells in the receiver Canvas with an // empty Cell in order to ensure all rows are the same length. Note that // the Canvas will not increase in width as a result. func (c *Canvas) AlignRight() { c.AlignRightWith(Cell{}) } // Draw will render a Canvas to a tcell Screen. func Draw(canvas IDrawCanvas, mode IColorMode, screen tcell.Screen) { cpos := CanvasPos{X: -1, Y: -1} if canvas.CursorEnabled() { cpos = canvas.CursorCoords() } screen.ShowCursor(-1, -1) for y := 0; y < canvas.BoxRows(); y++ { line := canvas.Line(y, LineCopy{}) vline := line.Line for x := 0; x < len(vline); { c := vline[x] f, b, s := c.ForegroundColor(), c.BackgroundColor(), c.Style() st := MakeCellStyle(f, b, s) screen.SetContent(x, y, c.Rune(), nil, st) x += runewidth.RuneWidth(c.Rune()) if x == cpos.X && y == cpos.Y { screen.ShowCursor(x, y) } } } } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: gowid-1.4.0/canvas_test.go000066400000000000000000000042731426234454000155000ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. package gowid import ( "io" "testing" "github.com/stretchr/testify/assert" ) func TestInterfaces1(t *testing.T) { var _ io.Writer = (*Canvas)(nil) } func TestInterfaces5(t *testing.T) { var _ IComposite = (*App)(nil) } func TestCanvas19(t *testing.T) { canvas := NewCanvas() c := canvas.BoxColumns() r := canvas.BoxRows() if c != 0 || r != 0 { t.Errorf("Failed") } r1 := CellsFromString("abc") r2 := CellsFromString("12") canvas.AppendLine(r1, false) canvas.AppendLine(r2, false) c = canvas.BoxColumns() r = canvas.BoxRows() if c != 3 || r != 2 { t.Errorf("Failed c is %d r is %d", c, r) } cs := canvas.String() if cs != "abc\n12 " { t.Errorf("Failed cs is %v", cs) } canvas.AlignRight() cs = canvas.String() if cs != "abc\n12 " { t.Errorf("Failed") } canvas2 := NewCanvas() r21 := CellsFromString(" X Z") r22 := CellsFromString("Y2") canvas2.AppendLine(r21, false) canvas2.AppendLine(r22, false) canvas.MergeUnder(canvas2, 0, 0, false) cs = canvas.String() if cs != "aXc\nY2 " { t.Errorf("Failed") } assert.Equal(t, canvas.BoxColumns(), 3) var n int var err error n, err = canvas.Write([]byte{'1', '2', '3', 'Q'}) assert.NoError(t, err) assert.Equal(t, 4, n) assert.Equal(t, canvas.String(), "123\nQ2 ") n, err = canvas.Write([]byte{'5', '\n'}) assert.NoError(t, err) assert.Equal(t, 2, n) assert.Equal(t, canvas.String(), "5 \nQ2 ") n, err = canvas.Write([]byte{0xe4, 0xbd, 0xa0, '\n'}) assert.NoError(t, err) assert.Equal(t, 4, n) assert.Equal(t, "你 \nQ2 ", canvas.String()) n, err = canvas.Write([]byte{'1', '2', '\n', 'R'}) assert.NoError(t, err) assert.Equal(t, 4, n) assert.Equal(t, "12 \nR2 ", canvas.String()) } type MyString string func (s MyString) Tester() int { return len(s) } type FooType interface { Tester() int } func MyTestFn(f FooType) { } func TestCanvas1(t *testing.T) { f := MyString("xyz") MyTestFn(f) assert.Equal(t, f.Tester(), 3) } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: gowid-1.4.0/cell.go000066400000000000000000000125231426234454000141020ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. package gowid //====================================================================== // Cell represents a single element of terminal output. The empty value // is a blank cell with default colors, style, and a 'blank' rune. It is // closely tied to TCell's underlying cell representation - colors are // TCell-specific, so are translated from anything more general before a // Cell is instantiated. type Cell struct { codePoint rune fg TCellColor bg TCellColor style StyleAttrs } // MakeCell returns a Cell initialized with the supplied run (char to display), // foreground color, background color and style attributes. Each color can specify // "default" meaning whatever the terminal default foreground/background is, or // "none" meaning no preference, allowing it to be overridden when laid on top // of another Cell during the render process. func MakeCell(codePoint rune, fg TCellColor, bg TCellColor, Attr StyleAttrs) Cell { return Cell{ codePoint: codePoint, fg: fg, bg: bg, style: Attr, } } // MergeUnder returns a Cell representing the receiver merged "underneath" the // Cell argument provided. This means the argument's rune value will be used // unless it is "empty", and the cell's color and styling come from the // argument's value in a similar fashion. func (c Cell) MergeUnder(upper Cell) Cell { res := c if upper.codePoint != 0 { res.codePoint = upper.codePoint } return res.MergeDisplayAttrsUnder(upper) } // MergeDisplayAttrsUnder returns a Cell representing the receiver Cell with the // argument Cell's color and styling applied, if they are explicitly set. func (c Cell) MergeDisplayAttrsUnder(upper Cell) Cell { res := c ufg, ubg, ust := upper.GetDisplayAttrs() if ubg != ColorNone { res = res.WithBackgroundColor(ubg) } if ufg != ColorNone { res = res.WithForegroundColor(ufg) } res.style = res.style.MergeUnder(ust) return res } // GetDisplayAttrs returns the receiver Cell's foreground and background color // and styling. func (c Cell) GetDisplayAttrs() (x TCellColor, y TCellColor, z StyleAttrs) { x = c.ForegroundColor() y = c.BackgroundColor() z = c.Style() return } // HasRune returns true if the Cell actively specifies a rune to display; otherwise // false, meaning there it is "empty", and a Cell layered underneath it will have its // rune displayed. func (c Cell) HasRune() bool { return c.codePoint != 0 } // Rune will return a rune that can be displayed, if this Cell is being rendered in some // fashion. If the Cell is empty, then a space rune is returned. func (c Cell) Rune() rune { if !c.HasRune() { return ' ' } else { return c.codePoint } } // WithRune returns a Cell equal to the receiver Cell but that will render the supplied // rune instead. func (c Cell) WithRune(r rune) Cell { c.codePoint = r return c } // BackgroundColor returns the background color of the receiver Cell. func (c Cell) BackgroundColor() TCellColor { return c.bg } // ForegroundColor returns the foreground color of the receiver Cell. func (c Cell) ForegroundColor() TCellColor { return c.fg } // Style returns the style of the receiver Cell. func (c Cell) Style() StyleAttrs { return c.style } // WithRune returns a Cell equal to the receiver Cell but that will render no // rune instead i.e. it is "empty". func (c Cell) WithNoRune() Cell { c.codePoint = 0 return c } // WithBackgroundColor returns a Cell equal to the receiver Cell but that // will render with the supplied background color instead. Note that this color // can be set to "none" by passing the value gowid.ColorNone, meaning allow // Cells layered underneath to determine the background color. func (c Cell) WithBackgroundColor(a TCellColor) Cell { c.bg = a return c } // WithForegroundColor returns a Cell equal to the receiver Cell but that // will render with the supplied foreground color instead. Note that this color // can be set to "none" by passing the value gowid.ColorNone, meaning allow // Cells layered underneath to determine the background color. func (c Cell) WithForegroundColor(a TCellColor) Cell { c.fg = a return c } // WithStyle returns a Cell equal to the receiver Cell but that will render // with the supplied style (e.g. underline) instead. Note that this style // can be set to "none" by passing the value gowid.AttrNone, meaning allow // Cells layered underneath to determine the style. func (c Cell) WithStyle(attr StyleAttrs) Cell { c.style = attr return c } //====================================================================== // CellFromRune returns a Cell with the supplied rune and with default // coloring and styling. func CellFromRune(r rune) Cell { return MakeCell(r, ColorNone, ColorNone, StyleNone) } // CellsFromString is a utility function to turn a string into an array // of Cells. Note that each Cell has no color or style set. func CellsFromString(s string) []Cell { res := make([]Cell, 0, len(s)) // overcommits, counts chars and not runes, but minimizes reallocations. for _, r := range s { if r != ' ' { res = append(res, CellFromRune(r)) } else { res = append(res, Cell{}) } } return res } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: gowid-1.4.0/decoration.go000066400000000000000000002374601426234454000153230ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source code is governed by the MIT license // that can be found in the LICENSE file. package gowid import ( "fmt" "os" "regexp" "strconv" "github.com/gcla/gowid/gwutil" tcell "github.com/gdamore/tcell/v2" lru "github.com/hashicorp/golang-lru" "github.com/lucasb-eyer/go-colorful" "github.com/pkg/errors" ) //====================================================================== // These are used as bitmasks - a style is two AttrMasks. The first bitmask says whether or not the style declares an // e.g. underline setting; if it's declared, the second bitmask says whether or not underline is affirmatively on or off. // This allows styles to be layered e.g. the lower style declares underline is on, the upper style does not declare // an underline preference, so when layered, the cell is rendered with an underline. const ( StyleNoneSet tcell.AttrMask = 0 // Just unstyled text. StyleAllSet tcell.AttrMask = tcell.AttrBold | tcell.AttrBlink | tcell.AttrReverse | tcell.AttrUnderline | tcell.AttrDim ) // StyleAttrs allows the user to represent a set of styles, either affirmatively set (on) or unset (off) // with the rest of the styles being unspecified, meaning they can be determined by styles layered // "underneath". type StyleAttrs struct { OnOff tcell.AttrMask // If the specific bit in Set is 1, then the specific bit on OnOff says whether the style is on or off Set tcell.AttrMask // If the specific bit in Set is 0, then no style preference is declared (e.g. for underline) } // AllStyleMasks is an array of all the styles that can be applied to a Cell. var AllStyleMasks = [...]tcell.AttrMask{tcell.AttrBold, tcell.AttrBlink, tcell.AttrDim, tcell.AttrReverse, tcell.AttrUnderline} // StyleNone expresses no preference for any text styles. var StyleNone = StyleAttrs{} // StyleBold specifies the text should be bold, but expresses no preference for other text styles. var StyleBold = StyleAttrs{tcell.AttrBold, tcell.AttrBold} // StyleBlink specifies the text should blink, but expresses no preference for other text styles. var StyleBlink = StyleAttrs{tcell.AttrBlink, tcell.AttrBlink} // StyleDim specifies the text should be dim, but expresses no preference for other text styles. var StyleDim = StyleAttrs{tcell.AttrDim, tcell.AttrDim} // StyleReverse specifies the text should be displayed as reverse-video, but expresses no preference for other text styles. var StyleReverse = StyleAttrs{tcell.AttrReverse, tcell.AttrReverse} // StyleUnderline specifies the text should be underlined, but expresses no preference for other text styles. var StyleUnderline = StyleAttrs{tcell.AttrUnderline, tcell.AttrUnderline} // StyleBoldOnly specifies the text should be bold, and no other styling should apply. var StyleBoldOnly = StyleAttrs{tcell.AttrBold, StyleAllSet} // StyleBlinkOnly specifies the text should blink, and no other styling should apply. var StyleBlinkOnly = StyleAttrs{tcell.AttrBlink, StyleAllSet} // StyleDimOnly specifies the text should be dim, and no other styling should apply. var StyleDimOnly = StyleAttrs{tcell.AttrDim, StyleAllSet} // StyleReverseOnly specifies the text should be displayed reverse-video, and no other styling should apply. var StyleReverseOnly = StyleAttrs{tcell.AttrReverse, StyleAllSet} // StyleUnderlineOnly specifies the text should be underlined, and no other styling should apply. var StyleUnderlineOnly = StyleAttrs{tcell.AttrUnderline, StyleAllSet} // IgnoreBase16 should be set to true if gowid should not consider colors 0-21 for closest-match when // interpolating RGB colors in 256-color space. You might use this if you use base16-shell, for example, // to make use of base16-themes for all terminal applications (https://github.com/chriskempson/base16-shell) var IgnoreBase16 = false // MergeUnder merges cell styles. E.g. if a is {underline, underline}, and upper is {!bold, bold}, that // means a declares that it should be rendered with underline and doesn't care about other styles; and // upper declares it should NOT be rendered bold, and doesn't declare about other styles. When merged, // the result is {underline|!bold, underline|bold}. func (a StyleAttrs) MergeUnder(upper StyleAttrs) StyleAttrs { res := a for _, am := range AllStyleMasks { if (upper.Set & am) != 0 { if (upper.OnOff & am) != 0 { res.OnOff |= am } else { res.OnOff &= ^am } res.Set |= am } } return res } //====================================================================== // ColorMode represents the color capability of a terminal. type ColorMode int const ( // Mode256Colors represents a terminal with 256-color support. Mode256Colors = ColorMode(iota) // Mode88Colors represents a terminal with 88-color support such as rxvt. Mode88Colors // Mode16Colors represents a terminal with 16-color support. Mode16Colors // Mode8Colors represents a terminal with 8-color support. Mode8Colors // Mode8Colors represents a terminal with support for monochrome only. ModeMonochrome // Mode24BitColors represents a terminal with 24-bit color support like KDE's terminal. Mode24BitColors ) func (c ColorMode) String() string { switch c { case Mode256Colors: return "256 colors" case Mode88Colors: return "88 colors" case Mode16Colors: return "16 colors" case Mode8Colors: return "8 colors" case ModeMonochrome: return "monochrome" case Mode24BitColors: return "24-bit truecolor" default: return fmt.Sprintf("Unknown (%d)", int(c)) } } const ( colorDefaultName = "default" colorBlackName = "black" colorRedName = "red" colorDarkRedName = "dark red" colorGreenName = "green" colorDarkGreenName = "dark green" colorBrownName = "brown" colorBlueName = "blue" colorDarkBlueName = "dark blue" colorMagentaName = "magenta" colorDarkMagentaName = "dark magenta" colorCyanName = "cyan" colorDarkCyanName = "dark cyan" colorLightGrayName = "light gray" colorDarkGrayName = "dark gray" colorLightRedName = "light red" colorLightGreenName = "light green" colorYellowName = "yellow" colorLightBlueName = "light blue" colorLightMagentaName = "light magenta" colorLightCyanName = "light cyan" colorWhiteName = "white" ) var ( basicColors = map[string]int{ colorDefaultName: 0, colorBlackName: 1, colorDarkRedName: 2, colorDarkGreenName: 3, colorBrownName: 4, colorDarkBlueName: 5, colorDarkMagentaName: 6, colorDarkCyanName: 7, colorLightGrayName: 8, colorDarkGrayName: 9, colorLightRedName: 10, colorLightGreenName: 11, colorYellowName: 12, colorLightBlueName: 13, colorLightMagentaName: 14, colorLightCyanName: 15, colorWhiteName: 16, colorRedName: 10, colorGreenName: 11, colorBlueName: 13, colorMagentaName: 14, colorCyanName: 15, } tBasicColors = map[string]int{ colorDefaultName: 0, colorBlackName: 1, colorDarkRedName: 2, colorDarkGreenName: 3, colorBrownName: 4, colorDarkBlueName: 5, colorDarkMagentaName: 6, colorDarkCyanName: 7, colorLightGrayName: 8, colorDarkGrayName: 1, colorLightRedName: 2, colorLightGreenName: 3, colorYellowName: 4, colorLightBlueName: 5, colorLightMagentaName: 6, colorLightCyanName: 7, colorWhiteName: 8, colorRedName: 2, colorGreenName: 3, colorBlueName: 5, colorMagentaName: 6, colorCyanName: 7, } CubeStart = 16 // first index of color cube CubeSize256 = 6 // one side of the color cube graySize256 = 24 grayStart256 = gwutil.IPow(CubeSize256, 3) + CubeStart cubeWhite256 = grayStart256 - 1 cubeSize88 = 4 graySize88 = 8 grayStart88 = gwutil.IPow(cubeSize88, 3) + CubeStart cubeWhite88 = grayStart88 - 1 cubeBlack = CubeStart cubeSteps256 = []int{0x00, 0x5f, 0x87, 0xaf, 0xd7, 0xff} graySteps256 = []int{ 0x08, 0x12, 0x1c, 0x26, 0x30, 0x3a, 0x44, 0x4e, 0x58, 0x62, 0x6c, 0x76, 0x80, 0x84, 0x94, 0x9e, 0xa8, 0xb2, 0xbc, 0xc6, 0xd0, 0xda, 0xe4, 0xee, } cubeSteps88 = []int{0x00, 0x8b, 0xcd, 0xff} graySteps88 = []int{0x2e, 0x5c, 0x73, 0x8b, 0xa2, 0xb9, 0xd0, 0xe7} cubeLookup256 = makeColorLookup(cubeSteps256, 256) grayLookup256 = makeColorLookup(append([]int{0x00}, append(graySteps256, 0xff)...), 256) cubeLookup88 = makeColorLookup(cubeSteps88, 256) grayLookup88 = makeColorLookup(append([]int{0x00}, append(graySteps88, 0xff)...), 256) cubeLookup256_16 []int grayLookup256_101 []int cubeLookup88_16 []int grayLookup88_101 []int // ColorNone means no preference if anything is layered underneath ColorNone = MakeTCellNoColor() // ColorDefault is an affirmative preference for the default terminal color ColorDefault = MakeTCellColorExt(tcell.ColorDefault) // Some pre-initialized color objects for use in applications e.g. // MakePaletteEntry(ColorBlack, ColorRed) ColorBlack = MakeTCellColorExt(tcell.ColorBlack) ColorRed = MakeTCellColorExt(tcell.ColorRed) ColorGreen = MakeTCellColorExt(tcell.ColorGreen) ColorLightGreen = MakeTCellColorExt(tcell.ColorLightGreen) ColorYellow = MakeTCellColorExt(tcell.ColorYellow) ColorBlue = MakeTCellColorExt(tcell.ColorBlue) ColorLightBlue = MakeTCellColorExt(tcell.ColorLightBlue) ColorMagenta = MakeTCellColorExt(tcell.ColorDarkMagenta) ColorCyan = MakeTCellColorExt(tcell.ColorDarkCyan) ColorWhite = MakeTCellColorExt(tcell.ColorWhite) ColorDarkRed = MakeTCellColorExt(tcell.ColorDarkRed) ColorDarkGreen = MakeTCellColorExt(tcell.ColorDarkGreen) ColorDarkBlue = MakeTCellColorExt(tcell.ColorDarkBlue) ColorLightGray = MakeTCellColorExt(tcell.ColorLightGray) ColorDarkGray = MakeTCellColorExt(tcell.ColorDarkGray) ColorPurple = MakeTCellColorExt(tcell.ColorPurple) ColorOrange = MakeTCellColorExt(tcell.ColorOrange) longColorRE = regexp.MustCompile(`^#([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$`) shortColorRE = regexp.MustCompile(`^#([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F])$`) grayHexColorRE = regexp.MustCompile(`^g#([0-9a-fA-F][0-9a-fA-F])$`) grayDecColorRE = regexp.MustCompile(`^g(1?[0-9][0-9]?)$`) colorfulBlack8 = colorful.Color{R: 0.0, G: 0.0, B: 0.0} colorfulWhite8 = colorful.Color{R: 1.0, G: 1.0, B: 1.0} colorfulRed8 = colorful.Color{R: 1.0, G: 0.0, B: 0.0} colorfulGreen8 = colorful.Color{R: 0.0, G: 1.0, B: 0.0} colorfulBlue8 = colorful.Color{R: 0.0, G: 0.0, B: 1.0} colorfulYellow8 = colorful.Color{R: 1.0, G: 1.0, B: 0.0} colorfulMagenta8 = colorful.Color{R: 1.0, G: 0.0, B: 1.0} colorfulCyan8 = colorful.Color{R: 0.0, G: 1.0, B: 1.0} colorfulBlack16 = colorful.Color{R: 0.0, G: 0.0, B: 0.0} colorfulWhite16 = colorful.Color{R: 0.66, G: 0.66, B: 0.66} colorfulRed16 = colorful.Color{R: 0.5, G: 0.0, B: 0.0} colorfulGreen16 = colorful.Color{R: 0.0, G: 0.5, B: 0.0} colorfulBlue16 = colorful.Color{R: 0.0, G: 0.0, B: 0.5} colorfulYellow16 = colorful.Color{R: 0.5, G: 0.5, B: 0.5} colorfulMagenta16 = colorful.Color{R: 0.5, G: 0.0, B: 0.5} colorfulCyan16 = colorful.Color{R: 0.0, G: 0.5, B: 0.5} colorfulBrightBlack16 = colorful.Color{R: 0.33, G: 0.33, B: 0.33} colorfulBrightWhite16 = colorful.Color{R: 1.0, G: 1.0, B: 1.0} colorfulBrightRed16 = colorful.Color{R: 1.0, G: 0.0, B: 0.0} colorfulBrightGreen16 = colorful.Color{R: 0.0, G: 1.0, B: 0.0} colorfulBrightBlue16 = colorful.Color{R: 0.0, G: 0.0, B: 1.0} colorfulBrightYellow16 = colorful.Color{R: 1.0, G: 1.0, B: 1.0} colorfulBrightMagenta16 = colorful.Color{R: 1.0, G: 0.0, B: 1.0} colorfulBrightCyan16 = colorful.Color{R: 0.0, G: 1.0, B: 1.0} // Used in mapping RGB colors down to 8 terminal colors. colorful8 = []colorful.Color{ colorfulBlack8, colorfulWhite8, colorfulRed8, colorfulGreen8, colorfulBlue8, colorfulYellow8, colorfulMagenta8, colorfulCyan8, } // Used in mapping RGB colors down to 16 terminal colors. colorful16 = []colorful.Color{ colorfulBlack16, colorfulWhite16, colorfulRed16, colorfulGreen16, colorfulBlue16, colorfulYellow16, colorfulMagenta16, colorfulCyan16, colorfulBrightBlack16, colorfulBrightWhite16, colorfulBrightRed16, colorfulBrightGreen16, colorfulBrightBlue16, colorfulBrightYellow16, colorfulBrightMagenta16, colorfulBrightCyan16, } colorful256 = []colorful.Color{ colorful.Color{R: float64(0x00) / float64(256), G: float64(0x00) / float64(256), B: float64(0x00) / float64(256)}, //'000000'), colorful.Color{R: float64(0x80) / float64(256), G: float64(0x00) / float64(256), B: float64(0x00) / float64(256)}, //'800000'), colorful.Color{R: float64(0x00) / float64(256), G: float64(0x80) / float64(256), B: float64(0x00) / float64(256)}, //'008000'), colorful.Color{R: float64(0x80) / float64(256), G: float64(0x80) / float64(256), B: float64(0x00) / float64(256)}, //'808000'), colorful.Color{R: float64(0x00) / float64(256), G: float64(0x00) / float64(256), B: float64(0x80) / float64(256)}, //'000080'), colorful.Color{R: float64(0x80) / float64(256), G: float64(0x00) / float64(256), B: float64(0x80) / float64(256)}, //'800080'), colorful.Color{R: float64(0x00) / float64(256), G: float64(0x80) / float64(256), B: float64(0x80) / float64(256)}, //'008080'), colorful.Color{R: float64(0xc0) / float64(256), G: float64(0xc0) / float64(256), B: float64(0xc0) / float64(256)}, //'c0c0c0'), colorful.Color{R: float64(0x80) / float64(256), G: float64(0x80) / float64(256), B: float64(0x80) / float64(256)}, //'808080'), colorful.Color{R: float64(0xff) / float64(256), G: float64(0x00) / float64(256), B: float64(0x00) / float64(256)}, //'ff0000'), colorful.Color{R: float64(0x00) / float64(256), G: float64(0xff) / float64(256), B: float64(0x00) / float64(256)}, //'00ff00'), colorful.Color{R: float64(0xff) / float64(256), G: float64(0xff) / float64(256), B: float64(0x00) / float64(256)}, //'ffff00'), colorful.Color{R: float64(0x00) / float64(256), G: float64(0x00) / float64(256), B: float64(0xff) / float64(256)}, //'0000ff'), colorful.Color{R: float64(0xff) / float64(256), G: float64(0x00) / float64(256), B: float64(0xff) / float64(256)}, //'ff00ff'), colorful.Color{R: float64(0x00) / float64(256), G: float64(0xff) / float64(256), B: float64(0xff) / float64(256)}, //'00ffff'), colorful.Color{R: float64(0xff) / float64(256), G: float64(0xff) / float64(256), B: float64(0xff) / float64(256)}, //'ffffff'), colorful.Color{R: float64(0x00) / float64(256), G: float64(0x00) / float64(256), B: float64(0x00) / float64(256)}, //'000000'), colorful.Color{R: float64(0x00) / float64(256), G: float64(0x00) / float64(256), B: float64(0x5f) / float64(256)}, //'00005f'), colorful.Color{R: float64(0x00) / float64(256), G: float64(0x00) / float64(256), B: float64(0x87) / float64(256)}, //'000087'), colorful.Color{R: float64(0x00) / float64(256), G: float64(0x00) / float64(256), B: float64(0xaf) / float64(256)}, //'0000af'), colorful.Color{R: float64(0x00) / float64(256), G: float64(0x00) / float64(256), B: float64(0xd7) / float64(256)}, //'0000d7'), colorful.Color{R: float64(0x00) / float64(256), G: float64(0x00) / float64(256), B: float64(0xff) / float64(256)}, //'0000ff'), colorful.Color{R: float64(0x00) / float64(256), G: float64(0x5f) / float64(256), B: float64(0x00) / float64(256)}, //'005f00'), colorful.Color{R: float64(0x00) / float64(256), G: float64(0x5f) / float64(256), B: float64(0x5f) / float64(256)}, //'005f5f'), colorful.Color{R: float64(0x00) / float64(256), G: float64(0x5f) / float64(256), B: float64(0x87) / float64(256)}, //'005f87'), colorful.Color{R: float64(0x00) / float64(256), G: float64(0x5f) / float64(256), B: float64(0xaf) / float64(256)}, //'005faf'), colorful.Color{R: float64(0x00) / float64(256), G: float64(0x5f) / float64(256), B: float64(0xd7) / float64(256)}, //'005fd7'), colorful.Color{R: float64(0x00) / float64(256), G: float64(0x5f) / float64(256), B: float64(0xff) / float64(256)}, //'005fff'), colorful.Color{R: float64(0x00) / float64(256), G: float64(0x87) / float64(256), B: float64(0x00) / float64(256)}, //'008700'), colorful.Color{R: float64(0x00) / float64(256), G: float64(0x87) / float64(256), B: float64(0x5f) / float64(256)}, //'00875f'), colorful.Color{R: float64(0x00) / float64(256), G: float64(0x87) / float64(256), B: float64(0x87) / float64(256)}, //'008787'), colorful.Color{R: float64(0x00) / float64(256), G: float64(0x87) / float64(256), B: float64(0xaf) / float64(256)}, //'0087af'), colorful.Color{R: float64(0x00) / float64(256), G: float64(0x87) / float64(256), B: float64(0xd7) / float64(256)}, //'0087d7'), colorful.Color{R: float64(0x00) / float64(256), G: float64(0x87) / float64(256), B: float64(0xff) / float64(256)}, //'0087ff'), colorful.Color{R: float64(0x00) / float64(256), G: float64(0xaf) / float64(256), B: float64(0x00) / float64(256)}, //'00af00'), colorful.Color{R: float64(0x00) / float64(256), G: float64(0xaf) / float64(256), B: float64(0x5f) / float64(256)}, //'00af5f'), colorful.Color{R: float64(0x00) / float64(256), G: float64(0xaf) / float64(256), B: float64(0x87) / float64(256)}, //'00af87'), colorful.Color{R: float64(0x00) / float64(256), G: float64(0xaf) / float64(256), B: float64(0xaf) / float64(256)}, //'00afaf'), colorful.Color{R: float64(0x00) / float64(256), G: float64(0xaf) / float64(256), B: float64(0xd7) / float64(256)}, //'00afd7'), colorful.Color{R: float64(0x00) / float64(256), G: float64(0xaf) / float64(256), B: float64(0xff) / float64(256)}, //'00afff'), colorful.Color{R: float64(0x00) / float64(256), G: float64(0xd7) / float64(256), B: float64(0x00) / float64(256)}, //'00d700'), colorful.Color{R: float64(0x00) / float64(256), G: float64(0xd7) / float64(256), B: float64(0x5f) / float64(256)}, //'00d75f'), colorful.Color{R: float64(0x00) / float64(256), G: float64(0xd7) / float64(256), B: float64(0x87) / float64(256)}, //'00d787'), colorful.Color{R: float64(0x00) / float64(256), G: float64(0xd7) / float64(256), B: float64(0xaf) / float64(256)}, //'00d7af'), colorful.Color{R: float64(0x00) / float64(256), G: float64(0xd7) / float64(256), B: float64(0xd7) / float64(256)}, //'00d7d7'), colorful.Color{R: float64(0x00) / float64(256), G: float64(0xd7) / float64(256), B: float64(0xff) / float64(256)}, //'00d7ff'), colorful.Color{R: float64(0x00) / float64(256), G: float64(0xff) / float64(256), B: float64(0x00) / float64(256)}, //'00ff00'), colorful.Color{R: float64(0x00) / float64(256), G: float64(0xff) / float64(256), B: float64(0x5f) / float64(256)}, //'00ff5f'), colorful.Color{R: float64(0x00) / float64(256), G: float64(0xff) / float64(256), B: float64(0x87) / float64(256)}, //'00ff87'), colorful.Color{R: float64(0x00) / float64(256), G: float64(0xff) / float64(256), B: float64(0xaf) / float64(256)}, //'00ffaf'), colorful.Color{R: float64(0x00) / float64(256), G: float64(0xff) / float64(256), B: float64(0xd7) / float64(256)}, //'00ffd7'), colorful.Color{R: float64(0x00) / float64(256), G: float64(0xff) / float64(256), B: float64(0xff) / float64(256)}, //'00ffff'), colorful.Color{R: float64(0x5f) / float64(256), G: float64(0x00) / float64(256), B: float64(0x00) / float64(256)}, //'5f0000'), colorful.Color{R: float64(0x5f) / float64(256), G: float64(0x00) / float64(256), B: float64(0x5f) / float64(256)}, //'5f005f'), colorful.Color{R: float64(0x5f) / float64(256), G: float64(0x00) / float64(256), B: float64(0x87) / float64(256)}, //'5f0087'), colorful.Color{R: float64(0x5f) / float64(256), G: float64(0x00) / float64(256), B: float64(0xaf) / float64(256)}, //'5f00af'), colorful.Color{R: float64(0x5f) / float64(256), G: float64(0x00) / float64(256), B: float64(0xd7) / float64(256)}, //'5f00d7'), colorful.Color{R: float64(0x5f) / float64(256), G: float64(0x00) / float64(256), B: float64(0xff) / float64(256)}, //'5f00ff'), colorful.Color{R: float64(0x5f) / float64(256), G: float64(0x5f) / float64(256), B: float64(0x00) / float64(256)}, //'5f5f00'), colorful.Color{R: float64(0x5f) / float64(256), G: float64(0x5f) / float64(256), B: float64(0x5f) / float64(256)}, //'5f5f5f'), colorful.Color{R: float64(0x5f) / float64(256), G: float64(0x5f) / float64(256), B: float64(0x87) / float64(256)}, //'5f5f87'), colorful.Color{R: float64(0x5f) / float64(256), G: float64(0x5f) / float64(256), B: float64(0xaf) / float64(256)}, //'5f5faf'), colorful.Color{R: float64(0x5f) / float64(256), G: float64(0x5f) / float64(256), B: float64(0xd7) / float64(256)}, //'5f5fd7'), colorful.Color{R: float64(0x5f) / float64(256), G: float64(0x5f) / float64(256), B: float64(0xff) / float64(256)}, //'5f5fff'), colorful.Color{R: float64(0x5f) / float64(256), G: float64(0x87) / float64(256), B: float64(0x00) / float64(256)}, //'5f8700'), colorful.Color{R: float64(0x5f) / float64(256), G: float64(0x87) / float64(256), B: float64(0x5f) / float64(256)}, //'5f875f'), colorful.Color{R: float64(0x5f) / float64(256), G: float64(0x87) / float64(256), B: float64(0x87) / float64(256)}, //'5f8787'), colorful.Color{R: float64(0x5f) / float64(256), G: float64(0x87) / float64(256), B: float64(0xaf) / float64(256)}, //'5f87af'), colorful.Color{R: float64(0x5f) / float64(256), G: float64(0x87) / float64(256), B: float64(0xd7) / float64(256)}, //'5f87d7'), colorful.Color{R: float64(0x5f) / float64(256), G: float64(0x87) / float64(256), B: float64(0xff) / float64(256)}, //'5f87ff'), colorful.Color{R: float64(0x5f) / float64(256), G: float64(0xaf) / float64(256), B: float64(0x00) / float64(256)}, //'5faf00'), colorful.Color{R: float64(0x5f) / float64(256), G: float64(0xaf) / float64(256), B: float64(0x5f) / float64(256)}, //'5faf5f'), colorful.Color{R: float64(0x5f) / float64(256), G: float64(0xaf) / float64(256), B: float64(0x87) / float64(256)}, //'5faf87'), colorful.Color{R: float64(0x5f) / float64(256), G: float64(0xaf) / float64(256), B: float64(0xaf) / float64(256)}, //'5fafaf'), colorful.Color{R: float64(0x5f) / float64(256), G: float64(0xaf) / float64(256), B: float64(0xd7) / float64(256)}, //'5fafd7'), colorful.Color{R: float64(0x5f) / float64(256), G: float64(0xaf) / float64(256), B: float64(0xff) / float64(256)}, //'5fafff'), colorful.Color{R: float64(0x5f) / float64(256), G: float64(0xd7) / float64(256), B: float64(0x00) / float64(256)}, //'5fd700'), colorful.Color{R: float64(0x5f) / float64(256), G: float64(0xd7) / float64(256), B: float64(0x5f) / float64(256)}, //'5fd75f'), colorful.Color{R: float64(0x5f) / float64(256), G: float64(0xd7) / float64(256), B: float64(0x87) / float64(256)}, //'5fd787'), colorful.Color{R: float64(0x5f) / float64(256), G: float64(0xd7) / float64(256), B: float64(0xaf) / float64(256)}, //'5fd7af'), colorful.Color{R: float64(0x5f) / float64(256), G: float64(0xd7) / float64(256), B: float64(0xd7) / float64(256)}, //'5fd7d7'), colorful.Color{R: float64(0x5f) / float64(256), G: float64(0xd7) / float64(256), B: float64(0xff) / float64(256)}, //'5fd7ff'), colorful.Color{R: float64(0x5f) / float64(256), G: float64(0xff) / float64(256), B: float64(0x00) / float64(256)}, //'5fff00'), colorful.Color{R: float64(0x5f) / float64(256), G: float64(0xff) / float64(256), B: float64(0x5f) / float64(256)}, //'5fff5f'), colorful.Color{R: float64(0x5f) / float64(256), G: float64(0xff) / float64(256), B: float64(0x87) / float64(256)}, //'5fff87'), colorful.Color{R: float64(0x5f) / float64(256), G: float64(0xff) / float64(256), B: float64(0xaf) / float64(256)}, //'5fffaf'), colorful.Color{R: float64(0x5f) / float64(256), G: float64(0xff) / float64(256), B: float64(0xd7) / float64(256)}, //'5fffd7'), colorful.Color{R: float64(0x5f) / float64(256), G: float64(0xff) / float64(256), B: float64(0xff) / float64(256)}, //'5fffff'), colorful.Color{R: float64(0x87) / float64(256), G: float64(0x00) / float64(256), B: float64(0x00) / float64(256)}, //'870000'), colorful.Color{R: float64(0x87) / float64(256), G: float64(0x00) / float64(256), B: float64(0x5f) / float64(256)}, //'87005f'), colorful.Color{R: float64(0x87) / float64(256), G: float64(0x00) / float64(256), B: float64(0x87) / float64(256)}, //'870087'), colorful.Color{R: float64(0x87) / float64(256), G: float64(0x00) / float64(256), B: float64(0xaf) / float64(256)}, //'8700af'), colorful.Color{R: float64(0x87) / float64(256), G: float64(0x00) / float64(256), B: float64(0xd7) / float64(256)}, //'8700d7'), colorful.Color{R: float64(0x87) / float64(256), G: float64(0x00) / float64(256), B: float64(0xff) / float64(256)}, //'8700ff'), colorful.Color{R: float64(0x87) / float64(256), G: float64(0x5f) / float64(256), B: float64(0x00) / float64(256)}, //'875f00'), colorful.Color{R: float64(0x87) / float64(256), G: float64(0x5f) / float64(256), B: float64(0x5f) / float64(256)}, //'875f5f'), colorful.Color{R: float64(0x87) / float64(256), G: float64(0x5f) / float64(256), B: float64(0x87) / float64(256)}, //'875f87'), colorful.Color{R: float64(0x87) / float64(256), G: float64(0x5f) / float64(256), B: float64(0xaf) / float64(256)}, //'875faf'), colorful.Color{R: float64(0x87) / float64(256), G: float64(0x5f) / float64(256), B: float64(0xd7) / float64(256)}, //'875fd7'), colorful.Color{R: float64(0x87) / float64(256), G: float64(0x5f) / float64(256), B: float64(0xff) / float64(256)}, //'875fff'), colorful.Color{R: float64(0x87) / float64(256), G: float64(0x87) / float64(256), B: float64(0x00) / float64(256)}, //'878700'), colorful.Color{R: float64(0x87) / float64(256), G: float64(0x87) / float64(256), B: float64(0x5f) / float64(256)}, //'87875f'), colorful.Color{R: float64(0x87) / float64(256), G: float64(0x87) / float64(256), B: float64(0x87) / float64(256)}, //'878787'), colorful.Color{R: float64(0x87) / float64(256), G: float64(0x87) / float64(256), B: float64(0xaf) / float64(256)}, //'8787af'), colorful.Color{R: float64(0x87) / float64(256), G: float64(0x87) / float64(256), B: float64(0xd7) / float64(256)}, //'8787d7'), colorful.Color{R: float64(0x87) / float64(256), G: float64(0x87) / float64(256), B: float64(0xff) / float64(256)}, //'8787ff'), colorful.Color{R: float64(0x87) / float64(256), G: float64(0xaf) / float64(256), B: float64(0x00) / float64(256)}, //'87af00'), colorful.Color{R: float64(0x87) / float64(256), G: float64(0xaf) / float64(256), B: float64(0x5f) / float64(256)}, //'87af5f'), colorful.Color{R: float64(0x87) / float64(256), G: float64(0xaf) / float64(256), B: float64(0x87) / float64(256)}, //'87af87'), colorful.Color{R: float64(0x87) / float64(256), G: float64(0xaf) / float64(256), B: float64(0xaf) / float64(256)}, //'87afaf'), colorful.Color{R: float64(0x87) / float64(256), G: float64(0xaf) / float64(256), B: float64(0xd7) / float64(256)}, //'87afd7'), colorful.Color{R: float64(0x87) / float64(256), G: float64(0xaf) / float64(256), B: float64(0xff) / float64(256)}, //'87afff'), colorful.Color{R: float64(0x87) / float64(256), G: float64(0xd7) / float64(256), B: float64(0x00) / float64(256)}, //'87d700'), colorful.Color{R: float64(0x87) / float64(256), G: float64(0xd7) / float64(256), B: float64(0x5f) / float64(256)}, //'87d75f'), colorful.Color{R: float64(0x87) / float64(256), G: float64(0xd7) / float64(256), B: float64(0x87) / float64(256)}, //'87d787'), colorful.Color{R: float64(0x87) / float64(256), G: float64(0xd7) / float64(256), B: float64(0xaf) / float64(256)}, //'87d7af'), colorful.Color{R: float64(0x87) / float64(256), G: float64(0xd7) / float64(256), B: float64(0xd7) / float64(256)}, //'87d7d7'), colorful.Color{R: float64(0x87) / float64(256), G: float64(0xd7) / float64(256), B: float64(0xff) / float64(256)}, //'87d7ff'), colorful.Color{R: float64(0x87) / float64(256), G: float64(0xff) / float64(256), B: float64(0x00) / float64(256)}, //'87ff00'), colorful.Color{R: float64(0x87) / float64(256), G: float64(0xff) / float64(256), B: float64(0x5f) / float64(256)}, //'87ff5f'), colorful.Color{R: float64(0x87) / float64(256), G: float64(0xff) / float64(256), B: float64(0x87) / float64(256)}, //'87ff87'), colorful.Color{R: float64(0x87) / float64(256), G: float64(0xff) / float64(256), B: float64(0xaf) / float64(256)}, //'87ffaf'), colorful.Color{R: float64(0x87) / float64(256), G: float64(0xff) / float64(256), B: float64(0xd7) / float64(256)}, //'87ffd7'), colorful.Color{R: float64(0x87) / float64(256), G: float64(0xff) / float64(256), B: float64(0xff) / float64(256)}, //'87ffff'), colorful.Color{R: float64(0xaf) / float64(256), G: float64(0x00) / float64(256), B: float64(0x00) / float64(256)}, //'af0000'), colorful.Color{R: float64(0xaf) / float64(256), G: float64(0x00) / float64(256), B: float64(0x5f) / float64(256)}, //'af005f'), colorful.Color{R: float64(0xaf) / float64(256), G: float64(0x00) / float64(256), B: float64(0x87) / float64(256)}, //'af0087'), colorful.Color{R: float64(0xaf) / float64(256), G: float64(0x00) / float64(256), B: float64(0xaf) / float64(256)}, //'af00af'), colorful.Color{R: float64(0xaf) / float64(256), G: float64(0x00) / float64(256), B: float64(0xd7) / float64(256)}, //'af00d7'), colorful.Color{R: float64(0xaf) / float64(256), G: float64(0x00) / float64(256), B: float64(0xff) / float64(256)}, //'af00ff'), colorful.Color{R: float64(0xaf) / float64(256), G: float64(0x5f) / float64(256), B: float64(0x00) / float64(256)}, //'af5f00'), colorful.Color{R: float64(0xaf) / float64(256), G: float64(0x5f) / float64(256), B: float64(0x5f) / float64(256)}, //'af5f5f'), colorful.Color{R: float64(0xaf) / float64(256), G: float64(0x5f) / float64(256), B: float64(0x87) / float64(256)}, //'af5f87'), colorful.Color{R: float64(0xaf) / float64(256), G: float64(0x5f) / float64(256), B: float64(0xaf) / float64(256)}, //'af5faf'), colorful.Color{R: float64(0xaf) / float64(256), G: float64(0x5f) / float64(256), B: float64(0xd7) / float64(256)}, //'af5fd7'), colorful.Color{R: float64(0xaf) / float64(256), G: float64(0x5f) / float64(256), B: float64(0xff) / float64(256)}, //'af5fff'), colorful.Color{R: float64(0xaf) / float64(256), G: float64(0x87) / float64(256), B: float64(0x00) / float64(256)}, //'af8700'), colorful.Color{R: float64(0xaf) / float64(256), G: float64(0x87) / float64(256), B: float64(0x5f) / float64(256)}, //'af875f'), colorful.Color{R: float64(0xaf) / float64(256), G: float64(0x87) / float64(256), B: float64(0x87) / float64(256)}, //'af8787'), colorful.Color{R: float64(0xaf) / float64(256), G: float64(0x87) / float64(256), B: float64(0xaf) / float64(256)}, //'af87af'), colorful.Color{R: float64(0xaf) / float64(256), G: float64(0x87) / float64(256), B: float64(0xd7) / float64(256)}, //'af87d7'), colorful.Color{R: float64(0xaf) / float64(256), G: float64(0x87) / float64(256), B: float64(0xff) / float64(256)}, //'af87ff'), colorful.Color{R: float64(0xaf) / float64(256), G: float64(0xaf) / float64(256), B: float64(0x00) / float64(256)}, //'afaf00'), colorful.Color{R: float64(0xaf) / float64(256), G: float64(0xaf) / float64(256), B: float64(0x5f) / float64(256)}, //'afaf5f'), colorful.Color{R: float64(0xaf) / float64(256), G: float64(0xaf) / float64(256), B: float64(0x87) / float64(256)}, //'afaf87'), colorful.Color{R: float64(0xaf) / float64(256), G: float64(0xaf) / float64(256), B: float64(0xaf) / float64(256)}, //'afafaf'), colorful.Color{R: float64(0xaf) / float64(256), G: float64(0xaf) / float64(256), B: float64(0xd7) / float64(256)}, //'afafd7'), colorful.Color{R: float64(0xaf) / float64(256), G: float64(0xaf) / float64(256), B: float64(0xff) / float64(256)}, //'afafff'), colorful.Color{R: float64(0xaf) / float64(256), G: float64(0xd7) / float64(256), B: float64(0x00) / float64(256)}, //'afd700'), colorful.Color{R: float64(0xaf) / float64(256), G: float64(0xd7) / float64(256), B: float64(0x5f) / float64(256)}, //'afd75f'), colorful.Color{R: float64(0xaf) / float64(256), G: float64(0xd7) / float64(256), B: float64(0x87) / float64(256)}, //'afd787'), colorful.Color{R: float64(0xaf) / float64(256), G: float64(0xd7) / float64(256), B: float64(0xaf) / float64(256)}, //'afd7af'), colorful.Color{R: float64(0xaf) / float64(256), G: float64(0xd7) / float64(256), B: float64(0xd7) / float64(256)}, //'afd7d7'), colorful.Color{R: float64(0xaf) / float64(256), G: float64(0xd7) / float64(256), B: float64(0xff) / float64(256)}, //'afd7ff'), colorful.Color{R: float64(0xaf) / float64(256), G: float64(0xff) / float64(256), B: float64(0x00) / float64(256)}, //'afff00'), colorful.Color{R: float64(0xaf) / float64(256), G: float64(0xff) / float64(256), B: float64(0x5f) / float64(256)}, //'afff5f'), colorful.Color{R: float64(0xaf) / float64(256), G: float64(0xff) / float64(256), B: float64(0x87) / float64(256)}, //'afff87'), colorful.Color{R: float64(0xaf) / float64(256), G: float64(0xff) / float64(256), B: float64(0xaf) / float64(256)}, //'afffaf'), colorful.Color{R: float64(0xaf) / float64(256), G: float64(0xff) / float64(256), B: float64(0xd7) / float64(256)}, //'afffd7'), colorful.Color{R: float64(0xaf) / float64(256), G: float64(0xff) / float64(256), B: float64(0xff) / float64(256)}, //'afffff'), colorful.Color{R: float64(0xd7) / float64(256), G: float64(0x00) / float64(256), B: float64(0x00) / float64(256)}, //'d70000'), colorful.Color{R: float64(0xd7) / float64(256), G: float64(0x00) / float64(256), B: float64(0x5f) / float64(256)}, //'d7005f'), colorful.Color{R: float64(0xd7) / float64(256), G: float64(0x00) / float64(256), B: float64(0x87) / float64(256)}, //'d70087'), colorful.Color{R: float64(0xd7) / float64(256), G: float64(0x00) / float64(256), B: float64(0xaf) / float64(256)}, //'d700af'), colorful.Color{R: float64(0xd7) / float64(256), G: float64(0x00) / float64(256), B: float64(0xd7) / float64(256)}, //'d700d7'), colorful.Color{R: float64(0xd7) / float64(256), G: float64(0x00) / float64(256), B: float64(0xff) / float64(256)}, //'d700ff'), colorful.Color{R: float64(0xd7) / float64(256), G: float64(0x5f) / float64(256), B: float64(0x00) / float64(256)}, //'d75f00'), colorful.Color{R: float64(0xd7) / float64(256), G: float64(0x5f) / float64(256), B: float64(0x5f) / float64(256)}, //'d75f5f'), colorful.Color{R: float64(0xd7) / float64(256), G: float64(0x5f) / float64(256), B: float64(0x87) / float64(256)}, //'d75f87'), colorful.Color{R: float64(0xd7) / float64(256), G: float64(0x5f) / float64(256), B: float64(0xaf) / float64(256)}, //'d75faf'), colorful.Color{R: float64(0xd7) / float64(256), G: float64(0x5f) / float64(256), B: float64(0xd7) / float64(256)}, //'d75fd7'), colorful.Color{R: float64(0xd7) / float64(256), G: float64(0x5f) / float64(256), B: float64(0xff) / float64(256)}, //'d75fff'), colorful.Color{R: float64(0xd7) / float64(256), G: float64(0x87) / float64(256), B: float64(0x00) / float64(256)}, //'d78700'), colorful.Color{R: float64(0xd7) / float64(256), G: float64(0x87) / float64(256), B: float64(0x5f) / float64(256)}, //'d7875f'), colorful.Color{R: float64(0xd7) / float64(256), G: float64(0x87) / float64(256), B: float64(0x87) / float64(256)}, //'d78787'), colorful.Color{R: float64(0xd7) / float64(256), G: float64(0x87) / float64(256), B: float64(0xaf) / float64(256)}, //'d787af'), colorful.Color{R: float64(0xd7) / float64(256), G: float64(0x87) / float64(256), B: float64(0xd7) / float64(256)}, //'d787d7'), colorful.Color{R: float64(0xd7) / float64(256), G: float64(0x87) / float64(256), B: float64(0xff) / float64(256)}, //'d787ff'), colorful.Color{R: float64(0xd7) / float64(256), G: float64(0xaf) / float64(256), B: float64(0x00) / float64(256)}, //'d7af00'), colorful.Color{R: float64(0xd7) / float64(256), G: float64(0xaf) / float64(256), B: float64(0x5f) / float64(256)}, //'d7af5f'), colorful.Color{R: float64(0xd7) / float64(256), G: float64(0xaf) / float64(256), B: float64(0x87) / float64(256)}, //'d7af87'), colorful.Color{R: float64(0xd7) / float64(256), G: float64(0xaf) / float64(256), B: float64(0xaf) / float64(256)}, //'d7afaf'), colorful.Color{R: float64(0xd7) / float64(256), G: float64(0xaf) / float64(256), B: float64(0xd7) / float64(256)}, //'d7afd7'), colorful.Color{R: float64(0xd7) / float64(256), G: float64(0xaf) / float64(256), B: float64(0xff) / float64(256)}, //'d7afff'), colorful.Color{R: float64(0xd7) / float64(256), G: float64(0xd7) / float64(256), B: float64(0x00) / float64(256)}, //'d7d700'), colorful.Color{R: float64(0xd7) / float64(256), G: float64(0xd7) / float64(256), B: float64(0x5f) / float64(256)}, //'d7d75f'), colorful.Color{R: float64(0xd7) / float64(256), G: float64(0xd7) / float64(256), B: float64(0x87) / float64(256)}, //'d7d787'), colorful.Color{R: float64(0xd7) / float64(256), G: float64(0xd7) / float64(256), B: float64(0xaf) / float64(256)}, //'d7d7af'), colorful.Color{R: float64(0xd7) / float64(256), G: float64(0xd7) / float64(256), B: float64(0xd7) / float64(256)}, //'d7d7d7'), colorful.Color{R: float64(0xd7) / float64(256), G: float64(0xd7) / float64(256), B: float64(0xff) / float64(256)}, //'d7d7ff'), colorful.Color{R: float64(0xd7) / float64(256), G: float64(0xff) / float64(256), B: float64(0x00) / float64(256)}, //'d7ff00'), colorful.Color{R: float64(0xd7) / float64(256), G: float64(0xff) / float64(256), B: float64(0x5f) / float64(256)}, //'d7ff5f'), colorful.Color{R: float64(0xd7) / float64(256), G: float64(0xff) / float64(256), B: float64(0x87) / float64(256)}, //'d7ff87'), colorful.Color{R: float64(0xd7) / float64(256), G: float64(0xff) / float64(256), B: float64(0xaf) / float64(256)}, //'d7ffaf'), colorful.Color{R: float64(0xd7) / float64(256), G: float64(0xff) / float64(256), B: float64(0xd7) / float64(256)}, //'d7ffd7'), colorful.Color{R: float64(0xd7) / float64(256), G: float64(0xff) / float64(256), B: float64(0xff) / float64(256)}, //'d7ffff'), colorful.Color{R: float64(0xff) / float64(256), G: float64(0x00) / float64(256), B: float64(0x00) / float64(256)}, //'ff0000'), colorful.Color{R: float64(0xff) / float64(256), G: float64(0x00) / float64(256), B: float64(0x5f) / float64(256)}, //'ff005f'), colorful.Color{R: float64(0xff) / float64(256), G: float64(0x00) / float64(256), B: float64(0x87) / float64(256)}, //'ff0087'), colorful.Color{R: float64(0xff) / float64(256), G: float64(0x00) / float64(256), B: float64(0xaf) / float64(256)}, //'ff00af'), colorful.Color{R: float64(0xff) / float64(256), G: float64(0x00) / float64(256), B: float64(0xd7) / float64(256)}, //'ff00d7'), colorful.Color{R: float64(0xff) / float64(256), G: float64(0x00) / float64(256), B: float64(0xff) / float64(256)}, //'ff00ff'), colorful.Color{R: float64(0xff) / float64(256), G: float64(0x5f) / float64(256), B: float64(0x00) / float64(256)}, //'ff5f00'), colorful.Color{R: float64(0xff) / float64(256), G: float64(0x5f) / float64(256), B: float64(0x5f) / float64(256)}, //'ff5f5f'), colorful.Color{R: float64(0xff) / float64(256), G: float64(0x5f) / float64(256), B: float64(0x87) / float64(256)}, //'ff5f87'), colorful.Color{R: float64(0xff) / float64(256), G: float64(0x5f) / float64(256), B: float64(0xaf) / float64(256)}, //'ff5faf'), colorful.Color{R: float64(0xff) / float64(256), G: float64(0x5f) / float64(256), B: float64(0xd7) / float64(256)}, //'ff5fd7'), colorful.Color{R: float64(0xff) / float64(256), G: float64(0x5f) / float64(256), B: float64(0xff) / float64(256)}, //'ff5fff'), colorful.Color{R: float64(0xff) / float64(256), G: float64(0x87) / float64(256), B: float64(0x00) / float64(256)}, //'ff8700'), colorful.Color{R: float64(0xff) / float64(256), G: float64(0x87) / float64(256), B: float64(0x5f) / float64(256)}, //'ff875f'), colorful.Color{R: float64(0xff) / float64(256), G: float64(0x87) / float64(256), B: float64(0x87) / float64(256)}, //'ff8787'), colorful.Color{R: float64(0xff) / float64(256), G: float64(0x87) / float64(256), B: float64(0xaf) / float64(256)}, //'ff87af'), colorful.Color{R: float64(0xff) / float64(256), G: float64(0x87) / float64(256), B: float64(0xd7) / float64(256)}, //'ff87d7'), colorful.Color{R: float64(0xff) / float64(256), G: float64(0x87) / float64(256), B: float64(0xff) / float64(256)}, //'ff87ff'), colorful.Color{R: float64(0xff) / float64(256), G: float64(0xaf) / float64(256), B: float64(0x00) / float64(256)}, //'ffaf00'), colorful.Color{R: float64(0xff) / float64(256), G: float64(0xaf) / float64(256), B: float64(0x5f) / float64(256)}, //'ffaf5f'), colorful.Color{R: float64(0xff) / float64(256), G: float64(0xaf) / float64(256), B: float64(0x87) / float64(256)}, //'ffaf87'), colorful.Color{R: float64(0xff) / float64(256), G: float64(0xaf) / float64(256), B: float64(0xaf) / float64(256)}, //'ffafaf'), colorful.Color{R: float64(0xff) / float64(256), G: float64(0xaf) / float64(256), B: float64(0xd7) / float64(256)}, //'ffafd7'), colorful.Color{R: float64(0xff) / float64(256), G: float64(0xaf) / float64(256), B: float64(0xff) / float64(256)}, //'ffafff'), colorful.Color{R: float64(0xff) / float64(256), G: float64(0xd7) / float64(256), B: float64(0x00) / float64(256)}, //'ffd700'), colorful.Color{R: float64(0xff) / float64(256), G: float64(0xd7) / float64(256), B: float64(0x5f) / float64(256)}, //'ffd75f'), colorful.Color{R: float64(0xff) / float64(256), G: float64(0xd7) / float64(256), B: float64(0x87) / float64(256)}, //'ffd787'), colorful.Color{R: float64(0xff) / float64(256), G: float64(0xd7) / float64(256), B: float64(0xaf) / float64(256)}, //'ffd7af'), colorful.Color{R: float64(0xff) / float64(256), G: float64(0xd7) / float64(256), B: float64(0xd7) / float64(256)}, //'ffd7d7'), colorful.Color{R: float64(0xff) / float64(256), G: float64(0xd7) / float64(256), B: float64(0xff) / float64(256)}, //'ffd7ff'), colorful.Color{R: float64(0xff) / float64(256), G: float64(0xff) / float64(256), B: float64(0x00) / float64(256)}, //'ffff00'), colorful.Color{R: float64(0xff) / float64(256), G: float64(0xff) / float64(256), B: float64(0x5f) / float64(256)}, //'ffff5f'), colorful.Color{R: float64(0xff) / float64(256), G: float64(0xff) / float64(256), B: float64(0x87) / float64(256)}, //'ffff87'), colorful.Color{R: float64(0xff) / float64(256), G: float64(0xff) / float64(256), B: float64(0xaf) / float64(256)}, //'ffffaf'), colorful.Color{R: float64(0xff) / float64(256), G: float64(0xff) / float64(256), B: float64(0xd7) / float64(256)}, //'ffffd7'), colorful.Color{R: float64(0xff) / float64(256), G: float64(0xff) / float64(256), B: float64(0xff) / float64(256)}, //'ffffff'), colorful.Color{R: float64(0x08) / float64(256), G: float64(0x08) / float64(256), B: float64(0x08) / float64(256)}, //'080808'), colorful.Color{R: float64(0x12) / float64(256), G: float64(0x12) / float64(256), B: float64(0x12) / float64(256)}, //'121212'), colorful.Color{R: float64(0x1c) / float64(256), G: float64(0x1c) / float64(256), B: float64(0x1c) / float64(256)}, //'1c1c1c'), colorful.Color{R: float64(0x26) / float64(256), G: float64(0x26) / float64(256), B: float64(0x26) / float64(256)}, //'262626'), colorful.Color{R: float64(0x30) / float64(256), G: float64(0x30) / float64(256), B: float64(0x30) / float64(256)}, //'303030'), colorful.Color{R: float64(0x3a) / float64(256), G: float64(0x3a) / float64(256), B: float64(0x3a) / float64(256)}, //'3a3a3a'), colorful.Color{R: float64(0x44) / float64(256), G: float64(0x44) / float64(256), B: float64(0x44) / float64(256)}, //'444444'), colorful.Color{R: float64(0x4e) / float64(256), G: float64(0x4e) / float64(256), B: float64(0x4e) / float64(256)}, //'4e4e4e'), colorful.Color{R: float64(0x58) / float64(256), G: float64(0x58) / float64(256), B: float64(0x58) / float64(256)}, //'585858'), colorful.Color{R: float64(0x62) / float64(256), G: float64(0x62) / float64(256), B: float64(0x62) / float64(256)}, //'626262'), colorful.Color{R: float64(0x6c) / float64(256), G: float64(0x6c) / float64(256), B: float64(0x6c) / float64(256)}, //'6c6c6c'), colorful.Color{R: float64(0x76) / float64(256), G: float64(0x76) / float64(256), B: float64(0x76) / float64(256)}, //'767676'), colorful.Color{R: float64(0x80) / float64(256), G: float64(0x80) / float64(256), B: float64(0x80) / float64(256)}, //'808080'), colorful.Color{R: float64(0x8a) / float64(256), G: float64(0x8a) / float64(256), B: float64(0x8a) / float64(256)}, //'8a8a8a'), colorful.Color{R: float64(0x94) / float64(256), G: float64(0x94) / float64(256), B: float64(0x94) / float64(256)}, //'949494'), colorful.Color{R: float64(0x9e) / float64(256), G: float64(0x9e) / float64(256), B: float64(0x9e) / float64(256)}, //'9e9e9e'), colorful.Color{R: float64(0xa8) / float64(256), G: float64(0xa8) / float64(256), B: float64(0xa8) / float64(256)}, //'a8a8a8'), colorful.Color{R: float64(0xb2) / float64(256), G: float64(0xb2) / float64(256), B: float64(0xb2) / float64(256)}, //'b2b2b2'), colorful.Color{R: float64(0xbc) / float64(256), G: float64(0xbc) / float64(256), B: float64(0xbc) / float64(256)}, //'bcbcbc'), colorful.Color{R: float64(0xc6) / float64(256), G: float64(0xc6) / float64(256), B: float64(0xc6) / float64(256)}, //'c6c6c6'), colorful.Color{R: float64(0xd0) / float64(256), G: float64(0xd0) / float64(256), B: float64(0xd0) / float64(256)}, //'d0d0d0'), colorful.Color{R: float64(0xda) / float64(256), G: float64(0xda) / float64(256), B: float64(0xda) / float64(256)}, //'dadada'), colorful.Color{R: float64(0xe4) / float64(256), G: float64(0xe4) / float64(256), B: float64(0xe4) / float64(256)}, //'e4e4e4'), colorful.Color{R: float64(0xee) / float64(256), G: float64(0xee) / float64(256), B: float64(0xee) / float64(256)}, //'eeeeee'), } term8 = []TCellColor{ ColorBlack, ColorWhite, ColorRed, ColorGreen, ColorBlue, ColorYellow, ColorMagenta, ColorCyan, } term16 = []TCellColor{ ColorBlack, ColorLightGray, ColorDarkRed, ColorDarkGreen, ColorDarkBlue, ColorYellow, ColorMagenta, ColorCyan, ColorDarkGray, ColorWhite, ColorRed, ColorGreen, ColorBlue, ColorYellow, ColorMagenta, ColorCyan, // TODO - figure out these colors } term256 = []TCellColor{ MakeTCellColorExt(tcell.ColorBlack), MakeTCellColorExt(tcell.ColorMaroon), MakeTCellColorExt(tcell.ColorGreen), MakeTCellColorExt(tcell.ColorOlive), MakeTCellColorExt(tcell.ColorNavy), MakeTCellColorExt(tcell.ColorPurple), MakeTCellColorExt(tcell.ColorTeal), MakeTCellColorExt(tcell.ColorSilver), MakeTCellColorExt(tcell.ColorGray), MakeTCellColorExt(tcell.ColorRed), MakeTCellColorExt(tcell.ColorLime), MakeTCellColorExt(tcell.ColorYellow), MakeTCellColorExt(tcell.ColorBlue), MakeTCellColorExt(tcell.ColorFuchsia), MakeTCellColorExt(tcell.ColorAqua), MakeTCellColorExt(tcell.ColorWhite), // MakeTCellColorExt(tcell.Color16), MakeTCellColorExt(tcell.Color17), MakeTCellColorExt(tcell.Color18), MakeTCellColorExt(tcell.Color19), MakeTCellColorExt(tcell.Color20), MakeTCellColorExt(tcell.Color21), MakeTCellColorExt(tcell.Color22), MakeTCellColorExt(tcell.Color23), MakeTCellColorExt(tcell.Color24), MakeTCellColorExt(tcell.Color25), MakeTCellColorExt(tcell.Color26), MakeTCellColorExt(tcell.Color27), MakeTCellColorExt(tcell.Color28), MakeTCellColorExt(tcell.Color29), MakeTCellColorExt(tcell.Color30), MakeTCellColorExt(tcell.Color31), MakeTCellColorExt(tcell.Color32), MakeTCellColorExt(tcell.Color33), MakeTCellColorExt(tcell.Color34), MakeTCellColorExt(tcell.Color35), MakeTCellColorExt(tcell.Color36), MakeTCellColorExt(tcell.Color37), MakeTCellColorExt(tcell.Color38), MakeTCellColorExt(tcell.Color39), MakeTCellColorExt(tcell.Color40), MakeTCellColorExt(tcell.Color41), MakeTCellColorExt(tcell.Color42), MakeTCellColorExt(tcell.Color43), MakeTCellColorExt(tcell.Color44), MakeTCellColorExt(tcell.Color45), MakeTCellColorExt(tcell.Color46), MakeTCellColorExt(tcell.Color47), MakeTCellColorExt(tcell.Color48), MakeTCellColorExt(tcell.Color49), MakeTCellColorExt(tcell.Color50), MakeTCellColorExt(tcell.Color51), MakeTCellColorExt(tcell.Color52), MakeTCellColorExt(tcell.Color53), MakeTCellColorExt(tcell.Color54), MakeTCellColorExt(tcell.Color55), MakeTCellColorExt(tcell.Color56), MakeTCellColorExt(tcell.Color57), MakeTCellColorExt(tcell.Color58), MakeTCellColorExt(tcell.Color59), MakeTCellColorExt(tcell.Color60), MakeTCellColorExt(tcell.Color61), MakeTCellColorExt(tcell.Color62), MakeTCellColorExt(tcell.Color63), MakeTCellColorExt(tcell.Color64), MakeTCellColorExt(tcell.Color65), MakeTCellColorExt(tcell.Color66), MakeTCellColorExt(tcell.Color67), MakeTCellColorExt(tcell.Color68), MakeTCellColorExt(tcell.Color69), MakeTCellColorExt(tcell.Color70), MakeTCellColorExt(tcell.Color71), MakeTCellColorExt(tcell.Color72), MakeTCellColorExt(tcell.Color73), MakeTCellColorExt(tcell.Color74), MakeTCellColorExt(tcell.Color75), MakeTCellColorExt(tcell.Color76), MakeTCellColorExt(tcell.Color77), MakeTCellColorExt(tcell.Color78), MakeTCellColorExt(tcell.Color79), MakeTCellColorExt(tcell.Color80), MakeTCellColorExt(tcell.Color81), MakeTCellColorExt(tcell.Color82), MakeTCellColorExt(tcell.Color83), MakeTCellColorExt(tcell.Color84), MakeTCellColorExt(tcell.Color85), MakeTCellColorExt(tcell.Color86), MakeTCellColorExt(tcell.Color87), MakeTCellColorExt(tcell.Color88), MakeTCellColorExt(tcell.Color89), MakeTCellColorExt(tcell.Color90), MakeTCellColorExt(tcell.Color91), MakeTCellColorExt(tcell.Color92), MakeTCellColorExt(tcell.Color93), MakeTCellColorExt(tcell.Color94), MakeTCellColorExt(tcell.Color95), MakeTCellColorExt(tcell.Color96), MakeTCellColorExt(tcell.Color97), MakeTCellColorExt(tcell.Color98), MakeTCellColorExt(tcell.Color99), MakeTCellColorExt(tcell.Color100), MakeTCellColorExt(tcell.Color101), MakeTCellColorExt(tcell.Color102), MakeTCellColorExt(tcell.Color103), MakeTCellColorExt(tcell.Color104), MakeTCellColorExt(tcell.Color105), MakeTCellColorExt(tcell.Color106), MakeTCellColorExt(tcell.Color107), MakeTCellColorExt(tcell.Color108), MakeTCellColorExt(tcell.Color109), MakeTCellColorExt(tcell.Color110), MakeTCellColorExt(tcell.Color111), MakeTCellColorExt(tcell.Color112), MakeTCellColorExt(tcell.Color113), MakeTCellColorExt(tcell.Color114), MakeTCellColorExt(tcell.Color115), MakeTCellColorExt(tcell.Color116), MakeTCellColorExt(tcell.Color117), MakeTCellColorExt(tcell.Color118), MakeTCellColorExt(tcell.Color119), MakeTCellColorExt(tcell.Color120), MakeTCellColorExt(tcell.Color121), MakeTCellColorExt(tcell.Color122), MakeTCellColorExt(tcell.Color123), MakeTCellColorExt(tcell.Color124), MakeTCellColorExt(tcell.Color125), MakeTCellColorExt(tcell.Color126), MakeTCellColorExt(tcell.Color127), MakeTCellColorExt(tcell.Color128), MakeTCellColorExt(tcell.Color129), MakeTCellColorExt(tcell.Color130), MakeTCellColorExt(tcell.Color131), MakeTCellColorExt(tcell.Color132), MakeTCellColorExt(tcell.Color133), MakeTCellColorExt(tcell.Color134), MakeTCellColorExt(tcell.Color135), MakeTCellColorExt(tcell.Color136), MakeTCellColorExt(tcell.Color137), MakeTCellColorExt(tcell.Color138), MakeTCellColorExt(tcell.Color139), MakeTCellColorExt(tcell.Color140), MakeTCellColorExt(tcell.Color141), MakeTCellColorExt(tcell.Color142), MakeTCellColorExt(tcell.Color143), MakeTCellColorExt(tcell.Color144), MakeTCellColorExt(tcell.Color145), MakeTCellColorExt(tcell.Color146), MakeTCellColorExt(tcell.Color147), MakeTCellColorExt(tcell.Color148), MakeTCellColorExt(tcell.Color149), MakeTCellColorExt(tcell.Color150), MakeTCellColorExt(tcell.Color151), MakeTCellColorExt(tcell.Color152), MakeTCellColorExt(tcell.Color153), MakeTCellColorExt(tcell.Color154), MakeTCellColorExt(tcell.Color155), MakeTCellColorExt(tcell.Color156), MakeTCellColorExt(tcell.Color157), MakeTCellColorExt(tcell.Color158), MakeTCellColorExt(tcell.Color159), MakeTCellColorExt(tcell.Color160), MakeTCellColorExt(tcell.Color161), MakeTCellColorExt(tcell.Color162), MakeTCellColorExt(tcell.Color163), MakeTCellColorExt(tcell.Color164), MakeTCellColorExt(tcell.Color165), MakeTCellColorExt(tcell.Color166), MakeTCellColorExt(tcell.Color167), MakeTCellColorExt(tcell.Color168), MakeTCellColorExt(tcell.Color169), MakeTCellColorExt(tcell.Color170), MakeTCellColorExt(tcell.Color171), MakeTCellColorExt(tcell.Color172), MakeTCellColorExt(tcell.Color173), MakeTCellColorExt(tcell.Color174), MakeTCellColorExt(tcell.Color175), MakeTCellColorExt(tcell.Color176), MakeTCellColorExt(tcell.Color177), MakeTCellColorExt(tcell.Color178), MakeTCellColorExt(tcell.Color179), MakeTCellColorExt(tcell.Color180), MakeTCellColorExt(tcell.Color181), MakeTCellColorExt(tcell.Color182), MakeTCellColorExt(tcell.Color183), MakeTCellColorExt(tcell.Color184), MakeTCellColorExt(tcell.Color185), MakeTCellColorExt(tcell.Color186), MakeTCellColorExt(tcell.Color187), MakeTCellColorExt(tcell.Color188), MakeTCellColorExt(tcell.Color189), MakeTCellColorExt(tcell.Color190), MakeTCellColorExt(tcell.Color191), MakeTCellColorExt(tcell.Color192), MakeTCellColorExt(tcell.Color193), MakeTCellColorExt(tcell.Color194), MakeTCellColorExt(tcell.Color195), MakeTCellColorExt(tcell.Color196), MakeTCellColorExt(tcell.Color197), MakeTCellColorExt(tcell.Color198), MakeTCellColorExt(tcell.Color199), MakeTCellColorExt(tcell.Color200), MakeTCellColorExt(tcell.Color201), MakeTCellColorExt(tcell.Color202), MakeTCellColorExt(tcell.Color203), MakeTCellColorExt(tcell.Color204), MakeTCellColorExt(tcell.Color205), MakeTCellColorExt(tcell.Color206), MakeTCellColorExt(tcell.Color207), MakeTCellColorExt(tcell.Color208), MakeTCellColorExt(tcell.Color209), MakeTCellColorExt(tcell.Color210), MakeTCellColorExt(tcell.Color211), MakeTCellColorExt(tcell.Color212), MakeTCellColorExt(tcell.Color213), MakeTCellColorExt(tcell.Color214), MakeTCellColorExt(tcell.Color215), MakeTCellColorExt(tcell.Color216), MakeTCellColorExt(tcell.Color217), MakeTCellColorExt(tcell.Color218), MakeTCellColorExt(tcell.Color219), MakeTCellColorExt(tcell.Color220), MakeTCellColorExt(tcell.Color221), MakeTCellColorExt(tcell.Color222), MakeTCellColorExt(tcell.Color223), MakeTCellColorExt(tcell.Color224), MakeTCellColorExt(tcell.Color225), MakeTCellColorExt(tcell.Color226), MakeTCellColorExt(tcell.Color227), MakeTCellColorExt(tcell.Color228), MakeTCellColorExt(tcell.Color229), MakeTCellColorExt(tcell.Color230), MakeTCellColorExt(tcell.Color231), MakeTCellColorExt(tcell.Color232), MakeTCellColorExt(tcell.Color233), MakeTCellColorExt(tcell.Color234), MakeTCellColorExt(tcell.Color235), MakeTCellColorExt(tcell.Color236), MakeTCellColorExt(tcell.Color237), MakeTCellColorExt(tcell.Color238), MakeTCellColorExt(tcell.Color239), MakeTCellColorExt(tcell.Color240), MakeTCellColorExt(tcell.Color241), MakeTCellColorExt(tcell.Color242), MakeTCellColorExt(tcell.Color243), MakeTCellColorExt(tcell.Color244), MakeTCellColorExt(tcell.Color245), MakeTCellColorExt(tcell.Color246), MakeTCellColorExt(tcell.Color247), MakeTCellColorExt(tcell.Color248), MakeTCellColorExt(tcell.Color249), MakeTCellColorExt(tcell.Color250), MakeTCellColorExt(tcell.Color251), MakeTCellColorExt(tcell.Color252), MakeTCellColorExt(tcell.Color253), MakeTCellColorExt(tcell.Color254), MakeTCellColorExt(tcell.Color255), } term2Cache *lru.Cache term8Cache *lru.Cache term16Cache *lru.Cache term256Cache *lru.Cache term256CacheIgnoreBase16 *lru.Cache ) //====================================================================== func init() { cubeLookup256_16 = make([]int, 16) cubeLookup88_16 = make([]int, 16) grayLookup256_101 = make([]int, 101) grayLookup88_101 = make([]int, 101) for i := 0; i < 16; i++ { cubeLookup256_16[i] = cubeLookup256[intScale(i, 16, 0x100)] cubeLookup88_16[i] = cubeLookup88[intScale(i, 16, 0x100)] } for i := 0; i < 101; i++ { grayLookup256_101[i] = grayLookup256[intScale(i, 101, 0x100)] grayLookup88_101[i] = grayLookup88[intScale(i, 101, 0x100)] } var err error for _, cache := range []**lru.Cache{&term2Cache, &term8Cache, &term16Cache, &term256Cache, &term256CacheIgnoreBase16} { *cache, err = lru.New(100) if err != nil { panic(err) } } if os.Getenv("GOWID_IGNORE_BASE16") == "1" { IgnoreBase16 = true } } // makeColorLookup([0, 7, 9], 10) // [0, 0, 0, 0, 1, 1, 1, 1, 2, 2] // func makeColorLookup(vals []int, length int) []int { res := make([]int, length) vi := 0 for i := 0; i < len(res); i++ { if vi+1 < len(vals) { if i <= (vals[vi]+vals[vi+1])/2 { res[i] = vi } else { vi++ res[i] = vi } } else if vi < len(vals) { // only last vi is valid res[i] = vi } } return res } // Scale val in the range [0, val_range-1] to an integer in the range // [0, out_range-1]. This implementation uses the "round-half-up" rounding // method. // func intScale(val int, val_range int, out_range int) int { num := val*(out_range-1)*2 + (val_range - 1) dem := (val_range - 1) * 2 return num / dem } //====================================================================== type ColorModeMismatch struct { Color IColor Mode ColorMode } var _ error = ColorModeMismatch{} func (e ColorModeMismatch) Error() string { return fmt.Sprintf("Color %v of type %T not supported in mode %v", e.Color, e.Color, e.Mode) } type InvalidColor struct { Color interface{} } var _ error = InvalidColor{} func (e InvalidColor) Error() string { return fmt.Sprintf("Color %v of type %T is invalid", e.Color, e.Color) } //====================================================================== // ICellStyler is an analog to urwid's AttrSpec (http://urwid.org/reference/attrspec.html). When provided // a RenderContext (specifically the color mode in which to be rendered), the GetStyle() function will // return foreground, background and style values with which a cell should be rendered. The IRenderContext // argument provides access to the global palette, so an ICellStyle implementation can look up palette // entries by name. type ICellStyler interface { GetStyle(IRenderContext) (IColor, IColor, StyleAttrs) } // IColor is implemented by any object that can turn itself into a TCellColor, meaning a color with // which a cell can be rendered. The display mode (e.g. 256 colors) is provided. If no TCellColor is // available, the second argument should be set to false e.g. no color can be found given a particular // string name. type IColor interface { ToTCellColor(mode ColorMode) (TCellColor, bool) } // MakeCellStyle constructs a tcell.Style from gowid colors and styles. The return value can be provided // to tcell in order to style a particular region of the screen. func MakeCellStyle(fg TCellColor, bg TCellColor, attr StyleAttrs) tcell.Style { var fgt, bgt tcell.Color if fg == ColorNone { fgt = tcell.ColorDefault } else { fgt = fg.ToTCell() } if bg == ColorNone { bgt = tcell.ColorDefault } else { bgt = bg.ToTCell() } st := StyleNone.MergeUnder(attr) return tcell.Style{}.Attributes(st.OnOff).Foreground(fgt).Background(bgt) } //====================================================================== // Color satisfies IColor, embeds an IColor, and allows a gowid Color to be // constructed from a string alone. Each of the more specific color types is // tried in turn with the string until one succeeds. type Color struct { IColor Id string } func (c Color) String() string { return fmt.Sprintf("%v", c.IColor) } // MakeColorSafe returns a Color struct specified by the string argument, in a // do-what-I-mean fashion - it tries the Color struct maker functions in // a pre-determined order until one successfully initialized a Color, or // until all fail - in which case an error is returned. The order tried is // TCellColor, RGBColor, GrayColor, UrwidColor. func MakeColorSafe(s string) (Color, error) { var col IColor var err error col, err = MakeTCellColor(s) if err == nil { return Color{col, s}, nil } col, err = MakeRGBColorSafe(s) if err == nil { return Color{col, s}, nil } col, err = MakeGrayColorSafe(s) if err == nil { return Color{col, s}, nil } col, err = NewUrwidColorSafe(s) if err == nil { return Color{col, s}, nil } return Color{}, errors.WithStack(InvalidColor{Color: s}) } func MakeColor(s string) Color { res, err := MakeColorSafe(s) if err != nil { panic(err) } return res } //====================================================================== type ColorByMode struct { Colors map[ColorMode]IColor // Indexed by ColorMode } var _ IColor = (*ColorByMode)(nil) func MakeColorByMode(cols map[ColorMode]IColor) ColorByMode { res, err := MakeColorByModeSafe(cols) if err != nil { panic(err) } return res } func MakeColorByModeSafe(cols map[ColorMode]IColor) (ColorByMode, error) { return ColorByMode{Colors: cols}, nil } func (c ColorByMode) ToTCellColor(mode ColorMode) (TCellColor, bool) { if col, ok := c.Colors[mode]; ok { col2, ok := col.ToTCellColor(mode) return col2, ok } panic(ColorModeMismatch{Color: c, Mode: mode}) } //====================================================================== // RGBColor allows for use of colors specified as three components, each with values from 0x0 to 0xf. // Note that an RGBColor should render as close to the components specify regardless of the color mode // of the terminal - 24-bit color, 256-color, 88-color. Gowid constructs a color cube, just like urwid, // and for each color mode, has a lookup table that maps the rgb values to a color cube value which is // closest to the intended color. Note that RGBColor is not supported in 16-color, 8-color or // monochrome. type RGBColor struct { Red, Green, Blue int } var _ IColor = (*RGBColor)(nil) // MakeRGBColor constructs an RGBColor from a string e.g. "#f00" is red. Note that // MakeRGBColorSafe should be used unless you are sure the string provided is valid // (otherwise there will be a panic). func MakeRGBColor(s string) RGBColor { res, err := MakeRGBColorSafe(s) if err != nil { panic(err) } return res } func (r RGBColor) String() string { return fmt.Sprintf("RGBColor(#%02x,#%02x,#%02x)", r.Red, r.Green, r.Blue) } // MakeRGBColorSafe does the same as MakeRGBColor except will return an // error if provided with invalid input. func MakeRGBColorSafe(s string) (RGBColor, error) { var mult int64 = 1 match := longColorRE.FindAllStringSubmatch(s, -1) if len(match) == 0 { match = shortColorRE.FindAllStringSubmatch(s, -1) if len(match) == 0 { return RGBColor{}, errors.WithStack(InvalidColor{Color: s}) } mult = 16 } d1, _ := strconv.ParseInt(match[0][1], 16, 16) d2, _ := strconv.ParseInt(match[0][2], 16, 16) d3, _ := strconv.ParseInt(match[0][3], 16, 16) d1 *= mult d2 *= mult d3 *= mult x := MakeRGBColorExt(int(d1), int(d2), int(d3)) return x, nil } // MakeRGBColorExtSafe builds an RGBColor from the red, green and blue components // provided as integers. If the values are out of range, an error is returned. func MakeRGBColorExtSafe(r, g, b int) (RGBColor, error) { col := RGBColor{r, g, b} if r > 0xff || g > 0xff || b > 0xff { return RGBColor{}, errors.WithStack(errors.WithMessage(InvalidColor{Color: col}, "RGBColor parameters must be between 0x00 and 0xfff")) } return col, nil } // MakeRGBColorExt builds an RGBColor from the red, green and blue components // provided as integers. If the values are out of range, the function will panic. func MakeRGBColorExt(r, g, b int) RGBColor { res, err := MakeRGBColorExtSafe(r, g, b) if err != nil { panic(err) } return res } // Implements golang standard library's color.Color func (rgb RGBColor) RGBA() (r, g, b, a uint32) { r = uint32(rgb.Red << 8) g = uint32(rgb.Green << 8) b = uint32(rgb.Blue << 8) a = 0xffff return } func (r RGBColor) findClosest(from []colorful.Color, corresponding []TCellColor, cache *lru.Cache) TCellColor { var best float64 = 100.0 var j int if res, ok := cache.Get(r); ok { return res.(TCellColor) } ccol, _ := colorful.MakeColor(r) for i, c := range from { x := c.DistanceLab(ccol) if x < best { best = x j = i } } cache.Add(r, corresponding[j]) return corresponding[j] } // ToTCellColor converts an RGBColor to a TCellColor, suitable for rendering to the screen // with tcell. It lets RGBColor conform to IColor. func (r RGBColor) ToTCellColor(mode ColorMode) (TCellColor, bool) { switch mode { case Mode24BitColors: c := tcell.NewRGBColor(int32(r.Red), int32(r.Green), int32(r.Blue)) return MakeTCellColorExt(c), true case Mode256Colors: if IgnoreBase16 { return r.findClosest(colorful256[22:], term256[22:], term256CacheIgnoreBase16), true } else { return r.findClosest(colorful256, term256, term256Cache), true } case Mode88Colors: rd := cubeLookup88_16[r.Red>>4] g := cubeLookup88_16[r.Green>>4] b := cubeLookup88_16[r.Blue>>4] c := tcell.Color((CubeStart + (((rd * cubeSize88) + g) * cubeSize88) + b) + 0) + tcell.ColorValid return MakeTCellColorExt(c), true case Mode16Colors: return r.findClosest(colorful16, term16, term16Cache), true case Mode8Colors: return r.findClosest(colorful8, term8, term8Cache), true case ModeMonochrome: return r.findClosest(colorful8[0:1], term8[0:1], term2Cache), true default: return TCellColor{}, false } } //====================================================================== // UrwidColor is a gowid Color implementing IColor and which allows urwid color names to be used // (http://urwid.org/manual/displayattributes.html#foreground-and-background-settings) e.g. // "dark blue", "light gray". type UrwidColor struct { Id string cached bool cache [2]TCellColor } var _ IColor = (*UrwidColor)(nil) // NewUrwidColorSafe returns a pointer to an UrwidColor struct and builds the UrwidColor from // a string argument e.g. "yellow". Note that in urwid proper (python), a color can also specify // a style, like "yellow, underline". UrwidColor does not support specifying styles in that manner. func NewUrwidColorSafe(val string) (*UrwidColor, error) { if _, ok := basicColors[val]; !ok { return nil, errors.WithStack(InvalidColor{Color: val}) } return &UrwidColor{ Id: val, }, nil } // NewUrwidColorSafe returns a pointer to an UrwidColor struct and builds the UrwidColor from // a string argument e.g. "yellow"; this function will panic if the there is an error during // initialization. func NewUrwidColor(val string) *UrwidColor { res, err := NewUrwidColorSafe(val) if err != nil { panic(err) } return res } func (r UrwidColor) String() string { return fmt.Sprintf("UrwidColor(%s)", r.Id) } // ToTCellColor converts the receiver UrwidColor to a TCellColor, ready for rendering to a // tcell screen. This lets UrwidColor conform to IColor. func (s *UrwidColor) ToTCellColor(mode ColorMode) (TCellColor, bool) { if s.cached { switch mode { case Mode24BitColors, Mode256Colors, Mode88Colors, Mode16Colors: return s.cache[0], true case Mode8Colors, ModeMonochrome: return s.cache[1], true default: panic(errors.WithStack(ColorModeMismatch{Color: s, Mode: mode})) } } idx := -1 switch mode { case Mode24BitColors, Mode256Colors, Mode88Colors, Mode16Colors: idx = posInMap(s.Id, basicColors) case Mode8Colors, ModeMonochrome: idx = posInMap(s.Id, tBasicColors) default: panic(errors.WithStack(ColorModeMismatch{Color: s, Mode: mode})) } if idx == -1 { panic(errors.WithStack(InvalidColor{Color: s})) } col := tcell.ColorDefault if idx > 0 { idx = idx - 1 col = tcell.ColorValid + tcell.Color(idx) } c := MakeTCellColorExt(col) switch mode { case Mode24BitColors, Mode256Colors, Mode88Colors, Mode16Colors: s.cache[0] = c case Mode8Colors, ModeMonochrome: s.cache[1] = c } s.cached = true return c, true } //====================================================================== // GrayColor is an IColor that represents a greyscale specified by the // same syntax as urwid - http://urwid.org/manual/displayattributes.html // and search for "gray scale entries". Strings may be of the form "g3", // "g100" or "g#a1", "g#ff" if hexadecimal is preferred. These index the // grayscale color cube. type GrayColor struct { Val int } func (g GrayColor) String() string { return fmt.Sprintf("GrayColor(%d)", g.Val) } // MakeGrayColorSafe returns an initialized GrayColor provided with a string // input like "g50" or "g#ab". If the input is invalid, an error is returned. func MakeGrayColorSafe(val string) (GrayColor, error) { var d uint64 match := grayDecColorRE.FindAllStringSubmatch(val, -1) if len(match) == 0 || len(match[0]) != 2 { match := grayHexColorRE.FindAllStringSubmatch(val, -1) if len(match) == 0 || len(match[0]) != 2 { return GrayColor{}, errors.WithStack(InvalidColor{Color: val}) } d, _ = strconv.ParseUint(match[0][1], 16, 8) } else { d, _ = strconv.ParseUint(match[0][1], 10, 8) if d > 100 { return GrayColor{}, errors.WithStack(InvalidColor{Color: val}) } } return GrayColor{int(d)}, nil } // MakeGrayColor returns an initialized GrayColor provided with a string // input like "g50" or "g#ab". If the input is invalid, the function panics. func MakeGrayColor(val string) GrayColor { res, err := MakeGrayColorSafe(val) if err != nil { panic(err) } return res } func grayAdjustment88(val int) int { if val == 0 { return cubeBlack } val -= 1 if val == graySize88 { return cubeWhite88 } y := grayStart88 + val return y } func grayAdjustment256(val int) int { if val == 0 { return cubeBlack } val -= 1 if val == graySize256 { return cubeWhite256 } y := grayStart256 + val return y } // ToTCellColor converts the receiver GrayColor to a TCellColor, ready for rendering to a // tcell screen. This lets GrayColor conform to IColor. func (s GrayColor) ToTCellColor(mode ColorMode) (TCellColor, bool) { switch mode { case Mode24BitColors: adj := intScale(s.Val, 101, 0x100) c := tcell.NewRGBColor(int32(adj), int32(adj), int32(adj)) return MakeTCellColorExt(c), true case Mode256Colors: x := tcell.Color(grayAdjustment256(grayLookup256_101[s.Val]) + 1) + tcell.ColorValid return MakeTCellColorExt(x), true case Mode88Colors: x := tcell.Color(grayAdjustment88(grayLookup88_101[s.Val]) + 1) + tcell.ColorValid return MakeTCellColorExt(x), true default: panic(errors.WithStack(ColorModeMismatch{Color: s, Mode: mode})) } } //====================================================================== // TCellColor is an IColor using tcell's color primitives. If you are not porting from urwid or translating // from urwid, this is the simplest approach to using color. Gowid's layering approach means that the empty // value for a color should mean "no color preference" - so we want the zero value to mean that. A tcell.Color // of 0 means "default color". So gowid coopts nil to mean "no color preference". type TCellColor struct { tc *tcell.Color } var ( _ IColor = (*TCellColor)(nil) _ fmt.Stringer = (*TCellColor)(nil) ) var tcellColorRE = regexp.MustCompile(`^[Cc]olor([0-9a-fA-F]{2})$`) // MakeTCellColor returns an initialized TCellColor given a string input like "yellow". The names that can be // used are provided here: https://github.com/gdamore/tcell/blob/master/color.go#L821. func MakeTCellColor(val string) (TCellColor, error) { match := tcellColorRE.FindStringSubmatch(val) // e.g. "Color00" if len(match) == 2 { n, _ := strconv.ParseUint(match[1], 16, 8) return MakeTCellColorExt(tcell.Color(n) + tcell.ColorValid), nil } else if col, ok := tcell.ColorNames[val]; !ok { return TCellColor{}, errors.WithStack(InvalidColor{Color: val}) } else { return MakeTCellColorExt(col), nil } } // MakeTCellColor returns an initialized TCellColor given a tcell.Color input. The values that can be // used are provided here: https://github.com/gdamore/tcell/blob/master/color.go#L41. func MakeTCellColorExt(val tcell.Color) TCellColor { return TCellColor{&val} } // MakeTCellNoColor returns an initialized TCellColor that represents "no color" - meaning if another // color is rendered "under" this one, then the color underneath will be displayed. func MakeTCellNoColor() TCellColor { return TCellColor{} } // String implements Stringer for '%v' support. func (r TCellColor) String() string { if r.tc == nil { return "[no-color]" } else { c := *r.tc return fmt.Sprintf("TCellColor(%v)", tcell.Color(c)) } } // ToTCell converts a TCellColor back to a tcell.Color for passing to tcell APIs. func (r TCellColor) ToTCell() tcell.Color { if r.tc == nil { return tcell.ColorDefault } return *r.tc } // ToTCellColor is a no-op, and exists so that TCellColor conforms to the IColor interface. func (r TCellColor) ToTCellColor(mode ColorMode) (TCellColor, bool) { return r, true } //====================================================================== // NoColor implements IColor, and represents "no color preference", distinct from the default terminal color, // white, black, etc. This means that if a NoColor is rendered over another color, the color underneath will // be displayed. type NoColor struct{} // ToTCellColor converts NoColor to TCellColor. This lets NoColor conform to the IColor interface. func (r NoColor) ToTCellColor(mode ColorMode) (TCellColor, bool) { return ColorNone, true } func (r NoColor) String() string { return "NoColor" } //====================================================================== // DefaultColor implements IColor and means use whatever the default terminal color is. This is // different to NoColor, which expresses no preference. type DefaultColor struct{} // ToTCellColor converts DefaultColor to TCellColor. This lets DefaultColor conform to the IColor interface. func (r DefaultColor) ToTCellColor(mode ColorMode) (TCellColor, bool) { return MakeTCellColorExt(tcell.ColorDefault), true } func (r DefaultColor) String() string { return "DefaultColor" } //====================================================================== // ColorInverter implements ICellStyler, and simply swaps foreground and background colors. type ColorInverter struct { ICellStyler } func (c ColorInverter) GetStyle(prov IRenderContext) (x IColor, y IColor, z StyleAttrs) { y, x, z = c.ICellStyler.GetStyle(prov) return } //====================================================================== // PaletteEntry is typically used by a gowid application to represent a set of color and style // preferences for use by different application widgets e.g. black text on a white background // with text underlined. PaletteEntry implements the ICellStyler interface meaning it can // provide a triple of foreground and background IColor, and a StyleAttrs struct. type PaletteEntry struct { FG IColor BG IColor Style StyleAttrs } var _ ICellStyler = (*PaletteEntry)(nil) // MakeStyledPaletteEntry simply stores the three parameters provided - a foreground and // background IColor, and a StyleAttrs struct. func MakeStyledPaletteEntry(fg, bg IColor, style StyleAttrs) PaletteEntry { return PaletteEntry{fg, bg, style} } // MakePaletteEntry stores the two IColor parameters provided, and has no style preference. func MakePaletteEntry(fg, bg IColor) PaletteEntry { return PaletteEntry{fg, bg, StyleNone} } // GetStyle returns the individual colors and style attributes. func (a PaletteEntry) GetStyle(prov IRenderContext) (x IColor, y IColor, z StyleAttrs) { x, y, z = a.FG, a.BG, a.Style return } //====================================================================== // PaletteRef is intended to represent a PaletteEntry, looked up by name. The ICellStyler // API GetStyle() provides an IRenderContext and should return two colors and style attributes. // PaletteRef provides these by looking up the IRenderContext with the name (string) provided // to it at initialization. type PaletteRef struct { Name string } var _ ICellStyler = (*PaletteRef)(nil) // MakePaletteRef returns a PaletteRef struct storing the (string) name of the PaletteEntry // which will be looked up in the IRenderContext. func MakePaletteRef(name string) PaletteRef { return PaletteRef{name} } // GetStyle returns the two colors and a style, looked up in the IRenderContext by name. func (a PaletteRef) GetStyle(prov IRenderContext) (x IColor, y IColor, z StyleAttrs) { spec, ok := prov.CellStyler(a.Name) if ok { x, y, z = spec.GetStyle(prov) } else { x, y, z = NoColor{}, NoColor{}, StyleAttrs{} } return } //====================================================================== // EmptyPalette implements ICellStyler and returns no preference for any colors or styling. type EmptyPalette struct{} var _ ICellStyler = (*EmptyPalette)(nil) func MakeEmptyPalette() EmptyPalette { return EmptyPalette{} } // GetStyle implements ICellStyler. func (a EmptyPalette) GetStyle(prov IRenderContext) (x IColor, y IColor, z StyleAttrs) { x, y, z = NoColor{}, NoColor{}, StyleAttrs{} return } //====================================================================== // StyleMod implements ICellStyler. It returns colors and styles from its Cur field unless they are // overridden by settings in its Mod field. This provides a way for a layering of ICellStylers. type StyleMod struct { Cur ICellStyler Mod ICellStyler } var _ ICellStyler = (*StyleMod)(nil) // MakeStyleMod implements ICellStyler and stores two ICellStylers, one to layer on top of the // other. func MakeStyleMod(cur, mod ICellStyler) StyleMod { return StyleMod{cur, mod} } // GetStyle returns the IColors and StyleAttrs from the Mod ICellStyler if they express an // affirmative preference, otherwise defers to the values from the Cur ICellStyler. func (a StyleMod) GetStyle(prov IRenderContext) (x IColor, y IColor, z StyleAttrs) { fcur, bcur, scur := a.Cur.GetStyle(prov) fmod, bmod, smod := a.Mod.GetStyle(prov) var ok bool _, ok = fmod.ToTCellColor(prov.GetColorMode()) if ok { x = fmod } else { x = fcur } _, ok = bmod.ToTCellColor(prov.GetColorMode()) if ok { y = bmod } else { y = bcur } z = scur.MergeUnder(smod) return } //====================================================================== // ForegroundColor is an ICellStyler that expresses a specific foreground color and no preference for // background color or style. type ForegroundColor struct { IColor } var _ ICellStyler = (*ForegroundColor)(nil) func MakeForeground(c IColor) ForegroundColor { return ForegroundColor{c} } // GetStyle implements ICellStyler. func (a ForegroundColor) GetStyle(prov IRenderContext) (x IColor, y IColor, z StyleAttrs) { x = a.IColor y = NoColor{} z = StyleNone return } //====================================================================== // BackgroundColor is an ICellStyler that expresses a specific background color and no preference for // foreground color or style. type BackgroundColor struct { IColor } var _ ICellStyler = (*BackgroundColor)(nil) func MakeBackground(c IColor) BackgroundColor { return BackgroundColor{c} } // GetStyle implements ICellStyler. func (a BackgroundColor) GetStyle(prov IRenderContext) (x IColor, y IColor, z StyleAttrs) { x = NoColor{} y = a.IColor z = StyleNone return } //====================================================================== // StyledAs is an ICellStyler that expresses a specific text style and no preference for // foreground and background color. type StyledAs struct { StyleAttrs } var _ ICellStyler = (*StyledAs)(nil) func MakeStyledAs(s StyleAttrs) StyledAs { return StyledAs{s} } // GetStyle implements ICellStyler. func (a StyledAs) GetStyle(prov IRenderContext) (x IColor, y IColor, z StyleAttrs) { x = NoColor{} y = NoColor{} z = a.StyleAttrs return } //====================================================================== // Palette implements IPalette and is a trivial implementation of a type that can store // cell stylers and provide access to them via iteration. type Palette map[string]ICellStyler var _ IPalette = (*Palette)(nil) // CellStyler will return an ICellStyler by name, if it exists. func (m Palette) CellStyler(name string) (ICellStyler, bool) { i, ok := m[name] return i, ok } // RangeOverPalette applies the supplied function to each member of the // palette. If the function returns false, the loop terminates early. func (m Palette) RangeOverPalette(f func(k string, v ICellStyler) bool) { for k, v := range m { if !f(k, v) { break } } } //====================================================================== // IColorToTCell is a utility function that will convert an IColor to a TCellColor // in preparation for passing to tcell to render; if the conversion fails, a default // TCellColor is returned (provided to the function via a parameter) func IColorToTCell(color IColor, def TCellColor, mode ColorMode) TCellColor { res := def colTC, ok := color.ToTCellColor(mode) // Is there a color specified affirmatively? (i.e. not NoColor) if ok && colTC != ColorNone { // Yes a color specified res = colTC } return res } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: gowid-1.4.0/decoration_test.go000066400000000000000000000070241426234454000163510ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source code is governed by the MIT license // that can be found in the LICENSE file. package gowid import ( "testing" tcell "github.com/gdamore/tcell/v2" "github.com/go-test/deep" "github.com/stretchr/testify/assert" ) func TestColor1(t *testing.T) { IgnoreBase16 = true c, _ := MakeRGBColorExtSafe(0, 0, 0) i2a, _ := c.ToTCellColor(Mode256Colors) i2 := i2a.ToTCell() // See https://jonasjacek.github.io/colors/ - we are skipping // colors 0-21 inclusive if i2 != tcell.Color232 { t.Errorf("Failed") } } func TestColor1b(t *testing.T) { IgnoreBase16 = false c, _ := MakeRGBColorExtSafe(0, 0, 0) i2a, _ := c.ToTCellColor(Mode256Colors) i2 := i2a.ToTCell() if i2 != tcell.ColorValid { t.Errorf("Failed") } } func TestColor2(t *testing.T) { c := NewUrwidColor("dark red") i2a, _ := c.ToTCellColor(Mode256Colors) i2 := i2a.ToTCell() if i2 != tcell.ColorMaroon { t.Errorf("Failed") } } func TestColor3(t *testing.T) { c := MakeGrayColor("g#ff") if c.Val != 255 { t.Errorf("Failed") } } func TestColor4(t *testing.T) { c := MakeGrayColor("g99") if c.Val != 99 { t.Errorf("Failed") } } func TestColor5(t *testing.T) { c := MakeGrayColor("g100") if c.Val != 100 { t.Errorf("Failed") } if v, _ := c.ToTCellColor(Mode256Colors); v.ToTCell() != tcell.Color232 { t.Errorf("Failed") } } func TestColor6(t *testing.T) { c := MakeGrayColor("g3") if c.Val != 3 { t.Errorf("Failed") } if v, _ := c.ToTCellColor(Mode256Colors); v.ToTCell() != tcell.Color233 { t.Errorf("Failed") } } func TestColor7(t *testing.T) { c := MakeGrayColor("g0") if c.Val != 0 { t.Errorf("Failed") } if v, _ := c.ToTCellColor(Mode256Colors); v.ToTCell() != tcell.Color17 { t.Errorf("Failed") } } func TestColorLookup1(t *testing.T) { res := makeColorLookup([]int{0, 7, 9}, 10) if deep.Equal(res, []int{0, 0, 0, 0, 1, 1, 1, 1, 1, 2}) != nil { t.Errorf("Failed") } } func TestIntScale1(t *testing.T) { if intScale(0x7, 0x10, 0x10000) != 0x7777 { t.Errorf("Failed val was %d", intScale(0x7, 0x10, 0x10000)) } if intScale(0x5f, 0x100, 0x10) != 6 { t.Errorf("Failed val was %d", intScale(0x5f, 0x100, 0x10)) } if intScale(2, 6, 101) != 40 { t.Errorf("Failed") } if intScale(1, 3, 4) != 2 { t.Errorf("Failed") } } func TestStringColor1(t *testing.T) { col1, _ := MakeRGBColorSafe("#12f") col2, _ := MakeRGBColorExtSafe(1*16, 2*16, 15*16) if deep.Equal(col1, col2) != nil { t.Errorf("Failed") } } func TestStringColor2(t *testing.T) { col1, _ := MakeRGBColorSafe("#12fgogogog") col2, _ := MakeRGBColorExtSafe(1*16, 2*16, 15*16) if deep.Equal(col1, col2) == nil { t.Errorf("Failed") } } func TestStringColor3(t *testing.T) { _, err := MakeRGBColorSafe("#34g") if err == nil { t.Errorf("Failed") } } func TestGray881(t *testing.T) { c := MakeGrayColor("g100") v, _ := c.ToTCellColor(Mode88Colors) assert.Equal(t, v.ToTCell(), tcell.Color80) } func TestDefault1(t *testing.T) { c, _ := MakeColorSafe("default") v, _ := c.ToTCellColor(Mode256Colors) assert.Equal(t, v.ToTCell(), tcell.ColorDefault) } func TestTCell1(t *testing.T) { c, _ := MakeColorSafe("maroon") v, _ := c.ToTCellColor(Mode256Colors) assert.Equal(t, v.ToTCell(), tcell.ColorMaroon) } func TestTCell2(t *testing.T) { c := MakeTCellColorExt(tcell.ColorMaroon) v, _ := c.ToTCellColor(Mode256Colors) assert.Equal(t, v.ToTCell(), tcell.ColorMaroon) } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: gowid-1.4.0/docs/000077500000000000000000000000001426234454000135615ustar00rootroot00000000000000gowid-1.4.0/docs/Debugging.md000066400000000000000000000043141426234454000160000ustar00rootroot00000000000000# Debugging Techniques In no particular order, here is a list of tricks for debugging gowid applications. ## Run on a different tty - Make a local clone of the `tcell` repo: ```bash git clone https://github.com/gdamore/tcell cd tcell ``` - Apply the patch from this gist: https://gist.github.com/gcla/29628006828e57ece336554f26e0bde9 ```bash patch -p1 < gowidtty.patch ``` - Make your gowid application compile against your local clone of `tcell`. Adjust your application's `go.mod` like this, replacing `` with your username (or adjust to where you cloned `tcell`) ```bash replace github.com/gdamore/tcell => /home//tcell ``` - Run your application in tmux - make a split screen. - On one side, determine the tty, then block input - On the other side, set the environment variable `GOWID_TTY` ![Screenshot-20190616154511-1085x724](https://user-images.githubusercontent.com/45680/59569057-33a9e100-9052-11e9-8d51-4171a870a872.png) - Run your gowid application e.g. using [tm.sh](https://gist.github.com/gcla/e52ea391c4001cedcfa2cf22d124a750) ```bash tm.sh 1 go run examples/gowid-fib/fib.go ``` ![image](https://user-images.githubusercontent.com/45680/59569085-bfbc0880-9052-11e9-8d17-eaebcca25b6b.png) Then you can add `fmt.Printf(...)` calls to quickly debug and not have them interfere with your application's tty. ## Watch Flow of User Input Events Gowid widgets are arranged in a hierarchy, with outer widgets passing events through to inner widgets for processing, possibly altering them or handling them themselves on the way. Outer widgets could call ```go child.UserInput(ev, ...) ``` to determine whether or not the child is handling the event. But instead, all gowid widgets currently call ```go gowid.UserInput(child, ev, ...) ``` instead. This has the same effect, but means that `gowid.UserInput()` can be used to inspect events flowing through the application. For example, you can modify the function in `support.go`: ```go func UserInput(w IWidget, ev interface{}, size IRenderSize, focus Selector, app IApp) bool { if evm, ok := ev.(*tcell.EventMouse); ok { // Do something fmt.Printf("Sending event %v of type %T to widget %v", ev, ev, w) } return w.UserInput(ev, size, focus, app) } gowid-1.4.0/docs/FAQ.md000066400000000000000000000351131426234454000145150ustar00rootroot00000000000000# FAQ Gowid is a new Go package, so this is my best guess at some useful tips and tricks. ## What is the difference between `RenderBox`, `RenderFlowWith` and `RenderFixed`? This concept comes directly from [urwid](http://urwid.org/manual/widgets.html#box-flow-and-fixed-widgets). Each widget supports being rendered in one or more of these three modes: - Box - Flow - Fixed A box widget is a widget that can render itself given a width and a height i.e. #columns and #rows. It should render a canvas of the correct size. You can render a box widget by calling its `Render()` function with a `RenderBox` struct for the `size` argument e.g. `RenderBox{C: 20, R:16}`. Gowid will always render the root of the widget hierarchy with a `RenderBox` `size` argument, so the root widget should be a box widget. A flow widget is a widget that can render itself given a width only. The idea is that the widget itself should determine how many rows it requires. A good example of this is a `text.Widget`. If its given fewer columns in which to render, it might need to build a canvas with more rows. A `listbox.Widget` renders its children with a `RenderFlowWith{C:...}` argument and lets each child determine how many lines it needs. You can see this in action by running `gowid-fib` and paging down a few times until the numbers are so long that each scrolls onto the next line. A fixed widget is a widget that will render itself without any guidance about the width and height. For example, a `checkbox.Widget` can render itself this way - it will make a 3x1 canvas which contains the text `[x]`. When a container widget, like a `pile.Widget` or `columns.Widget` renders its children, it will use one of these types of size arguments for each child. Sometimes the child widget may not support being rendered with a particular size type. For example, a fixed widget won't automatically expand to accommodate the size given with `RenderBox`. Gowid provides adapter widgets to let you choose how your application should handle this. `boxadadapter.Widget` is initialized with a child widget and an integer that means number-of-rows. The child widget should be a box widget. `boxadapter` allows it to be rendered in flow mode e.g. to be used in a `listbox.Widget`. When `boxadapter.Widget` renders its child, it turns its flow size into a box size by setting the number of rows to render from its initialization parameter. Another option is `vpadding.Widget`. It is initialized with a child widget, an alignment, and a "subsize" that tells the widget how to transform its size argument when rendering its child. A `vpadding.Widget` can turn a box size into a flow size, render its child in flow mode, and then align the rendered child within a canvas of the right size determined by the box, potentially chopping lines from the top and bottom if the child is too large. ## How does Gowid use goroutines? How can I stay thread-safe? A gowid app is typically launched with a line of code like this: ```go app.SimpleMainLoop() ``` That function does the following: 1. Starts a goroutine to collect events from tcell. These are pushed into a gowid channel for tcell events. 2. Enters a loop that runs a `select` on three channels: - tcell events channel - run-after-render channel - quit channel 3. The loop is terminated by an event on the quit channel. Then gowid will tell tcell to stop sending events and wait for the tcell event-collecting goroutine to stop. Example events that appear on the tcell event channel are key-presses, mouse-clicks and terminal changes like a resize. Gowid responds to user input by calling the root widget's `UserInput()` function. The quit channel receives an event when the gowid application calls `app.Quit()`. The run-after-render channel receives functions to be executed. A gowid application can send such a function by calling `app.Run()` and passing the function to call. On receipt of such a function, gowid will call the function, then redraw the widgets. Note that the function is executed by the main application goroutine - the one executing `app.SimpleMainLoop()` - so the `Run()` function will not race with any widget rendering or user-input processing. If your application starts other goroutines that might update the widgets' state or hierarchy, it is best to make those state changes in a function that is issued via `app.Run()`. For an example of this, see `github.com/gcla/gowid/examples/gowid-editor` - in particular code that runs on a timer and updates the editor's status bar. ## How do I write code to respond to a button click? Here is an example of a callback issued in response to a button click: ```go rb.OnClick(gowid.WidgetCallback{"cb", func(app gowid.IApp, w gowid.IWidget) { if rb.Selected { switch txt { case "256-Color": app.SetColorMode(gowid.Mode256Colors) [...elided for brevity...] case "Monochrome": app.SetColorMode(gowid.ModeMonochrome) } updateChartHolder(app.GetColorMode(), app) } }}) ``` This is based on the example `github.com/gcla/gowid/examples/gowid-palette`. These callbacks are run in the main gowid goroutine, within the call stack that starts with the root widget's `UserInput()`. The `OnClick()` function takes an `IWidgetChangedCallback`. A simple implementation of that is `WidgetCallback`. To satisfy the interface, you need an ID ("cb" here) and a function that is called with the app and the widget issuing the callback. The ID is present so you can easily remove the callback later if necessary - you just supply the ID, so it must be comparable. Note that if it's more convenient, you can just exploit Go's scoping rules to refer to and capture the callback-issuing widget by its name in the outer scope i.e. "rb". ## Why do all your interfaces start with "I" and not all end in "er"? When I started writing gowid, I didn't appreciate or understand the convention. For official discussion, you can read this - https://golang.org/doc/effective_go.html#interface-names. When programming I found I wanted a visible way to distinguish arguments to functions - interfaces or values. Using the old I-prefix made that simple for me. As time passed I could see the elegance of small simple interfaces that "do things", hence `Doer()`, of limiting functions with receivers and instead using free functions. But I haven't gone back to try to retro-fit to that new appreciation. So as gowid stands, interfaces start with an I. One of the most important gowid interfaces is `IWidget`. In light of a better understanding of this recommended naming convention, could `IWidget` be broken down? For example ```go type InputHandler interface { UserInput(ev interface{}, size IRenderSize, focus Selector, app IApp) bool } ``` So perhaps each composite widget could store one or more `InputHandler`s, and defer input to the right child. But in figuring out which is the right child to accept the input (e.g. mouse click), a composite widget such as `columns.Widget` may need to figure out the rendered-size of each child and do some arithmetic on the input event coordinates. Now the children need to implement `RenderedSizeProvider` too. Some widgets fall back to calling `Render()` in order to compute the rendered canvas size (slower but simpler) so then they would need to also implement a `Renderer` interface. This gets the requirements close to the current `IWidget` interface. So my view has been that `IWidget` represents a reasonable interface needed to satisfy all widget processing, and it's still pretty small - only four methods. ## How do I write a new widget? The quick answer is you need to implement `IWidget`: ```go type IWidget interface { Render(size IRenderSize, focus Selector, app IApp) ICanvas RenderSize(size IRenderSize, focus Selector, app IApp) IRenderBox UserInput(ev interface{}, size IRenderSize, focus Selector, app IApp) bool Selectable() bool } ``` The `focus.Focus` argument will be true if your widget has the application focus; otherwise false (`focus.Selected` is intended for letting widgets like columns and pile highlight the subwidget that *would* be in focus if the outer widget was in focus). Some helper types are provided for the common case. If your widget is always selectable, you can do this: ```go type MyWidget struct { ... gowid.IsSelectable } ``` Or if the opposite, use `NotSelectable`. If your widget will always reject user input, you can embed `RejectUserInput` which will provide a default implementation returning `false`. Sometimes it's simpler to extend an existing widget. There are some examples of this e.g. `github.com/gcla/gowid/examples/gowid-tutorial4` - see `QuestionBox`. It chooses to embed an interface, `IWidget`, so that it can replace the implementation at runtime. It starts out as an `*edit.Widget` and then is replaced with a `*text.Widget`. `QuestionBox` provides its own `UserInput()` function but the embedded `IWidget` provides the other functions needed to satisfy the widget interface. But be careful and remember that Go does not have dynamic dispatch for structs. If you embed another widget, and that embedded widget's method is called, the receiver will be the embedded widget, not the containing widget. You can't "escape" back to the containing widget. I misunderstood this fundamental design feature when I started programming with Go. Most gowid widgets are structured into two groups of functions. The essence of the widget is distilled into an interface that rests on `IWidget` - for example, here is a checkbox (in the `github.com/gcla/gowid/widgets/checkbox` package): ```go // IWidget scoped to "checkbox" here. type IWidget interface { gowid.IWidget IChecked } ``` So checkbox is a widget that satisfies `checkbox.IChecked`. The `checkbox` package provides a free function that implements some of the expected widget functionality: ```go func Render(w IChecked, size gowid.IRenderSize, focus Selector, app gowid.IApp) gowid.ICanvas ``` The checkbox widget's `Render()` method looks like this: ```go func (w *Widget) Render(size gowid.IRenderSize, focus Selector, app gowid.IApp) gowid.ICanvas { return Render(w, size, focus, app) } ``` The goal is to make it easier to override parts of a widget, and use the default implementations for the rest. The rendering algorithm is contained in a free function that needs only an `IChecked`, so a new implementation that can be rendered similarly can call the same free function. If instead your new widget's `Render()` function did this: ```go func (w *Widget) Render(size gowid.IRenderSize, focus Selector, app gowid.IApp) gowid.ICanvas { return w.IWidget.Render(size, focus, app) } ``` then the embedded `IWidget` - presumably a `*gowid.Checkbox` - would call `Render()` with a `*gowid.Checkbox` as the receiver, so calling the free function `Render()` with the `IChecked` argument being a `*gowid.Checkbox` instead of your new type. The effect would be your new widget would render like the original checkbox. ## What is the difference between being selectable and handling user input? A widget that is selectable is intended to be able to take the focus. For example, if a `listbox` is displaying a range of widgets, hitting the down arrow will make the `listbox` look for the next selectable widget to take the focus. If it can, it will skip any widget that is not selectable, like `text.Widget`s. But note that if *no* candidate widgets are selectable, then one will be chosen anyway. So your widget may still be rendered and provided user input (which you can then just reject). A widget that returns `false` to a call to its `UserInput()` function is indicating that it has not handled the input. Gowid will then try to give the input to another widget. For example, if you hit down arrow in a `listbox`, it will first see if the currently focused listbox widget will accept the keypress. If that widget is an `edit.Widget`, it might move the cursor down a line inside its editing area. The `edit.Widget` will return `true`, and the `listbox` will not process the keypress further. But if, say, the focus `edit.Widget`is on the last line in its editing area, it can't move down a line, and will return `false` to the invocation of its `UserInput()`. The parent `listbox` will then accept the keypress and try to change its own focus widget. You can see this in action in `github.com/gcla/gowid/examples/gowid-widgets2` with left and right arrow keypresses. ## How do I color my widget? You can use `styled.Widget` and pass it your own widget e.g. ```go styled.New(myWidget, gowid.MakePaletteEntry(gowid.ColorBlack, gowid.ColorCyan)) ``` The second argument must be an `ICellStyler`: ```go type ICellStyler interface { GetStyle(IRenderContext) (IColor, IColor, StyleAttrs) } ``` You can use `MakePaletteEntry()` to construct one on the fly. The first argument is foreground color, the second background. If you register a palette when you initialize your `app`, you can refer to entries in that palette: ```go styled.New(myWidget, gowid.MakePaletteRef("eyegrabbing")) ``` If you would like a different style to be used when your widget is in focus, then you can do this: ```go styled.NewWithFocus(myWidget, gowid.MakePaletteRef("boring")), gowid.MakePaletteRef("eyegrabbing")) ) ``` You can easily just invert the colors on focus by using `styled.NewWithSimpleFocus()`. It simply defers to `NewWithFocus()` and uses `ColorInverter{s}` as its third argument where `s` is the second argument. ## How do I apply text styles like underline? The `StyledAs` struct implements `ICellStyler`, providing no color preferences and the requested "style". So something like this: ```go styled.New(myTextWidget, gowid.MakeStyledAs(gowid.StyleUnderline)) ``` will do the job. ## Why do all the Set...() functions take an IApp Argument? I decided that it could be useful for widgets to support issuing callbacks when properties change - so that you could tie together the behavior of groups of widgets. Those callbacks might also wish to interact with the app e.g. to run the `Quit()` function, or to inspect the state of the mouse buttons. So that decision necessitates having access to the `App`. To make access possible, there are a couple of other options: - having a single, global app - having every widget store a pointer to its app when initialized Both aren't ideal, and put arbitrary restrictions on the applications using the widgets (though in practice, surely each application will only have one `App`?) Having a magic global `App` also seems to go against Go best practices such as those described in https://peter.bourgon.org/blog/2017/06/09/theory-of-modern-go.html. So I added an explicit `IApp` parameter to each function that might be connected to a subsequent use of the `App`, like calling `Quit()`. gowid-1.4.0/docs/Tutorial.md000066400000000000000000000523111426234454000157100ustar00rootroot00000000000000# Gowid Tutorial This tutorial closely follows the structure of the [urwid tutorial](http://urwid.org/tutorial/index.html). When a type is named without an explicit Go package specifier for brevity, then the package is `gowid`. ## Minimal Application ![enter image description here](https://drive.google.com/uc?export=view&id=12c4uZCWCynsusX6ELW9q08HB--YgKvFb) 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`. ```go 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.Widget` and renders strings to its canvas. This widget also supports rendering collections of text with style and color attributes attached, called markup. A `text.Widget` can render in urwid's "flow-mode", meaning its `Render()` 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 `NewApp` is an error, which you should check - though there's not much to do except exit gracefully. - The `app`'s `SimpleMainLoop()` 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 the `View` parameter to `NewApp()`. It may handle the event and it may also hand the event to its children. In this case, `text.Widget` is the root of the hierarchy, and it does not accept user input. Gowid will then hand the input to an `IUnhandledInput` which is provided in this case by `SimpleMainLoop()`. It checks for Escape, Ctrl-c, q or Q - if any are detected, the `app`'s `Quit()` function is called. After processing input, `SimpleMainLoop()` will then terminate. ## Global Input ![desc](https://drive.google.com/uc?export=view&id=1SgDht4cup0hhgMrwnQaE3e3lqvQvuKlC) 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`. ```go 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`'s`MainLoop()` function expects a type that implements `IUnhandledInput`. The gowid type `UnhandledInputFunc` is a simple function adapter that allows use of a regular Go function. - The function `unhandled()` is given the `app` and the user input in the form of a `tcell.Event`. Gowid relies throughout on the Go package `tcell` and 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 root `text.Widget` is updated to display the key that was pressed. ## Display Attributes ![desc](https://drive.google.com/uc?export=view&id=1D2rT70O_NPRGVFyFuHKZORt6jgVUn0Im) ![desc](https://drive.google.com/uc?export=view&id=1iSVmGLqUVbF4amSGWSGfHeJhQ4HU2-Rq) 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`. ```go 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 to `MakePaletteEntry()` 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 to `tcell` color primitives (depending on the color mode of the terminal), ready for rendering on the terminal screen. - `ColorBlack` is one of a set of predefined `TCellColor`s you can use. It trivially implements `IColor`. - `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](http://urwid.org/manual/displayattributes.html). - 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 the `app`'s `GetCellStyler()` function and providing the name of the palette entry. For example, "hello world" appears in a called to `text.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.Widget` an 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.Widget` and then inside a `vpadding.Widget` that is also styled. The `styled.Widget` will 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, `map2` will 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 rendering `vert`, use flow-mode to render `map`. 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! ## High-Color Modes ![desc](https://drive.google.com/uc?export=view&id=1PQbFW5O-_qE0C-tAdMQVi54GZbbXZzZS) 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`. ```go 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.Widget` is used. The blank lines are made with a `divider.Widget`, where `outside` and `inside` are styled with different colors. The widget pile is centered with a `vpadding.Widget` and `VAlignMiddle{}`, 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. ## Question and Answer ![desc](https://drive.google.com/uc?export=view&id=1gFKp4b48Jx3t2TUVoydNFyehR3wHfObO) ![desc](https://drive.google.com/uc?export=view&id=1wy67y8sfa6Pkjs22EIC-Epx27cbx95Mv) 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`. ```go 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. `QuestionBox` embeds an `IWidget` meaning that it itself implements `IWidget`. The `main()` function sets up a `QuestionBox` widget that extends an `edit.Widget`. That means `QuestionBox` will render like `edit.Widget`. But `QuestionBox` provides a new implementation of `UserInput()`, one of the requirements of `IWidget`. If the key pressed is not "enter" then it defers to its embedded `IWidget`'s implementation of `UserInput()`. That means the embedded `edit.Widget` will process it, and it will accumulate the user's typed input and display that when rendered. But if the user presses "enter", `QuestionBox` replaces its embedded widget with a new `text.Widget` that displays a message to the name the user has typed in. - When constructing the "Nice to meet you" message, the embedded `IWidget` is 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 implement `IWidget`. - 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's `UserInput()`. And it's true that our new implementation will be called from an `IWidget` if the interface's type is a `QuestionBox` pointer. But let's say you also provide a specialized implementation for `RenderSize()` another `IWidget` requirement. And let's say `UserInput()` calls a method which is not "overridden" in `edit.Widget`, and that in turn calls `RenderSize()`; then your new version will not be called. The receiver will be the `edit.Widget` pointer. 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. ## Widget Callbacks ![desc](https://drive.google.com/uc?export=view&id=11MhLJtGfjTnOtWehJvkdnQv1zHm3-9a_) ![desc](https://drive.google.com/uc?export=view&id=1EL8E6GPvitgPznUZ3B7XOaiNBmBH9syq) 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`. ```go 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.Widget` can call an interface method when it's clicked. `OnClick()` expects an `IWidgetChangedCallback`. You can use the `WidgetCallback()` adapter to pass a simple function. - The first parameter of `WidgetCallback` is an `interface{}`. It's meant to uniquely identify this callback instance so that if you later need to remove the callback, you can by passing the same `interface{}`. 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.Widget` can call an interface method when its text changes. In this example, every time the user enters a character, `ask` will update the `reply` widget so that it displays a message. - The callback will be called with two arguments - the application `app` and 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 to `reply` and not the callback parameter `w`. - The `` button is styled using `MakeStyleAs()`, 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. ## Multiple Questions ![desc](https://drive.google.com/uc?export=view&id=18F4F_34YzK9aFMHAx9gsimt_GBk8WZY2) 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`. ```go 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.Widget` and renders like one, but its input is handled specially. A `list.Widget` is a more general form of `pile.Widget`. You provide a `list.Widget` with a `list.IListWalker`which 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, see `github.com/gcla/gowid/examples/gowid-fib` which is plagiarized heavily from urwid's `fib.py` example. - The list walker in this example is a wrapper around a Go array of widgets. Each widget in the list is a `pile.Widget` containing either - A single `edit.Widget` asking for a name, or - An `edit.Widget` asking for a name and the user's response as a `text.Widget`. - When the user presses "enter" in an `edit.Widget`, the current focus `pile.Widget` is manipulated. Any previous answer is eliminated, and a new answer is appended. The walker is advanced one position, and finally, the `list.Widget` is 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 one `pile.Widget` to another. Within that `pile.Widget` there are at most two widgets - one `edit.Widget` and one `text.Widget`. An `edit.Widget` is "selectable", which means it is useful for it to be given the focus. A `text.Widget` is 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 an `edit.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. gowid-1.4.0/docs/Widgets.md000066400000000000000000000601701426234454000155150ustar00rootroot00000000000000# Gowid Widgets Gowid supplies a number of widgets out-of-the-box. ## asciigraph **Purpose:** The `asciigraph` widget renders line graphs. It uses the Go package `github.com/guptarohit/asciigraph`. ![desc](https://user-images.githubusercontent.com/45680/118377433-35141900-b59b-11eb-818d-546dca83fd9e.png) **Examples:** - `github.com/gcla/gowid/examples/gowid-asciigraph` - `github.com/gcla/gowid/examples/gowid-overlay2` ## bargraph **Purpose**: renders bar graphs. Based heavily on urwid's `graph.py`. ![desc](https://drive.google.com/uc?export=view&id=1mgG-4TnefC6xEwkQM2GlwHctcIB3bH-g) **Examples:** - `github.com/gcla/gowid/examples/gowid-graph` ## boxadapter **Purpose**: allow a box widget to be rendered in a flow context. **Examples:** - `github.com/gcla/gowid/examples/gowid-dir` ## button **Purpose**: a clickable widget. The app can register callbacks to handle click events. ![desc](https://user-images.githubusercontent.com/45680/118377515-b1a6f780-b59b-11eb-9003-1c37c008db39.png) **Examples:** - `github.com/gcla/gowid/examples/gowid-dir` - `github.com/gcla/gowid/examples/gowid-menu` - `github.com/gcla/gowid/examples/gowid-palette` - `github.com/gcla/gowid/examples/gowid-graph` - `github.com/gcla/gowid/examples/gowid-tree1` - ## cellmod **Purpose**: modify the canvas of a child widget by applying a user-supplied function to each `Cell` . **Examples:** - `github.com/gcla/gowid/widgets/dialog` ## checkbox **Purpose**: a clickable widget with two states - selected and unselected. ![desc](https://user-images.githubusercontent.com/45680/118377546-e61ab380-b59b-11eb-8d6c-54b88608269a.png) **Examples:** - `github.com/gcla/gowid/gowid-asciigraph` ## clicktracker **Purpose**: to highlight a widget that has been clicked with the mouse, but which has not yet been activated because the mouse button has not been released. The idea is to highlight which widget will be activated when the mouse is released, if focus remains over that widget. **Examples**: - `github.com/gcla/gowid/examples/gowid-graph` - `github.com/gcla/gowid/examples/gowid-widgets3` ## columns **Purpose**: arrange child widgets into vertical columns, with configurable column widths. ![desc](https://user-images.githubusercontent.com/45680/118377593-25490480-b59c-11eb-845b-51baf1936faf.png) **Examples:** - `github.com/gcla/gowid/examples/gowid-widgets2` - `github.com/gcla/gowid/examples/gowid-widgets3` - `github.com/gcla/gowid/examples/gowid-editor` - `github.com/gcla/gowid/examples/gowid-graph` - ## dialog **Purpose**: a modal dialog box that can be opened on top of another widget and will process the user input preferentially. ![desc](https://user-images.githubusercontent.com/45680/118377633-66411900-b59c-11eb-9c6f-74f7adb4c102.png) **Examples:** - `github.com/gcla/gowid/examples/gowid-editor` ## divider **Purpose**: a configurable horizontal line that can be used to separate widgets arranged vertically. Can render using ascii or unicode. ![desc](https://user-images.githubusercontent.com/45680/118377670-b1f3c280-b59c-11eb-9e5a-10d5689f527f.png) **Examples:** - `github.com/gcla/gowid/examples/gowid-graph` - `github.com/gcla/gowid/examples/gowid-helloworld` - `github.com/gcla/gowid/examples/gowid-palette` ## edit **Purpose**: a text area that will display text typed in by the user, with an optional caption/prefix. ![desc](https://user-images.githubusercontent.com/45680/118377720-f8492180-b59c-11eb-918d-833fdd4a3586.png) **Examples:** - `github.com/gcla/gowid/examples/gowid-editor` - `github.com/gcla/gowid/examples/gowid-widgets4` - `github.com/gcla/gowid/examples/gowid-widgets6` ## fill **Purpose**: a widget that when rendered returns a canvas full of the same user-supplied `Cell`. ![desc](https://user-images.githubusercontent.com/45680/118377735-1f9fee80-b59d-11eb-8aef-2e6ea2b3fc6c.png) **Examples:** - `github.com/gcla/gowid/examples/gowid-overlay1` - `github.com/gcla/gowid/examples/gowid-terminal` - `github.com/gcla/gowid/examples/gowid-widgets2` ## fixedadapter **Purpose**: a simple way to allow a fixed widget to be used in a box or flow context. **Examples:** - `github.com/gcla/gowid/widgets/list/list_test.go` ## framed **Purpose**: surround a child widget with a configurable "frame", using unicode or ascii characters. ![desc](https://user-images.githubusercontent.com/45680/118377753-43633480-b59d-11eb-8fc2-4ca276376f26.png) **Examples:** - `github.com/gcla/gowid/examples/gowid-widgets1` - `github.com/gcla/gowid/examples/gowid-tree1` - `github.com/gcla/gowid/examples/gowid-graph` - `github.com/gcla/gowid/examples/gowid-terminal` ## grid **Purpose**: a way to arrange widgets in a grid, with configurable horizontal alignment. ![desc](https://user-images.githubusercontent.com/45680/118377795-845b4900-b59d-11eb-82e5-b59e5b3d9619.png) **Examples:** - `github.com/gcla/gowid/examples/gowid-widgets3` ## holder **Purpose**: wraps a child widget and defers all behavior to it. Allows the child to be swapped out for another. **Examples:** - `github.com/gcla/gowid/examples/gowid-editor` - `github.com/gcla/gowid/examples/gowid-palette` - `github.com/gcla/gowid/examples/gowid-terminal` ## hpadding **Purpose**: a widget to render and align a child widget horizontally in a wider space. **Examples:** - `github.com/gcla/gowid/examples/gowid-fib` - `github.com/gcla/gowid/examples/gowid-graph` - `github.com/gcla/gowid/examples/gowid-menu` ## list **Purpose**: a flexible widget to navigate a vertical list of widgets rendered in flow mode. ![desc](https://user-images.githubusercontent.com/45680/118377820-ad7bd980-b59d-11eb-8368-966567e626ff.png) **Examples:** - `github.com/gcla/gowid/examples/gowid-fib` - `github.com/gcla/gowid/examples/gowid-menu` - `github.com/gcla/gowid/examples/gowid-widgets4` - `github.com/gcla/gowid/examples/gowid-widgets7` ## menu **Purpose**: a drop-down menu supporting arbitrarily many sub-menus. ![desc](https://user-images.githubusercontent.com/45680/118377834-c4bac700-b59d-11eb-9884-fea03e543be8.png) **Examples:** - `github.com/gcla/gowid/examples/gowid-menu` ## overlay **Purpose**: a widget to render one widget over another, only passing user input to the occluded widget if the input coordinates are outside the boundaries of the widget on top. ![desc](https://user-images.githubusercontent.com/45680/118377862-e2882c00-b59d-11eb-880b-5753239b92b0.png) **Examples:** - `github.com/gcla/gowid/examples/gowid-overlay1` - `github.com/gcla/gowid/examples/gowid-overlay2` ## palettemap **Purpose**: a widget that will render an inner widget with style X if it would otherwise be rendered with style Y, if configured to map Y -> X, and if the widget is styled using references to palette entries. **Examples:** - `github.com/gcla/gowid/gowid-widgets1` - `github.com/gcla/gowid/gowid-widgets4` - `github.com/gcla/gowid/gowid-fib` - `github.com/gcla/gowid/gowid-tree1` The `palettemap` widget is best used in conjunction with an app that relies on a global palette for its styling and colors. Let's say somewhere in your app's widget hierarchy you have a widget like this: ```go w := styled.New(x, gowid.MakePaletteRef("red")) ``` The widget `w` will obviously be rendered in `red` according to the app's palette. But if you wrap `w` in something like ```go z := palettemap.New(w, palettemap.Map{"red": "green"}, palettemap.Map{}) ``` Then when `w` is rendered and is the focus widget, the app's palette will be looked up with the name "green" instead. This provides a convenient way of changing the color of widgets, especially when they are in focus. ## pile **Purpose**: arrange child widgets into horizontal bands, with configurable heights. ![desc](https://user-images.githubusercontent.com/45680/118377912-31ce5c80-b59e-11eb-84af-888729e98b25.png) **Examples:** - `github.com/gcla/gowid/examples/gowid-helloworld` - `github.com/gcla/gowid/examples/gowid-palette` - `github.com/gcla/gowid/examples/gowid-widgets5` - `github.com/gcla/gowid/examples/gowid-widgets6` ## progress **Purpose**: a simple progress monitor. ![desc](https://user-images.githubusercontent.com/45680/118377933-54f90c00-b59e-11eb-9589-200d829f2a80.png) **Examples:** - `github.com/gcla/gowid/examples/gowid-graph` - `github.com/gcla/gowid/examples/gowid-widgets1` Here is an example initialization of a progress bar: ```go pb := progress.New(progress.Options{ Normal: gowid.MakePaletteRef("pg normal"), Complete: gowid.MakePaletteRef("pg complete"), }) ``` The struct `progress.Options` is used to pass arguments to the progress bar. You can also set the target number of units for completion, and the current number of units completed. If target is not set, it will default to 100; if current is not set it will default to 0. Any type implementing `progress.IWidget` can be rendered as a progress bar. Here is an example of how to customize the widget, from `gowid-widgets1`: ```go type PBWidget struct { *progress.Widget } func (w *PBWidget) Text() string { cur, done := w.Progress(), w.Target() percent := gwutil.Min(100, gwutil.Max(0, cur*100/done)) return fmt.Sprintf("At %d %% (%d/%d)", percent, cur, done) } func (w *PBWidget) Render(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.ICanvas { return progress.Render(w, size, focus, app) } ``` The `Text()` method provides an alternative label inside the rendered progress bar. Note that we also provided a `Render()` function. The `Text()` function is called when rendering the progress bar, and we need our implementation to take effect. Without a dedicated `Render()` method for `PBWidget`, when the enclosing widget - presumably holding an `IWidget` type - calls `Render()`, the implementation will be provided by the embedded `*progress.Widget`, meaning that will be the receiver type when `Render()` is called. It will defer to the free function `progress.Render()`, but the `progress.IWidget` will hold a `*progress.Widget` not a `*PBWidget`, and so `progress.Render()` will not use our new `Text()` implementation. For a fuller explanation, see the [FAQ](FAQ). ## radio **Purpose**: a widget that, as part of a group, can be in a selected state (if no others in the group are selected) or is otherwise unselected. ![desc](https://user-images.githubusercontent.com/45680/118377974-8a055e80-b59e-11eb-834e-94080b1ff53b.png) **Examples:** - `github.com/gcla/gowid/examples/gowid-graph` - `github.com/gcla/gowid/examples/gowid-overlay2` - `github.com/gcla/gowid/examples/gowid-palette` - `github.com/gcla/gowid/examples/gowid-widgets3` ## selectable **Purpose**: make a widget always be selectable, even if it rejects user input. **Examples:** - `github.com/gcla/gowid/examples/gowid-widgets2` - `github.com/gcla/gowid/examples/gowid-dir` - `github.com/gcla/gowid/examples/gowid-tree1` ## shadow **Purpose**: adds a drop-shadow effect to a widget. ![desc](https://user-images.githubusercontent.com/45680/118377988-a1dce280-b59e-11eb-9fcd-1bfe57e7206b.png) **Examples:** - `github.com/gcla/gowid/examples/gowid-graph` - `github.com/gcla/gowid/widgets/dialog/dialog.go` ## styled **Purpose**: apply foreground and background coloring and text styling to a widget. **Examples:** - `github.com/gcla/gowid/examples/gowid-dir` - `github.com/gcla/gowid/examples/gowid-fib` - `github.com/gcla/gowid/examples/gowid-helloworld` - `github.com/gcla/gowid/examples/gowid-menu` ## table **Purpose**: a widget to display tabular data in columns and rows. ![desc](https://drive.google.com/uc?export=view&id=1TZXfT_VVf5g2sNYi9hia2h36krcGUBI8) **Examples:** - `github.com/gcla/gowid/examples/gowid-table` Any type that implements the following interface can be used as the source for a table widget: ```go type IModel interface { Columns() int RowIdentifier(row int) (RowId, bool) // return a unique ID for row RowWidgets(row RowId) []gowid.IWidget // nil means EOD HeaderWidgets() []gowid.IWidget // nil means no headers VerticalSeparator() gowid.IWidget HorizontalSeparator() gowid.IWidget HeaderSeparator() gowid.IWidget Widths() []gowid.IWidgetDimension } ``` The interface distinguishes a row number (int) from a row identifier (RowId). In order to render the nth row, the widget asks the IModel for the identifier of the nth row. With that in hand, the widget then asks the IModel for the row widgets corresponding to the provided RowId - and with these it can render the row. For simple tables, the RowId value and row number will be the same - n for the nth row. For tables that support sorting (e.g. on a specific column), the underlying implementation of IModel can track the new ordering and ensure the correct row widgets are returned for the row to be displayed at the nth position - perhaps using a simple map. Any implementation of IModel should consider caching the widgets returned via calls to `RowWidgets()`. If their state has changed from the default, then returning a cached widget when `RowWidgets()` is called will result in the display reflecting that changed state - color change, clicked checkboxes, etc. This can be seen in the `gowid-table` example. If the widgets are "read-only" and do not change state, then they can be safely generated anew each time `RowWidgets()` is called. `HeaderWidgets()` should return an array of widgets used as column headers. It can also return `nil`, which means the rendered widget will have no column headers. `VerticalSeparator()`, `HorizontalSeparator()` and `HeaderSeparator()` can also return nil, if the cells don't need to be explicitly boxed or separated from the column headers. `Widths()` determines how the row widgets are laid out. - To use equal space for each column, return an array of `gowid.IRenderWithWeight` with value 1. - To have a table column use a fixed number of display columns and overflow into subsequent display rows, return a `gowid.IRenderFlow` in that column's position. - If a column widget is fixed (determines its own render size), return a `gowid.IRenderFixed` at that column's index. Each table row is rendered using `columns.Widget`, so values suitable for column widths are also suitable for table widths. An implementation of IModel that returns data from a CSV file is available as `table.NewCsvTable()`. Here is an example of its use: ```go model := table.NewCsvTable(csvFile, table.SimpleOptions{ FirstLineIsHeaders: true, Style: table.StyleOptions{ HorizontalSeparator: divider.NewAscii(), TableSeparator: divider.NewUnicode(), VerticalSeparator: fill.New('|'), }, }) ``` The `csvFile` argument should implement `io.Reader` and be suitable for processing by the standard library's `csv.NewReader()`. The table widget itself is then easily created: ```go table := table.New(model) ``` ## terminal **Purpose**: a VT-220 capable terminal emulator widget, heavily plagiarized from urwid's `vterm.py`. ![desc](https://user-images.githubusercontent.com/45680/118378264-92f72f80-b5a0-11eb-99d0-0e51b4108870.png) **Examples:** - `github.com/gcla/gowid/examples/gowid-terminal` This widget lets you embed a featureful terminal - or collection of terminals - in your application. The `gowid-terminal` example included demonstrates a very simple copy of tmux - three terminals open running different programs. You can resize the terminal panes using the default hotkey `ctrl-b` and then hitting any of `>`, `<`, `+` or `-`. You can have code execute on specific terminal events by registering callbacks: - when the terminal's process exits - when the terminal's bell rings - when the terminal's title is set You can create a terminal widget simply like this: ```go tw, err := terminal.New("/bin/bash") ``` There is also a `NewExt()` if you need more control, which takes a `terminal.Params` struct: ```go type Params struct { Command []string Env []string HotKey IHotKeyProvider HotKeyPersistence IHotKeyPersistence } ``` With that you can provide the environment for the terminal's running process. When a terminal widget has focus, it makes sense for the terminal to be able to process all of the user's keypresses. The `HotKey` field lets you choose a specific keypress (a `tcell.Key`) that will temporarily cause the terminal widget to reject keyboard input. If you have a terminal embedded in your app, this gives the user an opportunity to switch focus to another widget using the keyboard, just like the default `ctrl-b` key in tmux. You can configure how long the hotkey keypress will remain in effect with the `HotKeyPersistence` field. Terminal widgets expect to be rendered in box-mode. If your application reorganizes its widget layout, or perhaps if the user simply resizes the terminal window in which your app is running, the terminal widget(s) may be rendered with a different size than was used in the last call to `Render()`. `Gowid` will detect this and send `syscall.TIOCSWINSZ` to the underlying PTY. When the process underlying the terminal starts running (at least before the first render), the widget will start a goroutine to read from the terminal's master file descriptor. During normal operation, the data read will be terminal-specific control codes. For some examples, see http://www.termsys.demon.co.uk/vtansi.htm. Many of these codes will represent characters that are to be emitted at the current cursor position on the terminal screen, advancing the cursor. Other codes will have a special meaning, like "move the cursor" or "erase part of the screen". The widget implements some simple state machines to track multi-byte sequences, such as the ANSI CSI codes - `ESC[3;4H` - "move the cursor to row 3 column 4". The full-set of `terminal.Widget`'s emulation amounts approximately to VT-220 support. The widget has been tested by running within it the standard `vttest` program and checking the output. All of the credit for this terminal code parsing and state tracking belong's to `urwid`s `vterm.py` implementation. As with all other `gowid` widgets, `terminal.Widget` supports use of the mouse, where possible. The `gowid-terminal` example demonstrates this - try clicking inside `vim`, or change focus to `emacs` and do the same (you might need to run `M-x xterm-mouse-mode` first). There are several standards for encoding mouse events in the terminal. An SGR encoding of a left-mouse click, for example, might be `ESC[0;3;4M` - a click at row 4, column 3. An older style encoding might be `ESC[M $%` - where the click position is translated to a printable character. The terminal library underlying an application will typically send CSI codes to advertise the various modes the terminal supports and expects - `terminal.Widget` tracks these, and in particular which mouse mode is enabled. `Gowid`'s user input is provided via `tcell` APIs, meaning key-presses and mouse-clicks appear to `gowid` as one of `tcell.EventKey` or `tcell.EventMouse` - that is, because `gowid` runs on top of `tcell`, it does not see the exact byte sequences that the terminal containing the `app` generates. Instead it sees `tcell`'s representation. `terminal.Widget` will convert these `tcell` structs back into byte sequences to send to the widget's underlying terminals file descriptor, and will use its knowledge of the terminal's current mode to choose the correct conversion. To illustrate, let's say you have written a `gowid` application which embeds a `terminal.Widget`. When your app has started, `tcell` will have a PTY to talk to the terminal in which you started the application; and `terminal.Widget` will have a PTY to talk to the terminal running the command embedded in your widget. Your `gowid` application's `TERM` environment variable will determine which `terminfo` database is used to encode and decode terminal sequences to and from `tcell`. And the environment of the process running in your widget will determine the same for `gowid.Widget`. Let's say the user clicks a mouse button inside your widget. 1. Under your `gowid` app, `tcell` talks to the terminal. The app's `TERM` environment variable will determine the byte sequence `tcell` receives for the mouse event from the app's terminal. 2. `tcell` will translate that sequence into a `tcell.MouseEvent` 3. The `gowid` application will pass that event down through the widget hierarchy until it is accepted by the `terminal.Widget` 4. The `terminal.Widget` will understand from `tcell` that a mouse button has been clicked, and the coordinates of the click. `Gowid` itself will have translated the coordinates of the click as the event was pushed down through the widget hierarchy. The `terminal.Widget` will know the mouse-mode of its underlying terminal because it has tracked the CSI codes sent by its underlying terminal, determined by its process's `TERM` variable. 5. The `terminal.Widget` will convert the `tcell.EventMouse` back to a sequence of bytes according to the correct mouse mode, and send it to the underlying terminal's file descriptor. The terminal widget defers most of its state tracking to a specialized implementation of `gowid.ICanvas`. The terminal canvas embeds a `gowid.Canvas`, which it renders as normal, but also contains the state-machines and logic to decode and encode terminal byte sequences. The terminal's canvas, when rendered, will always represent the latest state of the terminal underlying the widget. The code is in `github.com/gcla/gowid/widgets/terminal/term_canvas.go`. The terminal canvas implements `io.Writer` allowing a client to write ANSI codes using this standard Golang interface. ## text **Purpose**: a widget to render text, optionally styled and aligned. ![desc](https://user-images.githubusercontent.com/45680/118378052-fed89880-b59e-11eb-8605-1ce32217b755.png) **Examples:** - `github.com/gcla/gowid/examples/gowid-widgets1` - `github.com/gcla/gowid/examples/gowid-widgets2` - `github.com/gcla/gowid/examples/gowid-widgets3` - `github.com/gcla/gowid/examples/gowid-widgets4` This widget provides a way to render styled and unstyled text. It represents text using this interface: ```go type IContent interface { Length() int ChrAt(idx int) rune RangeOver(start, end int, attrs gowid.IRenderContext, proc gowid.ICellProcessor) AddAt(idx int, markup ContentSegment) DeleteAt(idx, length int) fmt.Stringer } ``` The user constructs the widget by providing an array of `ContentSegment` each of which is a string with an associated `gowid.ICellStyler`. When rendering, the widget will built an array of `Cell` using the `RangeOver()` function - that will use the supplied `IRenderContext` (implemented by `gowid.App`) to turn each rune of the markup, along with its `ICellStyler` into a `Cell` - respecting the current color mode of the terminal. Here is an example of how to build a simple text widget: ```go w := text.New("Do you want to quit?") ``` Here is an example of how to build a styled text widget: ```go w := text.NewFromContent( text.NewContent([]text.ContentSegment{ text.StyledContent("hello", gowid.MakePaletteRef("red")), text.StringContent(" "), text.StyledContent("world", gowid.MakePaletteRef("green")), })) ``` There is also a `NewExt()` function that you can supply with these arguments: ```go type Options struct { Wrap WrapType Align gowid.IHAlignment } ``` - Wrap supports `WrapAny` meaning text will be wrapped to the next line, and `WrapClip` which means the text will be clipped at the end of the current line (and so will render to one canvas line only). - Align supports any of `HAlignLeft`, `HAlignRight` and `HAlignMiddle`. This option can be used to e.g. center each rendered line of text by sharing the white-space at either edge. ## tree **Purpose**: a generalization of the `list` widget to render a tree structure. ![desc](https://user-images.githubusercontent.com/45680/118378280-b02bfe00-b5a0-11eb-92e7-4178f718f844.png) **Examples:** - `github.com/gcla/gowid/examples/gowid-dir` - `github.com/gcla/gowid/examples/gowid-tree1` ## vpadding **Purpose**: a widget to render and align a child widget vertically in a wider space. **Examples:** - `github.com/gcla/gowid/examples/gowid-helloworld` - `github.com/gcla/gowid/examples/gowid-overlay2` - `github.com/gcla/gowid/examples/gowid-widgets1` ## vscroll **Purpose**: a vertical scroll bar with clickable arrows on either end. ![desc](https://user-images.githubusercontent.com/45680/118378077-292a5600-b59f-11eb-8dbb-c31fdf08337d.png) **Examples:** - `github.com/gcla/gowid/examples/gowid-editor` gowid-1.4.0/examples/000077500000000000000000000000001426234454000144475ustar00rootroot00000000000000gowid-1.4.0/examples/gowid-asciigraph/000077500000000000000000000000001426234454000176705ustar00rootroot00000000000000gowid-1.4.0/examples/gowid-asciigraph/asciigraph.go000066400000000000000000000022361426234454000223340ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source code is governed by the MIT license // that can be found in the LICENSE file. // An example of the gowid asciigraph widget which relies upon the // asciigraph package at github.com/guptarohit/asciigraph. package main import ( "github.com/gcla/gowid" "github.com/gcla/gowid/examples" "github.com/gcla/gowid/widgets/asciigraph" asc "github.com/guptarohit/asciigraph" log "github.com/sirupsen/logrus" ) //====================================================================== var app *gowid.App //====================================================================== func main() { var err error f := examples.RedirectLogger("asciigraph.log") defer f.Close() palette := gowid.Palette{} data := []float64{2, 1, 1, 2, -2, 5, 7, 11, 3, 7, 1} graph := asciigraph.New(data, []asc.Option{}) app, err = gowid.NewApp(gowid.AppArgs{ View: graph, Palette: &palette, Log: log.StandardLogger(), }) examples.ExitOnErr(err) app.SimpleMainLoop() } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: gowid-1.4.0/examples/gowid-dir/000077500000000000000000000000001426234454000163345ustar00rootroot00000000000000gowid-1.4.0/examples/gowid-dir/dir.go000066400000000000000000000234301426234454000174430ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source code is governed by the MIT license // that can be found in the LICENSE file. // A simple gowid directory browser. package main import ( "fmt" "io/ioutil" "os" "syscall" "github.com/gcla/gowid" "github.com/gcla/gowid/examples" "github.com/gcla/gowid/gwutil" "github.com/gcla/gowid/widgets/boxadapter" "github.com/gcla/gowid/widgets/button" "github.com/gcla/gowid/widgets/columns" "github.com/gcla/gowid/widgets/list" "github.com/gcla/gowid/widgets/selectable" "github.com/gcla/gowid/widgets/styled" "github.com/gcla/gowid/widgets/text" "github.com/gcla/gowid/widgets/tree" tcell "github.com/gdamore/tcell/v2" log "github.com/sirupsen/logrus" kingpin "gopkg.in/alecthomas/kingpin.v2" ) var pos *tree.TreePos var tb *list.Widget var parent1 *DirTree var walker *tree.TreeWalker var dirname = kingpin.Arg("dir", "Directory to scan.").Required().String() var expandedCache map[string]int //====================================================================== func IsDir(name string) bool { res := false if f, err := os.OpenFile(name, syscall.O_NONBLOCK|os.O_RDONLY, 0); err == nil { defer f.Close() if st, err2 := f.Stat(); err2 == nil { if st.IsDir() { res = true } } } return res } func NodeState(name string, cache map[string]int) int { state := notDir if lookup, ok := cache[name]; ok { state = lookup } else { if IsDir(name) { state = collapsed } else { state = notDir } cache[name] = state } return state } //====================================================================== type NoIterator struct{} func (d *NoIterator) Next() bool { return false } func (d *NoIterator) Value() tree.IModel { panic("Do not call") } //====================================================================== const ( notDir = iota collapsed expanded ) type DirIterator struct { init bool parent tree.ICollapsible files []os.FileInfo pos int cache *map[string]int // shared - track whether any path is collapsed or expanded } func (d *DirIterator) FullPath() string { return d.parent.(*DirTree).FullPath() + "/" + d.files[d.pos].Name() } func (d *DirIterator) Next() bool { if !d.init { // Ignore the error because if there's a problem, we'll simply have files thus no children d.files, _ = ioutil.ReadDir(d.parent.(*DirTree).FullPath()) d.init = true } d.pos++ return !d.parent.IsCollapsed() && d.pos < len(d.files) } func (d *DirIterator) Value() tree.IModel { var res tree.IModel state := NodeState(d.FullPath(), *d.cache) switch state { case notDir: res = &DirTree{ name: d.files[d.pos].Name(), cache: d.cache, parent: d.parent, notDir: true, } default: res = &DirTree{ name: d.files[d.pos].Name(), cache: d.cache, parent: d.parent, } } return res } //====================================================================== type DirTree struct { parent tree.IModel name string iter *DirIterator notDir bool cache *map[string]int // shared - track whether any path is collapsed or expanded } var _ tree.ICollapsible = (*DirTree)(nil) func (d *DirTree) FullPath() string { if d.parent == nil { return d.name } else { return d.parent.(*DirTree).FullPath() + "/" + d.name } } func (d *DirTree) Leaf() string { return d.name } func (d *DirTree) String() string { return d.name } func (d *DirTree) Children() tree.IIterator { var res tree.IIterator res = &NoIterator{} if d.iter != nil { d.iter.pos = -1 res = d.iter } else { state := NodeState(d.FullPath(), *d.cache) if state != notDir { d.iter = &DirIterator{ init: false, pos: -1, cache: d.cache, parent: d, } res = d.iter } } return res } func (d *DirTree) IsCollapsed() bool { fp := d.FullPath() if v, res := (*d.cache)[fp]; res { return (v == collapsed) } else { return true } } func (d *DirTree) SetCollapsed(app gowid.IApp, isCollapsed bool) { fp := d.FullPath() if isCollapsed { (*d.cache)[fp] = collapsed } else { (*d.cache)[fp] = expanded } } //====================================================================== // DirButton is a button widget that provides its own ID function, rather than relying on the default // address of an embedded struct. This is in case the button widget isn't the exact same object when // the mouse is clicked and when the mouse is released. Instead, the pathname is used as the ID, ensuring // that if the user clicks on "/tmp/foo" then releases the mouse button on "/tmp/foo", the button's // Click() method will be called. We need to provide a wrapper for UserInput(), else UserInput() will come // from the embedded *button.Widget, which will then result in the IButtonWidget interface being // built from *button.Widget and not DirButton. // type DirButton struct { *button.Widget id string } func (d *DirButton) ID() interface{} { return d.id } func (d *DirButton) UserInput(ev interface{}, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) bool { return button.UserInput(d, ev, size, focus, app) } //====================================================================== func MakeDecoration(pos tree.IPos, tr tree.IModel, wmaker tree.IWidgetMaker) gowid.IWidget { var res gowid.IWidget level := -1 for cur := pos; cur != nil; cur = tree.ParentPosition(cur) { level += 1 } pad := gwutil.StringOfLength(' ', level*3) lineWidgets := make([]gowid.IContainerWidget, 0) // indentation lineWidgets = append(lineWidgets, &gowid.ContainerWidget{ IWidget: text.New(pad), D: gowid.RenderWithUnits{U: len(pad)}, }) if ctree2, ok := tr.(tree.ICollapsible); ok { ctree := ctree2.(*DirTree) if !ctree.notDir { btnChr := gwutil.If(ctree.IsCollapsed(), "+", "-").(string) dirButton := &DirButton{button.New(text.New(btnChr)), pos.String()} // If I use one button with conditional logic in the callback, rather than make // a separate button depending on whether or not the tree is collapsed, it will // correctly work when the DecoratorMaker is caching the widgets i.e. it will // collapse or expand even when the widget is rendered from the cache dirButton.OnClick(gowid.WidgetCallback{"cb", func(app gowid.IApp, w gowid.IWidget) { // Note that I don't change the button widget itself ([+]/[-]) - just the underlying model, from which // the widget will be recreated app.Run(gowid.RunFunction(func(app gowid.IApp) { ctree.SetCollapsed(app, !ctree.IsCollapsed()) })) }}) coloredDirButton := styled.NewExt(dirButton, gowid.MakePaletteRef("body"), gowid.MakePaletteRef("selected")) // [+] / [-] if the widget is a directory lineWidgets = append(lineWidgets, &gowid.ContainerWidget{ IWidget: coloredDirButton, D: gowid.RenderFixed{}, }) lineWidgets = append(lineWidgets, &gowid.ContainerWidget{ IWidget: text.New(" "), D: gowid.RenderFixed{}, }) } } inner := wmaker.MakeWidget(pos, tr) // filename/dirname lineWidgets = append(lineWidgets, &gowid.ContainerWidget{ IWidget: inner, D: gowid.RenderFixed{}, }) line := columns.New(lineWidgets) res = line res = boxadapter.New(res, 1) // force the widget to be on one line return res } func MakeWidget(pos tree.IPos, tr tree.IModel) gowid.IWidget { var res gowid.IWidget pr := "body" ctree := tr.(*DirTree) if !ctree.notDir { pr = "dirbody" } cwidgets := make([]gowid.IContainerWidget, 1) cwidgets[0] = &gowid.ContainerWidget{ IWidget: styled.NewExt( selectable.New( styled.NewExt( text.New( tr.Leaf(), ), gowid.MakePaletteRef(pr), gowid.MakePaletteRef("selected"), ), ), gowid.MakePaletteRef("body"), gowid.MakePaletteRef("selected"), ), D: gowid.RenderFixed{}, } res = columns.New(cwidgets) return res } //====================================================================== type handler struct{} func (h handler) UnhandledInput(app gowid.IApp, ev interface{}) bool { handled := false if evk, ok := ev.(*tcell.EventKey); ok { handled = true if evk.Key() == tcell.KeyCtrlC || evk.Rune() == 'q' || evk.Rune() == 'Q' { app.Quit() } else if evk.Rune() == 'x' { pos := walker.Focus() tpos := pos.(tree.IPos) itree := tpos.GetSubStructure(parent1) if ctree, ok := itree.(tree.ICollapsible); ok { ctree.SetCollapsed(app, true) } } else if evk.Rune() == 'z' { pos := walker.Focus() tpos := pos.(tree.IPos) itree := tpos.GetSubStructure(parent1) if ctree, ok := itree.(tree.ICollapsible); ok { ctree.SetCollapsed(app, false) } } else { handled = false } } return handled } //====================================================================== func main() { kingpin.Parse() if _, err := os.Stat(*dirname); os.IsNotExist(err) { fmt.Printf("Directory \"%v\" does not exist.", *dirname) os.Exit(1) } f := examples.RedirectLogger("dir.log") defer f.Close() palette := gowid.Palette{ "body": gowid.MakePaletteEntry(gowid.ColorWhite, gowid.ColorBlack), "dirbody": gowid.MakePaletteEntry(gowid.ColorWhite, gowid.ColorCyan), "selected": gowid.MakePaletteEntry(gowid.ColorDefault, gowid.ColorWhite), } expandedCache := make(map[string]int) // We start expanded at the top level expandedCache[*dirname] = expanded body := gowid.MakePaletteRef("body") parent1 = &DirTree{ name: *dirname, cache: &expandedCache, } pos = tree.NewPos() walker = tree.NewWalker(parent1, pos, tree.NewCachingMaker(tree.WidgetMakerFunction(MakeWidget)), tree.NewCachingDecorator(tree.DecoratorFunction(MakeDecoration))) tb = tree.New(walker) view := styled.New(tb, body) app, err := gowid.NewApp(gowid.AppArgs{ View: view, Palette: &palette, Log: log.StandardLogger(), }) examples.ExitOnErr(err) app.MainLoop(handler{}) } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: gowid-1.4.0/examples/gowid-editor/000077500000000000000000000000001426234454000170445ustar00rootroot00000000000000gowid-1.4.0/examples/gowid-editor/editor.go000066400000000000000000000176041426234454000206710ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source code is governed by the MIT license // that can be found in the LICENSE file. // A poor-man's editor using gowid widgets - shows dialog, edit and vscroll. package main import ( "fmt" "io" "os" "sync" "time" "github.com/gcla/gowid" "github.com/gcla/gowid/examples" "github.com/gcla/gowid/gwutil" "github.com/gcla/gowid/widgets/columns" "github.com/gcla/gowid/widgets/dialog" "github.com/gcla/gowid/widgets/edit" "github.com/gcla/gowid/widgets/framed" "github.com/gcla/gowid/widgets/holder" "github.com/gcla/gowid/widgets/hpadding" "github.com/gcla/gowid/widgets/pile" "github.com/gcla/gowid/widgets/styled" "github.com/gcla/gowid/widgets/text" "github.com/gcla/gowid/widgets/vscroll" tcell "github.com/gdamore/tcell/v2" log "github.com/sirupsen/logrus" kingpin "gopkg.in/alecthomas/kingpin.v2" ) //====================================================================== var editWidget *edit.Widget var footerContent []text.ContentSegment var footerText *styled.Widget var yesno *dialog.Widget var viewHolder *holder.Widget var app *gowid.App var wg sync.WaitGroup var updates chan string var filename = kingpin.Arg("file", "File to edit.").Required().String() //====================================================================== func updateStatusBar(message string) { app.Run(gowid.RunFunction(func(app gowid.IApp) { footerContent[1].Text = message footerWidget2 := text.NewFromContent(text.NewContent(footerContent)) footerText.SetSubWidget(footerWidget2, app) })) } //====================================================================== type handler struct{} func (h handler) UnhandledInput(app gowid.IApp, ev interface{}) bool { handled := false if evk, ok := ev.(*tcell.EventKey); ok { if evk.Key() == tcell.KeyEsc { handled = true app.Quit() } switch evk.Key() { case tcell.KeyCtrlC: handled = true msg := text.New("Do you want to quit?") yesno = dialog.New( framed.NewSpace(hpadding.New(msg, gowid.HAlignMiddle{}, gowid.RenderFixed{})), dialog.Options{ Buttons: dialog.OkCancel, }, ) yesno.Open(viewHolder, gowid.RenderWithRatio{R: 0.5}, app) case tcell.KeyCtrlF: handled = true fi, err := os.Open(*filename) if err != nil { fi, err = os.Create(*filename) if err != nil { updates <- fmt.Sprintf("(FAILED to create %s)", *filename) } } if err == nil { defer fi.Close() _, err = io.Copy(&edit.Writer{editWidget, app}, fi) if err != nil { updates <- fmt.Sprintf("(FAILED to load %s)", *filename) } else { updates <- "(OPENED)" } } case tcell.KeyCtrlS: handled = true fo, err := os.Create(*filename) if err != nil { updates <- fmt.Sprintf("(FAILED to create %s)", *filename) } else { defer fo.Close() _, err = io.Copy(fo, editWidget) if err != nil { updates <- fmt.Sprintf("(FAILED to write %s)", *filename) } else { updates <- "(SAVED!)" } } } } return handled } //====================================================================== type EditWithScrollbar struct { *columns.Widget e *edit.Widget sb *vscroll.Widget goUpDown int // positive means down pgUpDown int // positive means down } func NewEditWithScrollbar(e *edit.Widget) *EditWithScrollbar { sb := vscroll.NewExt(vscroll.VerticalScrollbarUnicodeRunes) res := &EditWithScrollbar{ columns.New([]gowid.IContainerWidget{ &gowid.ContainerWidget{e, gowid.RenderWithWeight{W: 1}}, &gowid.ContainerWidget{sb, gowid.RenderWithUnits{U: 1}}, }), e, sb, 0, 0, } sb.OnClickAbove(gowid.WidgetCallback{"cb", res.clickUp}) sb.OnClickBelow(gowid.WidgetCallback{"cb", res.clickDown}) sb.OnClickUpArrow(gowid.WidgetCallback{"cb", res.clickUpArrow}) sb.OnClickDownArrow(gowid.WidgetCallback{"cb", res.clickDownArrow}) return res } func (e *EditWithScrollbar) clickUp(app gowid.IApp, w gowid.IWidget) { e.pgUpDown -= 1 } func (e *EditWithScrollbar) clickDown(app gowid.IApp, w gowid.IWidget) { e.pgUpDown += 1 } func (e *EditWithScrollbar) clickUpArrow(app gowid.IApp, w gowid.IWidget) { e.goUpDown -= 1 } func (e *EditWithScrollbar) clickDownArrow(app gowid.IApp, w gowid.IWidget) { e.goUpDown += 1 } // gcdoc - do this so columns navigation e.g. ctrl-f doesn't get passed to columns func (w *EditWithScrollbar) UserInput(ev interface{}, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) bool { // Stop these keys moving focus in the columns used by this widget. C-f is used to // open a file. if evk, ok := ev.(*tcell.EventKey); ok { switch evk.Key() { case tcell.KeyCtrlF, tcell.KeyCtrlB: return false } } box, _ := size.(gowid.IRenderBox) w.sb.Top, w.sb.Middle, w.sb.Bottom = w.e.CalculateTopMiddleBottom(gowid.MakeRenderBox(box.BoxColumns()-1, box.BoxRows())) res := w.Widget.UserInput(ev, size, focus, app) if res { w.Widget.SetFocus(app, 0) } return res } func (w *EditWithScrollbar) Render(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.ICanvas { box, _ := size.(gowid.IRenderBox) ecols := box.BoxColumns() - 1 ebox := gowid.MakeRenderBox(ecols, box.BoxRows()) if w.goUpDown != 0 || w.pgUpDown != 0 { w.e.SetLinesFromTop(gwutil.Max(0, w.e.LinesFromTop()+w.goUpDown+(w.pgUpDown*box.BoxRows())), app) txt := w.e.MakeText() layout := text.MakeTextLayout(txt.Content(), ecols, txt.Wrap(), gowid.HAlignLeft{}) _, y := text.GetCoordsFromCursorPos(w.e.CursorPos(), ecols, layout, w.e) if y < w.e.LinesFromTop() { for i := y; i < w.e.LinesFromTop(); i++ { w.e.DownLines(ebox, false, app) } } else if y >= w.e.LinesFromTop()+box.BoxRows() { for i := w.e.LinesFromTop() + box.BoxRows(); i <= y; i++ { w.e.UpLines(ebox, false, app) } } } w.goUpDown = 0 w.pgUpDown = 0 w.sb.Top, w.sb.Middle, w.sb.Bottom = w.e.CalculateTopMiddleBottom(ebox) canvas := w.Widget.Render(size, focus, app) return canvas } //====================================================================== func main() { var err error kingpin.Parse() if _, err := os.Stat(*filename); os.IsNotExist(err) { fmt.Printf("Requested file \"%v\" does not exist.", *filename) os.Exit(1) } f := examples.RedirectLogger("editor.log") defer f.Close() palette := gowid.Palette{ "mainpane": gowid.MakePaletteEntry(gowid.ColorBlack, gowid.NewUrwidColor("light gray")), "cyan": gowid.MakePaletteEntry(gowid.ColorCyan, gowid.ColorBlack), "inv": gowid.MakePaletteEntry(gowid.ColorWhite, gowid.ColorBlack), } editWidget = edit.New() footerContent = []text.ContentSegment{ text.StyledContent("Gowid Text Editor. ", gowid.MakePaletteRef("inv")), text.StyledContent("", gowid.MakePaletteRef("cyan")), // emptyContent, text.StyledContent(" C-c to exit. C-s to save. C-f to open test file.", gowid.MakePaletteRef("inv")), } footerText = styled.New( text.NewFromContent( text.NewContent(footerContent), ), gowid.MakePaletteRef("inv"), ) mainView := pile.New([]gowid.IContainerWidget{ &gowid.ContainerWidget{ styled.New( framed.NewUnicode( NewEditWithScrollbar(editWidget), ), gowid.MakePaletteRef("mainpane"), ), gowid.RenderWithWeight{1}, }, &gowid.ContainerWidget{ footerText, gowid.RenderFlow{}, }, }) viewHolder = holder.New(mainView) updates = make(chan string) wg.Add(1) go func() { defer wg.Done() for { status := <-updates if status == "done" { break } else if status == "clear" { updateStatusBar("") } else { updateStatusBar(status) go func() { timer := time.NewTimer(time.Second) <-timer.C updates <- "clear" }() } } }() app, err = gowid.NewApp(gowid.AppArgs{ View: viewHolder, Palette: &palette, Log: log.StandardLogger(), }) examples.ExitOnErr(err) app.MainLoop(handler{}) updates <- "done" wg.Wait() } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: gowid-1.4.0/examples/gowid-fib/000077500000000000000000000000001426234454000163165ustar00rootroot00000000000000gowid-1.4.0/examples/gowid-fib/fib.go000066400000000000000000000110101426234454000173760ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source code is governed by the MIT license // that can be found in the LICENSE file. // A port of urwid's fib.py example using gowid widgets. package main import ( "fmt" "math/big" "github.com/gcla/gowid" "github.com/gcla/gowid/examples" "github.com/gcla/gowid/widgets/hpadding" "github.com/gcla/gowid/widgets/list" "github.com/gcla/gowid/widgets/palettemap" "github.com/gcla/gowid/widgets/pile" "github.com/gcla/gowid/widgets/selectable" "github.com/gcla/gowid/widgets/styled" "github.com/gcla/gowid/widgets/text" log "github.com/sirupsen/logrus" ) //====================================================================== type FibWalker struct { a, b big.Int } var _ list.IWalker = (*FibWalker)(nil) var _ list.IWalkerPosition = FibWalker{} var _ list.IWalkerHome = FibWalker{} func (f FibWalker) First() list.IWalkerPosition { return FibWalker{*big.NewInt(0), *big.NewInt(1)} } func (f FibWalker) Equal(other list.IWalkerPosition) bool { switch o := other.(type) { case FibWalker: return (f.a.Cmp(&o.a) == 0) && (f.b.Cmp(&o.b) == 0) default: return false } } func (f FibWalker) GreaterThan(other list.IWalkerPosition) bool { switch o := other.(type) { case FibWalker: return (f.b.Cmp(&o.b) > 0) default: panic(fmt.Errorf("Invalid type to compare against FibWalker - %T", other)) } } func getWidget(f FibWalker) gowid.IWidget { var res gowid.IWidget res = selectable.New( palettemap.New( hpadding.New( text.NewFromContent( text.NewContent([]text.ContentSegment{ text.StyledContent(f.b.Text(10), gowid.MakePaletteRef("body")), }), ), gowid.HAlignRight{}, gowid.RenderFlow{}, ), palettemap.Map{"body": "fbody"}, palettemap.Map{}, ), ) return res } func (f *FibWalker) At(pos list.IWalkerPosition) gowid.IWidget { f2 := pos.(FibWalker) return getWidget(f2) } func (f *FibWalker) Focus() list.IWalkerPosition { return *f } func (f *FibWalker) SetFocus(pos list.IWalkerPosition, app gowid.IApp) { *f = pos.(FibWalker) } func (f *FibWalker) Next(pos list.IWalkerPosition) list.IWalkerPosition { fc := pos.(FibWalker) var sum big.Int sum.Add(&fc.a, &fc.b) fn := FibWalker{fc.b, sum} return fn } func (f *FibWalker) Previous(pos list.IWalkerPosition) list.IWalkerPosition { fc := pos.(FibWalker) var diff big.Int diff.Sub(&fc.b, &fc.a) fn := FibWalker{diff, fc.a} return fn } //====================================================================== func main() { f := examples.RedirectLogger("fib.log") defer f.Close() palette := gowid.Palette{ "title": gowid.MakePaletteEntry(gowid.ColorWhite, gowid.ColorBlack), "key": gowid.MakePaletteEntry(gowid.ColorCyan, gowid.ColorBlack), "foot": gowid.MakePaletteEntry(gowid.ColorWhite, gowid.ColorBlack), "body": gowid.MakePaletteEntry(gowid.ColorBlack, gowid.ColorCyan), "fbody": gowid.MakePaletteEntry(gowid.ColorWhite, gowid.ColorBlack), } key := gowid.MakePaletteRef("key") foot := gowid.MakePaletteRef("foot") title := gowid.MakePaletteRef("title") body := gowid.MakePaletteRef("body") footerContent := []text.ContentSegment{ text.StyledContent("Fibonacci Set Viewer", title), text.StringContent(" "), text.StyledContent("UP", key), text.StringContent(", "), text.StyledContent("DOWN", key), text.StringContent(", "), text.StyledContent("PAGE_UP", key), text.StringContent(", "), text.StyledContent("PAGE_DOWN", key), text.StringContent(", "), text.StyledContent("HOME", key), text.StringContent(", "), text.StyledContent("CTRL-L", key), text.StringContent(" move view "), text.StyledContent("Q", key), text.StringContent(" exits. Try the mouse wheel."), } footerText := styled.New(text.NewFromContent(text.NewContent(footerContent)), foot) walker := FibWalker{*big.NewInt(0), *big.NewInt(1)} lb := list.New(&walker) styledLb := styled.New(lb, body) lb.OnFocusChanged(gowid.WidgetCallback{"cb", func(app gowid.IApp, w gowid.IWidget) { log.Infof("Focus changed - widget is now %p", w) }}) view := pile.New([]gowid.IContainerWidget{ &gowid.ContainerWidget{ IWidget: styledLb, D: gowid.RenderWithWeight{W: 1}, }, &gowid.ContainerWidget{ IWidget: footerText, D: gowid.RenderFlow{}, }, }) app, err := gowid.NewApp(gowid.AppArgs{ View: view, Palette: &palette, Log: log.StandardLogger(), }) examples.ExitOnErr(err) app.SimpleMainLoop() } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: gowid-1.4.0/examples/gowid-graph/000077500000000000000000000000001426234454000166575ustar00rootroot00000000000000gowid-1.4.0/examples/gowid-graph/graph.go000066400000000000000000000263311426234454000203140ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source code is governed by the MIT license // that can be found in the LICENSE file. // A port of urwid's graph.py example using gowid widgets. package main import ( "math" "time" "github.com/gcla/gowid" "github.com/gcla/gowid/examples" "github.com/gcla/gowid/gwutil" "github.com/gcla/gowid/widgets/bargraph" "github.com/gcla/gowid/widgets/button" "github.com/gcla/gowid/widgets/clicktracker" "github.com/gcla/gowid/widgets/columns" "github.com/gcla/gowid/widgets/divider" "github.com/gcla/gowid/widgets/fill" "github.com/gcla/gowid/widgets/framed" "github.com/gcla/gowid/widgets/hpadding" "github.com/gcla/gowid/widgets/pile" "github.com/gcla/gowid/widgets/progress" "github.com/gcla/gowid/widgets/radio" "github.com/gcla/gowid/widgets/shadow" "github.com/gcla/gowid/widgets/styled" "github.com/gcla/gowid/widgets/text" log "github.com/sirupsen/logrus" ) //====================================================================== var app *gowid.App var controller *GraphController //====================================================================== func sin100(x int) int { return int(50 + 50*math.Sin(float64(x)*math.Pi/50.0)) } func round(f float64) float64 { return math.Floor(f + 0.5) } func sum(x ...int) int { res := 0 for _, i := range x { res += i } return res } //====================================================================== type GraphModel struct { Data map[string][]int Modes []string CurrentMode string } func NewGraphModel() *GraphModel { modes := make([]string, 0) data := make(map[string][]int) var a1 []int for i := 0; i < 50; i++ { a1 = append(a1, i*2) } a2 := append(a1, a1...) data["Saw"] = a2 modes = append(modes, "Saw") var a3 []int var a4 []int for i := 0; i < 30; i++ { a3 = append(a3, 0) a4 = append(a4, 100) } data["Square"] = append(a3, a4...) modes = append(modes, "Square") var a5 []int for i := 0; i < 100; i++ { a5 = append(a5, sin100(i)) } data["Sine 1"] = a5 modes = append(modes, "Sine 1") var a6 []int for i := 0; i < 100; i++ { a6 = append(a6, (sin100(i)+sin100(i*2))/2) } data["Sine 2"] = a6 modes = append(modes, "Sine 2") var a7 []int for i := 0; i < 100; i++ { a7 = append(a7, (sin100(i)+sin100(i*3))/2) } data["Sine 3"] = a7 modes = append(modes, "Sine 3") return &GraphModel{data, modes, modes[0]} } func (g *GraphModel) SetMode(mode string) { g.CurrentMode = mode } func (g *GraphModel) GetData(offset, r int) ([]int, int, int) { l := make([]int, 0) d := g.Data[g.CurrentMode] for r > 0 { offset = offset % len(d) segment := d[offset:gwutil.Min(offset+r, len(d))] if len(segment) == 0 { break } r = r - len(segment) offset += len(segment) l = append(l, segment...) } return l, 100, len(d) } //====================================================================== type GraphView struct { *styled.Widget controller *GraphController started bool startTime *time.Time offset int lastOffset *int graph *bargraph.Widget pb *progress.Widget } func NewGraphView(controller *GraphController) *GraphView { t := time.Now() pb := progress.New(progress.Options{ Normal: gowid.MakePaletteRef("pg normal"), Complete: gowid.MakePaletteRef("pg complete"), }) graph := MakeBarGraph() controls := MakeBarGraphControls(controller, pb) weight1 := gowid.RenderWithWeight{1} weight2 := gowid.RenderWithWeight{2} unit1 := gowid.RenderWithUnits{U: 1} vline := styled.New(fill.New('│'), gowid.MakePaletteRef("line")) view := styled.New( framed.NewSpace( shadow.New( styled.New( framed.NewUnicode( columns.New([]gowid.IContainerWidget{ &gowid.ContainerWidget{graph, weight2}, &gowid.ContainerWidget{vline, unit1}, &gowid.ContainerWidget{controls, weight1}, }), ), gowid.MakePaletteRef("body"), ), 1, ), ), gowid.MakePaletteRef("screen edge"), ) res := &GraphView{ Widget: view, controller: controller, startTime: &t, graph: graph, pb: pb, } return res } func (g *GraphView) Selectable() bool { return true } func (g *GraphView) UserInput(ev interface{}, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) bool { return g.Widget.UserInput(ev, size, focus, app) } func MakeBarGraph() *bargraph.Widget { w := bargraph.New([]gowid.IColor{ gowid.NewUrwidColor("dark gray"), gowid.NewUrwidColor("dark blue"), gowid.NewUrwidColor("dark cyan"), }) return w } func MakeBarGraphControls(controller *GraphController, pb progress.IWidget) *pile.Widget { modeButtons := make([]gowid.IContainerWidget, 0) rbgroup := make([]radio.IWidget, 0) p := gowid.RenderFixed{} f := gowid.RenderFlow{} var firstrb *radio.Widget for _, mode := range controller.model.Modes { capturedMode := mode rb1 := radio.New(&rbgroup) if firstrb == nil { firstrb = rb1 } rbt1 := text.New(" " + mode) rb1.OnClick(gowid.WidgetCallback{"cb", func(app gowid.IApp, w gowid.IWidget) { controller.model.SetMode(capturedMode) controller.view.UpdateGraph(true, app) controller.view.lastOffset = nil }}) modeButton := make([]gowid.IContainerWidget, 0) modeButton = append(modeButton, &gowid.ContainerWidget{rb1, p}) modeButton = append(modeButton, &gowid.ContainerWidget{rbt1, p}) modeButtonCols := styled.NewExt(columns.New(modeButton), gowid.MakePaletteRef("button normal"), gowid.MakePaletteRef("button select")) modeButtons = append(modeButtons, &gowid.ContainerWidget{modeButtonCols, f}) } animateText := text.New("Start") animateButton := button.New(animateText) resetText := text.New("Reset") resetButton := button.New(resetText) quitText := text.New("Quit") quitButton := button.New(quitText) animateButton.OnClick(gowid.WidgetCallback{"cb", func(app gowid.IApp, w gowid.IWidget) { if animateText.Content().Length() == 5 { controller.AnimateGraph(app) animateText.SetText("Stop", app) } else { controller.StopAnimation() animateText.SetText("Start", app) } }}) resetButton.OnClick(gowid.WidgetCallback{"cb", func(app gowid.IApp, w gowid.IWidget) { controller.ResetGraph(app) }}) quitButton.OnClick(gowid.WidgetCallback{"cb", func(app gowid.IApp, w gowid.IWidget) { app.Quit() }}) animateButtonStyled := styled.NewExt(animateButton, gowid.MakePaletteRef("button normal"), gowid.MakePaletteRef("button select")) resetButtonStyled := styled.NewExt(resetButton, gowid.MakePaletteRef("button normal"), gowid.MakePaletteRef("button select")) quitButtonStyled := styled.NewExt(quitButton, gowid.MakePaletteRef("button normal"), gowid.MakePaletteRef("button select")) animateButtonTracker := clicktracker.New(animateButtonStyled) resetButtonTracker := clicktracker.New(resetButtonStyled) quitButtonTracker := clicktracker.New(quitButtonStyled) buttonGrid := columns.New([]gowid.IContainerWidget{ &gowid.ContainerWidget{hpadding.New(animateButtonTracker, gowid.HAlignMiddle{}, p), gowid.RenderWithWeight{1}}, &gowid.ContainerWidget{hpadding.New(resetButtonTracker, gowid.HAlignMiddle{}, p), gowid.RenderWithWeight{1}}, }) controls := make([]gowid.IContainerWidget, 0, 7+len(modeButtons)) controls = append(controls, modeButtons...) controls = append(controls, &gowid.ContainerWidget{divider.NewBlank(), f}) controls = append(controls, &gowid.ContainerWidget{hpadding.New(text.New("Animation"), gowid.HAlignMiddle{}, p), f}) controls = append(controls, &gowid.ContainerWidget{buttonGrid, f}) controls = append(controls, &gowid.ContainerWidget{divider.NewBlank(), f}) controls = append(controls, &gowid.ContainerWidget{pb, f}) controls = append(controls, &gowid.ContainerWidget{divider.NewBlank(), f}) controls = append(controls, &gowid.ContainerWidget{hpadding.New(quitButtonTracker, gowid.HAlignMiddle{}, p), f}) controlsPile := pile.New(controls) return controlsPile } func (v *GraphView) GetOffsetNow() int { if v.startTime == nil { return 0 } if !v.started { return v.offset } tdelta := time.Now().Sub(*v.startTime) tdelta = tdelta * 5 x := v.offset + (int(round(tdelta.Seconds()))) return x } func (v *GraphView) UpdateGraph(forceUpdate bool, app gowid.IApp) bool { o := v.GetOffsetNow() if v.lastOffset != nil && o == *v.lastOffset && !forceUpdate { return false } v.lastOffset = &o gspb := 10 r := gspb * 5 d, maxValue, repeat := v.controller.GetData(o, r) l := make([][]int, 0) for n := 0; n < 5; n++ { value := sum(d[n*gspb:(n+1)*gspb]...) / gspb // toggle between two bar types if n&1 == 1 { l = append(l, []int{0, value}) } else { l = append(l, []int{value, 0}) } } v.graph.SetData(l, maxValue, app) var prog int // also update progress if (o/repeat)&1 == 1 { // show 100% for first half, 0 for second half if o%repeat > repeat { prog = 0 } else { prog = 100 } } else { prog = ((o % repeat) * 100) / repeat } v.pb.SetProgress(app, prog) return true } //====================================================================== type GraphController struct { model *GraphModel view *GraphView mode string ticker *time.Ticker } func NewGraphController() *GraphController { res := &GraphController{NewGraphModel(), nil, "", nil} view := NewGraphView(res) res.view = view res.mode = res.model.Modes[0] return res } func (g *GraphController) GetData(offset, r int) ([]int, int, int) { return g.model.GetData(offset, r) } func (g *GraphController) ResetGraph(app gowid.IApp) { t := time.Now() g.view.startTime = &t g.view.offset = 0 g.view.UpdateGraph(true, app) } func (g *GraphController) AnimateGraph(app gowid.IApp) { t := time.Now() g.view.startTime = &t g.ticker = time.NewTicker(time.Millisecond * 200) g.view.started = true go func() { for _ = range g.ticker.C { app.Run(gowid.RunFunction(func(app gowid.IApp) { g.view.UpdateGraph(true, app) app.Redraw() })) } }() } func (g *GraphController) StopAnimation() { g.ticker.Stop() g.view.offset = g.view.GetOffsetNow() g.view.started = false } //====================================================================== func main() { var err error f := examples.RedirectLogger("graph.log") defer f.Close() palette := gowid.Palette{ "body": gowid.MakeStyledPaletteEntry(gowid.NewUrwidColor("black"), gowid.NewUrwidColor("light gray"), gowid.StyleBold), "line": gowid.MakePaletteRef("body"), "button normal": gowid.MakeStyledPaletteEntry(gowid.NewUrwidColor("light gray"), gowid.NewUrwidColor("dark blue"), gowid.StyleBold), "button select": gowid.MakePaletteEntry(gowid.NewUrwidColor("white"), gowid.NewUrwidColor("dark green")), "pg normal": gowid.MakeStyledPaletteEntry(gowid.NewUrwidColor("white"), gowid.NewUrwidColor("black"), gowid.StyleBold), "pg complete": gowid.MakeStyleMod(gowid.MakePaletteRef("pg normal"), gowid.MakeBackground(gowid.NewUrwidColor("dark magenta"))), "screen edge": gowid.MakePaletteEntry(gowid.NewUrwidColor("light blue"), gowid.NewUrwidColor("dark cyan")), } controller = NewGraphController() app, err = gowid.NewApp(gowid.AppArgs{ View: controller.view, Palette: &palette, Log: log.StandardLogger(), }) examples.ExitOnErr(err) controller.ResetGraph(app) app.SimpleMainLoop() } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: gowid-1.4.0/examples/gowid-helloworld/000077500000000000000000000000001426234454000177315ustar00rootroot00000000000000gowid-1.4.0/examples/gowid-helloworld/helloworld.go000066400000000000000000000042101426234454000224300ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source code is governed by the MIT license // that can be found in the LICENSE file. // A port of urwid's "Hello World" example from the tutorial, using gowid widgets. package main import ( "github.com/gcla/gowid" "github.com/gcla/gowid/examples" "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"), ) sf := gowid.RenderFlow{} view := styled.New( vpadding.New( pile.New([]gowid.IContainerWidget{ &gowid.ContainerWidget{IWidget: outside, D: sf}, &gowid.ContainerWidget{IWidget: inside, D: sf}, &gowid.ContainerWidget{IWidget: helloworld, D: sf}, &gowid.ContainerWidget{IWidget: inside, D: sf}, &gowid.ContainerWidget{IWidget: outside, D: sf}, }), gowid.VAlignMiddle{}, sf), gowid.MakePaletteRef("bg"), ) app, err := gowid.NewApp(gowid.AppArgs{ View: view, Palette: &palette, }) examples.ExitOnErr(err) app.SimpleMainLoop() } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: gowid-1.4.0/examples/gowid-menu/000077500000000000000000000000001426234454000165225ustar00rootroot00000000000000gowid-1.4.0/examples/gowid-menu/menu.go000066400000000000000000000124231426234454000200170ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source code is governed by the MIT license // that can be found in the LICENSE file. // A demonstration of gowid's menu, list and overlay widgets. package main import ( "fmt" "github.com/gcla/gowid" "github.com/gcla/gowid/examples" "github.com/gcla/gowid/widgets/button" "github.com/gcla/gowid/widgets/checkbox" "github.com/gcla/gowid/widgets/columns" "github.com/gcla/gowid/widgets/hpadding" "github.com/gcla/gowid/widgets/list" "github.com/gcla/gowid/widgets/menu" "github.com/gcla/gowid/widgets/overlay" "github.com/gcla/gowid/widgets/pile" "github.com/gcla/gowid/widgets/styled" "github.com/gcla/gowid/widgets/text" "github.com/gcla/gowid/widgets/vpadding" tcell "github.com/gdamore/tcell/v2" log "github.com/sirupsen/logrus" ) //====================================================================== var ov *overlay.Widget var menu1 *menu.Widget var menu2 *menu.Widget var ovh, ovw int = 50, 50 //====================================================================== type handler struct{} func (h handler) UnhandledInput(app gowid.IApp, ev interface{}) bool { handled := false if evk, ok := ev.(*tcell.EventKey); ok { handled = true if evk.Key() == tcell.KeyCtrlC || evk.Rune() == 'q' || evk.Rune() == 'Q' { app.Quit() } else if evk.Key() == tcell.KeyEsc { if !menu1.IsOpen() { app.Quit() } else { menu1.Close(app) } } else { handled = false } } return handled } //====================================================================== func main() { f := examples.RedirectLogger("menu.log") defer f.Close() palette := gowid.Palette{ "red": gowid.MakePaletteEntry(gowid.ColorRed, gowid.ColorDarkBlue), "green": gowid.MakePaletteEntry(gowid.ColorGreen, gowid.ColorDarkBlue), "white": gowid.MakePaletteEntry(gowid.ColorWhite, gowid.ColorCyan), } fixed := gowid.RenderFixed{} menu2Widgets := make([]gowid.IWidget, 0) for i := 0; i < 10; i++ { clickme := button.New(text.New(fmt.Sprintf("subwidget %d", i))) clickmeStyled := styled.NewInvertedFocus(clickme, gowid.MakePaletteRef("green")) clickme.OnClick(gowid.WidgetCallback{gowid.ClickCB{}, func(app gowid.IApp, target gowid.IWidget) { log.Infof("SUBMENU button CLICKED") }}) cols := columns.New([]gowid.IContainerWidget{ &gowid.ContainerWidget{IWidget: clickmeStyled, D: fixed}, }) menu2Widgets = append(menu2Widgets, cols) } walker2 := list.NewSimpleListWalker(menu2Widgets) menuListBox2 := styled.New(list.New(walker2), gowid.MakePaletteRef("green")) menu1Widgets := make([]gowid.IWidget, 0) for i := 0; i < 40; i++ { content := text.NewContent([]text.ContentSegment{ text.StringContent(fmt.Sprintf("widget %d", i)), }) txt := styled.NewInvertedFocus(text.NewFromContent(content), gowid.MakePaletteRef("red")) btn := button.NewBare(txt) btnSite := menu.NewSite() checkme := checkbox.New(false) checkmeStyled := styled.NewInvertedFocus(checkme, gowid.MakePaletteRef("red")) checkme.OnClick(gowid.WidgetCallback{gowid.ClickCB{}, func(app gowid.IApp, target gowid.IWidget) { log.Infof("MENU checkbox CLICKED") }}) btn.OnClick(gowid.WidgetCallback{gowid.ClickCB{}, func(app gowid.IApp, target gowid.IWidget) { if menu2.IsOpen() { menu2.Close(app) } else { menu2.Open(btnSite, app) } }}) cols := columns.New([]gowid.IContainerWidget{ &gowid.ContainerWidget{IWidget: checkmeStyled, D: fixed}, &gowid.ContainerWidget{IWidget: btn, D: fixed}, &gowid.ContainerWidget{IWidget: btnSite, D: fixed}, }) menu1Widgets = append(menu1Widgets, cols) } walker1 := list.NewSimpleListWalker(menu1Widgets) menuListBox1 := styled.New(list.New(walker1), gowid.MakePaletteRef("red")) menu1 = menu.New("main", menuListBox1, gowid.RenderWithUnits{U: 16}) menu2 = menu.New("main2", menuListBox2, gowid.RenderWithUnits{U: 16}) clickToOpenWidgets := make([]gowid.IContainerWidget, 0) // Make the on screen buttons to click to open the menu for i := 0; i < 20; i++ { btn := button.New(text.New(fmt.Sprintf("clickety%d", i))) btnStyled := styled.NewExt(btn, gowid.MakePaletteRef("red"), gowid.MakePaletteRef("white")) btnSite := menu.NewSite(menu.SiteOptions{YOffset: 1}) btn.OnClick(gowid.WidgetCallback{gowid.ClickCB{}, func(app gowid.IApp, target gowid.IWidget) { menu1.Open(btnSite, app) }}) clickToOpenWidgets = append(clickToOpenWidgets, &gowid.ContainerWidget{IWidget: btnSite, D: fixed}) clickToOpenWidgets = append(clickToOpenWidgets, &gowid.ContainerWidget{IWidget: btnStyled, D: fixed}) } clickToOpenCols := columns.New(clickToOpenWidgets) check := checkbox.New(false) view1 := pile.New([]gowid.IContainerWidget{ &gowid.ContainerWidget{IWidget: clickToOpenCols, D: fixed}, &gowid.ContainerWidget{IWidget: check, D: fixed}, }) view := vpadding.New( hpadding.New(view1, gowid.HAlignLeft{}, fixed), gowid.VAlignTop{Margin: 2}, gowid.RenderFlow{}, ) app, err := gowid.NewApp(gowid.AppArgs{ View: view, Palette: &palette, Log: log.StandardLogger(), }) examples.ExitOnErr(err) // Required for menus to appear overlaid on top of the main view. app.RegisterMenu(menu1) app.RegisterMenu(menu2) app.MainLoop(handler{}) } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: gowid-1.4.0/examples/gowid-overlay1/000077500000000000000000000000001426234454000173205ustar00rootroot00000000000000gowid-1.4.0/examples/gowid-overlay1/overlay1.go000066400000000000000000000045531426234454000214200ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source code is governed by the MIT license // that can be found in the LICENSE file. // A demonstration of gowid's overlay and fill widgets. package main import ( "github.com/gcla/gowid" "github.com/gcla/gowid/examples" "github.com/gcla/gowid/gwutil" "github.com/gcla/gowid/widgets/fill" "github.com/gcla/gowid/widgets/overlay" "github.com/gcla/gowid/widgets/styled" tcell "github.com/gdamore/tcell/v2" log "github.com/sirupsen/logrus" ) //====================================================================== var ov *overlay.Widget var ovh, ovw int = 50, 50 //====================================================================== type handler struct{} func (h handler) UnhandledInput(app gowid.IApp, ev interface{}) bool { handled := false if evk, ok := ev.(*tcell.EventKey); ok { handled = true if evk.Key() == tcell.KeyCtrlC || evk.Key() == tcell.KeyEsc || evk.Rune() == 'q' || evk.Rune() == 'Q' { app.Quit() } else if evk.Key() == tcell.KeyUp { ovh = gwutil.Min(100, ovh+1) ov.SetHeight(gowid.RenderWithRatio{R: float64(ovh) / 100.0}, app) } else if evk.Key() == tcell.KeyDown { ovh = gwutil.Max(0, ovh-1) ov.SetHeight(gowid.RenderWithRatio{R: float64(ovh) / 100.0}, app) } else if evk.Key() == tcell.KeyRight { ovw = gwutil.Min(100, ovw+1) ov.SetWidth(gowid.RenderWithRatio{R: float64(ovw) / 100.0}, app) } else if evk.Key() == tcell.KeyLeft { ovw = gwutil.Max(0, ovw-1) ov.SetWidth(gowid.RenderWithRatio{R: float64(ovw) / 100.0}, app) } else { handled = false } } return handled } //====================================================================== func main() { f := examples.RedirectLogger("overlay1.log") defer f.Close() palette := gowid.Palette{ "red": gowid.MakePaletteEntry(gowid.ColorDefault, gowid.ColorRed), } top := styled.New(fill.New(' '), gowid.MakePaletteRef("red")) bottom := fill.New(' ') ov = overlay.New(top, bottom, gowid.VAlignMiddle{}, gowid.RenderWithRatio{R: 0.5}, gowid.HAlignMiddle{}, gowid.RenderWithRatio{R: 0.5}) app, err := gowid.NewApp(gowid.AppArgs{ View: ov, Palette: &palette, Log: log.StandardLogger(), }) examples.ExitOnErr(err) app.MainLoop(handler{}) } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: gowid-1.4.0/examples/gowid-overlay2/000077500000000000000000000000001426234454000173215ustar00rootroot00000000000000gowid-1.4.0/examples/gowid-overlay2/overlay2.go000066400000000000000000000107271426234454000214220ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source code is governed by the MIT license // that can be found in the LICENSE file. // A demonstration of gowid's overlay, fill, asciigraph and radio widgets. package main import ( "math/rand" "github.com/gcla/gowid" "github.com/gcla/gowid/examples" "github.com/gcla/gowid/gwutil" "github.com/gcla/gowid/widgets/asciigraph" "github.com/gcla/gowid/widgets/checkbox" "github.com/gcla/gowid/widgets/columns" "github.com/gcla/gowid/widgets/divider" "github.com/gcla/gowid/widgets/framed" "github.com/gcla/gowid/widgets/hpadding" "github.com/gcla/gowid/widgets/overlay" "github.com/gcla/gowid/widgets/pile" "github.com/gcla/gowid/widgets/radio" "github.com/gcla/gowid/widgets/styled" "github.com/gcla/gowid/widgets/text" "github.com/gcla/gowid/widgets/vpadding" tcell "github.com/gdamore/tcell/v2" asc "github.com/guptarohit/asciigraph" log "github.com/sirupsen/logrus" ) //====================================================================== var ov *overlay.Widget var ovh, ovw int = 50, 50 //====================================================================== type handler struct{} func (h handler) UnhandledInput(app gowid.IApp, ev interface{}) bool { handled := false if evk, ok := ev.(*tcell.EventKey); ok { handled = true if evk.Key() == tcell.KeyCtrlC || evk.Key() == tcell.KeyEsc || evk.Rune() == 'q' || evk.Rune() == 'Q' { app.Quit() } else if evk.Key() == tcell.KeyUp || evk.Rune() == 'u' { ovh = gwutil.Min(100, ovh+1) ov.SetHeight(gowid.RenderWithRatio{float64(ovh) / 100.0}, app) } else if evk.Key() == tcell.KeyDown || evk.Rune() == 'd' { ovh = gwutil.Max(0, ovh-1) ov.SetHeight(gowid.RenderWithRatio{float64(ovh) / 100.0}, app) } else if evk.Key() == tcell.KeyRight { ovw = gwutil.Min(100, ovw+1) ov.SetWidth(gowid.RenderWithRatio{float64(ovw) / 100.0}, app) } else if evk.Key() == tcell.KeyLeft { ovw = gwutil.Max(0, ovw-1) ov.SetWidth(gowid.RenderWithRatio{float64(ovw) / 100.0}, app) } else { handled = false } } return handled } //====================================================================== func main() { f := examples.RedirectLogger("overlay2.log") defer f.Close() palette := gowid.Palette{ "red": gowid.MakePaletteEntry(gowid.ColorRed, gowid.ColorDefault), } fixed := gowid.RenderFixed{} rbgroup := make([]radio.IWidget, 0) rb1 := radio.New(&rbgroup) rbt1 := text.New(" option1 ") rb2 := radio.New(&rbgroup) rbt2 := text.New(" option2 ") rb3 := radio.New(&rbgroup) rbt3 := text.New(" option3 ") data := []float64{2, 1, 1, 2, -2, 5, 7, 11, 3, 7, 1, 4, 7, 2, 2, 9} data2 := []float64{9, 2, 2, 7, 4, 1, 7, 3, 11, 7, 5, -2, 2, 1, 1, 2} conf := []asc.Option{} graph := asciigraph.New(data, conf) callback := func(app gowid.IApp, target gowid.IWidget) { if rb1.IsChecked() { graph.SetData(data, app) } if rb2.IsChecked() { graph.SetData(data2, app) } if rb3.IsChecked() { data3 := make([]float64, 40) for i := 0; i < len(data3); i++ { data3[i] = gwutil.Round(rand.Float64() * 14) } graph.SetData(data3, app) } } rb1.OnClick(gowid.WidgetCallback{gowid.ClickCB{}, callback}) rb2.OnClick(gowid.WidgetCallback{gowid.ClickCB{}, callback}) rb3.OnClick(gowid.WidgetCallback{gowid.ClickCB{}, callback}) c2cols := []gowid.IContainerWidget{ &gowid.ContainerWidget{rb1, fixed}, &gowid.ContainerWidget{rbt1, fixed}, &gowid.ContainerWidget{rb2, fixed}, &gowid.ContainerWidget{rbt2, fixed}, &gowid.ContainerWidget{rb3, fixed}, &gowid.ContainerWidget{rbt3, fixed}, } cols := columns.New(c2cols) rows := pile.New([]gowid.IContainerWidget{ &gowid.ContainerWidget{cols, gowid.RenderWithUnits{U: 1}}, &gowid.ContainerWidget{divider.NewUnicode(), gowid.RenderFlow{}}, &gowid.ContainerWidget{graph, gowid.RenderWithWeight{1}}, }) fcols := framed.NewUnicodeAlt(framed.NewUnicodeAlt(rows)) top := styled.New(fcols, gowid.MakePaletteRef("red")) bottom := vpadding.New(hpadding.New(checkbox.New(false), gowid.HAlignLeft{}, gowid.RenderFixed{}), gowid.VAlignTop{}, gowid.RenderFlow{}) ov = overlay.New(top, bottom, gowid.VAlignMiddle{}, gowid.RenderWithRatio{0.5}, gowid.HAlignMiddle{}, gowid.RenderWithRatio{0.5}) app, err := gowid.NewApp(gowid.AppArgs{ View: ov, Palette: &palette, Log: log.StandardLogger(), }) examples.ExitOnErr(err) app.MainLoop(handler{}) } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: gowid-1.4.0/examples/gowid-overlay3/000077500000000000000000000000001426234454000173225ustar00rootroot00000000000000gowid-1.4.0/examples/gowid-overlay3/overlay3.go000066400000000000000000000035761426234454000214300ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source code is governed by the MIT license // that can be found in the LICENSE file. // A demonstration of gowid's overlay and fill widgets. package main import ( "github.com/gcla/gowid" "github.com/gcla/gowid/examples" "github.com/gcla/gowid/widgets/fill" "github.com/gcla/gowid/widgets/framed" "github.com/gcla/gowid/widgets/overlay" "github.com/gcla/gowid/widgets/styled" "github.com/gcla/gowid/widgets/text" tcell "github.com/gdamore/tcell/v2" log "github.com/sirupsen/logrus" ) //====================================================================== var ov *overlay.Widget var ovh, ovw int = 50, 50 //====================================================================== type handler struct{} func (h handler) UnhandledInput(app gowid.IApp, ev interface{}) bool { handled := false if evk, ok := ev.(*tcell.EventKey); ok { handled = true if evk.Key() == tcell.KeyCtrlC || evk.Key() == tcell.KeyEsc || evk.Rune() == 'q' || evk.Rune() == 'Q' { app.Quit() } else { handled = false } } return handled } //====================================================================== func main() { f := examples.RedirectLogger("overlay1.log") defer f.Close() palette := gowid.Palette{ "red": gowid.MakePaletteEntry(gowid.ColorDefault, gowid.ColorRed), } top := styled.New( framed.NewUnicode( text.New("hello"), ), gowid.MakePaletteRef("red"), ) bottom := fill.New(' ') ov = overlay.New(top, bottom, gowid.VAlignMiddle{}, gowid.RenderFixed{}, gowid.HAlignMiddle{}, gowid.RenderFixed{}, ) app, err := gowid.NewApp(gowid.AppArgs{ View: ov, Palette: &palette, Log: log.StandardLogger(), }) examples.ExitOnErr(err) app.MainLoop(handler{}) } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: gowid-1.4.0/examples/gowid-palette/000077500000000000000000000000001426234454000172145ustar00rootroot00000000000000gowid-1.4.0/examples/gowid-palette/palette.go000066400000000000000000000247271426234454000212150ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source code is governed by the MIT license // that can be found in the LICENSE file. // A port of urwid's palette_test.py example using gowid widgets. package main import ( "errors" "os" "regexp" "strings" "github.com/gcla/gowid" "github.com/gcla/gowid/examples" "github.com/gcla/gowid/widgets/button" "github.com/gcla/gowid/widgets/columns" "github.com/gcla/gowid/widgets/divider" "github.com/gcla/gowid/widgets/holder" "github.com/gcla/gowid/widgets/pile" "github.com/gcla/gowid/widgets/radio" "github.com/gcla/gowid/widgets/styled" "github.com/gcla/gowid/widgets/text" tcell "github.com/gdamore/tcell/v2" log "github.com/sirupsen/logrus" ) //====================================================================== // t0 t1 t2 t3 t4 t5 t6 t7 t8 t9 t10 t11 t12 t13 t14 t15 t16 t17 t254 t255 var chart256 = ` brown__ dark_red_ dark_magenta_ dark_blue_ dark_cyan_ dark_green_ yellow_ light_red light_magenta light_blue light_cyan light_green #00f#06f#08f#0af#0df#0ff black_______ dark_gray___ #60f#00d#06d#08d#0ad#0dd#0fd light_gray__ white_______ #80f#60d#00a#06a#08a#0aa#0da#0fa #a0f#80d#60a#008#068#088#0a8#0d8#0f8 #d0f#a0d#80d#608#006#066#086#0a6#0d6#0f6 #f0f#d0d#a0a#808#606#000#060#080#0a0#0d0#0f0#0f6#0f8#0fa#0fd#0ff #f0d#d0a#a08#806#600#660#680#6a0#6d0#6f0#6f6#6f8#6fa#6fd#6ff#0df #f0a#d08#a06#800#860#880#8a0#8d0#8f0#8f6#8f8#8fa#8fd#8ff#6df#0af #f08#d06#a00#a60#a80#aa0#ad0#af0#af6#af8#afa#afd#aff#8df#6af#08f #f06#d00#d60#d80#da0#dd0#df0#df6#df8#dfa#dfd#dff#adf#8af#68f#06f #f00#f60#f80#fa0#fd0#ff0#ff6#ff8#ffa#ffd#fff#ddf#aaf#88f#66f#00f #fd0#fd6#fd8#fda#fdd#fdf#daf#a8f#86f#60f #66d#68d#6ad#6dd #fa0#fa6#fa8#faa#fad#faf#d8f#a6f#80f #86d#66a#68a#6aa#6da #f80#f86#f88#f8a#f8d#f8f#d6f#a0f #a6d#86a#668#688#6a8#6d8 #f60#f66#f68#f6a#f6d#f6f#d0f #d6d#a6a#868#666#686#6a6#6d6#6d8#6da#6dd #f00#f06#f08#f0a#f0d#f0f #d6a#a68#866#886#8a6#8d6#8d8#8da#8dd#6ad #d68#a66#a86#aa6#ad6#ad8#ada#add#8ad#68d #d66#d86#da6#dd6#dd8#dda#ddd#aad#88d#66d g78_g82_g85_g89_g93_g100_ #da6#da8#daa#dad#a8d#86d g52_g58_g62_g66_g70_g74_ #88a#8aa #d86#d88#d8a#d8d#a6d g27_g31_g35_g38_g42_g46_g50_ #a8a#888#8a8#8aa #d66#d68#d6a#d6d g0__g3__g7__g11_g15_g19_g23_ #a88#aa8#aaa#88a #a88#a8a ` var chart88 = ` brown__ dark_red_ dark_magenta_ dark_blue_ dark_cyan_ dark_green_ yellow_ light_red light_magenta light_blue light_cyan light_green #00f#08f#0cf#0ff black_______ dark_gray___ #80f#00c#08c#0cc#0fc light_gray__ white_______ #c0f#80c#008#088#0c8#0f8 #f0f#c0c#808#000#080#0c0#0f0#0f8#0fc#0ff #88c#8cc #f0c#c08#800#880#8c0#8f0#8f8#8fc#8ff#0cf #c8c#888#8c8#8cc #f08#c00#c80#cc0#cf0#cf8#cfc#cff#8cf#08f #c88#cc8#ccc#88c #f00#f80#fc0#ff0#ff8#ffc#fff#ccf#88f#00f #c88#c8c #fc0#fc8#fcc#fcf#c8f#80f #f80#f88#f8c#f8f#c0f g62_g74_g82_g89_g100 #f00#f08#f0c#f0f g0__g19_g35_g46_g52 ` var chart16 = ` brown__ dark_red_ dark_magenta_ dark_blue_ dark_cyan_ dark_green_ yellow_ light_red light_magenta light_blue light_cyan light_green black_______ dark_gray___ light_gray__ white_______ ` var chart8 = ` black__ red_ green_ yellow_ blue_ magenta_ cyan_ white_ ` var chartMono = ` black__ white_ ` var attrRE = regexp.MustCompile(`(?P[ \n]*)(?P((#...)|([a-z_]{2,})|(g[0-9]+_+)|(t[0-9]{1,3})))`) var attrREIndices = makeRegexpNameMap(attrRE) var fgColorDefault = gowid.NewUrwidColor("light gray") var bgColorDefault = gowid.MakeTCellColorExt(tcell.ColorBlack) var chartHolder *holder.Widget var chart256Content *text.Widget var chart88Content *text.Widget var chart16Content *text.Widget var chart8Content *text.Widget var chartMonoContent *text.Widget var foregroundColors = true //====================================================================== func makeRegexpNameMap(re *regexp.Regexp) map[string]int { res := make(map[string]int) for i, name := range re.SubexpNames() { res[name] = i } return res } //====================================================================== func updateChartHolder(mode gowid.ColorMode, app gowid.IApp) { switch mode { case gowid.Mode256Colors: chart256Content = text.NewFromContent(parseChart(chart256)) chartHolder.SetSubWidget(chart256Content, app) case gowid.Mode88Colors: chart88Content = text.NewFromContent(parseChart(chart88)) chartHolder.SetSubWidget(chart88Content, app) case gowid.Mode16Colors: chart16Content = text.NewFromContent(parseChart(chart16)) chartHolder.SetSubWidget(chart16Content, app) case gowid.Mode8Colors: chart8Content = text.NewFromContent(parseChart(chart8)) chartHolder.SetSubWidget(chart8Content, app) case gowid.ModeMonochrome: chartMonoContent = text.NewFromContent(parseChart(chartMono)) chartHolder.SetSubWidget(chartMonoContent, app) default: panic(errors.New("Invalid mode, something went wrong!")) } } //====================================================================== func modeRb(group *[]radio.IWidget, txt string) gowid.IWidget { rbt := text.New(" " + txt) rb := radio.New(group) widp := gowid.RenderFixed{} rb.OnClick(gowid.WidgetCallback{"cb", func(app gowid.IApp, w gowid.IWidget) { if rb.Selected { switch txt { case "256-Color": app.SetColorMode(gowid.Mode256Colors) case "88-Color": app.SetColorMode(gowid.Mode88Colors) case "16-Color": app.SetColorMode(gowid.Mode16Colors) case "8-Color": app.SetColorMode(gowid.Mode8Colors) case "Monochrome": app.SetColorMode(gowid.ModeMonochrome) case "Foreground Colors": foregroundColors = true case "Background Colors": foregroundColors = false default: panic(errors.New("Invalid mode, something went wrong!")) } updateChartHolder(app.GetColorMode(), app) // Update the chart displayed } }}) c := columns.New([]gowid.IContainerWidget{ &gowid.ContainerWidget{rb, widp}, &gowid.ContainerWidget{rbt, widp}, }) cm := styled.NewExt(c, gowid.MakePaletteRef("panel"), gowid.MakePaletteRef("focus")) return cm } //====================================================================== func parseChart(chart string) *text.Content { content := make([]text.ContentSegment, 0) replacer := strings.NewReplacer("_", " ") for _, match := range attrRE.FindAllStringSubmatch(chart, -1) { ws := match[attrREIndices["whitespace"]] if ws != "" { content = append(content, text.StringContent(ws)) } entry := match[attrREIndices["entry"]] entry = replacer.Replace(entry) entry2 := strings.Trim(entry, " ") scol, err := gowid.MakeColorSafe(entry2) if err == nil { var attrPair gowid.PaletteEntry if foregroundColors { attrPair = gowid.MakePaletteEntry(scol, bgColorDefault) } else { attrPair = gowid.MakePaletteEntry(fgColorDefault, scol) } content = append(content, text.StyledContent(entry, attrPair)) } else { content = append(content, text.StyledContent(entry, gowid.MakePaletteRef("redinv"))) } } return text.NewContent(content) } //====================================================================== func main() { // If this is set to truecolor when a gowid screen is setup, 24-bit truecolor // support is enabled. Then the program won't output the 256-color/16-color // terminal codes that this program is supposed to exhibit. So unset the variable // right away. os.Unsetenv("COLORTERM") f := examples.RedirectLogger("palette.log") defer f.Close() palette := gowid.Palette{ "header": gowid.MakeStyledPaletteEntry(gowid.ColorBlack, gowid.ColorWhite, gowid.StyleUnderline), "panel": gowid.MakePaletteEntry(gowid.ColorWhite, gowid.ColorDarkBlue), "focus": gowid.MakePaletteEntry(gowid.ColorYellow, gowid.ColorRed), "red": gowid.MakePaletteEntry(gowid.ColorRed, gowid.ColorBlack), "redinv": gowid.MakePaletteEntry(gowid.ColorBlack, gowid.ColorRed), "default": gowid.MakePaletteEntry(gowid.NewUrwidColor("light gray"), gowid.ColorBlack), } header := gowid.MakePaletteRef("header") // make this just text headerContent := []text.ContentSegment{ text.StyledContent("Gowid Palette Test", header), } headerText := styled.New(text.NewFromContent(text.NewContent(headerContent)), header) rbgroup := make([]radio.IWidget, 0) bggroup := make([]radio.IWidget, 0) btn := button.New(text.New("Exit")) btn.OnClick(gowid.WidgetCallback{"cb", func(app gowid.IApp, w gowid.IWidget) { app.Quit() }}) subFlow := gowid.RenderFlow{} // Put this in the group first so it is the one selected cols256 := modeRb(&rbgroup, "256-Color") pw1 := pile.New([]gowid.IContainerWidget{ &gowid.ContainerWidget{modeRb(&rbgroup, "Monochrome"), subFlow}, &gowid.ContainerWidget{modeRb(&rbgroup, "8-Color"), subFlow}, &gowid.ContainerWidget{modeRb(&rbgroup, "16-Color"), subFlow}, &gowid.ContainerWidget{modeRb(&rbgroup, "88-Color"), subFlow}, &gowid.ContainerWidget{cols256, subFlow}, }) pw2 := pile.New([]gowid.IContainerWidget{ &gowid.ContainerWidget{modeRb(&bggroup, "Foreground Colors"), subFlow}, &gowid.ContainerWidget{modeRb(&bggroup, "Background Colors"), subFlow}, &gowid.ContainerWidget{divider.NewBlank(), subFlow}, &gowid.ContainerWidget{divider.NewBlank(), subFlow}, &gowid.ContainerWidget{ styled.NewExt( btn, gowid.MakePaletteRef("panel"), gowid.MakePaletteRef("focus"), ), subFlow}, }) cs := columns.New([]gowid.IContainerWidget{ &gowid.ContainerWidget{pw1, gowid.RenderWithWeight{10}}, &gowid.ContainerWidget{pw2, gowid.RenderWithWeight{10}}, }) cs2 := styled.New(cs, gowid.MakePaletteRef("panel")) chart256Content = text.NewFromContent(parseChart(chart256)) chartHolder = holder.New(chart256Content) view := pile.New([]gowid.IContainerWidget{ &gowid.ContainerWidget{headerText, subFlow}, &gowid.ContainerWidget{cs2, subFlow}, &gowid.ContainerWidget{chartHolder, gowid.RenderWithWeight{1}}, }) app, err := gowid.NewApp(gowid.AppArgs{ View: view, Palette: &palette, Log: log.StandardLogger(), }) examples.ExitOnErr(err) app.SetColorMode(gowid.Mode256Colors) app.SimpleMainLoop() } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: gowid-1.4.0/examples/gowid-table/000077500000000000000000000000001426234454000166455ustar00rootroot00000000000000gowid-1.4.0/examples/gowid-table/README.md000066400000000000000000000001071426234454000201220ustar00rootroot00000000000000 To re-generate: $ go get github.com/rakyll/statik $ statik -src=data gowid-1.4.0/examples/gowid-table/data/000077500000000000000000000000001426234454000175565ustar00rootroot00000000000000gowid-1.4.0/examples/gowid-table/data/worldcitiespop1k.csv000066400000000000000000001350761426234454000236120ustar00rootroot00000000000000Country,City,AccentCity,Region,Population,Latitude,Longitude af,`ali kheyl,`Ali Kheyl,24,,36.697116,68.851365 af,bahadorgay,Bahadorgay,08,,33.183056,68.056944 af,bar-khankheyl',Bar-Khankheyl',37,,33.30201,69.777346 af,butah'i,Butah'i,05,,34.814257,66.855124 af,cal,Cal,27,,34.505057,68.037787 af,kudali,Kudali,29,,33.266111,68.864722 af,malek qyas kalay,Malek Qyas Kalay,18,,34.275501,70.031113 af,mu'menkhel,Mu'menkhel,08,,32.799713,67.752777 af,mukhammedadzhan-kot,Mukhammedadzhan-Kot,29,,32.92643,69.422562 af,qal'a-i-naw,Qal'a-i-Naw,19,,31.536224,62.758054 af,qal`eh-ye qowl-e heydar,Qal`eh-ye Qowl-e Heydar,08,,33.146022,67.504011 af,shinwari,Shinwari,03,,36.000837,68.570617 af,yak pahlu,Yak Pahlu,11,,34.334461,63.738327 af,zowli,Zowli,30,,36.656917,66.79588 al,gadurove,Gadurovë,44,,40.5186111,19.8488889 al,kashisht,Kashisht,44,,40.5983333,19.5405556 al,kopliku-e poshtem,Kopliku-e Poshtëm,49,,42.2136111,19.4363889 al,luniku,Luniku,43,,41.2891667,20.3236111 al,ngureza e madhe,Ngurëza e Madhe,44,,40.8261111,19.7163889 al,ples e zadrims,Ples e Zadrims,49,,41.9697222,19.5538889 al,trovne,Trovnë,49,,42.2208333,19.9297222 al,veseshte,Veseshtë,40,,40.5166667,20.2663889 am,ertich,Ertich,10,,39.7405556,45.2666667 am,shaab,Shaab,05,,40.2522222,44.6383333 ao,bango,Bango,07,,-16.383333,13.616667 ao,cambanzumbe,Cambanzumbe,17,,-8.216667,20.233333 ao,cassai-fana,Cassai-Fana,18,,-10.588912,21.969745 ao,chivanda,Chivanda,09,,-13.45,15.983333 ao,fazenda esperanca,Fazenda Esperança,12,,-9.433333,15.85 ar,rarro negro,Rarro Negro,10,,-24.3,-64.933333 at,badersdorf,Badersdorf,01,,47.2,16.366667 at,mirnig,Mirnig,02,,46.8,14.583333 at,obergassolding,Obergassolding,04,,48.216667,14.75 at,schlattenthal,Schlattenthal,03,,47.65,16.233333 au,cardigan village,Cardigan Village,07,,-37.51091,143.706161 au,sylvaterre,Sylvaterre,07,,-36.116667,144.233333 au,wiyarra,Wiyarra,04,,-28.266667,152.233333 az,hopurlu,Hopurlu,26,,40.03584,45.977294 az,kargalan,Kargalan,29,,38.746181,48.777724 az,kolkyshlak,Kolkyshlak,03,,40.116667,47.05 az,miadzhik,Miadzhik,01,,40.260833,49.438333 ba,dragan selo,Dragan Selo,01,,43.645,17.8033333 ba,laza,Laza,02,,43.2908333,18.5211111 ba,ustikolina,Ustikolina,01,4446,43.5836111,18.79 bd,ghaturia,Ghaturia,83,,24.9833333,89.1833333 bd,khagarjana,Khagarjana,81,,24.7333333,90.1666667 bd,mahmudkati,Mahmudkati,82,,22.6666667,89.3 bd,shahar ahmedpur,Shahar Ahmedpur,81,,24.7666667,90.6666667 bd,uttar chandkhana,Uttar Chandkhana,83,,25.85,89.0333333 be,calvenne,Calvenne,02,,51,4.716667 be,gammel,Gammel,01,,51.366667,4.766667 be,moereinde,Moereinde,01,,51.25,4.783333 be,sint-remigius-geest,Sint-Remigius-Geest,02,,50.75,4.866667 be,zand,Zand,02,,50.9,5.066667 bf,bobeta,Bobéta,69,,10.3666667,-3.5666667 bf,koudougou,Koudougou,19,86965,12.25,-2.3666667 bg,dimitur petkov,Dimitur Petkov,52,,43.7333333,27.1333333 bg,khambardzhilar,Khambardzhilar,43,,41.6166667,25.3833333 bg,obnova,Obnova,46,,43.4333333,24.9833333 bg,orchenlar,Orchenlar,52,,43.2333333,26.4833333 bg,rakovskovo,Rakovskovo,39,,42.8166667,27.75 bg,rebrevtsu,Rebrevtsu,46,,42.9,25.7666667 bg,sabol,Sabol,58,,42.8319444,22.5836111 bi,rurengera,Rurengera,11,,-3.1872222,30.6361111 bj,alafiarou,Alafiarou,10,,9.4,2.6833333 bo,cuchihilga,Cuchihilga,02,,-17.5833333,-65.2333333 bo,kullpachaca,Kullpachaca,07,,-19.1894444,-65.4277778 bo,la cueva chica,La Cueva Chica,01,,-20.9833333,-65.35 bo,lagunilla,Lagunilla,07,,-21.2333333,-65.65 bo,siraomonton,Siraomonton,01,,-19.8666667,-63.8 br,aterradinho,Aterradinho,14,,-16.95,-57.35 br,cajazeiras,Cajazeiras,22,,-6.483333,-38.1 br,curimatai,Curimataí,15,,-17.85,-43.983333 br,fazenda do canal,Fazenda do Canal,05,,-11.35,-41.666667 br,itajahi,Itajahi,26,,-26.883333,-48.65 br,marco prete,Marco Prete,06,,-3.983333,-40.316667 br,passagem,Passagem,15,,-19.283333,-43.216667 br,porto alegre,Pôrto Alegre,16,,-4.350278,-52.737778 br,quinzopolis,Quinzópolis,18,,-23.016667,-50.566667 br,tuneiras do oeste,Tuneiras do Oeste,18,,-23.816667,-52.916667 br,upamirim,Upamirim,05,,-10.466667,-41.25 br,urucu-mirim,Uruçu-Mirim,30,,-8.3,-35.6 by,bazilevka,Bazilevka,07,,55.5,29.05 by,peschanka,Peschanka,07,,54.9,30.2333333 by,volynets,Volynets,07,,55.7,28.1833333 by,wiejna,Wiejna,06,,54.55,27.7333333 ca,dorchester crossing,Dorchester Crossing,04,,46.166667,-64.566667 ca,saint-romuald,Saint-Romuald,10,,46.75,-71.233333 cd,busu-gbolonga,Busu-Gbolonga,00,,1.55,19.45 cd,diobo,Diobo,00,,2.266667,20.483333 cd,djiapanda,Djiapanda,09,,1.4,29.416667 cd,kamususomba,Kamususomba,00,,-6.583333,19.983333 cd,kembowala,Kembowala,02,,-6,17.833333 cd,koiekoie,Koiekoie,00,,-3.366667,23.25 cd,mapanda,Mapanda,00,,-6.666667,17.683333 cd,mulenda,Mulenda,05,,-5.116667,28.9 cf,bangaladeke,Bangaladéké,17,,4.6,18.3333333 ch,arconciel,Arconciel,06,,46.746348,7.124687 ch,jaggisbachau,Jaggisbachau,05,,46.96396,7.314665 ci,bofia,Bofia,81,,6.720045,-5.006264 ci,diamanbo,Diamanbo,83,,7.366234,-5.76568 ci,gitri,Gitri,88,,5.519518,-5.240379 ci,gounioube,Gounioubé,82,,5.393889,-4.146111 ci,korkana,Korkana,87,,9.588071,-5.010828 ci,kouenoufra,Kouénoufra,83,,6.816853,-5.777455 ci,mboka nguessan,Mboka Nguessan,74,,5.696907,-4.399312 ci,ouompleupleu,Ouompleupleu,78,,7.275385,-8.284204 ci,ouorosaniso,Ouorosaniso,75,,8.81597,-7.729201 ci,satiahary,Satiahary,81,,7.0475,-5.278333 cm,guendou,Guendou,12,,10.05,13.3166667 cm,hina-winde,Hina-Winde,12,,10.3333333,13.8666667 cm,mangengues,Mangengués,11,,3.5666667,10.7 cm,roum,Roum,11,,4.7333333,11.1333333 cn,beizhe,Beizhe,07,,23.904545,117.180579 cn,boniaoliangzi,Boniaoliangzi,32,,27.586911,102.53635 cn,cangqian,Cangqian,03,,26.762918,115.512334 cn,changyuho,Changyuho,24,,38.033333,113.183333 cn,chienyangtungtun,Chienyangtungtun,19,,39.9825,124.201389 cn,chingchiang,Chingchiang,32,,28.666667,103.966667 cn,chuchaichen,Chuchaichen,23,,31.21,121.287778 cn,chulin,Chulin,01,,33.141667,116.491667 cn,dadingmiao,Dadingmiao,25,,37.819167,117.347222 cn,dadipo,Dadipo,24,,39.181423,113.806135 cn,daotiejin,Daotiejin,26,,33.796955,106.997045 cn,datiankan,Datiankan,32,,28.662104,105.524478 cn,diaoya,Diaoya,09,,33.601134,112.001099 cn,donghuangnigang,Donghuangnigang,19,,41.113611,123.578056 cn,fujiutian,Fujiutian,30,,21.851963,110.158339 cn,ganpo,Ganpo,15,,35.350817,105.457115 cn,gaoqiaogou,Gaoqiaogou,26,,34.277172,107.4934 cn,heichiaotang,Heichiaotang,20,,41.396944,109.970278 cn,hengdaohezi,Hengdaohezi,05,,41.98507,127.117715 cn,hengshanxia,Hengshanxia,02,,30.114804,119.558857 cn,hojih,Hojih,06,,35.23178,100.986863 cn,houjiaping,Houjiaping,15,,33.196254,104.88896 cn,hsanghawng,Hsanghawng,29,,24.220852,97.786834 cn,huogusi,Huogusi,25,,35.029444,116.2725 cn,huomeichong,Huomeichong,11,,25.658611,112.15 cn,huxibei,Huxibei,07,,24.89095,116.767413 cn,jiajiapu,Jiajiapu,15,,35.682833,105.405902 cn,jiangtang,Jiangtang,02,,29.059295,119.450944 cn,jiuli,Jiuli,11,,29.694685,111.585702 cn,joerhkai,Joerhkai,32,,33.577954,102.964115 cn,kiucheng,Kiucheng,10,,38.188889,117.305556 cn,kuangshantsun,Kuangshantsun,10,,36.814722,114.175833 cn,laojunmiao,Laojunmiao,24,,39.296092,112.550023 cn,lichiaho,Lichiaho,26,,34.183333,110.3 cn,liumengpian,Liumengpian,31,,19.367391,110.330596 cn,malangou,Malangou,26,,33.34417,106.723814 cn,mansian,Mansian,15,,34.668074,104.482618 cn,maochang,Maochang,31,,19.489984,110.183741 cn,meixing,Meixing,32,,31.001069,102.36435 cn,namyung,Namyung,30,,25.116667,114.3 cn,nencheng,Nencheng,08,,49.183333,125.216667 cn,niuwanxu,Niuwanxu,30,,22.438852,112.842622 cn,panpanchiao,Panpanchiao,02,,30.384722,119.871389 cn,pinghsiho,Pinghsiho,26,,32.940514,106.435112 cn,pingle,Pingle,32,,29.013199,103.774636 cn,qiaogoucun,Qiaogoucun,26,,34.038151,109.600844 cn,renjia,Renjia,03,,28.661013,115.673105 cn,shanchungtsun,Shanchungtsun,24,,36.4,110.95 cn,shen,Shen,10,,38.008889,115.548056 cn,shihchichan,Shihchichan,11,,26.275278,111.433611 cn,shixi,Shixi,31,,19.243056,109.547467 cn,sotang,Sotang,14,,30.033333,95.25 cn,tsaiyuan,Tsaiyuan,05,,41.337513,125.712972 cn,tsaokota,Tsaokota,24,,34.919167,110.609444 cn,tuomu,Tuomu,32,,28.413721,103.069972 cn,wanshan,Wanshan,25,,36.574167,115.418333 cn,weiqu,Weiqu,26,,34.151111,108.945 cn,wenlongbao,Wenlongbao,26,,34.792778,108.957778 cn,wuguantang,Wuguantang,04,,31.661885,119.957061 cn,wuzi,Wuzi,15,,34.163942,103.272371 cn,xiaer,Xiaer,32,,29.005935,101.514394 cn,xiaogujiazi,Xiaogujiazi,19,,40.770014,122.295383 cn,xiasiqiao,Xiasiqiao,04,,31.660073,118.987201 cn,xingjialong,Xingjialong,12,,29.892625,115.324057 cn,yangfangying,Yangfangying,24,,37.493333,112.361389 cn,yaojiaxitou,Yaojiaxitou,12,,30.686247,112.963439 cn,yufenfang,Yufenfang,08,,44.993829,127.468469 cn,zhangqing,Zhangqing,24,,37.646389,112.652222 cn,zhongying,Zhongying,18,,26.066667,105.083333 cn,zhoujiagou,Zhoujiagou,19,,40.635278,125.143056 cn,zhoulichang,Zhoulichang,32,,29.893445,105.130617 cn,zhoulijia,Zhoulijia,12,,30.089564,114.308771 co,arrayan,Arrayán,33,,4.556686,-73.933675 co,bruselas,Bruselas,16,,1.77413,-76.172175 co,buenas aires,Buenas Aires,20,,1.58114,-77.077742 co,caserio el cobado,Caserío El Cobado,35,,10.15,-75.5 co,flores,Flores,02,,8.114845,-74.778523 co,guaspucal,Guaspucal,20,,1.043608,-77.440083 co,horta,Horta,26,,6.183333,-73.866667 co,manuel pastrana,Manuel Pastrana,12,,8.806667,-76.300278 co,tipacoque,Tipacoque,36,1036,6.42031,-72.691842 cr,el arbolito,El Arbolito,03,,9.883333,-85.433333 cu,guasimal,Guasimal,05,,20.9833333,-78.05 cu,islena,Isleña,01,,22.5,-83.5666667 cu,pueblo nuevo,Pueblo Nuevo,07,,21.6758333,-78.8875 cu,reparto sans souci,Reparto Sans Souci,02,,23.0602778,-82.4480556 cv,ribeira da barca,Ribeira da Barca,17,,15.1333333,-23.7666667 cy,perahorya,Perahorya,04,,35.0166667,33.3875 cz,kochanov,Kochanov,88,,49.794484,14.779141 cz,lazne libverda,Lazne Libverda,83,,50.891031,15.20255 cz,medlanka,Medlanka,78,,49.242113,16.571495 cz,suchdol,Suchdol,79,,48.682859,14.447513 de,badanhausen,Badanhausen,02,,49.016667,11.45 de,durnod,Dürnöd,02,,48.35,13.3 de,edenserloog,Edenserloog,06,,53.666667,7.733333 de,enghausen,Enghausen,02,,48.533333,11.9 de,englreiching,Englreiching,02,,48.75,13.183333 de,forsterei kesselsohl,Försterei Kesselsohl,14,,52.366667,11.616667 de,frenke,Frenke,06,,52.016667,9.45 de,goldbach,Goldbach,01,,47.783333,9.15 de,hardesby,Hardesby,10,,54.75,9.65 de,helmbacher,Helmbacher,08,,49.35,7.983333 de,korle,Körle,05,,51.166667,9.516667 de,lessau,Lessau,02,,49.916667,11.7 de,leyh,Leyh,02,,49.45,11.016667 de,marsmeir,Marsmeir,02,,48.133333,12.1 de,neuenothe,Neuenothe,07,,51.016667,7.716667 de,polvitz-neuemuhle,Polvitz-Neuemühle,14,,52.483333,11.416667 de,schaalby,Schaalby,10,,54.55,9.633333 de,schwenow,Schwenow,11,,52.15,14.05 de,sebexen,Sebexen,06,,51.816667,10.033333 de,seeon-seebruck,Seeon-Seebruck,02,,47.966667,12.466667 de,torring,Törring,02,,48.016667,12.75 de,tullingen,Tüllingen,01,,47.602065,7.64474 de,vielstedt,Vielstedt,06,,53.083333,8.466667 de,wallhofen,Wallhöfen,06,,53.316667,8.883333 de,wehe,Wehe,07,,52.466667,8.666667 dk,oster gammelby,Øster Gammelby,21,,54.99548,8.766006 dk,snevre,Snevre,20,,55.64832,11.621644 do,el ingenio abajo,El Ingenio Abajo,25,,19.5,-70.75 do,las uvas,Las Uvas,30,,19.3166667,-70.4666667 dz,agneb,Agneb,13,,33.9833333,1.5833333 dz,djillali ben amar,Djillali Ben Amar,51,,35.4455556,0.8508333 dz,douar ouled ali,Douar Ouled Ali,26,,35.3736111,0.55 dz,fleurus,Fleurus,09,,35.7111111,-0.4419444 dz,neurkache,Neurkache,35,,36.5833333,2.2333333 dz,zemoura,Zemoura,39,,36.2694444,4.8472222 ee,piiometsa,Piiometsa,04,,58.8822222,25.2841667 ee,sinalepa asundus,Sinalepa Asundus,07,,58.8266667,23.5947222 ee,terepi,Terepi,12,,58.1966667,27.2722222 ee,uearu,Uearu,01,,59.2469444,25.3569444 eg,`atf abu gindi,`Atf Abu Gindi,05,,30.9044444,30.9219444 eg,`izbat mastaruh,`Izbat Mastaruh,21,,31.4802778,30.7019444 eg,al bajhur,Al Bajhur,10,,28.6708333,30.7513889 eg,kafr tidah,Kafr Tidah,21,,31.2144444,30.8525 eg,nag` el-zuqeim muhafazet abd alla,Nag` el-Zuqeim Muhafazet Abd Alla,23,,25.9833333,32.8333333 es,ballestro,Ballestro,54,,38.841789,-2.456085 es,besians,Besiáns,52,,42.28211,.354326 es,cabeza de campo,Cabeza de Campo,55,,42.545833,-6.881131 es,cangues,Cangues,58,,42.472425,-8.089597 es,caserio pastranas,Caserío Pastranas,51,,36.733333,-6.4 es,denia,Denia,60,38953,38.841344,.10797 es,luama,Luama,58,,43.693873,-7.858789 es,morente,Morente,34,,43.332049,-5.838774 es,villardiga,Villárdiga,55,,41.819496,-5.464391 et,addi aro,Addi Aro,53,,13.216446,39.256772 et,argebet,Argebet,46,,11.533333,39.116667 et,awala bate,Awala Bate,51,,9.8,38.033333 et,dembeccia,Dembeccia,46,,10.55,37.483333 et,derouda ler,Derouda Ler,52,,7.698333,46.992778 et,kachisi,Kachisi,51,,9.6,37.833333 et,may dema,May Dema,53,,13.035871,39.693719 fi,hyrsyla,Hyrsylä,13,,60.383333,24.033333 fi,hyvolankyla,Hyvölänkylä,14,,63.233333,28.383333 fi,pasto,Pasto,15,,62.616667,23.133333 fi,pelkosenniemi,Pelkosenniemi,06,,67.110833,27.510556 fi,pietarinmaki,Pietarinmäki,13,,60.372778,24.773611 fi,storsvik,Störsvik,13,,60.066667,24.283333 fm,vinau,Vinau,04,,9.5297222,138.0922222 fr,albiac,Albiac,B3,,43.551701,1.78024 fr,beauvais,Beauvais,A8,,48.496127,2.467986 fr,brolles,Brolles,A8,,48.475205,2.691743 fr,claire-fontaine,Claire-Fontaine,A4,,49.427756,4.738675 fr,cleguerec,Cléguérec,A2,,48.125775,-3.071616 fr,courbette,Courbette,A6,,46.595845,5.565111 fr,craches,Craches,A3,,48.556575,1.810971 fr,crossac,Crossac,B5,,47.411195,-2.169522 fr,cuzoul,Cuzoul,B3,,44.750467,1.695915 fr,foret,Forêt,A7,,49.224227,1.529738 fr,la gautrais,La Gautrais,B5,,47.380565,-2.114972 fr,la mare goubert,La Mare Goubert,A7,,49.63772,.182824 fr,la ricamarie,La Ricamarie,B9,7527,45.416843,4.311988 fr,lalleyriat,Lalleyriat,B9,,46.150552,5.711573 fr,le chatellier,Le Châtellier,99,,48.676035,-.581481 fr,les platrieres,Les Platrières,B8,,43.55,5.433333 fr,pleine-seve,Pleine-Sève,A7,,49.817949,.754915 fr,rilly-sainte-syre,Rilly-Sainte-Syre,A4,,48.444761,3.956918 fr,roguinet,Roguinet,A3,,47.208093,.12752 fr,saint-mards-de-fresne,Saint-Mards-de-Fresne,A7,,49.077675,.46703 fr,santa-maria-maddalena,Santa-Maria-Maddalena,B8,,44.1,7.5 fr,subligny,Subligny,A3,,47.402951,2.755044 fr,vauciennes,Vauciennes,A4,,49.05,3.883333 ga,akoumounou,Akoumounou,08,,-1.6666667,9.35 gb,farnham,Farnham,E4,,51.9,.15 gb,folkton,Folkton,J7,,54.2,-.383333 gb,llanddowror,Llanddowror,X7,,51.801667,-4.531389 gb,patcham,Patcham,B6,,50.85,-.15 gb,pembrey,Pembrey,X7,,51.689167,-4.277222 gb,roch,Roch,Y7,,51.846944,-5.086667 gb,seven sisters,Seven Sisters,Y5,,51.766667,-3.716667 gb,thatcham,Thatcham,P4,24275,51.4,-1.266667 gb,westbury on severn,Westbury on Severn,E6,,51.816667,-2.416667 ge,sargveshi,Sargveshi,27,,42.0730556,43.2625 ge,shimokmedi,Shimokmedi,40,,41.9125,42.0925 gf,couriege,Couriège,00,,4.8,-53.2833333 gh,adeemmra,Adeemmra,02,,6.6,-2.05 gh,adugyansu akomada,Adugyansu Akomada,03,,7.3666667,-2.0333333 gh,alabolaboe,Alabolaboe,08,,6.1166667,0.2166667 gh,gupaneirigu,Gupaneirigu,06,,9.4833333,-0.95 gh,gyedua,Gyedua,09,,4.85,-2.0 gh,kuguri,Kuguri,10,,10.7833333,-0.2833333 gh,niflapo,Niflapo,08,,6.1333333,0.1666667 gh,takuve,Takuve,08,,6.45,0.7333333 gh,togodo,Togodo,08,,6.0997222,1.1097222 gh,zawire,Zawire,06,,9.4666667,-0.85 gl,ikerasagssuaq,Ikerasagssuaq,02,,65.5666667,-37.1166667 gm,makama,Makama,05,,13.6,-15.3166667 gn,dangoura,Dangoura,19,,10.8333333,-9.5 gn,nafaguile,Nafaguilé,26,,10.8,-8.75 gq,bantabare pequeno,Bantabaré Pequeño,05,,3.45,8.7833333 gq,bedoum,Bedoum,07,,2.15,11.0833333 gq,bokoricho-balacha,Bokoricho-Balachá,05,,3.4,8.5666667 gq,nsangayong,Nsangayong,09,,1.4166667,11.3 gr,ano vaskina,Áno Vaskína,41,,37.2,22.7833333 gr,bambalio,Bambalió,31,,38.8166667,21.3333333 gr,laringovi,Laringóvi,15,,40.4819444,23.5927778 gr,pirgos,Pírgos,48,,37.7116667,26.8041667 gr,porro mesoyia,Pórro Mesóyia,43,,35.4666667,23.6 gr,satobasi,Satóbasi,21,,39.7833333,22.4 gr,vakkhon,Vákkhon,41,,37.4666667,22.2 gt,la haciendita,La Haciendita,17,,15.133333,-92.083333 gt,patut,Patut,20,,14.4,-91.516667 gt,quimal,Quimal,03,,14.833333,-90.766667 gt,san agustin,San Agustín,17,,15.4,-90.283333 gt,san antonio suchitepequez,San Antonio Suchitepéquez,20,10726,14.533333,-91.416667 gw,quentico,Quenticó,04,,12.1,-15.2666667 gw,umaro bela,Umaro Bela,10,,12.4666667,-14.1333333 hk,so lo pun,So Lo Pun,00,,22.5333333,114.25 hn,alto verde,Alto Verde,17,,13.4405556,-87.4516667 hn,carrizalio,Carrizalio,08,,13.7166667,-87.2833333 hn,el tablado,El Tablado,02,,13.5738889,-87.3461111 hn,miraflores,Miraflores,02,,13.2833333,-86.75 hr,butkovici,Butkovici,04,,45.0097222,13.9091667 hr,miljci,Miljci,15,,43.5905556,16.9802778 hr,moravice srpske,Moravice Srpske,12,,45.4333333,15.0333333 ht,bayado,Bayado,08,,18.4833333,-73.4833333 ht,johanisse,Johanisse,06,,19.3333333,-72.5833333 ht,lochard,Lochard,11,,18.4333333,-72.6333333 ht,nan palmiste,Nan Palmiste,13,,18.2638889,-72.0816667 ht,salor,Salor,08,,18.4166667,-73.2333333 hu,alsoszallas,Alsószállás,10,,47.6,21.766667 hu,kulsofecskespuszta,Kulsofecskespuszta,03,,46.383333,20.816667 hu,miklospuszta,Miklóspuszta,04,,48.316667,20.4 hu,noszlopi tanya,Noszlopi Tanya,23,,47.183333,17.466667 hu,pillingerpuszta,Pillingérpuszta,09,,47.7,17.533333 hu,szantoresz,Szántórész,20,,46.8,20.133333 hu,ujpetre,Újpetre,02,,45.936667,18.3625 id,aluw bateu,Aluw Bateu,01,,4.8798,97.8397 id,baheesioena,Baheesioena,21,,-.7576,122.9605 id,balokan,Balokan,08,,-8.388985,114.119839 id,banjar umesalakan,Banjar Umesalakan,02,,-8.5538,115.3847 id,bantannyuh kelod,Bantannyuh Kelod,02,,-8.4289,115.6138 id,bantarkalong,Bantarkalong,30,,-6.856667,107.328889 id,batangwarak,Batangwarak,08,,-7.7525,111.371944 id,benjaran,Benjaran,10,,-8.046111,110.447778 id,blok lebak,Blok Lebak,30,,-6.705,108.351389 id,busak satu,Busak Satu,21,,1.2484,121.3598 id,cieurih satu,Cieurih Satu,30,,-7.2148,108.3551 id,galuwalakari,Galuwalakari,18,,-9.5987,119.5329 id,glodogan,Glodogan,07,,-7.220278,110.4175 id,kalagheng,Kalagheng,31,,3.4992,125.6376 id,kaliuwi,Kaliuwi,22,,-4.2571,122.1234 id,kayut,Kayut,07,,-7.0949,111.1515 id,kedewatan,Kedewatan,02,,-8.4265,114.9506 id,kedungdawa,Kedungdawa,07,,-6.927,109.0426 id,kembangrejo,Kembangrejo,07,,-7.565556,110.510278 id,kunciran,Kunciran,30,,-6.219444,106.667778 id,limboeng,Limboeng,38,,-2.5719,119.913 id,lowayu,Lowayu,08,,-6.9737,112.4192 id,manggarawan 2,Manggarawan 2,15,,-5.168356,105.634634 id,moeara boelan,Moeara Boelan,14,,1.2,117.55 id,pabuwaranjarak,Pabuwaranjarak,30,,-6.098333,106.598611 id,pajagan tonggoh,Pajagan Tonggoh,30,,-6.911389,106.607222 id,pandangalor,Pandangalor,24,,-.490279,100.183306 id,pangerian,Pangerian,08,,-7.0584,112.8758 id,patan bili,Patan Bili,01,,3.2013,97.2899 id,pekandelan,Pekandelan,02,,-8.3468,115.6151 id,pematangkelukut,Pematangkelukut,01,,3.5717,99.0311 id,pilar satu,Pilar Satu,30,,-6.566944,106.756667 id,ramung,Ramung,26,,3.851,97.5111 id,ruda,Ruda,18,,-8.7162,121.0827 id,sabrang-lor,Sabrang-lor,07,,-7.4775,110.351667 id,sangen lor,Sangen Lor,07,,-7.494167,110.718889 id,sepuntuk,Sepuntuk,07,,-7.55,111.080833 id,setukalibiru,Setukalibiru,07,,-6.9234,109.1546 id,sibodadi,Sibodadi,01,,4.3061,98.1829 id,sumbermanggis,Sumbermanggis,08,,-7.9215,111.8586 id,tegineneng,Tegineneng,15,,-5.20365,105.176144 id,temorleke,Temorleke,08,,-7.1141,112.7878 id,tjigalontjang,Tjigalontjang,30,,-7.3224,108.0052 id,urutsewu,Urutsewu,07,,-7.439444,110.536389 id,villabukutbandung,Villabukutbandung,30,,-6.928056,107.748333 id,warmandi,Warmandi,39,,-.366667,132.65 ie,bunnagee,Bunnagee,06,,54.9419444,-7.6941667 ie,dun leire,Dún Léire,19,,53.835,-6.3961111 ie,kilbraney,Kilbraney,30,,52.3244444,-6.8347222 ie,mullaghnaneane,Mullaghnaneane,25,,54.3755556,-8.5316667 ie,sandymount,Sandymount,07,,53.335,-6.2113889 in,astagaon,Astagaon,16,,19.666667,74.5 in,bagra,Bagra,24,,25.2,72.583333 in,binewal,Binewal,23,,31.283333,76.25 in,gangolli,Gangolli,19,,13.65,74.666667 in,hadgaon buzurg,Hadgaon Buzurg,16,,19.316667,76.333333 in,haftan,Haftan,12,,34.368056,73.85 in,kangra,Kangra,11,9159,32.1,76.266667 in,katghara brahmanan,Katghara Brahmanan,36,,26.4778,79.3579 in,madawara,Madawara,24,,25.366667,76.15 in,nal ka talav,Nal ka Talav,24,,25.138611,75.612222 in,pasrai,Pasrai,36,,25.5023,79.0269 in,prahladpur,Prahladpur,36,,25.433333,83.45 in,sehral,Sehral,35,,24.9405,78.8782 in,siryasar,Siryasar,24,,28.0771,75.314 in,watwanpur,Watwanpur,12,,34.1625,74.563889 iq,bahriz,Bahriz,10,,33.7055556,44.6505556 iq,chamsayda,Chamsayda,08,,37.1913889,43.2611111 iq,dewana,Dewana,05,,35.1788889,45.5725 iq,hizana,Hizana,08,,36.8583333,43.6902778 iq,hulwan,Hulwan,10,,34.3330556,45.2008333 iq,sayyid `abd `ali,Sayyid `Abd `Ali,09,,30.9236111,46.7258333 iq,suse-i kon,Suse-i Kon,05,,35.7672222,45.1083333 iq,wihab,Wihab,01,,33.3508333,44.1347222 ir,`abbasabad-e jadid,`Abbasabad-e Jadid,42,,35.035604,59.295116 ir,aqkul,Aqkul,34,,35.11,49.4675 ir,atmiyan,Atmiyan,33,,38.1687,47.2848 ir,banehdar-e `aziz,Banehdar-e `Aziz,13,,34.9126,45.8788 ir,benuk,Benuk,04,,27.395708,60.82304 ir,bushkan,Bushkan,07,,30.06,52.44 ir,chah-e sang,Chah-e Sang,29,,28.5027,56.0173 ir,chashmeh-e sabz,Chashmeh-e Sabz,29,,29.464444,56.425 ir,dar mian-e nahr,Dar Mian-e Nahr,29,,30.480343,57.320026 ir,daraj-e `olya,Daraj-e `Olya,42,,33.394317,60.171259 ir,emameh-ye bala,Emameh-ye Bala,39,,34.9,51.583333 ir,gaisur bala,Gaisur Bala,42,,34.266839,59.261859 ir,garang,Garang,29,,28.783333,56.816667 ir,gelyard,Gelyard,35,,36.61119,52.943054 ir,gham towrqeh,Gham Towrqeh,16,,36.277,47.8217 ir,givshad,Givshad,41,,32.654574,59.095305 ir,gowhar,Gowhar,13,,34.0266,46.329 ir,hajji `abbasi,Hajji `Abbasi,11,,27.226667,57.051944 ir,jenablu,Jenablu,32,,38.65,48.116667 ir,kalateh-ye avaz,Kalateh-ye Avaz,41,,31.648467,59.419373 ir,kallu,Kallu,36,,36.0131,48.9319 ir,kat cheshmeh,Kat Cheshmeh,35,,36.540233,53.957785 ir,kazab,Kazab,04,,25.566667,61.316667 ir,khanqah-e pa'in,Khanqah-e Pa'in,32,,38.4,48.333333 ir,kora'i-ye bala,Kora'i-ye Bala,15,,31.805,49.1985 ir,kurak,Kurak,42,,37.358183,59.14337 ir,kushk-e esma`ilabad,Kushk-e Esma`ilabad,07,,29.013,52.5672 ir,mir hashim,Mir Hashim,40,,31.461888,54.253496 ir,mo'asseseh-ye hajji khan dusti,Mo'asseseh-ye Hajji Khan Dusti,15,,31.537778,48.890278 ir,morad tappeh,Morad Tappeh,44,,35.7336,50.2919 ir,mowmenabad,Mowmenabad,25,,35.544364,53.297962 ir,mozdaran,Mozdaran,42,,36.156078,60.53095 ir,owli qeshlaq,Owli Qeshlaq,33,,39.125,47.1958 ir,ramzeyeh-ye yek,Ramzeyeh-ye Yek,15,,31.2534,48.4366 ir,salehabad,Salehabad,36,,35.9994,48.4241 ir,sarbir,Sarbir,11,,26.366667,58.033333 ir,sardehat ja`far,Sardehat Ja`far,36,,36.9649,48.0357 ir,seku mahalleh,Seku Mahalleh,15,,32.716667,48.5 ir,shah kandi,Shah Kandi,09,,34.64973,48.79507 ir,shambrakan,Shambrakan,05,,30.625567,50.725715 ir,sharafabad,Sharafabad,05,,30.682852,51.552946 ir,shekarkash mahalleh,Shekarkash Mahalleh,08,,37.09522,50.2575 ir,takab,Takab,15,,32.739997,48.283737 it,borgomale,Borgomale,12,,44.616667,8.133333 it,bovisa,Bovisa,09,,45.5,9.15 it,castellar,Castellar,12,,44.916667,8.966667 jm,cambridge,Cambridge,15,,18.4333333,-77.6 jo,mote,Mote,09,,31.0925,35.694444 jp,akagawa,Akagawa,12,,42.07,139.455 jp,hikimoto,Hikimoto,23,,34.1,136.233333 jp,kakazi,Kakazi,30,,33.666667,131.516667 jp,kami-kanagawa,Kami-kanagawa,46,,35.683333,138.683333 jp,kataoka,Kataoka,35,,35.05,135.933333 jp,katsura,Katsura,22,,34.983333,135.7 jp,kawara,Kawara,18,,30.333333,130.383333 jp,mukai-awagasaki,Mukai-awagasaki,15,,36.633333,136.633333 jp,obatake,Obatake,11,,34.7,133.25 jp,osame,Osame,03,,40.966667,141.373333 jp,oura,Oura,21,,32.516667,130.366667 jp,yunogo,Yunogo,31,,34.983333,134.133333 kh,kanchung,Kanchung,03,,11.9833333,104.7666667 kh,khum thma edth,Khum Thma Edth,03,,11.9666667,104.6666667 kh,krieng,Krieng,09,,12.6333333,105.9833333 kh,phumi krang kor,Phumi Krang Kor,19,,11.4333333,104.8666667 kh,phumi svay phaem,Phumi Svay Phaem,06,,10.75,104.5166667 kh,trapeang reang,Trapeang Reang,08,,11.2,103.7666667 kp,chongjadong,Chongjadong,06,,38.0088889,126.2588889 kp,hwajonni,Hwajonni,13,,41.4591667,128.2313889 kp,morori,Morori,01,,40.5416667,125.6416667 kp,saemaul,Saemaul,03,,39.7833333,127.4 kp,saetomaul,Saetomaul,06,,37.9641667,126.1925 kp,somunsi,Somunsi,11,,39.7111111,124.6663889 kp,taikayodo,Taikayodo,03,,41.075,128.9047222 kp,takkol,Takkol,07,,38.5219444,126.1538889 kp,tangchon,Tangchon,06,,38.3688889,125.0927778 kp,unjong,Unjong,14,,38.8383333,125.4111111 kp,wolpyongdong,Wolpyongdong,01,,40.8636111,126.0211111 kp,yanghari,Yanghari,03,,39.4519444,127.3533333 kp,yangsandong,Yangsandong,06,,38.2875,125.2075 kp,yokchidong,Yokchidong,11,,40.2733333,125.2752778 kr,andongni,Andongni,05,,37.0353,128.3161 kr,chonwolli,Chonwolli,03,,35.504167,126.793611 kr,gessho,Gessho,16,,34.581389,126.920556 kr,hambakkol,Hambakkol,20,,35.124799,128.452157 kr,hangyangtcheng,Hangyangtcheng,11,,37.5985,126.9783 kr,hasimdong,Hasimdong,17,,36.166667,126.65 kr,kombawi,Kombawi,12,,37.566111,126.675278 kr,majae,Majae,17,,36.3962,126.8927 kr,myongjuri,Myongjuri,16,,34.558611,126.856944 kr,naesadong,Naesadong,06,,37.77884,128.19417 kr,paenamukol,Paenamukol,17,,36.6484,126.8035 kr,taegok,Taegok,06,,37.201491,128.056638 kz,kondrat'yevo,Kondrat'yevo,15,,49.65,83.766667 kz,zamorenov,Zamorenov,07,,51.466667,51.483333 la,ban lamphen,Ban Lamphén,06,,20.025556,100.607222 la,ban samsao noy,Ban Samsao Noy,14,,18.75,103.3 la,same din,Same Din,03,,20.583333,104.1 lb,haret et tahta,Hâret et Tahta,03,,34.1833333,35.9 lk,bayagama,Bayagama,32,,7.7,80.2666667 lk,imiyangoda ihala,Imiyangoda Ihala,32,,7.6,80.25 lk,opata,Opata,34,,6.1,80.2 lr,gohnsua,Gohnsua,05,,7.7758333,-10.1786111 lt,milkunai,Milkunai,60,,55.1833333,26.4333333 lt,podvorishki,Podvorishki,56,,54.4333333,24.1333333 lv,duci,Duci,18,,57.3405556,24.4433333 lv,goveiki,Goveiki,19,,56.4166667,27.6666667 lv,jaungulbenes,Jaungulbenes,09,,57.0666667,26.6 lv,mazbrenguli,Mazbrenguli,31,,57.5166667,25.1333333 lv,sturi,Sturi,27,,56.6,22.75 ma,afauzouart,Afauzouart,55,,30.46,-9.5 ma,ait ali ou youssef,Aït Ali Ou Youssef,56,,32.104818,-6.186068 ma,ait arbi,Aït Arbi,58,,34.96,-4.53 ma,ait arfa,Aït Arfa,56,,32.005265,-6.650892 ma,ait ktab oufella,Aït Ktab Oufella,47,,30.885146,-9.099046 ma,amouguer,Amouguer,48,,30.784107,-5.233821 ma,andouz,Andouz,55,,31.018936,-7.81263 ma,daouina,Daouïna,58,,34.456752,-5.252795 ma,el khetarte n'ouzreg,El Khetarte n'Ouzreg,48,,31.228075,-5.211919 ma,imzil,Imzil,55,,30.521623,-8 ma,tarjicht,Tarjicht,53,,29.058273,-9.428775 ma,tioukarine,Tioukarine,55,,30.631695,-7.711785 md,buschi,Buschi,58,,47.7175,29.077778 md,romanesti,Romanesti,61,,46.331111,28.972778 mg,ankiakabe,Ankiakabe,03,,-14.75,48.9333333 mg,mitanonoka,Mitanonoka,04,,-17.7666667,48.9 mg,sahantaha,Sahantaha,01,,-15.05,50.35 mg,tembizoka,Tembizoka,02,,-23.6,47.0166667 mk,dlabocica,Dlabocica,52,,42.1986111,22.2413889 mk,dzepista,Dzepista,21,,41.4427778,20.5327778 ml,riziam,Riziam,07,,14.4833333,-5.9833333 mm,chaungnakwa,Chaungnakwa,13,,16.1655556,97.9675 mm,hwehawn,Hwehawn,11,,21.3666667,97.5 mm,kamahta,Kamahta,05,,17.3333333,97.8666667 mm,laikawng,Laikawng,04,,25.7666667,97.8 mm,loi-mawt,Loi-mawt,11,,23.4,98.1666667 mm,makaukpat,Makaukpat,10,,24.8,94.7833333 mm,natchaung,Natchaung,05,,16.0,98.15 mm,suangphei,Suangphei,02,,23.15,93.8333333 mm,ta-ni-la-le,Ta-ni-la-lè,06,,19.5166667,97.2333333 mm,thedangyi ywa,Thedangyi Ywa,12,,11.55,98.7333333 mn,baatar uan huryee,Baatar Uan Hüryee,21,,49.4166667,102.7 mw,muyala,Muyala,11,,-13.9833333,33.45 mw,zebediya kaunda,Zebediya Kaunda,21,,-11.5,33.7 mx,alonso osorio,Alonso Osorio,20,,16.183333,-95.166667 mx,amatitan,Amatitán,14,9791,20.833333,-103.716667 mx,capulhuac,Capulhuac,15,20517,19.191389,-99.466944 mx,casitas,Casitas,30,,20.25,-96.783333 mx,el botadero,El Botadero,18,,21.75,-105.266667 mx,el coacoyul,El Coacoyul,12,,16.766667,-99.5 mx,el coyul,El Coyul,20,,15.926389,-95.8125 mx,el laberinto,El Laberinto,27,,17.75,-91.3 mx,el limoncito,El Limoncito,15,,18.433333,-100.233333 mx,el lindero,El Lindero,11,,20.95,-101.633333 mx,el sonoreno,El Sonoreño,06,,28.433333,-106.783333 mx,hacienda san jose,Hacienda San José,07,,25.7,-101.716667 mx,la palma leada,La Palma Leada,12,,17.9,-101.733333 mx,la pinuela,La Piñuela,16,,18.736111,-100.994444 mx,la zarza,La Zarza,20,,16.118056,-94.195278 mx,los acosta,Los Acosta,27,,17.945833,-92.9 mx,los alisos,Los Alisos,11,,21.210833,-101.519444 mx,playa lauro villar,Playa Lauro Villar,28,,25.833333,-97.15 mx,polvoron,Polvorón,26,,27.366667,-110.2 mx,providencia de la parrita,Providencia de la Parrita,19,,24.9,-99.283333 mx,rancho el veracruzano,Rancho El Veracruzano,10,,26.65,-104.216667 mx,rancho rosas,Rancho Rosas,06,,28.35,-108.3 mx,rancho san jose de los brazos,Rancho San José de los Brazos,28,,26.816667,-99.366667 mx,rancho virgen,Rancho Virgen,26,,29.083333,-110 mx,ri playas,Rí Playas,30,,17.175,-93.841667 mx,rio del trovador,Río del Trovador,20,,16.55,-95.466667 mx,san esteban,San Esteban,21,,18.45,-97.283333 mx,san juan de amargos,San Juan De Amargos,07,,25.933333,-101.016667 mx,santo domingo,Santo Domingo,03,,25.491111,-111.917222 mx,santo domingo tonaltepec,Santo Domingo Tonaltepec,20,,17.606111,-97.361389 mx,yervanis,Yervanís,10,,24.737778,-103.843056 my,kampong batu sawar,Kampong Batu Sawar,06,,3.49,103.1245 my,kampong long kiput,Kampong Long Kiput,11,,4.016667,114.383333 my,kampong machang,Kampong Machang,03,,6.066667,102.15 my,kampong nangar,Kampong Nangar,11,,2.566667,111.416667 my,kampung menawo ulu,Kampung Menawo Ulu,16,,5.318,116.2017 my,kampung sama,Kampung Sama,06,,4.0345,101.9627 my,kampung sungai karas,Kampung Sungai Karas,01,,2.357,103.039 my,plaman buta,Plaman Buta,11,,1.35,110.1 my,taman perpaduan,Taman Perpaduan,07,,4.6361,101.1517 mz,alcolete,Alcolete,10,,-16.8138889,34.5558333 mz,chicoma,Chicoma,06,,-14.8611111,40.7625 mz,machachan,Machachan,02,,-24.4083333,33.1302778 mz,meteva,Meteva,06,,-14.4705556,40.4286111 na,epoko,Epoko,36,,-17.4666667,15.2 na,okahwa,Okahwa,32,,-18.7333333,13.8833333 na,tamsu,Tamsu,34,,-18.5833333,20.5666667 ne,dibilo,Dibilo,05,,14.2,0.7833333 ne,ouinditene,Ouinditéne,05,,13.7666667,2.9 ng,ajavwuni ugbevwe,Ajavwuni Ugbevwe,36,,5.878651,5.81072 ng,jauro tuku,Jauro Tuku,55,,10.067848,11.203937 ng,jegan maunde,Jegan Maunde,27,,9.283333,11.516667 ng,maidontoro tsofo,Maidontoro Tsofo,49,,9.716667,9.066667 ng,ndiavu,Ndiavu,45,,5.529419,7.752538 ng,rafin baura,Rafin Baura,40,,12.1,4.233333 ng,rikaka,Rikaka,51,,13.207485,5.667091 ng,sufa,Sufa,46,,12.1533,10.4289 ng,yaza,Yaza,35,,10.294981,13.257877 ng,zhirangwa,Zhirangwa,27,,10.436328,12.266269 ng,zideyeregbene,Zideyeregbene,36,,5.063034,5.764124 ni,el coquital,El Coquital,04,,11.9333333,-85.2 nl,overlangel,Overlangel,06,,51.776131,5.673488 nl,peelsehuis,Peelsehuis,06,,51.622605,5.666831 nl,vorenseinde,Vorenseinde,06,,51.533333,4.583333 no,bentsjord,Bentsjord,18,,69.533333,18.633333 no,bergby,Bergby,05,,70.15,28.916667 no,bo,Bø,20,,59.366667,10.283333 no,hareton,Hareton,01,,59.966667,11.5 no,hauskje,Hauskje,14,,59.216667,6.133333 no,mo,Mo,14,,59.483333,6.3 no,nore,Nore,04,,60.166667,9.016667 no,ristveit,Ristveit,04,,59.866667,9.833333 no,stordalsbugen,Størdalsbugen,16,,63.566667,9.783333 om,as samidah,As Samidah,02,,24.015,56.7527778 om,sur al `abri,Sur al `Abri,02,,24.6566667,56.5152778 pe,antashallalle,Antashallalle,08,,-14.6202778,-71.8877778 pe,cachipujo,Cachipujo,04,,-15.6033333,-71.0658333 pe,cauta,Cauta,04,,-15.5380556,-71.7955556 pe,chaclaya,Chaclaya,18,,-16.1666667,-70.55 pe,cruzcag,Cruzcag,10,,-10.3491667,-76.175 pe,hacienda casa blanca,Hacienda Casa Blanca,02,,-9.5,-78.1333333 pe,hacienda pabur,Hacienda Pabur,20,,-5.2197222,-80.0283333 pe,la palma hacienda,La Palma Hacienda,15,,-12.1166667,-77.0166667 pe,morullo,Morullo,21,,-17.15,-69.6166667 pe,ocona,Ocoña,04,,-16.4330556,-73.1077778 pe,san cristobal,San Cristóbal,10,,-10.2888889,-76.1919444 pe,sisiccaya,Sisiccaya,11,,-13.2,-75.75 pg,badilu,Badilu,12,,-4.8,146.2 pg,karum 2,Karum 2,08,,-6.3333333,145.1666667 pg,kokun,Kokun,12,,-4.3166667,144.6333333 pg,murupukio,Murupukio,06,,-7.6,143.2333333 ph,anibong,Anibong,33,,14.2149,121.4662 ph,bantolan,Bantolan,49,,10.787161,119.587405 ph,botong,Botong,11,,9.72256,124.212958 ph,katongkolan,Katongkolan,21,,10.7035,123.8079 ph,lambac,Lambac,33,,14.2569,121.4572 ph,lombayan,Lombayan,30,,10.6019,122.046 ph,ma-alan,Ma-alan,18,,11.35,122.883333 ph,manaois,Manaois,63,,15.6781,120.6004 ph,mocpoc norte,Mocpoc Norte,11,,9.883333,123.8 ph,munguia proper,Munguia Proper,48,,16.2971,121.1564 ph,oyungan,Oyungan,30,,10.625,122.1868 ph,san roque,San Roque,67,8637,12.533333,124.866667 ph,santa nino,Santa Niño,28,,17.9585,120.7145 ph,sominabang,Sominabang,16,,13.5223,122.9851 ph,tanawan,Tanawan,G8,,15.4017,121.3832 ph,tigbauan-maasin,Tigbauan-Maasin,30,,10.9197,122.4806 pk,al-flah town,Al-Flah Town,05,,24.8737,67.1635 pk,al-nur town,Al-Nur Town,04,,31.4762,74.3625 pk,badiana khurd,Badiana Khurd,04,,32.381462,74.6228 pk,badoke,Badoke,04,,32.266667,74.133333 pk,basto bagley adel khan,Basto Bagley Adel Khan,04,,29.365779,71.086617 pk,baware nau,Baware Nau,04,,32.121188,73.619848 pk,dansai jo,Dansai Jo,02,,27.253188,66.58141 pk,dhok kufri,Dhok Kufri,04,,32.927387,72.334282 pk,faqiran,Faqiran,02,,30.573992,66.641243 pk,firoz kirio,Firoz Kirio,05,,26.12071,68.501826 pk,gaufgarh,Gaufgarh,04,,31.139443,72.651883 pk,goth haji shahdad khushk,Goth Haji Shahdad Khushk,05,,24.552889,67.951628 pk,goth iqbal memon,Goth Iqbal Memon,05,,26.872022,68.371149 pk,goth kativar,Goth Kativar,05,,24.893837,68.049333 pk,gujarke,Gujarke,04,,32.187504,73.455955 pk,hafizabad,Hafizabad,05,,27.869444,68.419444 pk,imamwali,Imamwali,04,,31.537373,70.961926 pk,kachchi kothi,Kachchi Kothi,04,,31.313889,72.923611 pk,kamand rial,Kamand Rial,04,,33.352188,73.298559 pk,khadang,Khadang,03,,34.539048,72.812872 pk,kormal,Kormal,03,,34.930182,72.533297 pk,kriplian,Kriplian,03,,34.290278,72.848333 pk,makaure da wahan,Makaure da Wahan,04,,28.672222,70.214168 pk,mamiam,Mamiam,04,,33.510432,73.502667 pk,mulla katyar,Mulla Katyar,05,,25.113501,68.364715 pk,nawi lali,Nawi Lali,05,,27.982468,68.666921 pk,nola mela,Nola Mela,03,,32.95,71.15 pk,pathai,Pathai,04,,32.495748,74.638372 pk,qadiwind,Qadiwind,04,,31.168333,74.481389 pk,ribatdeh,Ribatdeh,03,,36.223536,71.709809 pk,salem khan jatoi,Salem Khan Jatoi,04,,28.743112,70.397122 pk,sarangpur,Sarangpur,04,,32.320997,74.682243 pk,sodawal,Sodawal,02,,29.482936,66.533716 pk,taralai,Taralai,03,,34.502976,72.713994 pk,wari,Wari,05,,26.891045,67.772507 pl,alt proberg,Alt Proberg,85,,53.829783,21.369387 pl,brzostow,Brzostow,86,,51.964954,17.430097 pl,dabrowa rzeczycka,Dabrowa Rzeczycka,80,,50.656578,22.0443 pl,drawsko kreuz,Drawsko Kreuz,86,,52.853788,16.030228 pl,giecz,Giecz,86,,52.320284,17.37095 pl,greulich,Greulich,72,,51.360189,15.764806 pl,nowinki,Nowinki,75,,51.735047,22.337006 pl,rabka,Rabka,77,,49.616667,19.95 pl,rumunki orlowo,Rumunki Orlowo,73,,52.766667,19.3 pl,schmolz,Schmolz,72,,51.072615,16.881818 pl,slawa,Slawa,87,,53.779732,15.897946 pt,aves,Aves,17,8506,41.370339,-8.410104 pt,paul de cima,Paul de Cima,14,,38.957757,-9.380862 pt,tojal,Tojal,14,,39.083333,-9.083333 ro,banesti,Banesti,42,5636,45.1,25.766667 ro,buzahaza,Buzahaza,27,,46.6,24.833333 ro,durnesti,Durnesti,07,4144,47.766667,27.1 ro,fundu lui bogdan,Fundu lui Bogdan,04,,46.516667,26.966667 ro,kisseten,Kisseten,36,,45.75,21.733333 ro,lacu baban,Lacu Baban,40,,45.6,26.966667 ro,majdan,Majdan,07,,47.666667,26.833333 ro,poiana marului,Poiana Marului,34,,47.4,26.016667 ro,urcu,Urcu,12,,44.835,21.833611 rs,brezani,Brezani,00,,43.324167,21.339722 rs,brvenik,Brvenik,00,,42.811111,21.426389 rs,donja susaja,Donja Susaja,00,,42.348889,21.689167 rs,kosavc,Kosavc,00,,42.089722,20.702222 rs,novi varos,Novi Varos,00,,43.474444,19.287778 ru,abazinka,Abazinka,43,,52.371111,40.196667 ru,antoshkovo,Antoshkovo,60,,56.983154,28.044173 ru,arapovka,Arapovka,09,,50.516679,36.291054 ru,balabin,Balabin,61,,47.418038,40.905633 ru,bobury,Bobury,69,,55.652199,34.03744 ru,bogatyye ploty,Bogatyye Ploty,43,,52.2047,38.2142 ru,borovskaya,Borovskaya,85,,60.587912,41.427837 ru,bukharevo,Bukharevo,21,,57.556909,42.273752 ru,buolkalakh,Buolkalakh,63,,72.933333,119.833333 ru,chernaya,Chërnaya,25,,53.982356,33.865987 ru,chertushkino,Chertushkino,73,,55.158716,51.016071 ru,farkovo,Farkovo,39,,65.716667,86.966667 ru,feklino,Fëklino,13,,55.5303,62.5169 ru,goloshchapovka,Goloshchapovka,41,,51.8762,37.6731 ru,gozhan,Gozhan,90,,56.524983,55.197102 ru,grobovo,Grobovo,71,,56.821483,59.53978 ru,isavshchino,Isavshchino,62,,53.716667,40.683333 ru,iso paljarvi,Iso Paljärvi,85,,60.983333,36.35 ru,kashino,Kashino,76,,53.929543,38.850109 ru,kazarevo,Kazarevo,69,,55.563611,33.346667 ru,kishkinskaya,Kishkinskaya,88,,57.9084,38.3193 ru,"kolik""yegan","Kolik""yegan",32,,61.722778,79.123056 ru,komlevskaya,Komlevskaya,85,,60.485293,43.056553 ru,kozlanga,Kozlanga,85,,59.3,41.083333 ru,krasnovskoye,Krasnovskoye,39,,56.4273,90.4276 ru,krutoye,Krutoye,56,,52.128106,37.190897 ru,kurovshchino,Kurovshchino,72,,52.895727,42.422968 ru,larina,Larina,40,,56.3599,63.5814 ru,lavyn,Lavyn,60,,58.670289,28.513795 ru,lebyazhye,Lebyazhye,75,,58.4879,82.7014 ru,luch,Luch,62,,54.447421,41.652052 ru,lukhino,Lukhino,52,,57.8073,30.8975 ru,lysmanovo,Lysmanovo,90,,57.976,56.8267 ru,malaya buinka,Malaya Buinka,73,,54.952973,48.238905 ru,mamosovo,Mamosovo,62,,55.064485,40.754622 ru,maslyanovka,Maslyanovka,54,,55.200385,72.659897 ru,molotovskoye,Molotovskoye,70,,45.846038,41.518852 ru,moma,Moma,63,,66.45,143.1 ru,monastyrka,Monastyrka,31,,53.217778,91.758889 ru,munay,Munay,29,,53.2134,86.9009 ru,nizhniy matveyevskiy,Nizhniy Matveyevskiy,61,,49.5868,42.1059 ru,ogarkov,Ogarkov,84,,49.483333,45.733333 ru,orelye,Orelye,52,,58.9,31.7 ru,osintseva,Osintseva,13,,54.916667,60.466667 ru,pentilia,Pentilia,42,,60.347091,28.678761 ru,podskalnoye,Podskalnoye,27,,44.0828,40.9664 ru,pogost dolgiy,Pogost Dolgiy,52,,58.46124,34.89096 ru,polyanskiy,Polyanskiy,81,,53.4443,47.8257 ru,prigorki,Prigorki,77,,57.758729,37.034264 ru,rodniki,Rodniki,90,,58.483585,56.754205 ru,rudenka,Rudenka,25,,54.831648,35.471399 ru,rudnitsa,Rudnitsa,77,,56.484784,34.268454 ru,rutaka,Rutaka,64,,46.714445,142.529389 ru,safronova,Safronova,32,,60.516667,65.316667 ru,sashino,Sashino,85,,59.702724,38.141314 ru,shalava,Shalava,88,,57.338865,39.817663 ru,shuklino,Shuklino,10,,52.761042,33.805808 ru,shumilovka,Shumilovka,42,,59.267471,29.936852 ru,sormovo,Sormovo,16,,55.6239,46.1939 ru,sorokino,Sorokino,69,,54.746293,34.231718 ru,suchki,Suchki,56,,52.798474,35.989271 ru,sukhona,Sukhona,06,,62.4237,40.5835 ru,svetlovo,Svetlovo,23,,54.897796,20.175694 ru,sysoyevka,Sysoyevka,69,,53.8527,33.0811 ru,toroetsukoe,Toroetsukoe,64,,46.920574,142.636883 ru,tretya alekseyevka,Tretya Alekseyevka,56,,52.575376,36.620852 ru,troitskiya ozerki,Troitskiya Ozerki,47,,55.091123,38.944901 ru,tumleyka,Tumleyka,51,,54.8166,42.5546 ru,turayki,Turayki,88,,57.8125,38.212 ru,turdali,Turdali,73,,56.15,52.916667 ru,turgenevo,Turgenevo,47,,55.33928,37.710371 ru,tyngiza,Tyngiza,53,,56.919812,76.719245 ru,ulazy,Ulazy,61,,48.846907,40.728971 ru,uspenska,Uspenska,78,,57.07454,65.063022 ru,voybokala,Voybokala,42,,59.881947,31.830927 ru,yakovlevskoye,Yakovlevskoye,88,,57.436263,39.383855 ru,yesino,Yesino,21,,56.723854,41.032016 ru,zaragat,Zaragat,08,,54.0386,54.6116 ru,zilanovo,Zilanovo,73,,55.416667,53.866667 sa,al-rawshan,Al-Rawshan,11,,20.01904,42.6168 sb,komurarata,Komurarata,08,,-9.4333333,160.2833333 sd,gebbid,Gebbid,33,,11.3833333,26.6333333 sd,korare,Korare,33,,12.9833333,23.4166667 se,ekeby,Ekeby,27,,56,12.966667 se,fansen,Fansen,03,,61.116667,16.233333 se,granberg,Granberg,10,,60.5,13.45 se,hermanstorp,Hermanstorp,06,,56.966667,12.583333 se,humlegardsstrand,Humlegårdsstrand,03,,61.35,17.166667 se,kvarnehagen,Kvarnehagen,28,,58.1,11.566667 se,salto,Saltö,28,,58.866667,11.116667 se,vahavaara,Vähävaara,14,,66.916667,21.9 si,brezovica pri gradinu,Brezovica pri Gradinu,04,,45.4511111,13.855 sk,novy svet,Nový Svet,04,,48.1,18.15 sk,svaty dur,Svaty dur,08,,48.95,18.85 sk,tura luka,Turá Lúka,06,,48.75,17.5333333 sl,allen town,Allen Town,04,,8.4136111,-13.1602778 sl,tefee,Tefee,01,,8.7,-11.2166667 sn,kolobane,Kolobane,07,,14.8191667,-17.0433333 sn,ndioubene galo,Ndioubène Galo,10,,13.9,-16.0 sn,nionaga,Nionaga,05,,12.4166667,-11.7166667 so,boosaaso,Boosaaso,03,,11.2847222,49.1825 so,ganbar,Ganbar,14,,1.45,43.9833333 so,marca,Marca,14,,1.7166667,44.8833333 st,germinia antonia,Germinia Antónia,01,,1.6333333,7.3833333 sv,ingeniero san andres,Ingeniero San Andrés,05,,13.8183333,-89.4044444 sy,ain daqne,Aïn Daqné,09,,36.5252778,37.0783333 sy,kawur kuy,Kawur Kuy,12,,35.95,36.25 sy,kharfan,Kharfan,09,,36.5833333,38.1666667 sy,mashat at turn,Mashat at Turn,04,,35.9416667,38.0805556 sy,tall basirah,Tall Basirah,14,,34.9833333,35.8666667 sy,tell el khodor,Tell el Khodor,14,,34.7333333,36.1 td,moundou,Moundou,08,135167,8.5666667,16.0833333 td,retgi,Retgi,04,,12.1,17.0 tg,amedjonokou,Amédjonokou,02,,6.2666667,1.5666667 tg,avedjikpodji,Avédjikpodji,10,,6.1952778,1.17 tg,lama tessi,Lama Tessi,22,,8.8333333,1.0833333 th,amphoe nakhon luang,Amphoe Nakhon Luang,36,,14.462807,100.608315 th,ban ba na ha ro,Ban Ba Na Ha Ro,70,,6.349806,101.272944 th,ban baek,Ban Baek,72,,15.833333,104.1 th,ban huai hua chang,Ban Huai Hua Chang,10,,17.707361,100.318334 th,ban huai po,Ban Huai Po,03,,19.992667,100.528389 th,ban kaeng noi,Ban Kaeng Noi,48,,12.907778,101.885111 th,ban khok chong,Ban Khok Chong,68,,6.586,100.700167 th,ban ko thak nuea,Ban Ko Thak Nuea,68,,6.895695,100.693778 th,ban krabuang,Ban Krabuang,16,,15.9,100.033333 th,ban mae ko luang,Ban Mae Ko Luang,03,,19.483333,99.6 th,ban map hua thing,Ban Map Hua Thing,68,,7.910278,100.289111 th,ban muang chum,Ban Muang Chum,03,,19.904917,99.952695 th,ban na muang thung,Ban Na Muang Thung,73,,17.054695,104.591222 th,ban saba yoi,Ban Saba Yoi,60,,9.183333,99.35 th,ban soi suk san,Ban Soi Suk San,48,,13.221806,102.169333 th,ban talad mai,Ban Talad Mai,35,,14.553,100.32175 th,ban thung sai,Ban Thung Sai,58,,10.533333,99.266667 th,ban wo,Ban Wo,06,,18.390833,99.346111 th,bang phae,Bang Phae,52,29548,13.691568,99.929816 th,king amphoe non din daeng,King Amphoe Non Din Daeng,28,,14.313529,102.748145 tm,esenguly,Esenguly,02,,37.4655556,53.9725 tr,abdicikmaza,Abdiçikmaza,04,,39.413072,43.132358 tr,aratepe,Aratepe,52,,40.684467,37.851923 tr,asagi kayi,Asagi Kayi,82,,40.566667,33.1 tr,bagbasi,Bagbasi,64,,38.746944,29.482222 tr,carikbozdag,Carikbozdag,45,,38.433333,28.65 tr,dokmetepe,Dökmetepe,60,,40.312245,36.291795 tr,durulova,Durulova,44,,38.35421,37.835367 tr,gullu,Güllü,72,,37.250822,40.715768 tr,guzeres,Guzeres,70,,37.324108,43.746803 tr,hacialiler,Hacialiler,43,,39.044444,29.504722 tr,hop,Hop,13,,38.147242,42.370205 tr,kekerli,Kekerli,49,,39.065834,42.347946 tr,kurugol koyu,Kurugöl Köyü,50,,38.433333,34.533333 tr,ortulu,Örtülü,03,,37.902967,29.781803 tr,parcinik,Parçinik,63,,37.418206,38.934371 tr,sorkun,Sorkun,03,,38.444722,30.054167 tr,susuz,Susuz,71,,37.305844,31.942972 tr,yasikaya,Yasikaya,04,,39.643615,43.388898 tr,yulari,Yulari,07,,36.359782,32.227241 tw,luku,Luku,04,,23.7463889,120.7547222 tw,putoukeng,Putoukeng,04,,25.25,121.5166667 tw,tingwutso,Tingwutso,04,,24.0666667,120.4333333 tz,kimu,Kimu,18,,-5.45,38.9666667 tz,magogoni,Magogoni,20,,-5.1666667,39.7833333 tz,mwakinda,Mwakinda,15,,-4.2166667,33.45 ua,beresna,Beresna,02,,51.571605,31.784558 ua,derebchin,Derebchin,23,,48.75559,28.335622 ua,klinki,Klinki,05,,47.290027,38.248897 ua,kommunary,Kommunary,11,,45.547881,34.222914 ua,kudobintse,Kudobintse,22,,49.701545,25.170552 ua,lidykhiv,Lidykhiv,22,,50.012626,25.393697 ua,maryevka,Maryevka,11,,45.114372,36.241136 ua,myslyatin,Myslyatin,09,,50.089091,26.713 ua,novoestoniya,Novoestoniya,11,,45.499596,34.242982 ua,stefanesti,Stefanesti,03,,48.616667,25.65 ua,yuryampol,Yuryampol,22,,48.739881,25.943217 us,allenfarm,Allenfarm,TX,,30.3991667,-96.2436111 us,bartholows,Bartholows,MD,,39.3738889,-77.2322222 us,basking ridge,Basking Ridge,NJ,,40.7061111,-74.5497222 us,belleville,Belleville,IL,40919,38.5200000,-89.9838889 us,bonny kate,Bonny Kate,TN,,35.8833333,-83.8997222 us,cedar mill,Cedar Mill,OR,13975,45.5250000,-122.8097222 us,cobham park,Cobham Park,VA,,38.0583333,-78.2619444 us,coldstream,Coldstream,MD,,39.4130556,-77.3097222 us,colony woods,Colony Woods,GA,,34.0988889,-84.5019444 us,de kalb,De Kalb,MS,,32.7675000,-88.6508333 us,diamond,Diamond,IA,,40.7630556,-92.9655556 us,elmwood,Elmwood,ME,,43.7225000,-70.5672222 us,etter,Etter,TN,,36.5613889,-85.0980556 us,fairbanks,Fairbanks,CA,,38.8658333,-120.6691667 us,fairthorne,Fairthorne,DE,,39.7727778,-75.6036111 us,gettysburg,Gettysburg,OH,,40.1113889,-84.4952778 us,grandview,Grandview,ID,,43.0530556,-112.7875000 us,high point,High Point,MO,,38.4844444,-92.5905556 us,hillsdale,Hillsdale,LA,,30.7444444,-90.6208333 us,huron,Huron,KS,,39.6383333,-95.3513889 us,jefferson center,Jefferson Center,PA,,40.7755556,-79.8350000 us,johnson,Johnson,ID,,46.4269444,-115.9155556 us,jonesboro,Jonesboro,AR,58619,35.8422222,-90.7041667 us,kimball,Kimball,NE,,41.2358333,-103.6625000 us,kinder,Kinder,LA,,30.4852778,-92.8505556 us,la push,La Push,WA,,47.9088889,-124.6352778 us,lambert,Lambert,AR,,34.3080556,-93.2063889 us,moscow,Moscow,AR,,34.1463889,-91.7950000 us,mountain park,Mountain Park,AL,,33.5650000,-86.7713889 us,new baltimore,New Baltimore,VA,,38.7672222,-77.7286111 us,newburg,Newburg,WV,,39.3883333,-79.8530556 us,oakway,Oakway,SC,,34.6011111,-83.0258333 us,palisades on the severn,Palisades on the Severn,MD,,39.0427778,-76.5777778 us,park view,Park View,IA,,41.6941667,-90.5455556 us,peola,Peola,WA,,46.3091667,-117.4819444 us,quincy hollow,Quincy Hollow,PA,,40.1536111,-74.8625000 us,saint francisville,Saint Francisville,IL,,38.5911111,-87.6466667 us,scoville,Scoville,ID,,43.4805556,-112.9952778 us,thomaston,Thomaston,NY,,40.7861111,-73.7141667 us,travis bridge,Travis Bridge,AL,,31.4541667,-86.7888889 us,verona,Verona,CA,,38.7861111,-121.6175000 us,wallingford,Wallingford,PA,,39.8908333,-75.3633333 us,wardlaw,Wardlaw,TX,,31.5463889,-97.0644444 us,waterway estates,Waterway Estates,FL,,26.6408333,-81.9091667 us,whitney heights,Whitney Heights,SC,,34.9777778,-81.9250000 ve,cachito de venado,Cachito de Venado,18,,8.8822222,-69.0155556 ve,el guarico,El Guarico,02,,9.7416667,-63.9958333 ve,el paradero,El Paradero,16,,9.7666667,-63.3166667 ve,jebucabanoco,Jebucabanoco,09,,9.2311111,-60.8444444 ve,la florida,La Florida,20,,7.7861111,-72.0466667 ve,las trincheras,Las Trincheras,06,,6.95,-64.9 ve,oficina guamal,Oficina Guamal,15,,10.35,-66.9666667 ve,palmira,Palmira,15,,10.0461111,-66.2775 ve,rancho chico,Rancho Chico,02,,9.2955556,-64.6661111 vn,ap go dat,Ap Go Dat,21,,9.866667,105.2 vn,ap hung hoa tay,Ap Hung Hoa Tay,03,,10.083333,106.483333 vn,ban suk,Ban Suk,07,,12.8,108.466667 vn,dam ngoc bai,Dam Ngoc Bai,37,,10.433333,106 vn,loung than,Loung Than,17,,23.116667,105.5 vn,luong ma,Luong Ma,31,,11.666667,106.733333 vn,mao-sao-phing,Mao-Sao-Phing,22,,22.316667,103.25 vn,muong te,Muong Te,22,,22.466667,102.616667 vn,phu dien,Phu Dien,28,,13.216667,109.25 vn,ple brang tpe,Ple Brang Tpe,10,,13.75,108.4 vn,som quc,Som Quc,19,,21.716667,104.9 vn,tien loc sach,Tien Loc Sach,26,,19.416667,105.15 vn,van truong,Van Truong,15,,21.016667,105.833333 vn,xom gai,Xóm Gai,86,,21.246882,105.202138 ye,`uruq,`Uruq,16,,15.4933333,44.2561111 ye,al jinnat,Al Jinnat,16,,15.6844444,43.9427778 ye,al-buqairain,Al-Buqairain,04,,14.5525,49.1255556 zm,mwambula,Mwambula,06,,-14.1333333,31.2833333 zr,bengengai,Bengengai,09,,4.8333333,27.6833333 zr,bingo,Bingo,09,,0.5333333,29.3166667 zr,ileo,Ileo,00,,-0.45,20.4166667 zr,kimwanga,Kimwanga,10,,-3.7166667,26.6833333 zr,lieke,Lieke,09,,-1.2833333,23.85 zr,nkame,Nkame,08,,-4.7944444,15.0819444 zr,tshimpuki,Tshimpuki,03,,-5.9786111,22.5275 zw,mtoroshanga,Mtoroshanga,04,,-17.15,30.6666667 gowid-1.4.0/examples/gowid-table/statik/000077500000000000000000000000001426234454000201445ustar00rootroot00000000000000gowid-1.4.0/examples/gowid-table/statik/statik.go000066400000000000000000002020241426234454000217720ustar00rootroot00000000000000// Code generated by statik. DO NOT EDIT. // Package statik contains static assets. package statik import ( "github.com/rakyll/statik/fs" ) func init() { data := "PK\x03\x04\x14\x00\x08\x00\x08\x00w\x8fqM\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x14\x00 \x00worldcitiespop1k.csvUT\x05\x00\x01\x13W\xf0[d\xbdOo\xe4\xb8\xb2/\xb8\xf7\xa7\xd0\xeel\xc2\x82\xf8O$\x97vUw\xd5i\x97\xab|\xca\xd5\xdd\xb7{\xd5\xccL:E\xa7$f\xe9\x8f\xdd\xf6n\xbe\xc5`v\xe7\xe1\x02\xef\xf6\\\x1c<\\\xf4\xe0\xe0apw\xfeb\x83\x08RJ\xd7\x19\x03\x16\x7fdFP\xfc\xcf \x19\x0c\xbd\x89s?\x0dO\xf0&LOp\xb1\xdd\xfa~\"\xf8\xd9\xefC\xec\xe1&\x1e\xe7\xd6M\x08?\xb8)L\xf3\xce\xc3\x87\xd8\xef \x9d\xb9;\xf8\xcd\xb5\xa184\xfe\xa9\x85\xdf.\xdaP\\\x11\xe4\x12@\xd4em5c5\xd4\xa64\x8a\x89Z!\xc3\xc65n\x17\x87\xbd{\x82\xcb\x13\xac\x0c\x80\x10%3\xa2R\xc4P\xa9\xdaJ\x99\x18\x86\xf3C\xe3zz\xc9_\xe0\xd2\x0d\xe7W'\xaf\xd0\xc4(*^1\xa8m\xa9\xb5\x16\xb2&\xbeyr\xcd_\x02\\f\xb7R\x00B\x96\x86I\xae4\xd4ui\x94b\x9c^\xb1u-\xbcq-pM$\xaaR\x15\x92\x98\xb2\x12Z\x1b\x8d$\x87y\xe7\xda\x00W\xc9\xe1\x96\xde\xca\xeb\x9a1F\xf9\xab\xa5\xe6\x1c ;\xd7\xfaC\xf1\xf5\xc9\x8d\xc5\xc1\xb5\xee \xae)\xe0o\x18pE\x01\xcc\xd0[\xb8V\xaab\xa0\xab\xb2\x12\x8c1A\xcc\xf3_:\x8fYk\xe1\xfa\x04\xa9px\xa9\xad\xd5L@\xadK\xad\xb8\xd6:1\x1c\x1a\xd7u~\xe7v\xcf\x8d\xeb\xcf\x0fq\x82\xeb\x7f \xbb\x8aSJ0/-\xaf\xa5\xc0b\x92\x9c\xab\x9a\xd2\xfb\xd5\xb5\x7fq\xe7\xe1\xbcw\x8f\xf0\xb7\x8c?\xbaG`\xc8\xc1J%j\xce%\xd4\xbc\xd4\xcaTJf\x96\xdf|s\xfe\xe4\x8b\xaf\xf1\xb1=\xf7E\xe3\x9fvn@\xf6\x1c\xfe\xb7\x14\xfe>\x85/\x95+\xeb\x8asL\xbf\xaad\xc5\x18F56\xa1\x7ftC\x80\xdb\x05T\x82\x9aNUUFP\x1d(]\xd5\x8c2\xfb\xe4\x0e\xc5\xd15\xed\x0c\xbf\xb8CqC\x881*L!\xa4\xac\x19\xd4\xa2\xd4\xc2\x08N\xe4\xcf\xf1\xb1\x0d\xf0+=E\x95\x1a\xa4\xaa-\xa3\xca\xd7V\x19s\xe6Z\xd8\xbb\xdd<\xc4\x07\x0f\xef\x12x\xf9O\x90\x12@V\xa5b\x86\xaa\x97\xd9\xd2Hc\x8c\xb1H\x7fpc\x13\xc6f\x82\xab\x05,\xe4\xd6\x08!\x04\x92+Y)\xa5j\"\x8f\xc76\x1c\xe6s_\x1c\xe3\xd8L\xbe\x83\xab5\xe4\x06C^\xfe\xb3\x03i\x01$/9\x13\xcb\x0b\xa5\xa8E~a;\xf7\xe10\xc3\x87\xe4H\x01 Y\xc9\x8deu\xad\x81W\xa5\xe0\xc4\x85\xa4\xfd~\x1e\xfc\xb3+|\xd1\xb9]\xe3\xe1\xe3~\x1e^\xfe\x93\x02\xae) '\xd5p\xe4\xa0\x17i\xb6\xbe\xe8\xd8\xfa\xb1\xf0\xc5\xb3\xdb\x0d\xa1\x1b\xe1&y\x7f\xcd^J#+mm5\xe7\x9cr\xa9\xc4R(\xd3\x10\x1fz\x0f_\xd0\xc1\x02\xcc\xf9\xe1\x95\xc9%b9\xb1!\xed\x83\x1f=\x96\x04\xfc\x94\x00\xd2W\xb9\xc0\xeb:g\x8a\xd79Y\x1d\xf8a\n\xdb\x06\xbeK\x0e\xc3\x8a\xb4\xa5N%\x0cR!)r!\xe9\xd88\xb7\x81[zbw\x97U\xc9\x15\xc7?\x90\xb2\xac\x05U\xd0\x99\x8b\xb0q\xfd>\xc2%=+\x0dp\xce\xeaR\xe4\xea\x13e\xcdR\x84\x11\xb6\xae\xdb\xb8\xfey\xee6\x1e\xde\xbc\xc2\x0cyL\xc9\xd9\x9a^\xb1D\xbdu\xe3\xe8\xc2\xf9\x9d\xeb\x1d\xbcI\xf8{\xc4\xd8\xeb\xcfYU*c,\xe3\xc0SQJE>8\xf8\x94\x1cY\xd3[\xe4\xf2\x96\xb5\xcd\x12\xe9\xb0m|\x8f1~ZQN\x16_\x18\xeaR\x9e\x18\x06w\x88\x0f\xe3!>\xe0@\xbeB\x91f]\xb3$\x07\xe5D\xa2\xf6\x9b\xc1?L\xe3\x0c\x9fWD\xe9\xe1\xa5\xc5D\xebSQ\x8dn\x13[\xb8\xa5\xa72):\xc1\xac\x94\x12;@\xee\x90g\x9b\x00\xc3<\xf8~\xef\x07\x07\x9fW\x84\xa2\xd89\xca\xf0\x9af\\Q\x955\xd1\xb3\xb3\xcd=\xb8\xd6\xdd\x057\xc4\x19.V\x84\x13\x8f-%\xf0\xb2^\xf2\x16a;o\x9b\xd0\x84v\xef\xe0\xcd b\xab9g:\xcf&8S\xa9\xa5l\x90\xe90\xb7\xed\xd1m\x1b\xb7up\xf5\n\xa7\xb9\x1dG\x05\xcc\x83$6\x89\xb2\xb36\xc8\xd6\xbab;\xfb\x07Wl\x9b\xb0\xc51\xabxC\xde7\xe4\xc5~p\xce\xabutAf\xa1\x12\xdf~\xeeq.\x81\x0f+\xa2Wq\xb6\xd6\x18R\xd7D=\x86\xc1\xc5.\xf6S\xec\xe1\xf6\x15\xa6\xf8Q\xb8\\\x9ay-Js\xb6\x19\x80\xe6\x1c\xb7\x0b}\x13\xe1\xe2\x15f2I*V\xc1\xb9\xd2\x94\x96\x01\xb6\xee\xde=\xfb0\xb8\x11\xde\x9c \xc7\xf2Z\xda\x0c\x9c\x0bS2\"\x9e\x87\xd0\xb9\xc9\x05x\x93\xd1\xcb?\x80\xa9T\xb6F\xc1\xb9\x14\x8b\xb0\xb1\x19Vac\x17\x8b\xad\xeb]\xbb\xca\x1a\xbbX\xbc\xa1\x80\x8aXY)\x90\x95\x95KC\x1a L\xee\xde5\x01\xfe\x9a]\x9c\xe5\xcey]\x9a\x9c i\xa8p\x06\xe8\xdc\xb0\x8d\xc5q\xf0\x93\x87k\xc27\x84\xab\x9a\x1a\x93]\xe8\xabR\xb0%\xf2#\x8aU{\xdf\xc1\xcd\x02R\x16l\xc9\x17r\x91\x05 \"\x8f\xc3\x14\x0b\xd7\xfa\xfd\xe0\xe1\xe6\xe5\xffA\xdfE\xf21|\x8b,\x85\xaa\xb86p\xaex\xa9Ej\x1c\x03|\x9dC\xff\x1c\x8f\xb1\x0d#\xfc\x0d\xf1\xcb\x9f\xc9C\x92\x1c\x17e\x95f\xd5sU\xa5\xe1\x89^6\xcd=\xd5\x00\x96Q\xf4\xe3\xe4\xe1\xcb\xab\x90O\x14\xb2D`\x96\x08xi\xd7\xd4\xceG\xd7\x85!t\xf0\xe3\x02R!W\xa5\xcc\xcd\x04\xc5\x7f*\xbby\x98\xb7\xf3y\xa6\x1e\xe6\x97\xff9\x9f_\x93\x07W;\xe7\x06\x05:\xa1\xca\xfal\xf3\x04\x1b\xf7\x1cZ\xffppp\xb9\"l\xb1J\x95\n\xb8E\xb9`\xf3\x04G?\xe2\x8ctpp\xb3\"\xa2\x92\xa5\xc5\xfe\xbc\xf6\xb8'x\x88\xedS\xef\xa7\x11~Z@\x8eN\x037\xa7Y\xf8 \x1e\x83\xbf\xefQ6\"\x07\xabU\xc9R)\x1a\x9ert[\x07;\x1a\x03\xc7\xc9\x0f\xc5v\x88\xe3\x88\x12\xe1\xdbS\xd8\x9b%\x8c\xa4\xc4\xbadK\x8f\x91K\xd1o\x1d\x8c\x8e\xa6\xa8\xd8\xcd\xae\xdd\xc1-\xf9>g\x1f\x0e5\xb2\xc6Y\xe9\\/}\xf4l\xbb\x83\xcd<\xce\xe7\xfbMlc\xbfwp\x89\xbew\x8b\xaf\xaa\x00\x18&\x15\xd7c\n\xa9w!n\"\xbc\xa5'\xfe\xca\x17\x11\x8fW\xb9\xab\x11\xd5}pG\x92\xe2\xdf\xae\x08\xe5y\x86C\x9d-e\xaa\xea\xed\x0e\x0e\xae\x9b\xc7y\x8c\xdd\xc6\xc1\xd5+\x8cQ\x9f\xd7y\xac\xa3\xd5\xd3\x1a\xf7\xc1w\x9b\xf8\xe8Z\x07W+\xa2\xb1\xb1&!l\xcd\xd7!\x06\x8f\xffp\xb5\x00\x8aT,2\x00\x17\xd8\x86\xb6(\xf1\xa4\x04^/ M\xef\xceC\x12\xd3yh&\xd2\xb9\xf5D\x9a]j\x97j\x11.\xb9)\xed\xd9\xf6\x8e\x96V\xaeu;\x7f\xf0\xb4\xc0B\xfc\xf2\xc7\xe1\xe5\x0fZ4\xc9\xb2FyN,5\xdf\x00\xf6\xf9~\x1b|\x0b\x17+\xc2V\x82\xb5%k!\x0d\xe8\x92qY\x1b\x8d\xd4\xf7n\xbf\x0f\xe3\x06\x87\xf7\x19~x\xed\xa1\xf5^]\xdaZ\xd8\x1at)\x98\xacku\xb6\x0d\xb0\x89w\x01E\x0b|\xa2HV\x97\x9aW\x95\xc41\xb4\xac\xaa\x9a\xd7\x12\xa9v\xc1u\xae\xa7\xda\xcd\x00\xe5/\x8d\xe5\xc5\x85DZ]\xab\xda \xe9>LC\x80w\xf44\x06@\x95\x8aY\xc5p\xfc(\xb9\xac\x84\xb6D\x15\xe7>\xc4y\xe3\xe1]F/\x7f\x90$\xa9Jaqm\x8b\x03\x0f\x934\xa1n\x03\x1c\xe2p I5\xbbF\xe3\xdc\xa8\x8c\xa94\xa3\x94\xb2\xcap\x93(g\xdf\xc7\xf9n@\xe2\xf9\xe5\x8f\x8c1\xb95\x0e)F J\xae\xd6RQ\x01t\x9bxpE\xbf\x9f\xfd8\xba\x1e\xae\xc9\xfbq\xf1j\x89I\xaamm+Mc\xa1\xb5\x82q\xe4\x8bs\xec\x8e\xad\x9f\xf1\x1f>\xbd\xf6h\x83E\xc3\xb5\x128g\x98\x92\x1b\xc9+\x99y\x868\xba>\x8c\x11YV\xac\x15\x80)\x0dSV\xc3\xb9.5\xb7\xbc\xa2|\x8fn\n(6?\xc1\xed\x8a\xb0\x96tYIMu\xc4I\x06=\xdbv\xb0\x9f}\xbf\x8b3\xbc\xcb.\xaesYUV\n\x17\xed\x82\xe5\x81\xa0\x83&\xf4\xee\xfc\x91d\xd9\xf7\x08\x7f&\x98\xa9s\xd3C\x96<\xf1\"K\xe7\xfa\xbd\xa7\"\x82\xeb\x0c_\xfe\x18\xd3\xee\xd2\"|\"7\x11\x0fq\xee\xe03>\xf0\xf7\xd3Z\x82\xb1Uv\xdc\xf6\xb0\xf1\xe1\xb9\xf1p\x99\x1c\x1c\x1d\xb9(m%\x15.\x98\x98.\x99\xa9\x146\x95\x1e6\xb1\x0f.\xb6\xc1\xf5\xfb\xe7\x00\x97\xdf\xf8\x04\xae=P\xee\xa9-.\x85*^*Q\x0b\x85l[\xd7\xef\xbf\x06\xd7\xc3\x9b\x05\xe0r\x8f\xd7\xa5\xae\xb9e\x06\x18\xc3\x96\xc9\x85\x90D\xdd\xb8~\xff47\x11\xde\xac\x88\xb6iM^\\\x00c\"\x0f\xdc\x89>\xf8\xfe\xc9\xf5\xfbi\xa6\x7fx\xf3\xaf\x01\xb4C\x88\x83\x13G\xb9[\x96\xbcb\xc2\xd8\xcc\xdb\xef\xb7\x0df\x01\xd9VL\x991\xeb\xe0R\x89\xd2\xe6\x1a@\x9ey\xdb\xb8\x80\xf2/\xbcy\x85\xb9\xa0}H\xce\x80\xa1tehzN\xf4m RtP\x96\xa2-F\xda\x15c\xac.%m\x90!\xe5\x0e\xc5\xa7}\x17\\\x84\xb7'\xc8\x15\x80\xd0\xa5a\x96\x11\x87.\x85\xa4-\xaa\xccqL\xd4\xc7\\J(C2\xc9S)\x99\xaaf\xa9\x0ev.N\xc1\xdf\x87\x1e\xde\xae\x08e\x1d!Jmk\x8b\x93GU\x97\xd6\xeaJf\xfa)\xe0\xb4\x8a\xf4\x0bZK\x85\xb3J\x02\xabT\xa9\xb8\x94)\x97\xbb\xe0\xe2\x93\xc3A \x9d*m;\xd7\x15cB\x02c\xbc\xac*VY*\xf3]\xec\xf7\xcd\xec\xfa}\x1f\xf6\x8ef\xcfo\xfd,\xed\xdf1\xdac\x04\xc6E\xa9\xb4\xa9T\x8d\xbcw\xf3}\x9819\xf0\xfd\x8aP\x86\xe0\xac4\x8a\xd9\x1as]\x95\x0c'$z\xd7\xde\xf5\xc7\x08\xef\xe8\x89\xa2\x97@\xc1\xb82LS\xea\xa5\xd2\x8c\xa9D\x17\xbf\x06\x17q\x85\xf6\xee\x04\xa9|d\xc9\xb5f\x9a\x03\xabt)mj\xa3\x8d\x0f\xd8R\xe2\x84 ~\xff\xda\xc3+J\xbd\xb0\xb5\x95XF\xb6\xb4\x1ae\xb6\xc4\xd5\xefw.6\xfe9\xc0\xfbW\x98f\x04VZ\xa3*\x0d\x0c\x17vL\xeb\x940d\x19\x1b\xd7\xff\x1e\x1c\xb1,\x18\xa7RQ\x95\x8cI\x83UA\x9b\x9c\xc6(jFM\xbc\x0f\x0d\xbc\xa7'NP\x02W L\x1b`\x15\xae\x11jS\x8bD6\xe3\xac\x8f\xc2\xca\xfb\x13\xa4R\x12%\xb35W\x98~Y\x1ac,\x15~3\xba~\xdf\xb8Gd8Ani\xf3\x80\xf3\xca(\x0eV\x97\xda\xd4&\x97\xd2\x1c\xf7\xf3\x18\xe0}vy\xaa\x81\x8a\xd3:\x0d\xdb>\xd7\\e\xca\x0eK1b\xdc\xaf0\x0e[\x1c\x97'\x86\xda\x02\xe3e.\x96\xf9\xf7\xb0\xf1\x18sri\xd0\x92\xa5\xb1\x95U\x14\xb1\xae\xb5d\x94\xcd\xfb\xe00o3\xfc\xb0\x80\xdc\x10j\x83Bxj\x08\x95\xb2\x15\xcf\xd4\xfd\x9e*\xf2\x87\x15aY\x93\xe4i9\xc5\x8eBVe\xa5L\xf4s\x1b\xe0\x07zRbmY[Y\x1b\xa4c\xa52J\xe7x\xa3\x1f\x9a\x83\x0b\xf0\xc3\x02\xb03 l\xdb\xdaRA\xf3\xd2\xd627\xc7C\x98qH\xd9\xc3\xd5\x02h7\x19\xc5U\xac\x8d4\x08\xa4\x9d{\xa4\xc6\xce\x83\x0dc\x1a\xe7\x1e\xae\xbe\xf1\xb1t\x9c`\x18\x0e\x19\xc0\x98,\x99\xc6\xde\x81|\xad\x8b\xf7sOc\xcc\x87\x13\xcc\x83\x08\xb7ue9\x15\xb9RU\xc5\x13\x075\xf4&\xc2\x87\x05\xe4.\xc2\xb2\x04\x88\xd3V\"\x9c;\xdf\xef\x8f\xd8??\xbc\xc2\x82\x01\x8a\x89\xa2\xd6\xc2\xb2D.*\x95\x9aW\xe7Z\xd7c\xb7\xbb^@\x1e\x9f\x84\x94\xd4aQ\x1e\x12\x86\xc9D\xdc\x8f\x18\xe1uvY:3\xabkS\xe9\xd4l\xa5\xe153\x896\xd2\x9c\x02\xd7\x0b\xc8\xc9\x90\xc6Z#\xd3\x90a\x84\x96\x8c\xa8}\xf8\x1d;\xc2uv\xa9\x9e\x18\x8d_\xb5\xa5z\x12\xb5Lcj\xef\xba\xa7\xb9\xdf\xc3\xc7\xec\xd2H\xb4\xca\x99X\xd8T\x18\xbd\xefS%~\\@e\x00\xa4]K\x8d\xabe\x11\x88\xd4a~t\xfd\xef3|\\\x00E\xcbK)\x0c\xf60\xac\x11#y\x9d&\x80\xa3\xeb\x8f\xae\xa7\xe1\x07n^\xe1<>\x08\x93\xeb\xdd\x96F/\x93\x1e\xf6\xf3f\x0cM\x84\x9b\x15QQ\xf3\xd2\xcaJ1Ie-\x85b\x8c/\xf4\xad'\xe2\xd6\xa7I\xc0\x96\x15\x13\xccZ\x9a\x1a5\x8a\xbfT\x85y\xe0\xdc\xce=\xfc\xed\x04s\x1b\xa9\x84a\x8a\xd1\x90XW\x95I\xfdg\xf0\xfd}p\xf099$\x16\xe0\x04\xc3*&H,\xa8\xb5`\x15\x1566\xe9m\x833:\xb6\xeb\xdbo|\xf9\x147U\xa5\xcd\xe4\x1e\xa9|\xbft\x9d\xaa\xca]G\x95J.\xf3\xc9\xd8\x84f\xdb\x04l\x15p\xfb\nSW\xc6\xe1I\xe1r\x1b\xbb\xb2\x148\x1de\x9e\xdf\xe9\x18\xf0\xf7\xb04$.\xe94\x18\xb3\xa6\xa4\x96\xa9\"\xc74!\xdc&\x87I\xaa\x90,\xc1XU\xa6\x81o\x1a]x\x9a]\x0f_\x16\x90'\x03!\xb4b\xa9mh\xc6\xad\xe6\x99:\x1e\xe2\xe4\x90:\x01\xca\xb9,\xed\"\x1cTe\x8dC\x13\x95\xed4\xc7n\x86/\xf4\xccS\xb7dB\xa3\x80R\x89\xb2\xaam\x8e\xf5\xd1\xf5X\xb8\xf0svi\x98\xaeK\xa5e\x8aS\x95\x92\x99,l=\xfa\xf0u\x86\x9f\xe9\xb9\xf4}\x95\x8e\xe8*S\xda$;<\xfa\x1e\x17\xa6\x1b\x17\xe1\xe7\x13\xcc\xe4\xdarMs\x91)\xadZd\xa4\xc7y?\xbb\x9e\n\xea\xe7\x13\xc45\xb4`\xd8\x1c\x8cI\x03\xaf\xa53\xd6\xc4\xf1\x1c\xe0g|\xe4\xae\xcfja%\xa7\xacq\xcd\x85&\xaa\xdf\x83\xf3\x03\xfc\x1b=\x97\x86[)+P\xdaa\xa5bRX\x99\xe9\xe2\x1e\xe7\xc0\xe7\x80\xd4+&Y\xa4*\xb5\xae*\xec\x16\x9c\x97\xdc*aD\xe6\x19\x03\xb6x\xe4\xc8hMqUil\xc0\xa6\xb4F\xd3\x02\x02\xe9\xfb\xfd}pX\x1c\xf0o\xaf0K\xe92\x96\xd7(\x9f2U\n.\xab4\x95\xa3\x14{\x87B0\x8eD\xbf\xbc\xf6P\xc5\x93@\x92\x06^\x1c\x96\x96\x0e\xfe\xe4\xe2}p\xbf\x87)\xce\xf0\xcb+\xcc\xd2\xa0P\x9b\x9aKM<\xb6\x162IJO\xf3\x9d\xef1v\xf8eE4L\xc9\xd2Za\xb8%\xc1D\xd6F\xd6D\xff\xdc\x90,\xdf\xef\xe1\xd7\x15\xe54\xd5\xb2\x16\xd4\xd3xY\xd3\xa1f\xa2\x8f9\xe5\xbf\xae\x88\x19\xeae\xd5\"e\xab\xb2Z\xa5\xfa\xe7$\x9e\xe04\xf0\xeb \xe6\xfa\xa8E\xea\x978\xd8R\xd7[8\xda\x90\x06\xf8__a\xb1\x94\xaf\x90\xb8\xa0\xa9T\xc9\x04\x1d\xd4\xaf<8\xf4\xfc\xba\xa2\\J\x95\xb1\xaa\x96i\x14\xaf\x8c\xc6\xf6\x14\xc1\x0d\x83{r=\\\xa0\xfb\xf2\xf7\x1e\x84\xc0u\x95Rumj8\xd7\xa2\xb4B\xd4Z!\xedf\x98G\xdf\xba\x11.\x17\xc0j\x00\x86\xc3%\x13p\xae\xeb\x92i\xce2\xed\xec{7\x16.\x0c~\x84\xcb\xe4\xb9 \x0fO{;\x861 \xe7Z\x97\x15.\x979\xf2l\xdd\xe8\x87\x10\x0b\xdf\x16\xdb\xb8q\xbb\x08o0\xe4\xe5\x1f\xb1\xf8\xae-\xde\xa4 \xa1h-\xc9\x14\x9ckU\xd2\xbb\xee\xda\x88\x11\x7f\x9f\x1c\x9c(\x0c\xc9\x91\x12id\xa9\xb5Q8\xdbG\xd8\xcfn<\xce[\xd7\xc2\xbb\x15\xa5\xe4TR\xd4\x95\xa1\xf4HYU\x86\xc8\x9b8L\x0e\xde\xd3\x13\xfbz\xbd\xccoX,\xf9\x80e\x1bq\xe6\x9e}[\x1c\xdd8\x0d\xaew8\x83\xa3\xfff\xf13J\x90\xa9\xd2\xbe\x99\xaeKQ%\xf99\xc2\x14\x8en\x1b\xbf\xce\x1e\xbe\xacH\xe0\xb0+j\xa8K\xc9+\xc1\xe0\\\xf3\xb2\xb6\xcc`\x19\x0d\xe0\xdb\xc2\x0d\x9b\xd8\x86)\xc2wmq\xb1`\x9ch\xec\xba\xc7kT>\xef8\xdb\xce\x94\xeb\xd0\xe5L\x13\xc0\x11\xf9\xf5\xee\xba6%NI3\x84\xb1\xf5\xbd\x83\xbf\x8e\xad\x7f\xf9\xaf\xb4\x0d\xcfy\xa9\xe0\xdc\x9c\xce\x81\xb63\x1cg\xbfic\xd1\xcf\xfe!\xc2M\xf2|$\x0f\x89\xad\xac\xacI$K1\x1b\xa3)\xee\xc1\x1f\xdd0\xc5bt\xfdX\x8cq\xde\x06\xf8\x9c\x83n1\xe8\x96\x82H4\xc5\xb1\xbc\xa2A\xf5\xdc\xf0R\xe2\xe4\x86=\xe2\x01\x06\x94\x8d\x07W\xec\\\xb1q\xc3\xd6\xc1\xe7S\xc0%\x050\x0d\xc0\xd4\xb2\x19@{\xbd\xcb!\xcb\xf6 \x8e~pM\x1c\x9e\x1c\xdc\xac\x88F8\x95\xb6\x94kM\xe2\x19\xa5\xf8\x19\x0e$\\\xc5\x07\xb8Z\x80Ib\x8e\xb6R\xa2\x98\x85m\xcb2\x94\xb2\x9e\xa1u\xcf\xbd/\xda\xb0y\xf0\xc3\x8e\x0ea{_|X\xbcF\xd0\xd1\x9b\xb1\x0c\xab\x94\xa9\x92W\\\xd1K:\xbfki\xb7\xf7z\x01:\xbd\x84K\xcep\xbe\xc4y\x8bIK\xc4\xe3\xbcmv\xb1\x85\xdb\xecjK'\xf7(\xf8+\x8b\xe9\x91\x12'\xd9\xb3\x9d\x87\x8d\xdb\xb9\xbeq\xf3\xe8{\xb8|\x85\xe9`\xd8.;\xe8(\x07($\xdf\xcdC\x1fw\xf0\xf6\xe5\xbf\x87\xfe\xe5\x9f\xe9\xacP\x9aR\xa4\x0d\x1e\xa4\xf0;\xdf\x8f~hc\xdc\xc3w\xaf0\xed+\x8bega\xd9Z&\x0e\\Q\xd1K\xbf[Q\x8eW\xad\xdb66S\xb6\x03-:\xfb=\x12\x9f<\x99^S:\xf2\x16\xc9\xce\xc3]\x1c\xc6\xc9\x0f>\x14\x07?\x8e\xbe\x1dc\xd3\xc2\xf7/\xff\\B\xafN\xa1(\xa7(\xbe\xec\xbe2\xb6\xe8\x98`,\x83\xef\x0f\x1e\xbeO\x0ee\x84/%cs\xc1\xecc\xbb\xdb\xb8m\x03\xef\x16\x90\xd50\xd2\xf9,X\\\xc0\xed<4n\xd8\xf9q\xf3\x04\xef\x17\x80b\x9a\x92\x98t[\xd6\x89\xc6\xb7\x1dF\xe1\x07x\x7f\x82Yv\x16\n\xf4\xb2\xdd\xbc\xf3p\x88C\xeb\xe1\xea\xe5\x9f\xe8`\x87Ul\xd9{\xb7I]\x87\xb2\xd0\xfaqt3|HN\xae\\\xbbVn\xa6yj\xe0\x03>\xf2\xef\xb4\x0d\x963\x8a\x04\x9d\x1b\xc6\xce\x87\x01\xae\x17\x90\x8b\x9d\xe5j\xe2%C\xba\xde\xcf\xbe\x8fS\xe3\xe1\xe3\x8a\xe8\xeca\x89\x0c\xab\x7f\x8d\xf5\x18\xdb\x870=\x9f#W77(p\xe7\x10\xe4\xee^\xfe\x1b\x83r\xed\xc8e\xb9\xb5\xec\xd1\xef<\x8c\xdb\xc6\xb9v\xf3\x04\xb7\x0b\xc8%\xaa\xa8D\xd7f6n\x9bG\xdf\xc7G\xa4K\x00\x85]\x85+k\xec\x12\x15\x15\xfd\xe87\xfew\x14\xa0\xb3K\x95\xcd\x96s \xb6\xc8\xb1\x89\xd4\xc7\xfe|\xf4~3\xcc\xdb\x03\xdc\x92\xf7v\xf1R\xc9\xe8\xbc{\x86%\x93\xce\x86\x90q\x8a\xc3\x80m\xf6\xcb\xcb?\x13\xc8\xa5\xb8\xf45^jJ\xcb4\xb7m\xe8\xf7\xbe\x87//\xff\xbd\xc0\xdc\xaa\xea\x8aW5\xb6\x84ZJ-\x91\xfa!\xf8v\x9c\xfcn\x82\x9fV\x94\xfb\\\x12.\xc0\xbcJ\xc3\xa3k\xdb&\xdey\x14~\xdb\xb6y\xf9\xe7\xdd\x92\xd9\xbcO\xabq.Z[\xd9\xa3o<\xfc\xec\x97z\\r\x03\xcbN\xe1\xd9\xee\x00\x91\xce\x81\x92Z\xc3\xe6 ^\xfeO\xf2\xbf[\xfc\x9c\xa5S*\xab\xa4\x01\x83\xa3mU\xd5\xc87\xf6\xfea\xf0p\x9b\x1c\x9cg\x95*ki\x04\xa7^\xc8Y-\xe5\xd9.\xe2\x94FE\x10b\xe16\xee\x9e\xe6\xb5\xbf\xe6\x80\x0b\n@\xf9\x9dY\x9c\x87tEe\x18\xa1uc1?\xb8\x11>\xb8\xb1\xf8\x11\x01.*q%\xbe\x1cP\xe9|l\x87\x99x\x06\xb7\xef\xfd\x06.\xe8\xc9\x04\xad\xc2\x97\xe9\x8f-G\xdbH\xb7\xbb\x0fm\xeb\xdaPl|_\xb8\xce\x0d\xf0v \xb9\xf4}q\x81!\x8a\xd1\x8c!\xa5\"E\xb8\xaa4\x8aj\x82\xf8\xe3\xec\x86\"\xce\xad\xdf\x15\xae\x0d\xf0\x96\xfc\x9f\xc8\x7f\xd1\xa6\xd3X\xa1J\xa1\x93\x82MU*\x85lw\xad\x9f\x87\x19e\x98\xe4\xd2v#.\x94\xe8\x0f\xce\xabRJR\x06@\xe2\xde\xcf\xc3\x01\xc7\x0e\xec\x86\x19\x89\xbc\xc4\xc9y\xe2\xeb\x81\xe1\xee\x19\x9e}\x17\xe7\xc1\xc1\xaf\xd9\x15\x96hy\x9dN\xe6eIkj\xce\xcf\xbc\x87c\x08\xb1\xf3\xd3\xe8\xe0fE8C*l5I\x9f\x0f\x17\xf7\x86v{\x91a\x0c\xbdk\xfd\xd1\x15n\x9c\xfb\xdd<\xc2\xed\x12p\x91\x03\xa8e\x99\xd2\xf0\xf5\xfcK\xd9\xb4\xdf\xeb=\xe0p}\x0c\xf0%9(*)S\xb2\xdc\xbb\xb8\xc6\x95\xcf\x92\xb4\xd9\xbba\x86\x1f\xe9I\x8a28G\xd6ICB\x95\x82\xb4\x96\xe5\x99\xdf\xc3on\xba+\xdcf.\xf6\xa1\xdf\x05\xf8\xedb\xba+.6s\xf1\x8e\xbc\xa4\x8c\\\x95\xb6\x92\x94{\x84\x9c\xad\xac\xe1y\xe3\xa6\xa2s\xe3\xe4\x86\xb9\x81\xdf\xfeJ\xfe\xeb\xc5\x8f\x8d]\xb0R\x9a$\x9e\x88\xaa\xd4\xd5\xca\xec\xdab\xe3\xee\x9by\x80\x8b\xb6\xb8L\x08\x87,n\xcaZ'=,dP\x8c\xf4+\xfd\x1e\x0e\xeen(\xa6\xb0s\x0d\\!\xfcB0\xbf\x83\xb35\x81Fq\x85\xf4\xbd\xdb\xffV\xf8\xf6\xfcy\xfe\xeaCWts\xe3\xee\xdc\xb3\x9f\n\xb7\xc1\xd6\xd6:\xf8\x98)~M\x14\xd7+\xc5\xc5\x06\xdb_\xebhS\x9f\xab\xb5\xf1\x0b\x9e\x8f.\xc5\x99\x1fa\xe3\xda\xd6\x8f\xd3\x10\xe1rE*\x9dT\x18\xc9\xb4\xb1p\xceK\xa9\xea\xca(\"\xf7cp\xfd\x08\x97~\x0c/\x7f\xef\xc7\xa4:\xc3Kn8cP\n%\x05\xaf\x91p\xeb6\xfe\xd9\x15;_l]wD\xb9~\xf1\xbf!\xbfR\xc4\xa7$\xb6_8\xafKc\x18\x13,\xb1\xa6\xb3\xa17\xd9\xcd\xba1Rs\xc9\xe9\x04\x0c\x975V'\xd2\xb4~X$\xf1\xf1\xb4~\xb8Y\x83\xa8\xf3\xd6YJ!E\x0dd\xdd\xf9>8xK\xcf\xba\x02a\xac\x129\xd7BJ(Y\xa5\xd3;\xda\xd9u\x0e>\xd0\x93\x92\"\xca\xda\n\xa3Q\xd6-\x8d2\x1akv\x84.\x0e\xbe\x9f<\\gWH\"\x15\x82W\xd2\xc2\xb9*\x8d0Z\xd3\xabIU\x92\xb4$\xe1\xa7\xd0\xb6/\x7fOX\xa5]\x13\xc3\xac\xb45r\xc8Z\n\xcb\xce\xfc\x04n\xb7\x0b\x85\x1b\"\\ \xb8\xc0:\x12\x80\xf2\x11\x0d\xa95mv\xaaZkN\xc4\xc3\xdeo\xfc\x04\x17\xd9\x95\xb8dc\x8b\x00&l\xde\xdc#\xd2G\xd7\xa2`=y\xb8 x\x89\x10\x0b\xcc\x96\xe6tZ\x85\xa4;\xdfm\xfcvKe\xb6 \x8a\x19\x074Z\xc6\x9b\x13\xe9\x10\xe7\x9d+Z?\xc0\xdb\x8c?\xf8\xa4f\xa5\xcb\x9a\x1a\"\x1d\x1f\xd36\nr\xe0\x88\x16\xc6\x00W\xd9M)\xa8\xe9\xcchMA\xe7\x9e\x8a\x9d\xefp\xb9\xf5T\xbcE\x90\x0b\xa1\x12\xcah\x869\xab\xad\xd0\xcc\x9e\xdd\x05h\x9e\x86\xf1\xa9u\xf0\x9e\xdc\x97\x7f\xa7)\xa0\xae\x16\xc5c.\x97\xac\x11\xedCDA<\xd1?\xbc\xfc\xb3}\xf9w\xf4!\x97\x04\xa8\x17\xcd0\xec\xd6Y\xad\xf9.\x00\xb6\xb9\x087\xf4d\n\xa0\xe6Y\xce\xc4\xf1\x8e\xad\x91\x1f}{\x88\xa3\xef\xfb\xe0\xbb\x007\xdf\xf8p\x96\xaeu\xc9\x18)\x82r\xd2\x91\xc5u\x0f\xb2\x05?\xb9!\xf4\x9d;\x04\xb8Y\x84\x03\xdcN(\x19#\xca\xd4y\x83\x82\xcb\xac\xb6sv\xd7\xc1C\xe8\xdd\x0c?\xd1\x13G|[*\x9e\x15\xce\xb1\xeam\x1a\x87\xef\x06p\xed&\xb8-\\$\xe7RP\xcbV\x8a\xe9\x8a\x01+\xb5\xa9\xb8D\xb2\x8dw\xf3\x83\x0b8:dpaH\x1c\x92\xb6f\\\x03\xca\x1a\xda\x9a\x9ah\x87\x88\xc3\x0d\\fw\xa1\xd4\x8aW\nh\xfd\xab\xa5@\xcam\xeb\xc2\xe0\xcf\xefb?\xb9\xd0{x\x93\xfc\xdf/\xfe\x0b\x99$\\\xae\xb5\xaa\xe9\x80\xd8\xd4Z%N\xbf\x9f\xfd\xe0\xb7\xf0\xa6}\xf9c?\xbf\xfc\x81\xf8\"\x0b\xba\\i\xad\xe0\\\x94\x95f5\xa3Dm\xe3\x9d\x93v\x94?\x1f\x9f\x06\x0f\x9f)\xe46\x85\xdcb\xc8E\xd2\xbe\x97R\xea\x9a\x81(\xad\xaa-\xa3\xb2\x18\xe2~\x0e\xbd\x9f\xe0\xf3\x02.\x92\xda=\xafLe\x05\x94\x8ckE\xb5\x94\xf4\xaf:7\xec\xc6\xf3\x9d?\xbf\x1b\xfc\xd8\xfb\xac\x87u\xbd\x84~\x9fBsb+\xadk\xad\x00;a%R\x1c\xfd\xe40\x8e\x80\xcf\xdd\xce\xd1V\xd0-\x85^S\xe8\xf5\x1az\x99\xf6Q\x19\xe8\x92\xf29\xce\x9b6\xec\xfb'\xb8]@N\xa9\xac\xb8U\x0cp\xe5\xa3*I-\xe3\xc1\xcd\xdb\xe0\xfb\xde\x8f\xf0\xd3 \xe6~[)\x10\xcb\xf2d\xef\xc0\x1d\xe2\xdc\xc5\xb9\x8f3\\\x9c`E\xf79V\xa5p\\:\x9f\xed7p\xe7\x86\xbeq\x1d|\x9f\xdd\xef$-\xf1,\xe0\n\x1d\x7f\x8f\xeda\x8a=|\x9f\xdd\x1f\x92z\x1d\x87\xf3e\x1c\xdfo\xa0m]\xbf\xdb\xc5\xc7!\x0e\xf0\xe1\x15\xfe\xb7\xb4\xcc5\x15K\x8a\x80\xa5\x12\xb49\xbd\xdf\xc0\xd1M[|\xdfMv/\xeb\xb4\xd7\x83-.\xbd\xf9\xe8\xbb\xcd\xe0\x9f\xe0&\xbb9\xb2\xda\xd0\x19\xc79\x1d\xc8\xe3\xe0\xba\xdf\xc0\x10\xb7\x0d|\xc6\xc7/\xf9\x8d$\xe2\x92\x1e\x92\xa1\x89z\xbf\x01l\x81}1\x06\\\x88\x8dpK\xbe\xdb\xec\xfb%m\x15\xe8\xbc\n\x12\xcb\x82|\xbf\x81\xa9\xc9)\xfc\xb2\x80\x1b \\r\xad\x90Cb\x99&\x19\x1d\x89\x1f\xfd8m\xe6\xe1\xa9\x88}\x81\xef\x1bz\xf8\xf9U\xd0m\n\xfa\xee\x9be4\n\x87\xf9m\x1eF7\xec\x1f\xfc\xd8\x04\xb8]\x11\xd7$\xbcUZTt\x85H\x94\xbc\xe6\x8a\xa8\x9b\xd0\xc5C\xe7wt\x10\xb5@\x99t\x0f,\xe3\x8a\xf8,\x12\xdf\xd1\xc8\x1c\xfc>\x0d\xcc\xe1\xe5?\xf6I\xc7N\x96\x06\xce\x95\xc8s\x9a8\xdb7\xe0v\xdew\xdd\xe0\xe0b\x01\xb8&\xaf\xcb\x1a\xd3Z\xa9D2\xef\x9f\\?\xce\x85;\xc4\xce\xed\x90v \xb9\xc8!\xd5\xa2\x90\xb6ds\xb9\x14\x80\xfc\xad\xdbD\xfc\xf7pq\x82\xd8F\xf3\x9d\x95ZC\x95\x8eC\xb1`\x1a\xd8\xcfG\xd7\xfb0\x84\xfd\x0c\xef^a\x9c\xff\xed\xa2k\x8eK>K\xe9\xdb?\xf9\xdd\xec\xe0]rpa(\xa9m\xf1\xb2\xc2\x9f\x0f\xf3~\x1e\x02\\%\x07\x97\x1b\xac\xca{S\x14\xc9\xab\xc2\xe8\xc3]\xeb\x8e\x11>f7\xa71e\x05N\xb71\xf6\x0dL\xee0?x\xf8\x92\x9cD(\x15T\xab\xe2(\xd2\xc4}\xdcE\xf8\x92\x9cDS\xd9,2\x948\xddQ\x9bn\xe0\xd9=\x86\xc1\xc3\xaf\xc9\xc9\xd9\\\xca\x12;\xca\xd9\xbe\x85p\xf0\x83\x1b\xdd~\x1cg\xf7\x15\xfe\xfa\x8d\x8f\xeaL\xad\xca`\xe7B/%{\xb6\xef\xa0s\x07Gr 9\xb8\xbcc\x02+\x98\xa9U3m\xdf\xc3\x8eN\xe0\x07\x07o\x17\xc0\xd2U\x87\xbc\xfc\x81s[*$\xec\xdd\x9d\xdb\xcf\xa1\xf5\xf01\xa3\x97?h\xd9\x8e\xb4\xb8\xe0\xd0\xeal\xff\x15680np*<\xfa\xaf\xb3\xef\xe9N\x1b\x85\xbc\xfcQ\xdc`\xd0\xcb\x7f\xc5\xb4\xd8\xc4\x923K\x9d\x10\xaf\xdf\xc5\xb9\x83\xcb\xe4\xd0\x9e9mV\xb1\xbc\xa3\x93\x88\xe2!\x0ea\xdb\xc4\xf3\x8dk\xdd\xb6qp\xb9\x86\\R\xc8\xcb\xdf\x97\xf8\xc1\xac\xdb\xf3\xfb\xaf\xd0\x8f\xae\xdf\xbb\xa7\xd8\xef\xe1\xe3 f=\xd8\xdc\"\x19+\xc5\xd9~\x00\xd7\xc7\xe2\xc1\x8d\x87\xd0;x\xf9?\xfaX\xfc\xe4\xc6\xc3\xcb?z\x07\x92\xd1\xd9\x17\x07\xceOi\x1f`\xe3\xba\x8dk\x03f\x97\xc0\xcb\x9ftf\x8ck\xa5\x1c5g\xab\x92\xe9\x1e'\xf4!\xf4\xfb\xf8\x10\xe0\x03\xa1\x97?\x1f\xd2 \xa7\xacJi\xf2\x05\x07Q*\x94\xfe\xb5A\x8ec\x18\xf6q\x84\x9b\x97\x7f\x90+\x0d\xa5C/\xca\xaeui\xaa\xb4\xfb\xb0'\x05\xf3!\x16\x9d\x1f\xe3Spp\xf3\xf2'z\xaf\xfd\xf8\xf2'\xfa\xa5H\x1b5\xf5\xba\xf3P#\xd3\xe8\xa6\xb8q#\x0eL\xd3\xcb\x9f\x84h\xcdm\xd7~\xc3y)\x91\xf2\xc1\x1d\x0eM\xec\xe1\xa7\x97\xbf'\x90\x0be\x8d\x91\x97\xfcl?\xa1\xb4\xd38\x9c\xcava\xa2\xcb\x0c\xefO\xbeoN$\xe0\xdc\xf2\xe5Lp?\xe1\xd41O8q\xccS:\x88\x928\x0c[\xb6l\xf0\xee'\xf8:\xd3\x91\xcd\xdf\x92\x83\x03\x11\x93\xe5\xd2\xbdm\xb5\\(\xdaO8\x81\x17n?\x8fS\xe8q\xda..\x10\xbf\xfc\xa3_R \x89\x9e\xaf/'\xfa~\x8a}\x88\xc58o\x9b0yj\xca\xcf\x89;\xffr\x9b\x7fy\xf9\x83~\xe2\x15\xb0J\xf3\x9a\xee\x17.\xa9Xwm\xf7\x8f\x80}a\n\xdb\x08\x7fK\xe0\xe5OZM0^2\xea\x92\xcb\x9d\xd1\xfd#\xcc\x9d\x1bb\xb1\xf1\xad\x83\x1f ^\"\xa4!\x8c\x9f\x06\x08&W\xd5\xce\xe6\x00c,\xdaX\x1c\xe7\x1enc\xf1!\x167s\x9f\xf4\xc1yN\x8e\xa0\x03N\xae\xce\x9a\x1e\\;\xc5\xe2\xc1\x0f;\x1c\x9a\xa7X\xfcD\x90\x8aC\x942\xdfc=7\xba\x94\xb9\xbc\x9b\x1e\xb6n\x18\xc23\xb5\xf07'\x88\xa3\x1b\xcbs)&\xca\xe8u`mz\xf0m1\xb9M\xebv\xb4w\xf9%C\x1c\xb5\x98(\x95\xa6\xbb\xba\xc4#d\xba\x95\xd3\xf4\xd0\x85\xc1\xe5\x83\xcb\xeb\x13\xcc<|\xa9`S\xe3p\xd3\x0c\xb0\x99\xa7C|\x08[\xbaT\x9f\x11i\xe4\xab\xb2\xaa\x96eZi\xab\xa4z\xd9\x0c\xd0\x85\xf6~\x1b\xe0:9\xd4\xe1\xb0\x97\xa5<\xb3\xba\xb4i\x0b\x8bH\xe3\xe0\x1e\xc2\xd6\x17\xe3p\x1c\x0f\xb4i\x91\xfc\xb7\xc9\xcf8\xbdg\xb9A\xc5\xd4:\xfd5\x13l\xdc\x13f\xf629TN\xe64\x89i\xb1^\x9ej&\xb8\x8f\x8d\xeb\xc38z\xf8aE8\x1d0\xbb\xea\x06\x9fk\xben\xc76\x13\xb4q\xdb\xb8a\x07\x1f\xb2\xcbX\x8a\xff\x15u}JK\xef\xfa\xe2\xe8\xda\x0eE!\xf8\xe8\xfa\xe2f\xf1\xe0\xba\x97\x99\x92\xd7\xb9.4\xf6\xc2\\\xe7\xd8\x13\xda8\xc0-=\x97\x1c\xac\xdb\xc8\xebM\xb0\xb3f\x06\xd7\x8eq|vm\xebF\xb8h\xc7\x97?\xc7\xe7\x97\xbf\xb7\xed\xcb\xdf\xc7t\xcfA\x975\x8e\x7f\xb9S63\x1c\xe6v\x8cw~;\x1e\xfcx\x9c\xc7\xe7\x89nI\xfdk\x10]\x16]\xefA\xf3\xaa\\\xd26C\x17\x0em\\\xe8\xae\xc3\xa1}\xf9s\xe5J+\x87\xbc\xad\xcf\xabR\"C\x1f\xc7\xe76\x1eC1\xb9\xfe\xc9\xc1\xc7\xc5\xfb\x85\xbc<\xc9\xe5\x8b\xa2\x96^\x8e\x0e\x9a\x19\x8e!\x9dE\x0c9\xfa\x9b\xe4\x7f\xf9c \xd9C\x97t7A\xadE2>\xe3 2\xf8\xf1\x19n\x9f_\xfe\xdeO/\x7f\x0e/\x7f\x8c\xcfIq\xb4.\x0d\xa6\x8c\xad\xe4\xf3\xfd\xd1O\x83\x87\x97\xff+\x83*\xb5.+\xd2\xbcdJ\x81\"a\xd8\x81k\xe7G\xda\x8d\x9a\xe1\x02\xe1%A:')\x8d\xb6\x06\xac.\x8d\xb0\x1ai7\xae\xf1~\x0c\x91\x16'\xaf0\x8e\xeb\xe7\xa5V\xba&]\x17[W*\x91\xb7\xf1\xe0z\xb8\xcc.\xad(L)\x8c\xb1\xa4\x97#K\\\x90\n\x9bh\xfb{7\x14s\xe7G\xd7\xba\xc4E!?\x9eB\xe8\x1e\x88\xa1k\xf9I\xdf\xc5\xc8\x9c\xac~r}\xff47\xc5\xc1\xb7q\x97d\x84\x14pE\x01\x99S\xf2\xac\xc9U3aV\xce\xe1\x90\x14j._{D\xba&bT*\xafJ\x97\x82\x931\x00\xe2\x9a\\\xbf\x7ft\x83;\xc0\xe5+L\xf9#\xe3\x15I\x8dSh\x9cx\x89\xc3c^0S\x0b`\xe9\x06S%\xd3\x0d`V\x95R\xd2\xd4\x8c\xd4m<\x14\xad\xdf`\xf4\x08?\x10\xcc)\xd2\x95\"%(\xa1he\x14\xe8\x8a\x8f;\x14\xa3\x9bf\xb8$x\x8b\x10\xeb\x84\x95\x9cN\xd0QVP\x96\xe2\xde\x06?\x0f\xa1I\xe4o\xb2\x87\x18(~]r&M~\x81b\xc8\xb1\xc7\x16B50\x04x\xf7\xdaC\xd7\xbcl\xa9\xac\xd1I\x95XpJ\xd0\xbe\x8d\xbb\xb8w=\xbc[\x00\xdd\x1d\xd4%\xe7U\xd2\x8b\xabJ\xc94\xb5\x91\x83k\xdd>\xe9\xac\xae\x88$\x9dRZ\xcbIU\xa7\x16\xba\xce\xa4a~\x0cHH.]\x01\xc4IH3jv\x8c\x0b\x99\xe8\x9e\xe6 \xae\xe8\x99_\\Yi\xa9J\x98b\xe9\xb5~\xe7\x1f\xb1\xea\xe0jEk#\xa9S\xe3\xb4\xaa\xaa3\xed\xdc\xefw\xee\xd1!\xf1\x02)\xe6\xba\xb4\\\x93\xfe^%y&\xee6\xae\xdf\x0f\xfe>\xd2\x0d\xa6\x05\xe7\x84\xa8:\xcd\x0c\xac*\x15#\xfd\x14\xe4\x99\xfbm\xc0Fq\xb5\x80\\\xd7\xe9\xc4\x84\xd4*\xebzi\x1dm\xe86\x11\x8b\xe9\xc3\x02\x04]\xb7+\x95f6i\xbb1A\x84\xf1\xd1=\xcd\xf0!9\xd48\xeb\xd2j\x91T\xb8$\xb3\x1c\xa9:\xd7\xef\xf7np\x8f\xae/8\xdd\x0d9\xf9X\xbe\x00U\x1bAj\x8aX\x17\xb2N\xa5\xdcE\xef\x06Wl\xa2o]\x0f\xd7\xc9w\x99|LR\xdb#}cE\xe5}t\x9b\x19; \xb5\xfe\x03\xdc|\xeb\xcd\xd9\xad\xd2.5fWYS3\x968\xef\xe9J\xfe\x14\xfb\xfd>6p\x93\xfd_\xb2?\xf3Z\xc6Hy\x0c\x8b\xaa\xa2%\x11\xf1\xf6;\xba\xa1\x15\x07\xb8y\x85\xb9\xc4\xf1J\xda\x8akK\xda\xed8P\xa7\xca>:\x1c\x9b\x83\xeb\x91!\xa3\xdc\xaf+e\xd2\x9d\x04\xa3\x95I\xb4\x93\xeb\x8bMh\x03\n\x97\xae/.\x11\xd2M\x0d\xba+\x82C'7\x96\xfa\xc4\xd1\x1f\\\xbf\xa3\xd2\xb99\xc1\xdc\xe4\x84\xacM\x1e\x97R\x9f;\xfa\x8e\xc6\x95\x83o\xe7\x03\xca\xae\xff\xe2O\xefP\x9ai\xb0\x96\xcc\xeb\x10Wh\xdd\x90z\xf6\x0d\xc1S\xbf\xaeq\xe5bs[\xd24\xa8!\xc7\xe0\xba\xb9\xdf\xc3\xe7\xe4\xd0\xa9ii\x14\xc3\x84\xd3\x8e+\x92\xcc;\x07\x9f\xf1\xc1\xd2\xf0\xadY\xcdiH\xa9\x0c\xa7HF\xb7\x19\\\xbf?O\xb3\xfb \xe7&/\xb5VIe[\xb1\xfc\xda\x91n \x15\x89\x81\xe0\x87W\xf4V.:\xa9\x9a-C\xee\xe8\x8fs?\xcd\x07\xb8]\xc0\xd2\xa1\xd2P[\x19\x14\xf7\x13\xe54\xe3H\xb1 \xc3\x0c\xb7\xaf=k\x8f\x15\xe9\x82\x05S\x92\xaa|\x0c\x9b\xb8s\xbb\x00\xb7\x0bH\xb3\x9f\xa8j\x06\xd6\x94\xcc\xa4am\x9c\xbb\x8d\x1f\xa8\xbf\x84\x11n\xbf\xf1\xe5&b9K\xe91\xcaP\xdc\x93\xdf\x87\xde\xf7\xd8K\xbf\x9c`\xeeW\xbc\x12u\xd6,\xd45K3\xc5\xe4\xbb8\xb4\xfe\xe0\xe1\xcb\x8ar\xe4\x8c\xc9t\x97A\x9b4\x12L\xf7\x01\xdbs?\xdd;\x8c\xff\x1b_\x1e\xcd\x05\xe7\x92F\xf3\xaaR\xd4%\xe6a\x9eF\xff8\xc3\x8f\x0bX\x8a]\xe4\xdb\x15\x15]\xc0J\xc5N\xa7f\x1blr\x1b\xd7\xe3\xd0G\x87g\xdf\x86,\x1d\x90\x9b\xa4\xcb\xacKM\xc2(\xf2?:,\xa1]\x80\x9f\x17 ,v\xbdEMH\xf0\xb2Vg\xc1\xc3f\xee{\xb7\xf7\x1e.\x17\x90/\xd2\xdat \x8f)\xacmZ\x90\x06\x0f\xbb\xb9/Z\x1f\x06\x0fo_\xfe\xdf\xbe\xf8\xf0\xf2\x07bfI\x01\xc3\x08\x85 \x126\xad\x02\x82\x87Ch\xb1Y\xfa'\xb8Z\x11&[\xf1Rp\x99/\xce\x97&\xdf\x8f\n\x1e\xba\xb9\xc5\xc9\xa8w\xbdw\xbd\x87\xebo\xbd\\Q\xd2\x84VyiS\xaa|w;xl\xdb\xbb\xa7.\xce\xfd\x84m{\x81t:/J\x91\x92\xc6Y:\x9c\x0e=\xb8qr{\x17{\xb8X\x00K\xa2z^\x98iY*\xa4\xdb\xb8\xfd\x80B\x17>q\xf8\xe2\xaa\xe4\xb0J\xf1D\x11z\xff\xe8Z\xb8\xcc\xeer\xaf,\xc9\xa0\xba\xc6\xd5Z\xa0\xabM\xfb\xd8\xb68\x95g@;4\xa2\xac\x15\xbe,\xaf\x1bC\x0f\x8d\xdbaz\x8a\xcd\xfc<\x0f{x\x9f\xbd\x97\xc9\x9bS\x99\xa5b]\xe7\xa5Eb\xbc\xc3\xe9\xf4}rH-V\x96\xa2\xa6\xe6\xa1q|A\xa2\x03N\x8c\x0e\xae\x92\xc3\x18X\xa6,\x08\\\xbfbR\xd7T\x1c\xdc\xb4oh\xa2\x19\\\xd3\xb9\x9e\xac\xc5\xe4\xa0\xcb5H\xd4\xa4\x11,\xb56\xa0m)\x94\xa6\xc2\xed\x1cN\xd5\x83\x83\xeb\x05\xe4\xa2\xcb\xedO\xd7%\xa3\xd4\xf4\xae-\x0e\xae\x98\\\xeb\x1e\xe0c\xf2|!Of`\x82n\x12i\x1c\xa2\xe9\x84.\xf4pt\xe3\xe0p\xe8'\x87\x92\xa0JUq\x81I\xa8xMI8\x0e\xaei\x1d\x99K\xb99\xc1L\x9c\x0d\x18\x19QJJ\xc6\xe8\x9b\xc1\xb5p\x9b\x1c\xa1\x92\xed\x17Y)\xd0\xa64\xda\xd0k\xc70<\xb9\xd1\x0dp\xbb\x00J\xa3)+\xad)\x85\x82I\xa4{t\xd3\xa3\xeb\xf1m?\xaf(W\x06\xab9U\xb6J6\xa8\xc2W\x94\xec\x87\xf0\x8cB=:t\xf5A\xa0\xa8\x99lP\xc9\xb2V\xe9\xdaP\xf8\n\xdb\xc6u\xa3{\"cN\x0b\xaa\xd2\xee\x11\xb3\xd4\xae\xd3\x8es\xb2\x83\x13\xbe\x02\xcaW\xbd\x83\xb7\xc9\xa9\xd2-*\xa6\xd3\x95$\xa9J\xa5\xb1e~\x85&<#\xc1\xfb\xe4P\x94(\x80\xa7\xf6K\x1a\x00i-\x8d\xa4s\xfb\x88M,9\x94\\\x89-\xb0ZLfUIU(|\x85\xd1==\x85]\xf1\x9b\xdb\xe0\xa3\x0dp\x9b\x03.0\xe0\x02'l\x9bTU\x92\x991\xba\x97\xcd\xd5\xca>\x8f\xfe<\x14\x87\xd8\xc3m\x82W\xb1_2\xa1\xebd D\xaa\x92\xe5\xed\xa6\xf0\x15\x1eC\xe36\xf03=\xf3\xb5M\x91T\x97\xe84g\x19e\x06\xf8\xcdm6nt\x1b\xb7;\xf7\xc5\xbd\xdb\x85\x1d\xfcv\xf1*\xe8\x07\n\x929dL\xbeO\xe8\x93\xe9N\x9e\xb0R0\x8dYg\x9aqe\x91\xcdw\xaeK&\x037\xaeu\xf0\xdd\xea\xbdD/\xe9\x88\xc9\xd2\x82b\xebH>\xc0\xde\x85q\x1e\x12\xc3\xbb\x84\x89Z\xa6\xde\xcb\xeb\xda\x08K-\xa1f&\xbd\x06e\xf6~\x0f\xef\x92\x93K-\xeb\xce\xaaz\xd9\xe8@B\xdf>\xb9a\x07\xef\xb2\x9b5\xda\xb0\xe5[\xac\x0e+E\xa5\xa8J\xf6\x8d\xeb\x8a)>\x0e_}\x03\xef\xd0\xf3%{p\xec\x17u\xc955\"\xc3Y\x8a9<\x8c\x8d\xdb\xc1\xbb\xec\xd2&.\xce\xefRij\xb7\x95U\xa2\xa2j\xd8\xc7\xc7\xc6\x0d\xf0.9\xb9mU\xbc\xaei{\x86S\x8e\x1aw\x7f\x1f\x8a\xd4E\x02\xbcO\xbe\x8b\xe4\xa3kV\xb8\x9c\xa4Q\\\xa1h\x9eV\xda\x03\xdc\xfb\xdem\xda\x19~\xc8.]\xc338\xb9I\xb3(\xe1\x84\x81\x96\x9dS\xaa\x08\xf7\xe0\x9ei\xf1\x99\xfd\x17\xe8\x97I;\xac\x96F\xe2\x1b,.\x97Dj{\x07\xd7\xb632`\xec\xa9$*&\xc8Z\x99\x15\xcc&\x92\xa9\xd86\x9e\x9a$NW\xc5\x9b\xc5\xb3(\x10\xca\x8ac\xcd\x08\xba\xcbdTbzv\x1b\xb8\xa2g\x95f\x9dd\xfa\x03j\xb6\xd8\x86A\xb2\xc6\xf5_\xa9\x83\x1c\xdd_B\x0fW\xab\xff\x86\xfc9\xc3\x92\xf6\xac\xc4\xd2\xa4\x0eqp\x7f k3\xbcZ\xbd\xd4\xb0\xe8*\x14+M\xa5\xe8z\xa1\xcd \x9aqmvEOjz\xba\x14\xca0#\xb0<\x98\x14\"\xa5\x07;\xf5\xb9/\xfc\xd8\xb9\xdfB\x8bC\x17\\\xe5\xb0\xef^\x85\xd1\xf9\x0b\xdd\xff\xc3f\xa6jMC`\x17\x86\x02\xfbn\xe8\xe0:\x0c\xc5\xfb\x04e\x95\xf4\xffj\x94\xff\xe9\xf4X i\xa9\x1bv\xf1/n\x1c\xfd\x98\xea*5\x12,\x92b7\x8fS\x80\xebo~N\xad\x06K\xa8xK?\xe7\x8c*\xb2\x81\x83%dlZ\x84\xa7\xed\xd9]1\xb9\xe3\xd17\xb47\xbb+\xbe$\x8fL#\xab\x16\xa2\x06U\x95\xdc\xa6J\xee\xe2c\x87\x8d\xcc\xed\xe0\xfa\x04\xf3]e%\xa5\xa8%V0\xb7\xda\xd6)\xaf\xf1yG{?\xd7\x0b\xa0RE\x19\xa4\xae4\x0d\x99JT\x96\xca>>\xb6\xa1\xf8\xea\xc7\xa6u_\xe1\x13z\xfe\x96=4\x84\xdb\x92\xcejq\xc6U\x94\xfa\xc1u\xcf\xfe)\xe5\xfa\xc9\x1fpE\xb7\xfa\x7f\xf1\x87%\xe3X\x8e\xa4\x06!j*\xcd\xd1\xb5\xbe\xa1t\xdf\xaeH$\xddYkm\"\xe5\x92%\xd2a\x13p\xbdFN\xbe\xe8\x98\xe5(\xb5*\xad%\xc2\x9do\xdcT\xdc\xbb\xdf\xee\x1cq$\xff\x0f\xc9\x9f\xfb\x8c\xad\xa5%\xb5m\xa1\xa8\x1d\x8d\xfe0\x17\x9dk\\\xdb\xfa\x06n\xd1w\xbd\xf8(\xf5<\x9f\xb1\x93\xbe\x0fq4\xae)\x0e\xb4\xaa\xb8ExE\xb0JCj-\xad\x16t\xa7\xc1\xaaJg\xf2n3\xd0F\xe2\xed f=\xd5\x9a+\x85\x19\xa9p\xee\xd7l\x89\x7fpw\xa9tNpa0\xdc(N\xe3\xb6\xe2V\xa6\xd2l\xfc\xc1\x0d\x0776\xaf2r\n[\xb3\x93\xa5\xa5\xca*\xce\xa9E\xa94[O\xee\xe06\xf0\x85\x9eK\x96\x85\xb5\x96\xb2\xcc\x8d\xd0\xd8\xe3&\xd8\xc4a\x1f;\xd7z\xb8\\\x11\x1d\x1e\xc8E\x19n\xb9R\x90\xa8\x1f\xc2\xe8\xe029\xb4\xb9\xacJ\x95.T\x84 \xb6n\x9c|\xdb\xba\x01\xde\xac(Gf\x97\xc8\xb2\x0d\x8b\xfb\x8e\xcc\x85\x0ea\xb7O\xc6B\x13\xc2\x94~sX\xa0\xcb\xfa\xec>B\x17I3s\xf2\xa9J\x18\xe9\x15\xd0\x05zR\x88>\xbb?\x82;\xb8\xbd{tp\x91]\xc6\xb3\xe2\x020aK\xa9\x14\x125\xe1\x10\xba8Ex\xbf\x00\x9e&\x0c\x06L\xacf1\xef\x8fpp\x07\xf7\x1c\xe0*9\"\xc9\xb0\xf5\xb2\xd2\\O\xfb\x88\xb2\x0b\xe7\x07\xd7\xa7\xb7^}\xe3\x93u\xbe\xe5\x9f\xb6\xeeq\xee0\xa77L.\x1e\x90#\xb9\"\x1b&P\xc0\xb0\xc7\xbcJ\xc94\xce\xb4\xbaI.O\x13\xf7b?\x17\x07\x93DFk\x92\xab\xe4\x90\xd5\xe4\xc5|\x0b0\xb1\xe8M\"e7\x1f\\8w\x8fn\xefFw\x08p\xfd/~\x96g\xf1\x85w\x81\xc8\x1b7nr\x07\x0f\x9f\xb2\x9b\x0d\nc\x99\x90\x8d$$\x19]\xe7\xe1\x13=\xb3E\xcd\xe5\xe6\x85d\xa5\xd0kT\x98\x99O\x94\xa34\xb9\xa7\"M\x89\xad\x97\xd2}\x9a\xfb\xb8\x8f\xf0Kr\x04\xfb6\xef\xcb9\xe3\xd9\xa1\xc1E ]\xc6\xc6e`\x02t\xf2\xcaN\x97\x06\xaa\xd5\xbc$\xd17sWLM\xe7\n\xbf\x9b\x1a\xb8B\xef\x17\xf4~\x87\xde\x85w\xa9\xf2J\xae\xf6(\x91w\x08\xb4\xcb\x9c\x1c:\xa6_O\xb5h\x0ff\xb1Wxh\xe0\xd8\xcc](\x0e(K\x15\x878\xc0\x0d\xf9\xaf\xc8\x7f\x15\x87\xb4bf\xa7\xa3\xbaJ\xae\xa6vV\xee\xf1\xc1=\x15\xc7\xc6\xf9.\xb3\xdfb\xc0\x0d\x05TI\xcfA+b\xcdV\x81\x91u\x1a\xdc\xd1\xe3[\x06O[:\x8b\xf7\xb3_\xee\xd52\xdaW\xadN\xf7\xed\x0eG \x9b\x17\xf7n\x17Ix>a2\xe2\x91o\xb0\x1b\x0b\x8c\xd7%W\xc9\xce\xf3\xe1\x08\xcd\xa3\xbb\x8f}\x1f\xe0\xfd\x02X2\xf7(U2\xbd\xcc\xb8)\xb9H;\x15\x87#N\x90q\xc0\x19\x96\x9clOU\xc9\xdc\x02\xb8*\xeb|<}8\xc2\xe8|\xe7\xe6\x16n\xb3[\x89o\x0e\xfe\xe9Jp\xa6\x9b\xe2B\x99\x11\xa5Z\x93U\x8b\x14s]2\xcb\x15\x91\xc7n\xee\xc7\x00\xb7\xd9eY\x9f \xdf\xdf`\x9cj|I\xf0\xe4\xc2\xc1=\x91\xf2\xcc\x8a\xaa\x94\xc5\n\x8b\x9e\x9b\xd2Vi1F\xd4\x87Clq\xf4E\x87\x96(d\xcd5\xed\x8aa\"\xb25h\xa2\xed\xf7X\xe8\xf0e\x01\xb9\xa8E\xbd\x14\xb5\xc2\xe1\x8e\xb6\xec\x0fG\x98\xfb{\xac\x8f\x1f\x93\xc3\xb2\xaa\xffb\x8f\x99+\xd2J\xc5\xd5\xf2\xe1\x08\x8f\xb1=>\xc5~O5\xf8\xf3kO.rSg\xab\xb1\xbc.+\xbe\xf2=\x91e\x95!\xd0u\xeff54\x8e\xc3\xe8\x92\x07\x94\xdfr\x1bO\xf4\xa3\xeb)\xe6_^\xe1\x9c\x13n\xa8\x88p)\xad\xa9\xec\x9f\xe2a\xdb\x84D~\x82,\x9b\xd5\xd5b\xcd\x0b\x19?\xc0|\x0f\x90\xa2\xec\x03\\,\xa0JF\x89*\xa1\x04U\x80`5CJ,\xc4G\xda\x8dz\xb3\xa2*\xe9\x95\xa8*\xed\x07\xf3\xba\xd4\x96\x94\xae\x0f\xb8\x8c\x19\xc7&\xc2\xbb\xe4\xb0d\"@\x99t\x06\xc0\xeb\xd2rR\xea>\xe0:\xa2\xdb\xa4:}\xbf\"^\xa5\xb52\x97\xdaZJ\x87T\x9c)\x9d\xe8\xfb=\x19\x81J&7\xde\x7f\xebeI3E\xd1\x01&\xbeG\x1b\x91\xb8\xc6\xd0Q\x89\xbc_\x11\xd3I\xba\xcb\xc3\x11\xaf\xcbZ!\xed!v\x1b\xf7\x18\xe0*\xbb,\xc9\xd6\xaa^\xab\xb5&\xfb\x11H\xdb\xb9{\xe7\xe1\x9a\x9e9>ai\xff\xbd.\x8d\xe5\x94\xe2\x0e[\xc8\xfd\x8c}sEK\x89d\x038\x9cN2q\x89t\x18\xa0w~L\x03\xc4\xc7\x15\xe5N\xa7\xb5\xa1\xf3BS2+\x19\xc5~t\xbew\xdd\x8c\xc5vs\x829-u:^\xacKS \xca\xdb\xe4\xfc>\x1e\xe0Krr\xac\xbcb\xd22\x8a\xb6R\xd8?\xcf\x0e\xcfp\x88\xfdnp\xd3_\x9e\xfcC\x84\xab\xd7\x1e\xd2\xa2\xb0\xb8d3b9\xe3?<\xc3\xb3\xa3\xeb\x1e\xf1\x01~]Q\xbe\x16\x99\xaf\xd3!JSf\xeb`\xe3\xfa\xa2u\xdd\xb1\xa1\x9b\xb9}\xf1\x01\xf1\xcb\x1f\xa9\xab\xf2\xaa\xacx:\x89\xab\xaa\xe5\xc0(3\x8d\xae\x1b],\xfa\xf8D|\xb7\xc9\xfb1>\xa5c\xadtK\xb6\x12%\xbd\x05\xa7\xccbG\xaa@\x9d/\xde\x86l\xa2\xacZm\x1bV\xb2dg\xed\x06\x1a7\xf8\xa9\xf0S1\xb9fr\xf0\xfe\xe5\x7fd\xff\x17\xf2W\xe2\x95=\x1cA\xf2\xf7Y{ \xb5\x8f\xbd\xeb\x1c)~\x10\x10t\x9fC\x83\xa9V\x1d\x9f\xf6\x00\x81\xb6w\xf6q\xe7\x8a\xd0\xe0\x8a\xee\xaf\xa7\x80\xbfR@\xe2\xab\x89O!K<\xba\xc9\xc1'z\nI*\x8a\xf4\xe3Y\x8b\xeb\xf3\xa6\x1fg\x07\xef\xb2\x8b\xbd\x16\x1bG\xben\xce\xaa\x92i\xfat\xc0Y;A\x17\xda\xc3\xdc;R\x82I\xa0N\x97\x10\x97\xcc\xf0z\x99)\x91\xfc\x18w\x0fq\x08cs\x08p\xf3\n\xab\xb4\xdf\xff\xca\x80\xf0\xa2\x8f\xd4>\xc0n\xde\x06x\x8b\x0f\x94\x92\x94.EV+\xe2\xb2\x94r%\xdb\xc7\x07\x1f\x0e\x01\xdee\x97\x0e\x04\xeaU\xcb\x84\xebU h\x1f\xe0\xde\xcd\xfd~n7\xbe\xf7#\xfc\xf0\xda\x83\xc2\x81\xd2\xe9\xfaF\xd2\x85\xab\x91\xa1s\xcf\x9b\xc1#Y\x80\xebW\x18%\x1c\xa5O\xc6\xfd\xd5\xeb\x94\x8f\x13v\xc7[zrM\xc9\xa9I\xd7O\x9du\x0e\xdc\x9d\x9b\x9f\xe3\xec\x86 .NP\xa5%\x86\xac\x93\xb6$\xd2\x85\xa9pm(\xe2\\<\xc5y\x1c\xfd\x1d\\\xbc\xfc\xaf\xa9\xb8hC\xf1i.~\xc9a*Y\xe3a\x954\xcc\xc0y]2SW\xb5Yc\x186!\xf3!R\xe93\x1d\xb6N\x8a\xd6'\xaa;\xb7P\xdd\xb9%\xce\xaaR\xbc\xa6\xc3\x8bZU\xc6\xf2\x85\xfa0\xb9M\x11\xe7;\\9$\xae+\x0c\xf9\x94Cd\xda\xea3F\xb1\x94\x9d\xca\xdaJ\xd6\xc4\xdd\xc5y?\xfb\x01.\x16 \x93\x04\xac\x8dd\x95\xa6\x032!\x0cgD\xdc\xef\xe2\xfcL\xf3\xc8\xfc\x9c\n\x88\x95\x153V\xd4t%\x8c\xf1\x9a2\xb0sq\x0e\xbd\x83\xb7.\xce/\xff\xabwK&\xa5\xc21\x95\xe2T\\[*T\xdf\x16\x87\xc6On\x98|\xd1\xff%\xce\xcf\x83\xdf\xc3wmqu\n\xfc\x94\x02)a\xac\xe4\xdcT\xd9d#c\x96Y\x8c$t\xcf\xa1\x85\xbf\xd23\xd7\x9b\xe2\xac\xe6\x02\xce\xa9\xdc'7\xdc\x87m3\xc1\x97\x05(\x91\xcd\x82\x19\xaeI\x1fVr\xa3Ss\x98B\x9c\x0fn\x08\xbd\x87/'\x98\xa3\xad\x05\xab\xad\"\x05\x13\xc6\xb4Qg\x1d\xa9~l\x9b\x00\x97\xc9\xa1\x9br\xf8\xb3N&o\x93\x19\xe8n\x07C\xec\\\xef\xc7)\xc0\xe7\x15\xd5,)C \x12\x9dP\x16\xa2\xbbMg\xdd\x1e\\\x7f\x08\xb8\x08\xf5p\xb1\"\x1c\xa0\xce\xe9\xa3\x05i\xa3+5\xf0n\x0f]\x98\\\x1f{\\\x0e]\x9f }!\x80\xe9\xd56<\xf2 \xf5\xe8\x1a\xd7O\xaeqp\xbb\xa2d&\x9a\x96Q\xaa*\x85B\xba\xc9w\x9b\xf0\x8c1}Y\x11\x1d\x96sQ\xd6d\xe7?\x0b\xcd\xdd\x01v\xad\xdb\xc4m\xd8:x\xbb\xa2|E\x92\xd9\xf4\x85\x13\xceK.\x93$\x8b\x0c\xcf\xfe\x18\xc6\xc9\xc1\xdb\x05\xe0r\x06%_2\x9dM\xaaTJ$\xe9\xadka\x08\xcf\xc1u\xf0998\xe50y\xd2\xbd;-\x1c\xba\x0e\xb6\x0d\x8e#\xbd;<\xd2\xe9\xc8\x8aI1\x0eE\x81t\xa0bQ\xbc\xc5*\xef\xa0y\xf4\x8d{\xec\xe1}vic\x85\xadJ\xf0V\x97Dvp\x1d\xcd\x13W\xd9%\xfdk\xbd\xea\xf3Y\xbd.@\xba\x0eZ\x14w\x1f\xfb=|X@\xdeO\xd4\xa7X\x0d\x11\xc6p\xde\xb9\xc7 >,\x80^/JI\xe7\xde\xa7\x18;wp\xf3\xe1\xe8&\xd2\xffN\x88\xee\xdc\xca\xd2\x80\x95\xab\xe6r\xd7AO\x97\x1e\x92y\xb3\x05Qj\xeb\xb2\xa2X)?\xe3\xec\xfa\xfd\xb1\xf1\x01nW\x94\x8d\x920\x05V\xac\xd7e\xbb\x0e&w\xde\x87\xf3\xd6\x9d\xb7\x1e\xbe\xac\xf8\xe5?\x16\xa5\xc6e\xe0\xb5z\xd5!D\xae\xc6\xef\xc8\xe8Q\xf1\xf4\xe8\xe0\xcb\xea\xfb%\xef30\xb2|l\xcd\xaak\xdf\xf5\xb0qnrC1\xbb\xbeh\xe6\xe1\xc9{\xb8L!?\xba\xbex\xff\xf2\xdf\x14Dm\xc5\x9eT\xbc+^\xea\xb3\xee\x11\xba\xf9 '\xda\xeb\xe4\x90\xadxv\xba\xfe.\xe8\x8c\xaf{\x84g\xbf\xf1\xbb\xf0\xe4\x8a\x83\x9b\xfb\x9d\x83_\x17\xffU\xf2\x93.\x1d&\x8eN\xe0\xce\xba\xdf\xc1\xb5\xb1\x1fc\x11\xc78\x84\x08\x17\xc9\xf7)\xf9H\x9d\xf9d1\xc7\xaar\xe9\x17\xbf\x83\xeb\xdc\x84=\x12.\x08\xbc\xfc\x9d\x14s\xac\xb6\x8ct\x1fs\xfb\xa5\xb5$[x\xb6\xee8\xb7\xcd\xec\xb6\xf0fEL\x01\xaf\x14\xd3X\xd6t\xc2g\xe1\xdc\xd2\xdd\x02\x14(\x89i\x0cS\xba\x07L.\x19\x9e\xab\xe8;\x05\xb6^\xbe\xb0\xd0\xfd\x8e\x03\xee&Nn\xe7\x07\xd2\xe0\xbd\\0\xd9sbd\xbb\x1a\x17\xe2|\xcd\x01\x99(r\xdb\xf84\xb7@\xb6\x892\xa6\xfa\xab\xd7\xdb7\x96f\xc9L\xbe\xd2\"\xa0\xe2Q\xa5\xe5uJ\xb5\xc2\x99b\xa1m\xdd\xc6\x0f\xa1O&~>\xac\x1e\x9c\xa5q\xe4R\xa4r\xbd$\xbc\x0d]\xec\xb7\xd9\x1e\xd0\x87\xd5\xf3\xcd\xe6\x17&\x7f\xfdR\xce\xc2\xd6/\xd9\xfd\x90!u\xb1\x8al\xdc\xb3\x8a-\x1b5\x89|\x8c=I\xb6H\x7fK\x98\xae7\xd4\xc9\x08\xdb\xfa\x92\xd7\x85\x9a5\xe2]1\xba\xbe\xb8\x8f\xa3\x87\xf7K\xc8\xad\xeb\x8b\x1f\xe2\xf8\xf2G\xda|W\xa5N\xaf\xa7\xd0\xef\xda\xe2\xa7W\xa146\xa3\xa8\x86\x19\x97\x8b\x95\xc8S$C\x1c\xdd\xb8\xf0~&Ond\x82XL\xf9\xfa\x95K\xcb\xa2t\xc6\xb1\xd8\x0c\xee9\xae\xeck+[~\xbeL?\xf3d\xa5m\xb9\xb4f\xed\xb2Ex\x8a\xf8!\x0c{\xdf/\x11\xfd\x94|T\x94v\xb1\xba\x82EI\x1c\xa1\xa0J\x1b\xe1\xf3\xcb?\x8a\x9b\x04\xc9\x18\x89.\x19\xf5UQfs\x19D\x1d\x8b\x9do\x8bi\x88\x0fn\x17\x07dJ!_\x96\x90\xdc\xfc\x94\xa2aA\xaeI\xc3\xcc\xfaq\xf2\x1b\x97\xeew|\x971\xcf:\xf1\x8a\x1a\xc2\xa9Z\xa8pp\xc6\xd8y\xb2d\xb2\x8f#\xf1\xfd\x80ao=\xd92\xc1\xb0\xdc\x05\xed\xda\x89W\xd3@)\x92)\x16\xbb\xd8\x85~\x1f\xe96h,\xdef_\x95LIH\x9bl\x940\xc6J\xcbh\xc1\xfa\xaf\x8c\xc5\x14{\xd7N\xfe\xe8\xb7\xdf\xc6Q|9\xfd\xc0S\x99\xd5U\xea\xa5V/\x06\x00\xbb\xdf\xe1\xc9\x0f\x0f\xae\x0f#\xfcB\xe0\xe5\x1f\xe32\xcb\xa7\xafF\xa4\xd9\xc2$\xd3y\xdd\x13J%\xc7\xd8\xef\x8b\x8d\x9b\xe6bt\x8fn@\x01\x85\x82.1\xe8\x96\x82h\x17\xa0\x94\xc9\xe8'\xe38\x0d\x9eX[|\x1c\xc2\x914vS\xd0\x07|\\QP2?\xbeZ\xd1\x92\xcb\x96\xf8\xab\x08:\x97\xcc\xf5-\xdc\xd7\xd9_\x91\xb9\xf8\xea4I\xb3o\xde\xdb\xbb~\xff*\xb9\x1f\x93\x97\x06\x8d\xe5\xd8\x95\x9d\xec$e\xc6\x19\xdf\xe7{\xf7\x18\x8b\x99\xce\x80S\xd0u\n\xfa\xb1\x9di\x0cT\xa5 \xe3\xe4u\xc9+\xf6\x0d\xef\x88+\xf8\x85\xeb\x96\xee\xd1\xd5\x94A!\x93eI[\xf3o\x19\xe6~\xefBqp\x83\x1bO\x8c)\xf0\x8a\x02\xc9x\\)T\xb26^ \x8b\xec\xc7\xd6u\xae/63\x8e3 _\"\xa6{\x1ddm\x0c\x97\xf0H9\xd1\x8fG?\x1c\xdd\x8e\xcc\x89\x92\xfff\xf5W\xe9\x93\x06\xa2f\x94>\xa60C\xcf\xe0\xdaml\xfd\xe4\xe1b\x01\xa4\xcf\xce\xb0\xbb\xa7\xbb\x1f\xb4\xf9\x94\xb4h\xbag\xd86a\x1b;\xfa\xfc\x1b\xb9\xf4\xf1\x13&K\x93\xb4\x84\xc8Vf\x8d\x13\xf33P}\x921\xd5\xeb\x15%\xc9_\x962\x0f\nB\x94L$e \xe4\xf0\x93\x7fpp\x9d\x9c%j\xa9\xf3\x07\xf4*\\`\xd1fE\xef\xc0\x1f\xe3!\xc2w\xf4\x14uZ\xa3,w\xa0\x98*9\xd2\xc4\x83k\x1e\x1d|J\x8e\xa0\x0f\xf3\x98\x93\x0d\xfc\xe5\xe6\xb4@\xda\xc9u\xe3\x8c\xa56\xce\xb4\x8b\x82\x94\xabu\xa0j\xbd.\xd8{\xd8\x85Mh#\xbcM\x0eI\xc4\xb2\xe4\xb0\xde\"E\x1a\\\xbb\xee\xc2\xe4{\x0f\x9f\x12|\xf9\xa3\xf7\xcbe\xcbEz\xc7i\xaf\xdf\x83\xbbw\x0f\x8fs\x1f\x8ay\xbf\xf1\x0f\x8f\x1e.\x96\x80\x1fs\x80\xa0\xc6h\xb4\xa9\x15C\xc0*\xcd\x91\xf3\x9e&\xb9i>\xcc\xf0\x03\xc1/\x08U2\xf9X\xd5\xdaHC\xe7\x0c\x95\xb0B\x13\x83\xdf\xbb\xbe\xe8P@\xf5\xf0\x03y\xae\x93\x87\xd3W\x1c\xb2\x8e![\xcf\xd8\xfa=t.\xecb?E|\xd3\x18\xef\"\\\x9f\x02\xbeP\x80\xb4\xc8\x9b\x0fu\xed\xf2\xb1\xac~\x0f\xfd.\xb8\x87\x19>&G*\xfa\x02\x05\xb7\x92YH\xb7+\x84A\xb2\xc1\xdd\x85\xbe\xd8\xb8yp\xf0\x99\xf0%aY\xe5\x8bp\xeb'\xec\x908\x1c\xdc\xc1\xc1\xe7\xe4(\x96.\x80UZ\x1a\x05\xaa\xack]Y\x86t\xe3|\xe7\xe0\x16\x1fdh\x05\x87\x0d\xda\xc6\xa3\xbb#H\xf0\xe4\x9e\x1d\xfc\x82\x8fl#\x93[i\x0d\xa3\xe8\x946\x9ar\xf0\xdc\x84\x81\xae\x85\xc0\xaf+\"\xe9\xa5\xa2\x0f_rC\xdf\xf1\xaak^S\x9c\xcfa\xe7\x9f\xfc\xe0\xf7\x1b\xac\xfa_\xbf\xf1\xa5J\xacjQ \xb8\x84\x93\x8c\xcb\xb3>$i\xf7\xeb\x1c&\x97\x05\xde\x8c\xe9\x1e [\x96\xe8d\xa5\x92\x9f\xf5-\xc4\x07?\xb4\xae\xdf\xfb\x16>\x9d`\xb6\xd0\xa6u\xcd\x046\x92Z\x0bi\x0c\xd2\x1f\xbdoG\xdf\xcca\x84\x9b\x13\xcc\xf45\xe7u\x95J\xae6\x82!\xfd\x03J\xaec\xfa\xc2\xdaO\xafp\xe6\xc8\xd7\x18\xd7\xef%\xf6\x116\xbe\x9f\xc6\xfb8\xec\xe0rE\xb8\x16\xa8\xedj\xb9\xd0,\xf21\x91\x0f\xfb\xcd\x13\\&\x87\xf6$\xc9B)7\xcb\xd7\x85\x90(\xc2\xe5\xcb\xffNv\xd0\xec\xaag\xbc^\xc8\xecc\xda\x89\x8d=\xbc\xcfn6r\xb5\x1cx\xb2R%\xaay<\xdc\xa30\x9d\\\xb2\xa0g\x97\xaf7.\xf7\xb9\x91\xb2\x8bp\x1d\x97\xdf\xb3\x85\xbd\xba\xa4\x9fP\x80\x87\x8f\xf8\xc0J\xa9\xab\x93i\xc1jM\xf0\x10\xc6\xe9\xc1\x87 >/\xa0JQ\x99\x85\xd6\xac\xaf\x1a\xa78\xec\\;nf\x14\x94n\xa7\x97\xff\xfd\xca\x8bSO-\x96\xc9k9l;\x8b\x1d\xb8\x11g\x1e\xb2zu1\xe2\xbcC\x90\x96\xdb8\xb9*Pu\xfe\xe2\xaeA\xf2q\x1e\n\xd7\x16\xbf\xb9\xcd\x80\xcb\xf3\xe4\xb9@Of\xa9\xf3;T]*\x96\x0e{\x8e\x1e\\?\xb9\xb1q-\xd9\x16\x81\x8bo|\xc9\x96\x84,k\x9e\x0d\x8ejV\x1a\x93\xf6\xa7\x8e\x1e\xb6n\xdb\x84\xe3|\x1f\xe1\xcd\x8a\xd2\x0e\x92*\xebl\xfa\x1aY\xaa:\xcd(\xc4\x82\x13\xda\x1bz.\xa4J\x90 S\"\xd5\x96\xb6Z\x88\xb4q[\x94\x17\xe1\xcd\x02\xd2\xa7J\xebe\x87\x83\xac\xd8)E\xb4\xc3\xfc\xbcu{x\x93\xdd4\xadU\xa5H_\xdb\xc8\xe6w\x89t]xm\xdd\xe8\x8aMK\x1f+]\xd7^o0\xf02\x05V\xe9s\xa5\x8a\x0c\xb5.{\xc3\xafc8\xba\xcd<\x9cxo\xc8\x8bM\x98\xb6\x1a\xd3\xbd\xd3sS\x95Un\xc5G\x7fZ\xb9-\x91\x9c\x16oK<\xf9\xa3a|\xb5\x8f@v\x80\xf3\xfe\xc0\xd1C\x17\x87\xb9m\xb1\xf5&7\xed\x84\x08\xd7+\"yC\x975}eu\xd9\x7f:6\xe0\xfa\xb0\x89\xfd\x1e.\xb2+\xd2\x9dr\xceP\x10\xe6tf\xc5\x91n\x83\x82z\x9b\xee;& m\xb6.\xa1\x19\x8a]\xcc\x96\xcahY)\xa2\x8e\x13]UL\x0e#\x93]\x9asU\xa7\x0f\xd40n\x95A\xba\x83C\x82CL_W=aZ\xc5T\xa5\xaeP\x08\xe4\xa24\x95\xb6H\xdf\xban\xe3\xb6\xf0!9KZU\x9d\xd3\xaa4\xa5\xb5\x8d\xdd\x86\xecX\x7fX\x80H\x960\xea\x8aY\xba\x83W\xc9\x1a ;wN\xdfs\xbd\xce.K\x9a\x0b\xf4R\xbeX\x9e!\xba\xde\xc5@\x9f$\"\xb7\x16\xb4_Sk\x9cP9\xc6[I\xa2\x8b\xdbc\xdc\x16}\x1cH\xa7\x88<\x1f\xc9\x93\x8a [d\xa6\x1c\x11\xfd\xdc\xef\xe7\xe0\x8a\xe3\x10\x8f~\x80\xeb\xec\xbdI^ih\xe9\xc7\xadN\xdf\xdca\xaa\xa6\xb7\xc4'\x94\xac{\xf8\x94\xdd%s<%\x9b\x99\x9a\"\xc7\x16>\x90\x11il\xdd\x9f \xd5\x1aL-\xc8\xfa\xe82u\xf1\xf5\xf3\xa0\x89grE\x1f\xfa\xb4\xbas\xc5\xc7\xf0\xf2_\x91\xd6\xc8L\x97V\xd1qrUj&\xa9\x9aG\\\xb7\xb9M\xfa\x0e\xc0\ni\x83E\x94\x8as\x91\xee\xd9\x1a\xc5\x90zr\xbd{$\xe1=\xb9\xefL\xb2DP1\x9d\xee~\x1aA\xd57\x85\xfd\xc6\xcd\xae?\xef\x9c\x1bC\x0f_\x16\xffu\xf2\xe7\xfcZf5\xc5/MU\x9f\x1d\x0f\xe0\xda\xf3\xbb\xd65\xc5\x14\x1f{\xb8h\xcf\xbfG\xcf\x17\xf4T\xe9\x8a\x82\xd1B\x93Q\xb5Z\xa8\xcc\xd0\xcf\xc3J\xffq\x1e2y2s/u\xcdA\xcbt\x05\xf9x\xa0\xee\xeczW\x1c\x9a\x19e\x81\xec\xbb\"\x1f\xb1\xf0R\x18&\x13S\xcd\xb9\xc9L\x91\xbe\xc0FN&\xcb\xc6/\xf5\xaadD\x84\xe3\x14\x8b\x8d\xdb\xb7\xfe\xa9p;:\xf5\xa1\xee\x86\xc1\x97)\xf8\x02\x83\xafh\xa9!i\xf7A\xd4Jk\x0b8\xe7\x98\xbaf:E\xf4\xe8\x06_\xf4\x0eG\x1e\x82\x1f\xb3\x117\xc1K\xc6\x193\x06\xb4(kf\x8d\xa4$\xee\\?\xbaP\xdcGx\x9b\xd0\x0f\xc9\\\x01\xd7%W\x02\xc9\xeb\x9a\xace1\xa2n\xe2\xa18\xccwC\x80\xb7\x08\xaf\x08\xe6\xe8-\xd7\xc2h\xd0\xbc\x14Br\xc3\x91\xe1\xce}\xa5\xcb\xa2\xdfg7\x7f\xbbCia-\xc7\xa8IT\xa42\xb8\x0bC|.\x0ea\x08\x11\xbe'|E\x98\xea\xaf.\x19\xaf4}\xe5_U\xccp\xaa\xf3\xbd\x9b\xef\xf6nh\xe0\xdd\x02r\xe51a\xa5\x14\x98\x92Z1c(\xfa}\x9c\x9a\xa2q\xf7\xa1\x18\x1b\xd7\xec\xdc\x0e\xabrl\x0e\xf0\x0e\x7fx\x8f?\xdc\xe6\x1f\xae\xd2\x0f\xb9\xe1(\xc5q\x1a\xa8\xb1\x0b\xb0:U,E\x16\xben\\[t\xbe\x8b}\x8a\xe4\xaf\x14pM\x019\xd5Fs\xfa\xdc\xbe)\x85fL\xda\x95\xf9\xe0\xa6\xf0@\xfa\xe5SS\\e\xcf\xd2T\xad\xc8\x9f\xdd\xaf\xe8\x1b\x08\xc44\xdf\xbb\xe1\xe0\xe1]v\x97*5ZU\x12\xabT*e\x155\xd5\xc6\xdd\x85g\xd2\x03}\xbf\"\x8aY\x97&\x19I\xadM\x99m\xc8\x1e\x0f\x10:\xd7=\xba6\xc0_\x17\x90KQ -\xb4@\x01\xd6\xd6\xcc\xa6\"?\xb8m\xb3mBq\x88S\x93\xcc2\xa2\xef\x8a|\x99-ica\xe1\xa7\xdb*\x89\xads\xfd\xae\x18\x82k\xe9x\xa8\xdf\x15\x9fC^\x07\xd0\xd5\x13\x9e\x1b&\xb7F)*\xa5C\xe3v\xb4g\x92\xdd\xac}\xa0\x84\xad\xa4\xc1\xd8\x0d\xe3FS#;\xc4\xa1\xc3\x88\x93\x93 \xad\xc0vB\xd7\xcf\x84\xe0\x96z\xc7a\x08\xc76\xe0t\xb3\x80L\xccI\xc5\x9abM\xf7\x02\x8f\x87t\x9e4\xf8b\xe7\x8aG\x97\xd6\xfak\xc0\xcfn\xed\x87\xa6\xcc\x17lt\x85\x13'\xabMb\xee\x82\xeb\xe0:99\x9b\x8aURp\xcc\xa6\xaa8\x0d\xba\x87t\x8b\x0f[\xc3\x93\x1b\xd2\x1d>l\x0dOKcP%cBU\xd4\xf0E-5\xa3\x1a\xee\xddc(Z\xac\xaa\x8f\x88>P\xa5\xa5\x1a\xb6\x86\xcb\xda y]\xd7\x96S\xe9\xf7\xb1uE\xe7[\x07\x1f\x11]#\xa2\x8c\xf3\xd2*\x1c?R\xacG75tW\x8c\x9c\xdc\xc0\xa4U\x1a\xcb;}\xb9?\x95\xf7W\xb7\x0b\x8f\xa1\xdf\xc1\xdf\x16\xb0\xf4;RCEbIjNH<\x84\x8d\x9bv\xbe\x81\xcf\x0b\xa0W\xd7%\xe7B\x89\x1a_\xaf+k*\"\x1e]\xeb\xbb\xa4+\x7f\xef\xa6\x18H\xe5\xbbK\xda\xf1?P@.s-\x05cT\xe6\xc2j\xc6yb\xc6U,}\x04|E\xcb\xd0\xcc+ki\xcc\xad\x0d\xcf\xe3\xcd\x18w\xee\x11\xc5\xbf\xec\xe6OaI\xc3\xad\xa8i\xd4\x13B3j\xf5\x93\x1b\\\xeb\x02|\xc9\xee\xd2\x16+nu\x8d\xadF3a-\xf5\xa6G7\xd0M\xd2\xb0\x8e\x00\x96UR\xe1\xf0\xa15W\x95>;\xb6\xe0\xda \xa7}\\,\xc2E;\xe1\x9cO\xd8\xa8t9\x94[mD:b\xb5\xc2\x10\xcbfx\x8e\xe3\x14\x1f\xe1r\x01&\xadYm-\xe9#[\xba\x94\xa2\xaa,\x11\xef\xdcf\x88\x8f\xae\x18\x9e\xfd\xf6\xf9i{p\xf06\x87|^CLE\xb6\xe9jU+m\x80\xc4\",\x97\x16v\x83{\x1c\x0f\xb18\x0c~~\x86\xb7\xd9wE>\x93\x0c\xe3\x1b%\xb41t\\**\x9a\xebZ\xd8\x07\xbf}\x86w\xf4\xccd\x82W\xdcP\xd2\x84\xae\xac\"\xaa\xc1\xd3\xc7K\xe0\xdd\x024\xcf\x9fE\xaf\x98\xb1(\x12\xe8:\xcd\xe9-\xf4\xf11\xf4\x87\x00\x1f\xb3\xab\xb3A;\xa1*I\x16\x97\x84\xd0U\"\x1d\xdc\xe6\xe0\xe03=u6D\x99wdm\x99^=\xcc\xdd\xdc\x1fB\x11\x876>F\xf8\x9c\xbd\x9f\x92W\x0bJr>\xaec\xb6\xa4\xb2\x18\xb7M\x17\xdbg\xb8\xcdnNl\xa5y\xcd\x14\xe6\xdf\x18f\x18\xe5\x7fl\xdd\xa3\x83[z\x9at\x93Vk\xab\x05\xc7<\x19\xab-J\x9d\x13\xb8\x07?\xc2\x05>\x98\x06\xa3\xaa\x9a>\x88\xa4+!,\x19v`\x15Cqr\x82\xa3\x9b[2\x93\x1c:\x077\xd9\xf3\x06=Y7\xd3*\xad\x95\xc6E\x9b0\x95AA}\x82)\xde\xbb\x16\xbe\xd0\x93\xa5\x8f\x9d-g\x14\x0b:\x1b\"\xdd\xa5\x1b\xa7@W\xe8\xd0\x95\x1cT-\xe8\xd6\x1c[\xcf\xe05Q\xce\xcf\xaeq\xcf\x0e.\x17@\x86\xfbHAh1>\x85t\xbbyHQ\xbd]@\xa5A2)I\xd9#\xef \xea\x92!\xed\xdd\xdc\xef\xe6\xa2\x9dC\xb1\x89\xfb\x1d}\xefp \xb8L\x01U\xfa\xc0p\xd6\xdc\xe6\xf5\xa2\xdd?D8\x84q\xf4\x93\xef\xe1j\x01\xa2\xa6\xeb\x02Z\xd1\xf9\xec\x9a\xa2\xd6m\xe7b\x83B)|@xI\x90\xcc\x08*L\xfd\xebH;w\xbf\xa3\x01\x9e\x1c\xdaO\xd6\x8bV>v\xe15\xd2c$\xb1\xafs\xc3\xdc\xce\x01n\x92\xf7:{\xc9\xb4\xb3.%}\xf8\x87-\x91\xcf\xc3v\x86\x1f\xf1\x91\xef,\x18AI5\xe9\xab[\xc3\x08\x9b\xc1?\xbb>\xc0ev\xc9\x82\xa1(\x05'\xa5R2\xa2\x86\xeb\xebD\xfa\xe0\xfbp\x80\xcb\xec\x12)\xce\x85IC\x86\x95\x92\x8e\x91\x91t\x17\xfb{W\x8c\xf3\xe8\xee\x1d\xbc%\xcfm\xf2d&!i}\xcb\x17\xf3\x93\xc8t\x88\xa3{\xd8\xc2Ur2ae\xf0\xf5\x80\xb2\x7fE\x17\x8b\x87\x11\xfa\xf8\x10\x8a\x077\xc4\x11>\"\xfc\x89`N\xb9\xd4t\xa7\x91\xbe\x05Nk\xeea\x06\xb7q\xcf\xa1?8\xb8X\x80L]N\xe8e{\x9d\x8c\xbdk\"\xee\xa786\xf4A\xfd\x8b\x13$\x9d\xbd\xba\xb4F0%\xe96\xb1\x94L\x0bb\x18\xdc1>`\xec\x0b \x05\xb9\x8a\xda\x90\xb64\xdfXV)\x89\xc4\x1b\xd7\xbaM \x9b?\xe4\xd6,\x9b\xdd5\x950t\xb1\xa0R\xb5\xa0x7q3\x0fOp\x99\x9c\xda\xa6/\x17(\xce\xacM\xdf}\xd32E\x19\xf7nzz\xf2\xc5\xb1\x8d\x13\xd2g\xef\x0dysN9\x0e[\xc2\xa0\x98\xc0\x13\xd3\x10\x1f\xc6\x83{rpy\x828 \xd4\x15\xae\xa7-\xe3\xa4\xf1\xc3\xb5\x11T,\x9b\xf9\xd0\xb8\xc1?D\xb8\\\x11\xcf\x9a}\xaa\xb6\x95%\x93\xebZh\x95\xe2\x9fc{p\xad;4py\x82\xb8~E\xe1,/\x01\xd9\xff\xc7\xd5\x974G\x8e#\xe9\xde\xf5+hu\x86hXI\xe0(\xe5\xa2\xcc\xd2\x92\xea\x94:\xb3\xabO\x8d\x88\x80\"\x98d\x10Q\\B\x8f\xfa!\xef\xfe\xcc\xfaYO[\x1f\xe6\xd0fu\x18\xab9\xe9\x8f\x8d\xb9;\xc8P\x8d\x0e\x01\x07\xe5\xce\x05\x8bcs\xff|\xd9\xbc\xebF\xb6\xde\x85\xae\xa5m\xa8\xd7\x7f\x11%iLrV*S\xc0,\xc6\x16\xc6\xd9r\xe6\x1e`\xaa\x0cK\xc6wo3\xa8MM.\x8c-E\x91\x82\x82\xf0R\x80\xd0\x93\xef\xb02?\xa6T9DtL[\xec\xf6\xd4-G\xf6\x14\xea\x06\xee\xf6\xf1\xf5_D\x08\xba\xadQ\\\xb1\x02=;\x1c\xf0mc\x13\xfb\xddz\x97j\xfe\xea\x8fY-\x08\x98\x14\x16t\xaa\xc4\xa8{$\xf4\x02s\xb8+J\x1c5-#\xb5\xb3\n_\xdd\x95\x82c)n\xbb\xb8\x82\x17\xbdJi)\x90\xd5J\xa1\xc9\xcd\xd0(GM\xbc\xea\xfd\x11\x1e\x0c\xaf\xfa\xf9\x0d]H\x1a\x0e\xe8\x135\x9f\xbduP$f\x07\xdf\xfc\xf0\xdd\xb1b\x9f\xfb\x98\xdd\xfb\xe6\xc7\xeb\xdf!\x97\xdaArJQE\xae\x0cH\xd4\xbe\xc7\x9b^\xa7\xb4\xa4\x88\x1eN:\xa3 \xa8\xdep\xc1\x1d\xb1\xbeP\x1b\xb9\x9e\x89\xd4\x80\x0d\x9a\xe7S\xfc\xc9\xb9\xb0k4qm\xa9\x0d^\xbf\xcdX2gu\xdcj\xf4\x1f\x10\x0e_\xfd\xa7:6U\xfd\xd3OS\xd8\xfa\xf6'\xf6\xd3\xf5\x1f\xb2J2V\x88\xbc\x942\xe1\x1d\x08\x89'\xb2\xf0\xa4\xb8oBj\xec\xd7o\xe8\xf4\xc1\xda\x1a\xe9\xd0\x99\x9e\x9b\xc2\x18E\"/\x8do\xb7\xc0\x9f\x08\x9c*\xb9\\\xa1\xa7\xc4R\x9cu\xe7\xfb\x16:R\x9c\x02\xbb~\x9bQ\xc9\xccV\x96\x8a9\x0e)\xbdK7\x0e\xc4K\xa9\xa1\x99\x8b\x90V\xf0\x82\xe0\x02@\xfb!\xeb\x08]4U\xe9\xf5\xdb\x0c\xce\ndn\x9d)e\x89! \xa4t\x056 \xc4\xbc\xf4\x04xI\xa7>\x06*\xd29\xdc\x06\xb7B\x13\xd3q\x82Q\n~Q\xc7a\x94\x08 \xba\xd9\xe6F\xa8\xd2a\xcd7a5\xf9\x97\xdd\x14\xd8\xcdB\xe1\x94\xc8\xe6\xda\x96\x8eY\x99\x97<\xddq\\\xef\xd8\x0d\xfc`\xdb\xc3hPZ\n(\xac\xc2HN\xea\xa1\x19k|\xfd\x9b\x94\x1a\x89\xf5ly\x89\xd1)\xac+\xe9\xb1S\xbf\xf7-\xb4\xfc\x9b\x85\xc2\xeeR\xe60\xfb\xc5\xae@Mh\xef\xd1>e5\xa2z\xbf\xa5\xdc%\xe5P#\xe8\xdc!\x049\xfa\n*\xeb\xb8!\xb1}\xec\xe1\xae\xb73\x81/mr^hm\x0d\x1e\xbc\x1a]HI\xcc}3\xc1K\xe0\x13N\xb4\xd1(\"9W\xd6\xd0\xca\xdf\xa5\x8a\xdb\xc7&\x0es;\xb8}\x9b)i6`u\x81\x8a_\xe4FXK\x85\xb3\x8f{\xcfn\xe1\x07\xf4e\x81\x98\xb7B\xab\\\xd0?[\xdf\x0fS\x07\xefp\"\xd1\xc6Z\xe5R\xa0\x19\x82\x13yi\xd0\x19\x07\x04\xc6\xd6O\xec\x16\x7f\xa5KlJ\xa3\xba\xe3\xd4Y\xdb\xeae\xd7VS\xb6\xf7\xc31L\xd0'\xaa\x89\xdd\xa5\x8b\xb7o/\x16d\xdcgla\xd1~\x94\x1b\xbcA\xdc\xa2Fe_Rj\x13\xfe=\xe9\x0fm\x96\xb9\xd1\xc8b\x17\x9a)\xb0/\x94\x18\n\xb5\xe2`\x05\x87\x05\x16\xfb\xaa\x1d\xfap\xf4\xec\xcbB\x89T\x7f\xe9l\x89\xcf\xd6(\xdd\xc8\x0e\xa1\x1d\xaa\xa6\xf2\xec~&\xb4\xa4H\x04\xba\xe4N\xd0\x12\xd9\x96\x05\x16\xdd!n\xfa\xda7-\x14\xff\xfd\x1b\x1a\xa7\x95:\xe7V\xda\xe4\xda\xa7\x89}\x1b\xfb!\xdb\xc4f[M\xec\x9er\xef)\x97\xde[\x17Bj\x18\x8c\xad\xe3\xae !h\x16XV\xf7'\xd2R\xf5h\xad\x15A\x02\x18z\xfb\xae\xda\xc6\xae\xae\xd8\xfdL\xc0\xfa\xc1\x94P{\xa5t\xe4 \xa4%\xbdN\x177mUW\xeckJ\x1dO\xddO\x19\x9b\x8e\xa8\xb4\xa4V\xdd\x8d\x9b\x00\x0d\xffkJ\x13 \x8fU\xa2\xd0\x16\xb1hq\xd1\x98X\xdbj\xe8\x91\x97\x88\x92\xac\xf0\xb5\xd5\xa5\xd5\x84\xa7`5M^\xbaq\xc0CbJ\n\x9a)\x97Bc\x00H-sP\x9e\xd4\xe8z\xff\xd4\xc56\x1e={X(E5\x93f\xd6\x85\x99\x9d\xf6\x91\x9d\x06\x94\x87\x94&\x0d[rYJT\xfcB\x0bE\xea\xa5\xdf\xf9\xc6\xc3}S\x9aF\x08\xa5\xac-\x0c,:\xac(\x8bB\x11\xebHc\xf7\xc3L\x08\x9eVZ\x82k\x89\x13\nn,\xb7\x89y_5\xd8\x9f\x1fN$\xb4&\x04\x92(u)`\x89\xeeT\x91zi\x1f\xbb=(\x8c\x87\x94\xa2i\x8b\xc9\x0b\xa9\x1cF p\xca%\xb6\x88\xf3\x92\x87\x99\xc0\x81P\xe7\xa5.`\xac\x81\x02V\xa2\x14\xf4\x12\xe3zWW\x18Xov\xf9\x90y\xe9\xac.5\xfa\xbcX'i&\xd3\x8f\xf5.\xb6\x9e=\xa4\x14\xa3v\x80\xf2W8\xc8\x1bK\x03v\x7f\x0cC\x83/9\x13\x92\xfa\x92ue\xe9\n\xc4\xc4,M\xe1\xa8d\xa7>N\x01\x0b`\xa1\x8a\x04\x19e$\xc6$\xe4V\xe0\xe3\x87\xd8\xc50\xf4c\x1d\x03{|C\xa7&\xe1$7\xa5\xc6&Q\xa8\xc2Z\xac\x8d\xa1\x0b\xc3\xe43\xdf\x84\xba\x0ft\xf7G\xbat\xf1\xe6R\xfafS\x1aU\x16\xe8B\x85\x01\xd0\xe9\x06\xb1\x1a\xa0;\xf9,\xbe\x04\xe8-\x8f\xa7+_\xe8\x8a.I{;!$\xceH\x9c\xd6\x8e\xd3;\x8f\xfb&L\xf0\xd4\x990\x143\xcc\x8a\xa2\xc0\xb8?F\x17\xc4\xd8\xf9 \xee\x9e\xd2\xd4\xc2\xac\x90\x86f\xd02qm|\x83\\\x98\xe2 S\xe4\xc2 J\xc8\xd2\xb2\x87\xb1\xdb\x86\x16\xe6?\x8f\x0b\x95^R)'-\x81JsE\xd5:L\xed\xb6z\xf1\xec1\xa5\x86n\xea\x84\xb3B\xb2\x12:\x9b\x93\x1a\xabvl\xfc\xcb\xc4\xfe\x8c\xbf\xa8\x95-\x82\xf4s\xac\xffRZGw\x1c\xfbC\x80\xb9\x14\xfb\xf3L\x94\xf49\xbc\xd4FC/\xe4\x85\xe24\xbe\x1d\xe3\xb4\x8a0Sg\xdf\x16*u\x00k\x85\x83\x05\x04,\x1d\xb9\x93\xf8i\x93\xaf\xe3\x11gP\xa0D\x7f\xf9C.\x95\x99V\x85,0\xbe\x8f\xb2\xca\x1a|\xf1)\xf4\xd0\x0b~\xa1D\xd2\x8c\xb6\x94\xca\x1a\x8d\x13*%\xb9\xc0zx\xf1\x9d\xdf\xfa\x81\xfd5\xa5\xdcbuqe\x0bH\x0b\x91\xd8\xaa\x86&\x06\x7f\x9d\x89\xb4\x02H\xae\xb7f\x89\x92\xda{\xe6\x9b\xf3\xce?c8\xe6\x8b\xe6\xfck\"\x93\xc91\x17\x8ekh \x85(\xecY\xbf\x82\xd9\xe2\xd8\xf9\xce\x0f8Y\x9cIN\xb0\x9e\x8bwsq\x82\xb7\xef7l\x1bV\xabj\xc3\xae(Q\xe4s\xadf\x93\xa7b9.\xed7\x08:\xd2\x05\x04\x1b\xe9\x02\xb1\xca\xc5 ^\xaa\xd9\x84\xfe\xac\x0f,\xd4a5\xb1\x0f\xf8KnR\xc8\xbb\xfc\xff\xc9\xb7}h\xd9GJ\xd0\xb6O,\x01\xd3\x17\x10\x80>\xb0m\xe7[\xdc\xef\xbb\x9a \xd0\x89\xa0\x96\x11\x8d\xda\x00\xcf\x0eq\xfb\xfa!v\x07\xf6\xe9\x0d\x8d\x96*\xc5\x9b\xa0\x83\xc9T\x05D\xa0Om}\xb7\xe91\xc2\xd5\x86}\xc2\x0b\xaf\xff\xfft%\xbd\x942h\x92\xba\xbcy}\xf4]\x1bv~\x1bZv\xfd\x86\x96\x96\x82\xb0\xa1\xc5\xc9\xc2\xdd\xfbf\x80a\xa2\x19^\x7f\x9bY\xecb\x99\x92\xf0k\xfa\xc0\x8e~\xe7\x8f\xdew\x9e}{\xfd\xfb\xee\xf5\xefDc\xe4\xa4b\x9eHH\x91\xbb\xb3\xbe\xc2-\x90x\xac\xd6>;tU\xb6\xed\xfc\xa6jG\xdc\x109]\xbdJW\x13\xd8\xb56\xc9\xa3\x1aT\xa39\xebk\xd6\xc6\xe3\x94\x81\xbaew\xf1\xf8\xfa\xdf\x19(\xdc\x19\x1aY0\x81~\x1d}\xcd\xfa\xa3\x1f\xa6l3v\xeca\xa18\x85\x19r\x06\xd8,\xb2\x81\n\xca\x9a\x11\x95U\xf7\xfa\xff\xb2\x9b\xd7\xff\xaa\x931$\xf9W\xce\xd0\xc7\xea\xaco\x98o\x9a\xd0\xce\xa7\x89@.\x87\x89\x18\x99<\x99\x9b\xab\\P8[\x10\x19\xc2S\x08\xec\x11\x7f\xb9\x00F\xb4\xca^\x02D\xf4-\xabc\x13W\xbe\x85\xf6\x99\x88\xe4\xdacE\xb2\xf5\x10e\xce\x93sa\xdf\xb2vS\xc5q\x15\xda\x90m}\x13\xd9\x1df_\xff\xa3\x0d\xd9\x15\xe4\x11P]\xa1\xb1|\x91s\x14\xa8b\xeb\xb7\x9e\xdd\xa5\x14\xcd\xf8\xe4 \xf3Z\x88\x19\xe9\xfc\xac\x8fl\x15c\xef}\x1f\xd9\xe5L$<\x03Iq\x03)\xf0\xbf4\xc0\xbb\xf5\xed\xcaw\xec\x8a\x12B[\xd5\x06\x96u\xb3/R\x1f\xd9\x1e#\xf5\xdeR\xbc^d\x99q\xd5\xb5^L\x18\xfb\x81mC\xb7\xaf\xda\xca'\x94z\xcf\xae\xe6\x0b\x17\xed\xf0\xfao\xb8\xc2\xd1nt\x06K(\xe7\xee~\xd6\x1f\x19\xc5\x94\x0c\x1dY\x89\xfbv\xd3\x85\x9e}^.\x12\xc6\xfd\xa6{\xfdg?\x9b1\xda\xd9Y\xc5\xba\\Sx\xc0\xb3~b\xbej\xb3\x8d\xff\xb5\x0d\xec\xe2\xf5?\xdb\xec\xbd\xff\xb5}\xfd'A\x87\x14\xb9\x91\x86\"\x01\x82^O\x9drb\xb5\x7f\x1e\xbb\xac\x1e'v\x8d\xd4\xf58\x913\xb5\x81\xc6\xa6\x10\xf1\x10\xd8v\xbe{\xf2\x88\x94\x84\xe9|\xcf\xa4\x88\xd4\xc9\xd3\xa9\x9f`\x0d\xb4\xf3C\xe6\x87l\x18\xbb\x16\x96A)\xfb\x08YNP@.\xa9^esn \x18\xaf\x9f\xd8\xe0\x9b&[\xf9\xbe\xea\xfc\x8e=B\xe62e\x84~\x83\x81\x81\xde\xbd\xf6\xf4\xc4!4M\x86\xa7\xd2q\x13;\xf6\x98\xb2\xd7\x94M\xa2\xb3\xf5\xa9*rq6l\xd8>\x8e\xed&\x8e\xec6\xa5\xdc2\xa1\x8c@\xb8\x969\x80\x07\xb4\xc3TQ\xc3\x86ua\xd8V\xec+\xfe\xf29T\x00\xb4\xf0\xb3a\xcb\xfc>l~\xc46\xd6qd\x17\xfb\xd7\x7f.\x19\x8a\xe3\"\xe7;.\x06\xad s\x0c\x9b\x1fU}\x88\x9b\x1f\x15\xbb8\x82\xd0\x9cC\x95K\xee\x1c\xa5e\"\x17(\xd0\xf8\xbd\xcf\x86\xd0\xf7\x15\xbb\x01\xf2\x11IIq\xbb\xd3\xf7\x9dbs\x0c;\xe6\xf7\x87]\x0cY\xeba\x0e\x985\xa3o\xb7\xec\x82\xae\xdd\xd1\xb5\x1b\xbc\xa6\nr\xc9+\xa4\xe5e\xf2\xe8\xb6J\x18\xb8\xc7\xca\xb7\xd9\xcag\xad\xcfv>\xc3\x88\x8bmv\xe9\xb3;\x9f}\xf2\xd9\xd7\x88\xcb\xd4\"W\xdaY^\xa0\x89\xb3,\xa5\xd3\xfa$\x1a\xea$\x13j\xdc\x8c\x10\x8bC\x08\xf9v'\xc6\xdd\xe8+\xf8\xc9\xc8\xfa\x1cD>\xc1\xa5O\xa3\xcf\xde\xe1%A\x86\xf7%/\xc9\x9a\x9a\xe7\nz\x82\xfe\xc3\x1d\x0e\xf1$z\x9fT\x80\xcb\x9d\x93d\xc6\xc8s#-,A\x92L\xedC\xbb\xcd\xda\x88\xc7\x0d\xd95\xe6\xeebE\x16.2w\x1c\x97\xcb\xf0Y\xd6\"\xa0\xef,\xb7\x8bu\x86\xb0!$\x08YD\x0ea\x05\x06\x981\x96<\xe3K\xce\x05\xd4v\x92\x8a\xd9\xb0\xf3u\xd6\x8e\xc1\x93\\\xcc\x1e\xe1\xc2\x1d\\ I\xebL\x01\x9a\x1e*\xc1)\xd0\xc6\xb3p\xe7W\xe3\\4\xd7s\x06MZLN(\xcc|\xa9\xf9\x15\x9a\x1b\x07xb\xb3\x08\xdd\xfa\x00O\xa4:O%\x93V\xe0\xce\xe5\xc5I\xee\x80\x151`dk\x12<`=<\xe2\x15x\xcf2w\x82\xa0\xc89Ln\xdc\x9b\xa2\xd9\xc3\xed\xb3\xf5n\xdc\x93,f\xdfAv\xae\x0c\xae\x1d\xe1.;#\x0b\xb7\xb4\xb2\xd6'\xd9\x01\x91l@\xf8\xce'\xf9G\xbc\x04\xf36\xe8qFS\x11\xe9\xdc8!\xa5\x9c\xef\xd0\xfb\x95\xcf\xa6T\x99\x0f\x90\xf9%\x927\xbd\x9b=\xfc\x1c\x86\xe1\x9a\xf9c\x95\xf5c\x0d\x9a\x97Db\x95=\x8c5(]j\x01*\x97RP\xc3\xc6xwoJw\xf0\x8d\xdfd{O\x0f{\xc4\xdc\xad\xaf\xc8\xa8Y\xe7\xc6(j\xa2R\x94\xcb\xf3\xf0\xc3\xb2~\x96\xd9\x91\x17\x03\xf9\x1d#l\xf0\xfc\x8a\xc9\x83/\x89=S\x9b\xfeN\xb6r\xc2\xe6\xca\xa1O\x16|\x0b\xc2\xdf'\xc6-\xc2\xe5\x00\xef\x16qr\x98\x91Lb\xbcc\xa1\xf2\xc2 SX,u\xe9\xac\xc0\xda\xae\xabv\x9b\xcdJ\"\xb6\xd9\x06\x87\x10D\xf9\x81\xff\xcc\xaa\"\xb6\xd9\xfb\nF\x14\xf8\x0fZ\\\xe9\\ e\xa4#\xe7Mm\x856g\xc3\x9e\x85\x1e=\xfa'\xf6a&\xb8L\x81h\xc8o\xd7\xa8\xdc\x95\xd2\x9c\x0d\x1d\xf3\xabM\xb5\xae\xea\xbd\x7f\xf1\xecb\xb5\xa9^\xff\x912\x9c\x8e\x06\xb5P\xbc\x940$\x0b%\x95\xb1(\xd3\xf9!\x1c\x02\xbbH):)\xf3\xbc\xb0Z\xc3`R\xe6\xd6\x08'\x15\xb2\xf6~[e\xb5\x9f*v\x81\xe45\x90\x96\x04\x92\xf9\xadR\xa0~:\xb6\xf2[DE\xbcLiAg\x98%\x854\xa3cwle\x1d[\xfb\xae\xaaW\xf1e\xe3\xb7\xec\xdd\x1bZ\x1b\x02\x0d\xa4\x1a\x946/\xf0#7\xb1\xde\x07|\xd5\xf7\xaf\xbf\xcd$\xb4G\x0d\xcaKJm\xd2\xe1O\xe9\x88\x7f\xec\xc6&\x1e={?\x13\x9a\xdeE\x19-\x05E\x165\nZF\xc7\xb6c\xd3\x8c\xec\xea\xf5\xf7\xa6y\xfd\x1d5\xab*si\xb8\x85\xa9\x0e\xcfKa\xca\xc2\x12\xe3\x0bF\x0f\xbcJ)hlU\xe2 \x1e\xb7P\xc0\xa5.,\xc7R\xdb\xf9u\xe5\x9b\xaa dL\x9bHM88)\x12\xb1t\xb9A\xd4\x1f\x14\x88\x07\xf6)\x1e\x08\x82\xd2\xe6\x02\xe3\xdd\xe2\xf9]\xc9%\xc7o\xaaC\x1d\xba\xa6b\xd7)\xd5\x8e\xeeV\x18\xab4\x1d\xf5\xe1\xf9\xf3\x80\x80\x89\xe366Y\x1d\xa7\x91]C\xe6\xf5\xb7&\xbb~\xfdmz\xfd\x9d\x19\xfe\xb6\x88\xd5\x1c\xd7\x07\xe4b7\x8c\xcd\xc8^\xffo7\xbc\xfe\x0e\xa5\x816\x10e\xee\xb8t0\x8fwyiE\xfa\xc4\x83\xef\xd6U[\xd5\xec\xdew\xaf\xff@\xaa n-\xac\xe4\x05\xee (\x0d\xeb\xed\xa1c}\xecj\x0c\xe2\x83 \xa7\xaf\xd4\x1a>\x1f\x11X\x8d\x16T\x1b\xfd\xd8\x8f/\xec\x01\x7fK\xc2\xb8Q\xdcX\xadaI\xec\xb4t%\x96\xd7\xe4\xfb\n\xcf\x13~\x99\x89\xd4\xdc\x0b\xad\n\x813Pt\xff\xc1\x8a\x9b\xc6\x06\xf1\x88(\xe1 \xbd\xc6\xb8\xd2J\xb4\xd5\x93\xa5\xd4\xe2lxf\xcdX\x8f\xec\x06~\xd0\x02\x05kT\x11\x8c\x12nO\xa3\xb3\xdb\xf0\xcc\x0e\xe3\x10\xc7\x1az\xf1\xfdB\xf1\x84\x85m\xd0\xd2q\x86\xd2\x1a\x9e\xd9P\xb5\xdb\xe7q\xe8#{\\(d\xd6\x0b\xb0\x07\xdc~F&\x19^X]\xedGv\x0d?h\x1f\x0e\xab ,\xcdy\xca\xf3\xc2\xf6~\x1b\xb7\xb1\xad\xd8\xedL$#\xed\xd9\x90\xfc\x84q\x85\xec\xcf\xbe\xae\xda\x8dg\xb73\x81\x16\xd9z^\x85$\xa7\xf0\xd1\xb3\x15\xb4\xec\xd6\xb3\xcb\x94r\xb2\x850\xa5(\xb8\xc1\xbdc\xab\x8d\xb1\xc0\xba ]X\xadwU\xcb\xde/\x94Ti\xe1d\x0c\x9ek(e\n)\x81\xbbn\xd0\xba\xe3\x9a\x12N\x11H\xa5\xe3\\\xd2i\xa9\xb6\xd6\x95\xc8\x18\xf7\xfb\xb1\xf5\xdd\xc4\xae\x17\n\xbd\xefLnti\xad\xc0\x8d<)\x9d\xd0\xc8>n\xe2\n\xf7\xaf\xd9\xf5\x89\x84\xb9\x9cvy\xc9\x85\xd1\x06\x0d\xacJn\x0c\xbeHSm\xa6zW\x1d\xd9\xcdL\x00\xb3\xe19\x17\xb2\x90\x05\x82r;U\xd0\xcb\xec}G\xbbe\xb73\x91^E\x08\xadJ\x89JG\x0b\xa1\nd\x9e\xfaf\xf2C\xd5\xb2\xdb\x85J\x07\xd4\xdc:\xdc$/\xf2R(\xe0m\xe31\x86\x1e\x969\x18\xb3\xe7M&=@;g\\\x81\xdf\xaa\xa5\xb3\xf8\xea\xfd\x10\x9e\x921\xc7\xc3\x89\xe4T\xe6s\xf8a\x03*s\xf4l\x1a\xbb\xc9\xef\x0f\xb1a\xbf,\x94\xa4\xe8\xb7\xa5rP\x8e\x12\xd6\x0f0\xae\x9e\x8d=-m\x9f|\xb7\xa7\x95-R\x8f\x7f! @\x97\x96\xa1\x0e\xbe\x16W\xb9 \xb1\xf2\xdd\xb0\x8bM|\xee\xd9\xe5\x89\xbc}\x8f\xddP\xcdQ\xb0\xca2\x97\x8ab \xa3L\x8fc%\x016^\xa6\xdcW\xcc\xdd\xfd\x8c\xea\xbcD\x7fP\xc1\xceK\x9d\x1bMA\x05A24M8VM\x13\xd8\xe5\x89\xfc|\xc34w\xc2\x11D\x1a\x87?\\\xce9K\x10i \x18\xdbv\xcaj?\x04v\x89\xe45\x90\x8fw\xb8~J\xabOvnUn\xdd\xf2\xb0u |\xe6\xa6a\xef\x90\xbc\x05\xf2\xcbW&\x94+\x0d6Ei\xe8ahF\xceO\x92q\xb5\xf3\xfb\xec\xe0\xbb\x9a\xbd#\xfa\x1e\xe8o\x17\x84\x80\x97\xd0\x98\xceK\x9b\xcb\x82\xac3Q\xaa\xd9\xf4C\x17\xfc\x9e\xbd;\x91\xa9(a\x00'\xf7\x06P\x85o\x9e\xd4\xc4v\xca\x9ec\xdc\xf4 \x05\x99\xef\x98\xb9\xba \x18c\x97\\\x1b\xac\xce\x0d_\x9e\xb5 Y\xed\x9b\x15{\x1f\xb2kHo\x1f\x08\xdf\xb3(\x0d\x95\x9eE`\x1d\xd0\x1c\xc0]\xf9}l7\xec}J?_P%\x15\xe9\x9d\x9c\xcc\x1d\xcdI\x80;4{x\x1f\xf6!\xa5\xb7\x1f\xd0R\xa4\x94\x92\xee]r\xc4\xdbM_\x10\x86!t\xec\x03\xfeb}\x14\xb9)\xc8\xae\xf4\xdc\x1ax\x7f\x9e\xee\xfb\xe4\xabn\xe5\xdb\xbag\x1f\x17\xea\x1d\x95\xa8-f,(\xc9\xf3\xa2\xa0xhId\xd8\xc5\xae\x0d(\x93\xc8\xf7\x1f\x08\xa7\xaf$x\x95\xf3\x12\xddz\xe6\x16\xbd\x0d\xc30\xf5\xab\xb1\xdb\xb2\xab\x13\xf9\xe5\x13~\xb2\x10\xf3\xab\xe9\\\xd3\x82\x12e:\xdfn\x8eUx\xc6}>\xa2>\xbfgt\xb2\x9d\n)\x85\xca\x802\x00\x91]\xb5\xdde\x87X\xb5\x03\xfb\x04\xe4=\x92\xb7_hL\xb4)\x0c\x84\x93s\xf86\x92i\x9a~\xe3\x9b\xc0>-\xd4\xcd\x05\xa1\x14\xe9Y\x82\xe3\x1e~\xaa\xb9\xdd\xd8\xc5\x96}\xc2\xdf\xeb\x07\x1a\x1b\xd5l&f(j\x13\xf5\x90\x1f\xe1\xe9)t}l\xb3uh\xa12~^.\xbc\xa3\x0b\xf7\xa9\xd6\xe78\x13\xa5\x83\xf9\x13O\xdf\xf3#\xee\xda>\xb6\xec\xe7\x94\xe2\xe7\x17\xb9\x96d\x87|.`]%\x96F\xf2#\xb6\xa1_\xc5.\xb2\x9f\x17\xea\xe2+3\xb6\x80~lr\xabQ]P\xb4\xc3\x14\xfaq\xecaP\\\xf9\xa6\x81q\x11\xd3\xbb\x0f\x88\x94\x03SZ5\xc3\x87\x14\x85\x9c\x0b\xb9F\x90\x0b\x98\x7fC\x92\x8aJ[\xda\x08\x80\xc2\xb5f)\xdc\xc6g\x87\xb1\xdf\xa1\xe3\x12\xa4\xdf/ptr \xae\xf2\x1c\xe1\x1c\xd5R\xe5\x8d\xdf\xa7\xd8\xd8\x94^|%\xd4\x7f\x9e\x9c\xbe\x9c\xca%/\xe6\xd2\xdd\xc7~\x1d\x9f\xd9-%\x89W\xa4y\xc5\xb9C\x07\xb1\xb9$1D\x87\xafZ\xd2\x1f\xb7s\x0e5\xc8\xc5\x0d\x99\x17\x17I\xf1\xd8\"/\xcb\xa5\x0e\xdb\xf0\x9c\xad|3T{\xf4\xf4\x0b\xcf\xd9\xe5\x92K\xbag\x8e\x11\x00z\xa4L\x8e\xc8$\x89\xcd\xfc.\xa5\xdf\xbf\x91\x02\xb7\xb3\xa6r\xb9\xa5\x86\x0c\xdc\xd1\xd7\xcf~b_(yxG\x10\xc7\x9c6iA\x8b\xf2\x14\xb1`\xec\xd9\xc17U\xef7\xa1\xcf\",\xd9\xc2\x1cW\xf8\xfe\x7f_O\xc1\x85\x93\xbe\xe3z\xee\x9cEnJr\xe7\xc2\xbbuu\x86\xdd\x0b\x8a#\xfb\x86\x1d\xed\x02\xdb@\x8a\xc7\x82-\xc6\xe8\xa5\xa1\x1dBl<\xbb\xc7_\xac\xd2\x02\xf4g\xdaQ\x15\xe5\x1c\x8e\x14X\x7f\x1d\xabv=e\xbb\xd84\xf1\x99\xfd\x89r\x9f(\x97\x1a\xbf0i\x7f\xb7\xd4\xb9=\xb53\x0c\xc5\x9d=u\xbe]W=\x0dJ\x18\x87;\xfb\xf8\xf6\xd2\xe7\x1bB\xf1tsA\x95y\x91\xce\xd6\xe1\x1e\xeb\x98$g\"i\x10mS\xccJ\xd0 \xee\xa4t\x86]\xdc{\x980\xb0\xc7\x85\xba\xfb\x85\xfa\xa8\x9d\x87O\x95\x97b\xe9=C\xe7\x8fU\x9f%\xa4\xe4G\xca]R\x0e[\x96\xc8\xb5I\xa5\x08-\x8b\xdc\xe2@\xf2\x18\xba\xd8z\xf6\x8d\x92\xa4u\x97\xa7\xc0\\\xb7\x10\x8bj{\xf6\x18C\xf0)v\x1b\xf6\xfd\x0d\x0de\xa8\\n\x1dE\x9f\x00\xbd\xab\x92?-Ju\x9b\xc6?\xb3\xef)\xc5y\x87\xc8\xcd\xd2IJ\xb4\x12Iu\xf5\xec\x87\xd0=\xfb)\x0b\xfd\xe0\x87\xd0\xb3\xef\xf3\x85\x0f\xe9\xc2\xc7\x1b\x02\x1d\xd1\xe9iV,\x912A~W\x0dm\x98\xb2]\xa8\xb6\xbb\xa1g\xdfS\xfeS\xca\xa7F\xed\xa8\xe9\x914\x8d\xf6g\xc7\xe4!:\xc4l\x13\xb2ch\xfd&y\x8a\xd2\x95ot\x05f\xed6\xb76\xe9\xb1\xc2\xe5<\xa9\xbfc`\xa1\xc9\xb6\xa3\xef\xaa5b\xeb\\%\x12\xe6\xd9./\xd3v\xeey\xa1\xa0\xba\xb1#\x91\xc8\xc1w\x0b\\\xd1\xfdL\x0b\x0c\xad<{\xe3\x83\xd0\x1c\x00\xf9\x18\xd8\x8f\xb0\x1a\xd7~\xe5\xdb\xb8\x8e\xec\xe7\xb7\x19\x8e\xee\xeeR\xa5\xb6X\xf0\x9c\xc6\x1c\x0db\x8d\xcf\x9e\x9a\xd8U\xe4\xc7\xf91\x91\xb0\xba(\xdf\xb4-\xf4\xaf\x9b\x9f\xd4\xf8>\x1b\xba\xaa]\xefB\xe7{v\xe3\xfb\xec\xf1\x94\xc5\x03s\x04\x19*t\xee\x80?>U\xeb\xaa\xf5P\x0c{\xdf\xb0/){EYA\xce\xec\x18\x9f\xa8X\xd6<\xc7\xc00\xd2g\xe7\xd9}J\x13#\x05.\x14\xc8-\xcb\xd2\x00kBuA\xb0\x87\x19\xd4\x05\x11\x1fR9K\x97\x06\xb2\x02qz\x11\xb9\xf6\xd82\x7f\xc8\xb61\xdb\xf8\x81]\x1c\xb2\xab\x98\xbd\xf7\x03\x1e\x9b..\xd0\x08\n\x958q\xd3i\x17}6\xf8 \xf8?A\xfeS\xf4\xd9\xa3\x9fh\x87\x8e\xcff\xd9\x82\x173\x1e\xe8\xb1\xa5\x0d\xb3\x916s\x1fR\xfc0!s\x8au8\x97j\xcb6~\x9f\xb5\xdb\xb8\xceV\xbeb\xef\xfd>\xbb\x83\xcc\xa5\xaf\x98\x9a\xfd\xf7\xe7\x9b\x03{\x13G\xdc\xf1C\x9f\xc8\x11w\xfa<\x85\x02\x96j9\x9f\xe4&7\xc8<\x12` \xbb\x19 \xa9\x04\xcd\x9a\x84X@\xcay1\x1b\x12\x1d[\xb6\xf7\xf1\xbc\xf7\xf1\xfc\x80\xbb\x96\xb7>\x9e?\xf8x~\x8f9XQH9G_\x821X\xe2\x13\xf6x\xe3!\xb0[$\x1e\xc3\xcc\xa9\xe7'\xc8\xb4b\x01\xee\xc3n\xcc6Uh\xd9\xfdn\xcc\xdeW\xe9PR\xa8\xd9\xcb]p\x97\xee{hB\x86\x81\xdd\xb2\xe1\x10\xd8}\x13\xb2K\xcc=\x1e\xc2|\xf0URXJ\x0d\xec}\xdcg\xbf\x8ek\xf6\x10\xf7\xd9\x9f\xc65!\x1a\xcd\xb8S\xb8\xf1\xe9\x80m\xa80\x08\xdc:\xeb\xfdz\xc7\x1e+\x8c\x03\xb7\xce\x1e ' \xdbM\x9f\x8aP\xe0\x9b\x1c}\x9b\x0d\x1d|\x1d\xfb\xe6\xdb\xec\x91Hh\x94R,@1|\xde\xa3\x07\x89\xff\x13\xf7\xd9\xd6W\xec/\xaf\xff\xdegW\xbeB\x0f\n)r\xa9\x0bk%5..\x85\xb2gS`\x7f\x1b\xbb\xf1W\xf6\xb7?\xc3o\xda\x9b\xd6\xc9\x9aW\xa3C,6\xda)0\xdfd?\xaa\xb6\x85F\xdbd?\x13\x95\x04\x8a4\x97\xd4*w4\xa8\x92\xc0\xf9j\xfc\xd5W\x9d\xaf\xf0l\xfer\xc9\xe0i\x0c\xfa\xc2Q\xf8\x0dIJ\xebe\xcf\xf6\xcf~\xbf\x1a\x1b\xdc@ bF7\x99\xc3\xc9/\x01\xbe\xd4\xd9\x0b\x86\xe3\xd9\x86\x16>\xf6r\xa1R(\xfb\xc4/\xcbd K\xfc\x080tI0C\x8e1\xbeDw\x96nQj/\x1d\xab\x9a\x10\xd9g\xf8\xe1\x9c\xb1s\x9e\xc3\xea\x9e/\xe7\xf7/\x1dL\x13\x9f\xc9\x88t&\xd0\xd5\xfb\x14\xcb\x19\x06\x86\xd3\x83\x9b*\xd4\x81\xdd\xe0/<\xf8|\xf9\x0c\xe87\xd6\x00O[\xfb}`w\xf8\x8b& :/\x1d\x95\xac09O\xb3\x88\x97\x8e\x0d\xfd\xae\xda\x1f\xc6\xbab\x8f\x0b\x85p\x95&w\xe5\x82\xffhdi\xce^\x9e\xd9~\x88]\xecw\xf8\x8e\xb7oh\xae\x17\xefw\xc5\x17\x9c\xd8\xff \x00\x00\xff\xffPK\x07\x08\x15\x0b'\xdatY\x00\x00>\xba\x00\x00PK\x01\x02\x14\x03\x14\x00\x08\x00\x08\x00w\x8fqM\x15\x0b'\xdatY\x00\x00>\xba\x00\x00\x14\x00 \x00\x00\x00\x00\x00\x00\x00\x00\x00\xa4\x81\x00\x00\x00\x00worldcitiespop1k.csvUT\x05\x00\x01\x13W\xf0[PK\x05\x06\x00\x00\x00\x00\x01\x00\x01\x00K\x00\x00\x00\xbfY\x00\x00\x00\x00" fs.Register(data) } gowid-1.4.0/examples/gowid-table/table.go000066400000000000000000000066061426234454000202730ustar00rootroot00000000000000//go:generate statik -src=data // Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. // A demonstration of gowid's table widget. package main import ( "fmt" "io" "os" "strconv" "strings" "github.com/gcla/gowid/examples" _ "github.com/gcla/gowid/examples/gowid-table/statik" kingpin "gopkg.in/alecthomas/kingpin.v2" tcell "github.com/gdamore/tcell/v2" "github.com/rakyll/statik/fs" log "github.com/sirupsen/logrus" "github.com/gcla/gowid" "github.com/gcla/gowid/widgets/divider" "github.com/gcla/gowid/widgets/fill" "github.com/gcla/gowid/widgets/table" ) //====================================================================== type handler struct{} func (h handler) UnhandledInput(app gowid.IApp, ev interface{}) bool { if evk, ok := ev.(*tcell.EventKey); ok { if evk.Key() == tcell.KeyCtrlC || evk.Rune() == 'q' || evk.Rune() == 'Q' { app.Quit() return true } } return false } //====================================================================== var ( file = kingpin.Arg("file", "CSV file to read.").String() colTypes = kingpin.Flag("column-types", "Column data types (for sorting e.g. 0:int,2:string,5:float).").Short('t').String() ) //====================================================================== func main() { //f := examples.RedirectLogger("table.log") //defer f.Close() kingpin.Parse() palette := gowid.Palette{ "green": gowid.MakePaletteEntry(gowid.ColorDarkGreen, gowid.ColorDefault), "red": gowid.MakePaletteEntry(gowid.ColorRed, gowid.ColorDefault), } var csvFile io.Reader if *file == "" { statikFS, err := fs.New() if err != nil { log.Fatal(err) } stFile, err := statikFS.Open("/worldcitiespop1k.csv") if err != nil { log.Fatal(err) } defer stFile.Close() csvFile = stFile } else { fsFile, err := os.Open(*file) if err != nil { log.Fatal(err) } defer fsFile.Close() csvFile = fsFile } model := table.NewCsvModel(csvFile, true, table.SimpleOptions{ Style: table.StyleOptions{ HorizontalSeparator: divider.NewAscii(), TableSeparator: divider.NewUnicode(), VerticalSeparator: fill.New('|'), }, }) if *file == "" { // Requires knowledge of the CSV file loaded, of course... model.Comparators[3] = table.IntCompare{} model.Comparators[4] = table.IntCompare{} model.Comparators[5] = table.FloatCompare{} model.Comparators[6] = table.FloatCompare{} } else { if *colTypes != "" { types := strings.Split(*colTypes, ",") for _, typ := range types { colPlusType := strings.Split(typ, ":") if len(colPlusType) == 2 { if colNum, err := strconv.Atoi(colPlusType[0]); err == nil { switch colPlusType[1] { case "int": model.Comparators[colNum] = table.IntCompare{} case "string": model.Comparators[colNum] = table.StringCompare{} case "float": model.Comparators[colNum] = table.FloatCompare{} default: panic(fmt.Errorf("Did not recognize column type %v", typ)) } } } } } } table := table.New(model) app, err := gowid.NewApp(gowid.AppArgs{ View: table, Palette: &palette, Log: log.StandardLogger(), }) examples.ExitOnErr(err) app.MainLoop(handler{}) } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: gowid-1.4.0/examples/gowid-terminal/000077500000000000000000000000001426234454000173715ustar00rootroot00000000000000gowid-1.4.0/examples/gowid-terminal/terminal.go000066400000000000000000000221131426234454000215320ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source code is governed by the MIT license // that can be found in the LICENSE file. // A very poor-man's tmux written using gowid's terminal widget. package main import ( "os" "strings" "syscall" "time" "github.com/gcla/gowid" "github.com/gcla/gowid/examples" "github.com/gcla/gowid/widgets/columns" "github.com/gcla/gowid/widgets/fill" "github.com/gcla/gowid/widgets/framed" "github.com/gcla/gowid/widgets/holder" "github.com/gcla/gowid/widgets/pile" "github.com/gcla/gowid/widgets/styled" "github.com/gcla/gowid/widgets/terminal" "github.com/gcla/gowid/widgets/text" tcell "github.com/gdamore/tcell/v2" log "github.com/sirupsen/logrus" ) //====================================================================== type ResizeableColumnsWidget struct { *columns.Widget offset int } func NewResizeableColumns(widgets []gowid.IContainerWidget) *ResizeableColumnsWidget { res := &ResizeableColumnsWidget{} res.Widget = columns.New(widgets) return res } func (w *ResizeableColumnsWidget) WidgetWidths(size gowid.IRenderSize, focus gowid.Selector, focusIdx int, app gowid.IApp) []int { widths := w.Widget.WidgetWidths(size, focus, focusIdx, app) addme := w.offset if widths[0]+addme < 0 { addme = -widths[0] } else if widths[2]-addme < 0 { addme = widths[2] } widths[0] += addme widths[2] -= addme return widths } func (w *ResizeableColumnsWidget) Render(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.ICanvas { return columns.Render(w, size, focus, app) } func (w *ResizeableColumnsWidget) RenderSubWidgets(size gowid.IRenderSize, focus gowid.Selector, focusIdx int, app gowid.IApp) []gowid.ICanvas { return columns.RenderSubWidgets(w, size, focus, focusIdx, app) } func (w *ResizeableColumnsWidget) RenderedSubWidgetsSizes(size gowid.IRenderSize, focus gowid.Selector, focusIdx int, app gowid.IApp) []gowid.IRenderBox { return columns.RenderedSubWidgetsSizes(w, size, focus, focusIdx, app) } func (w *ResizeableColumnsWidget) SubWidgetSize(size gowid.IRenderSize, newX int, sub gowid.IWidget, dim gowid.IWidgetDimension) gowid.IRenderSize { return w.Widget.SubWidgetSize(size, newX, sub, dim) } //====================================================================== type ResizeablePileWidget struct { *pile.Widget offset int } func NewResizeablePile(widgets []gowid.IContainerWidget) *ResizeablePileWidget { res := &ResizeablePileWidget{} res.Widget = pile.New(widgets) return res } type PileAdjuster struct { widget *ResizeablePileWidget origSizer pile.IPileBoxMaker } func (f PileAdjuster) MakeBox(w gowid.IWidget, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.IRenderBox { adjustedSize := size var box gowid.RenderBox isbox := false switch s2 := size.(type) { case gowid.IRenderBox: box.C = s2.BoxColumns() box.R = s2.BoxRows() isbox = true } i := 0 for ; i < len(f.widget.SubWidgets()); i++ { if w == f.widget.SubWidgets()[i] { break } } if i == len(f.widget.SubWidgets()) { panic("Unexpected pile state!") } if isbox { switch i { case 0: if box.R+f.widget.offset < 0 { f.widget.offset = -box.R } box.R += f.widget.offset case 2: if box.R-f.widget.offset < 0 { f.widget.offset = box.R } box.R -= f.widget.offset } adjustedSize = box } return f.origSizer.MakeBox(w, adjustedSize, focus, app) } func (w *ResizeablePileWidget) FindNextSelectable(dir gowid.Direction, wrap bool) (int, bool) { return gowid.FindNextSelectableFrom(w, w.Focus(), dir, wrap) } func (w *ResizeablePileWidget) UserInput(ev interface{}, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) bool { return pile.UserInput(w, ev, size, focus, app) } func (w *ResizeablePileWidget) Render(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.ICanvas { return pile.Render(w, size, focus, app) } func (w *ResizeablePileWidget) RenderedSubWidgetsSizes(size gowid.IRenderSize, focus gowid.Selector, focusIdx int, app gowid.IApp) []gowid.IRenderBox { res, _ := pile.RenderedChildrenSizes(w, size, focus, focusIdx, app) return res } func (w *ResizeablePileWidget) RenderSubWidgets(size gowid.IRenderSize, focus gowid.Selector, focusIdx int, app gowid.IApp) []gowid.ICanvas { return pile.RenderSubwidgets(w, size, focus, focusIdx, app) } func (w *ResizeablePileWidget) RenderBoxMaker(size gowid.IRenderSize, focus gowid.Selector, focusIdx int, app gowid.IApp, sizer pile.IPileBoxMaker) ([]gowid.IRenderBox, []gowid.IRenderSize) { x := &PileAdjuster{ widget: w, origSizer: sizer, } return pile.RenderBoxMaker(w, size, focus, focusIdx, app, x) } //====================================================================== var app *gowid.App var cols *ResizeableColumnsWidget var pilew *ResizeablePileWidget var twidgets []*terminal.Widget //====================================================================== type handler struct{} func (h handler) UnhandledInput(app gowid.IApp, ev interface{}) bool { handled := false if evk, ok := ev.(*tcell.EventKey); ok { switch evk.Key() { case tcell.KeyCtrlC, tcell.KeyEsc: handled = true for _, t := range twidgets { t.Signal(syscall.SIGINT) } case tcell.KeyCtrlBackslash: handled = true for _, t := range twidgets { t.Signal(syscall.SIGQUIT) } case tcell.KeyRune: handled = true switch evk.Rune() { case '>': cols.offset += 1 case '<': cols.offset -= 1 case '+': pilew.offset += 1 case '-': pilew.offset -= 1 default: handled = false } } } return handled } //====================================================================== func main() { var err error f := examples.RedirectLogger("terminal.log") defer f.Close() palette := gowid.Palette{ "invred": gowid.MakePaletteEntry(gowid.ColorBlack, gowid.ColorRed), "invblue": gowid.MakePaletteEntry(gowid.ColorBlack, gowid.ColorCyan), "line": gowid.MakeStyledPaletteEntry(gowid.NewUrwidColor("black"), gowid.NewUrwidColor("light gray"), gowid.StyleBold), } hkDuration := terminal.HotKeyDuration{time.Second * 3} twidgets = make([]*terminal.Widget, 0) //foo := os.Env() os.Open("foo") tcommands := []string{ os.Getenv("SHELL"), os.Getenv("SHELL"), os.Getenv("SHELL"), //"less cell.go", //"vttest", //"emacs -nw -q ./cell.go", } for _, cmd := range tcommands { tapp, err := terminal.NewExt(terminal.Options{ Command: strings.Split(cmd, " "), HotKeyPersistence: &hkDuration, Scrollback: 100, Scrollbar: true, EnableBracketedPaste: true, HotKeyFns: []terminal.HotKeyInputFn{ func(ev *tcell.EventKey, w terminal.IWidget, app gowid.IApp) bool { if w2, ok := w.(terminal.IScrollbar); ok { if ev.Key() == tcell.KeyRune && ev.Rune() == 's' { if w2.ScrollbarEnabled() { w2.DisableScrollbar(app) } else { w2.EnableScrollbar(app) } return true } } return false }, }, }) if err != nil { panic(err) } twidgets = append(twidgets, tapp) } tw := text.New(" Terminal Demo ") twir := styled.New(tw, gowid.MakePaletteRef("invred")) twib := styled.New(tw, gowid.MakePaletteRef("invblue")) twp := holder.New(tw) vline := styled.New(fill.New('│'), gowid.MakePaletteRef("line")) hline := styled.New(fill.New('⎯'), gowid.MakePaletteRef("line")) pilew = NewResizeablePile([]gowid.IContainerWidget{ &gowid.ContainerWidget{twidgets[1], gowid.RenderWithWeight{1}}, &gowid.ContainerWidget{hline, gowid.RenderWithUnits{U: 1}}, &gowid.ContainerWidget{twidgets[2], gowid.RenderWithWeight{1}}, }) cols = NewResizeableColumns([]gowid.IContainerWidget{ &gowid.ContainerWidget{twidgets[0], gowid.RenderWithWeight{3}}, &gowid.ContainerWidget{vline, gowid.RenderWithUnits{U: 1}}, &gowid.ContainerWidget{pilew, gowid.RenderWithWeight{1}}, }) view := framed.New(cols, framed.Options{ Frame: framed.UnicodeFrame, TitleWidget: twp, }) for _, t := range twidgets { t.OnProcessExited(gowid.WidgetCallback{"cb", func(app gowid.IApp, w gowid.IWidget) { app.Quit() }, }) t.OnBell(gowid.WidgetCallback{"cb", func(app gowid.IApp, w gowid.IWidget) { twp.SetSubWidget(twir, app) timer := time.NewTimer(time.Millisecond * 800) go func() { <-timer.C app.Run(gowid.RunFunction(func(app gowid.IApp) { twp.SetSubWidget(tw, app) })) }() }, }) t.OnSetTitle(gowid.WidgetCallback{"cb", func(app gowid.IApp, w gowid.IWidget) { w2 := w.(*terminal.Widget) tw.SetText(" "+w2.GetTitle()+" ", app) }, }) t.OnHotKey(gowid.WidgetCallback{"cb", func(app gowid.IApp, w gowid.IWidget) { w2 := w.(*terminal.Widget) if w2.HotKeyActive() { twp.SetSubWidget(twib, app) } else { twp.SetSubWidget(tw, app) } }, }) } app, err = gowid.NewApp(gowid.AppArgs{ View: view, Palette: &palette, Log: log.StandardLogger(), EnableBracketedPaste: true, }) examples.ExitOnErr(err) app.MainLoop(handler{}) } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: gowid-1.4.0/examples/gowid-tree/000077500000000000000000000000001426234454000165155ustar00rootroot00000000000000gowid-1.4.0/examples/gowid-tree/tree.go000066400000000000000000000145721426234454000200140ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. // A demonstration of gowid's tree widget. package main import ( "fmt" "github.com/gcla/gowid" "github.com/gcla/gowid/examples" "github.com/gcla/gowid/gwutil" "github.com/gcla/gowid/widgets/button" "github.com/gcla/gowid/widgets/checkbox" "github.com/gcla/gowid/widgets/columns" "github.com/gcla/gowid/widgets/framed" "github.com/gcla/gowid/widgets/list" "github.com/gcla/gowid/widgets/palettemap" "github.com/gcla/gowid/widgets/selectable" "github.com/gcla/gowid/widgets/styled" "github.com/gcla/gowid/widgets/text" "github.com/gcla/gowid/widgets/tree" tcell "github.com/gdamore/tcell/v2" log "github.com/sirupsen/logrus" ) var pos *tree.TreePos var tb *list.Widget var parent1 *tree.Collapsible var walker *tree.TreeWalker //====================================================================== func MakeDemoDecoration(pos tree.IPos, tr tree.IModel, wmaker tree.IWidgetMaker) gowid.IWidget { var res gowid.IWidget level := -1 for cur := pos; cur != nil; cur = tree.ParentPosition(cur) { level += 1 } pad := gwutil.StringOfLength(' ', level*10) cwidgets := make([]gowid.IContainerWidget, 0) cwidgets = append(cwidgets, &gowid.ContainerWidget{text.New(pad), gowid.RenderWithUnits{U: len(pad)}}) if ct, ok := tr.(tree.ICollapsible); ok { var bn *button.Widget if ct.IsCollapsed() { bn = button.New(text.New("+")) } else { bn = button.New(text.New("-")) } // If I use one button with conditional logic in the callback, rather than make // a separate button depending on whether or not the tree is collapsed, it will // correctly work when the DecoratorMaker is caching the widgets i.e. it will // collapse or expand even when the widget is rendered from the cache bn.OnClick(gowid.WidgetCallback{"cb", func(app gowid.IApp, w gowid.IWidget) { // Run this outside current event loop because we are implicitly // adjusting the data structure behind the list walker, and it's // not prepared to handle that in the same pass of processing // UserInput. TODO. app.Run(gowid.RunFunction(func(app gowid.IApp) { ct.SetCollapsed(app, !ct.IsCollapsed()) })) }}) cwidgets = append(cwidgets, &gowid.ContainerWidget{ framed.NewUnicode( styled.NewExt( bn, gowid.MakePaletteRef("body"), gowid.MakePaletteRef("fbody"), ), ), gowid.RenderFixed{}, }) } inner := wmaker.MakeWidget(pos, tr) cwidgets = append(cwidgets, &gowid.ContainerWidget{inner, gowid.RenderFixed{}}) res = palettemap.New( columns.New(cwidgets), palettemap.Map{"body": "fbody"}, palettemap.Map{}, ) return res } func MakeDemoWidget(pos tree.IPos, tr tree.IModel) gowid.IWidget { var res gowid.IWidget cbox := checkbox.New(false) cbox.OnClick(gowid.WidgetCallback{"cb", func(app gowid.IApp, w gowid.IWidget) { log.Info("Clicked checkbox in tree") }}) res = columns.New([]gowid.IContainerWidget{ &gowid.ContainerWidget{ framed.NewUnicode(cbox), gowid.RenderFixed{}, }, &gowid.ContainerWidget{ styled.NewExt( framed.NewUnicode( selectable.New( text.NewFromContent( text.NewContent( []text.ContentSegment{ text.StyledContent( fmt.Sprintf("tr %s:%v", tr.Leaf(), pos.String()), gowid.MakePaletteRef("body"), ), }, ), ), ), ), gowid.MakePaletteRef("body"), gowid.MakePaletteRef("fbody"), ), gowid.RenderFixed{}, }, }) return res } //====================================================================== type handler struct{} func (h handler) UnhandledInput(app gowid.IApp, ev interface{}) bool { handled := false if evk, ok := ev.(*tcell.EventKey); ok { handled = true if evk.Key() == tcell.KeyCtrlC || evk.Rune() == 'q' || evk.Rune() == 'Q' { app.Quit() } else if evk.Rune() == 'x' { f := walker.Focus() f2 := f.(tree.IPos) s := f2.GetSubStructure(parent1) if t2, ok := s.(tree.ICollapsible); ok { t2.SetCollapsed(app, true) } } else if evk.Rune() == 'z' { f := walker.Focus() f2 := f.(tree.IPos) s := f2.GetSubStructure(parent1) if t2, ok := s.(tree.ICollapsible); ok { t2.SetCollapsed(app, false) } } else { handled = false } } return handled } //====================================================================== func main() { f := examples.RedirectLogger("tree1.log") defer f.Close() palette := gowid.Palette{ "title": gowid.MakePaletteEntry(gowid.ColorWhite, gowid.ColorBlack), "key": gowid.MakePaletteEntry(gowid.ColorCyan, gowid.ColorBlack), "foot": gowid.MakePaletteEntry(gowid.ColorWhite, gowid.ColorBlack), "body": gowid.MakePaletteEntry(gowid.ColorBlack, gowid.ColorCyan), "fbody": gowid.MakePaletteEntry(gowid.ColorWhite, gowid.ColorBlack), } body := gowid.MakePaletteRef("body") leaf1 := tree.NewTree("leaf1", []tree.IModel{}) leaf2 := tree.NewTree("leaf2", []tree.IModel{}) leaf3 := tree.NewTree("leaf3", []tree.IModel{}) leaf4 := tree.NewTree("leaf4", []tree.IModel{}) leaf5 := tree.NewTree("leaf5", []tree.IModel{}) leaf21 := tree.NewTree("leaf21", []tree.IModel{}) leaf22 := tree.NewTree("leaf22", []tree.IModel{}) leaf23 := tree.NewTree("leaf23", []tree.IModel{}) stree1 := tree.NewCollapsible("stree1", []tree.IModel{leaf4, leaf5}) stree2 := tree.NewCollapsible("stree2", []tree.IModel{leaf21, leaf22, leaf23}) parent1 = tree.NewCollapsible("parent1", []tree.IModel{leaf1, stree1, leaf2, stree2, leaf3}) parent1.AddOnExpanded("exp", tree.ExpandedFunction(func(app gowid.IApp) { ch := parent1.GetChildren() newLeaf := tree.NewTree("foo", []tree.IModel{}) parent1.SetChildren(append([]tree.IModel{newLeaf}, ch...)) })) pos = tree.NewPos() walker = tree.NewWalker(parent1, pos, tree.NewCachingMaker(tree.WidgetMakerFunction(MakeDemoWidget)), tree.NewCachingDecorator(tree.DecoratorFunction(MakeDemoDecoration))) tb = tree.New(walker) tb.OnFocusChanged(gowid.WidgetCallback{"cb", func(app gowid.IApp, w gowid.IWidget) { log.Infof("Focus changed - widget is now %v", w) }}) view := styled.New(tb, body) app, err := gowid.NewApp(gowid.AppArgs{ View: view, Palette: &palette, Log: log.StandardLogger(), }) examples.ExitOnErr(err) app.MainLoop(handler{}) } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: gowid-1.4.0/examples/gowid-tutorial1/000077500000000000000000000000001426234454000175025ustar00rootroot00000000000000gowid-1.4.0/examples/gowid-tutorial1/tutorial1.go000066400000000000000000000012511426234454000217540ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source code is governed by the MIT license // that can be found in the LICENSE file. // The first example from the gowid tutorial. package main import ( "github.com/gcla/gowid" "github.com/gcla/gowid/examples" "github.com/gcla/gowid/widgets/text" ) //====================================================================== func main() { txt := text.New("hello world") app, err := gowid.NewApp(gowid.AppArgs{View: txt}) examples.ExitOnErr(err) app.SimpleMainLoop() } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: gowid-1.4.0/examples/gowid-tutorial2/000077500000000000000000000000001426234454000175035ustar00rootroot00000000000000gowid-1.4.0/examples/gowid-tutorial2/tutorial2.go000066400000000000000000000020011426234454000217500ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source code is governed by the MIT license // that can be found in the LICENSE file. // The second example from the gowid tutorial. package main import ( "fmt" "github.com/gcla/gowid" "github.com/gcla/gowid/examples" "github.com/gcla/gowid/widgets/text" tcell "github.com/gdamore/tcell/v2" ) //====================================================================== 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, err := gowid.NewApp(gowid.AppArgs{View: txt}) examples.ExitOnErr(err) app.MainLoop(gowid.UnhandledInputFunc(unhandled)) } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: gowid-1.4.0/examples/gowid-tutorial3/000077500000000000000000000000001426234454000175045ustar00rootroot00000000000000gowid-1.4.0/examples/gowid-tutorial3/tutorial3.go000066400000000000000000000026001426234454000217570ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source code is governed by the MIT license // that can be found in the LICENSE file. // The third example from the gowid tutorial. package main import ( "github.com/gcla/gowid" "github.com/gcla/gowid/examples" "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, err := gowid.NewApp(gowid.AppArgs{ View: map2, Palette: palette, }) examples.ExitOnErr(err) app.SimpleMainLoop() } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: gowid-1.4.0/examples/gowid-tutorial4/000077500000000000000000000000001426234454000175055ustar00rootroot00000000000000gowid-1.4.0/examples/gowid-tutorial4/tutorial4.go000066400000000000000000000024521426234454000217660ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source code is governed by the MIT license // that can be found in the LICENSE file. // The fourth example from the gowid tutorial. package main import ( "fmt" "github.com/gcla/gowid" "github.com/gcla/gowid/examples" "github.com/gcla/gowid/widgets/edit" "github.com/gcla/gowid/widgets/text" tcell "github.com/gdamore/tcell/v2" ) //====================================================================== 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(ev, size, focus, app) } } return res } func main() { edit := edit.New(edit.Options{Caption: "What is your name?\n"}) qb := &QuestionBox{edit} app, err := gowid.NewApp(gowid.AppArgs{View: qb}) examples.ExitOnErr(err) app.MainLoop(gowid.UnhandledInputFunc(gowid.HandleQuitKeys)) } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: gowid-1.4.0/examples/gowid-tutorial5/000077500000000000000000000000001426234454000175065ustar00rootroot00000000000000gowid-1.4.0/examples/gowid-tutorial5/tutorial5.go000066400000000000000000000032511426234454000217660ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source code is governed by the MIT license // that can be found in the LICENSE file. // The fifth example from the gowid tutorial. package main import ( "fmt" "github.com/gcla/gowid" "github.com/gcla/gowid/examples" "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, err := gowid.NewApp(gowid.AppArgs{View: view}) examples.ExitOnErr(err) app.SimpleMainLoop() } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: gowid-1.4.0/examples/gowid-tutorial6/000077500000000000000000000000001426234454000175075ustar00rootroot00000000000000gowid-1.4.0/examples/gowid-tutorial6/tutorial6.go000066400000000000000000000043361426234454000217750ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source code is governed by the MIT license // that can be found in the LICENSE file. // The sixth example from the gowid tutorial. package main import ( "fmt" "github.com/gcla/gowid" "github.com/gcla/gowid/examples" "github.com/gcla/gowid/widgets/edit" "github.com/gcla/gowid/widgets/list" "github.com/gcla/gowid/widgets/pile" "github.com/gcla/gowid/widgets/text" tcell "github.com/gdamore/tcell/v2" ) //====================================================================== 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() curw := w.Walker().At(focus) focusPile := curw.(*pile.Widget) pileSubWidgets := focusPile.SubWidgets() ed := pileSubWidgets[0].(*gowid.ContainerWidget).SubWidget().(*edit.Widget) focusPile.SetSubWidgets(append(pileSubWidgets[0:1], answer(ed.Text())), app) walker := w.Widget.Walker().(*list.SimpleListWalker) walker.Widgets = append(walker.Widgets, question()) nextPos := walker.Next(focus) walker.SetFocus(nextPos, app) w.Widget.GoToBottom(app) } else { res = w.Widget.UserInput(ev, size, focus, app) } return res } func main() { app, err := gowid.NewApp(gowid.AppArgs{View: NewConversationWidget()}) examples.ExitOnErr(err) app.SimpleMainLoop() } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: gowid-1.4.0/examples/gowid-widgets1/000077500000000000000000000000001426234454000173055ustar00rootroot00000000000000gowid-1.4.0/examples/gowid-widgets1/widgets1.go000066400000000000000000000130611426234454000213640ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source code is governed by the MIT license // that can be found in the LICENSE file. // A gowid test app which exercises the pile, button, edit and progress widgets. package main import ( "fmt" "github.com/gcla/gowid" "github.com/gcla/gowid/examples" "github.com/gcla/gowid/gwutil" "github.com/gcla/gowid/widgets/button" "github.com/gcla/gowid/widgets/divider" "github.com/gcla/gowid/widgets/edit" "github.com/gcla/gowid/widgets/framed" "github.com/gcla/gowid/widgets/holder" "github.com/gcla/gowid/widgets/palettemap" "github.com/gcla/gowid/widgets/pile" "github.com/gcla/gowid/widgets/progress" "github.com/gcla/gowid/widgets/styled" "github.com/gcla/gowid/widgets/text" "github.com/gcla/gowid/widgets/vpadding" tcell "github.com/gdamore/tcell/v2" log "github.com/sirupsen/logrus" ) //====================================================================== // An example of how to override type PBWidget struct { *progress.Widget } func NewPB() *PBWidget { return &PBWidget{progress.New(progress.Options{ Normal: gowid.MakeEmptyPalette(), Complete: gowid.MakePaletteRef("invred"), })} } func (w *PBWidget) Text() string { cur, done := w.Progress(), w.Target() percent := gwutil.Min(100, gwutil.Max(0, cur*100/done)) return fmt.Sprintf("At %d %% (%d/%d)", percent, cur, done) } func (w *PBWidget) Render(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.ICanvas { return progress.Render(w, size, focus, app) } //====================================================================== type handler struct{} func (h handler) UnhandledInput(app gowid.IApp, ev interface{}) bool { if evk, ok := ev.(*tcell.EventKey); ok { if evk.Key() == tcell.KeyCtrlC || evk.Rune() == 'q' || evk.Rune() == 'Q' { app.Quit() return true } } return false } //====================================================================== func main() { f := examples.RedirectLogger("widgets1.log") defer f.Close() styles := gowid.Palette{ "banner": gowid.MakePaletteEntry(gowid.ColorBlack, gowid.ColorWhite), "streak": gowid.MakePaletteEntry(gowid.ColorBlack, gowid.ColorRed), "bg": gowid.MakePaletteEntry(gowid.ColorBlack, gowid.ColorBlue), "test1focus": gowid.MakePaletteEntry(gowid.ColorBlue, gowid.ColorBlack), "test1notfocus": gowid.MakePaletteEntry(gowid.ColorGreen, gowid.ColorBlack), "red": gowid.MakePaletteEntry(gowid.ColorRed, gowid.ColorBlack), "invred": gowid.MakePaletteEntry(gowid.ColorBlack, gowid.ColorRed), "magenta": gowid.MakePaletteEntry(gowid.ColorMagenta, gowid.ColorBlack), "cyan": gowid.MakePaletteEntry(gowid.ColorCyan, gowid.ColorBlack), } flowme := gowid.RenderFlow{} pb1 := NewPB() nl := gowid.MakePaletteRef mh := text.NewContent([]text.ContentSegment{ text.StyledContent("abc", nl("invred")), text.StringContent("def"), text.StyledContent("ghijk", nl("cyan")), text.StyledContent("lmnopq", nl("magenta")), }) mti := text.NewFromContent(mh) mtj := palettemap.New(mti, palettemap.Map{}, palettemap.Map{"invred": "red"}) mt := holder.New(mti) xt := text.New("something else") xt2 := styled.New(xt, gowid.MakePaletteEntry(gowid.NewUrwidColor("dark red"), gowid.NewUrwidColor("light red"))) tw1 := text.New("click me█ █xx") tw := styled.NewWithRanges(tw1, []styled.AttributeRange{styled.AttributeRange{0, 2, nl("test1notfocus")}}, []styled.AttributeRange{styled.AttributeRange{0, -1, nl("test1focus")}}) bw1i := button.New(tw) bw1 := holder.New(bw1i) dv1 := divider.NewAscii() e1e := edit.New(edit.Options{Caption: "Name:", Text: "(1)abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzab(2)CDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCD(3)efghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdef(4)&^&^&^&^&GHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZ"}) e1 := vpadding.New(e1e, gowid.VAlignTop{}, gowid.RenderWithUnits{U: 2}) e2 := edit.New(edit.Options{Caption: "Password:", Text: "foobar", Mask: edit.MakeMask('*')}) e2e := edit.New(edit.Options{Caption: "Domain:", Text: "12345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890"}) bw1i.OnClick(gowid.WidgetCallback{"cb", func(app gowid.IApp, w gowid.IWidget) { pb1.SetProgress(app, pb1.Progress()+1) if mt.SubWidget() == mti { mt.SetSubWidget(mtj, app) } else { mt.SetSubWidget(mti, app) } }}) pw := pile.New([]gowid.IContainerWidget{ &gowid.ContainerWidget{pb1, flowme}, &gowid.ContainerWidget{dv1, flowme}, &gowid.ContainerWidget{bw1, flowme}, &gowid.ContainerWidget{dv1, flowme}, &gowid.ContainerWidget{e1, flowme}, &gowid.ContainerWidget{dv1, flowme}, &gowid.ContainerWidget{e2, flowme}, &gowid.ContainerWidget{dv1, flowme}, &gowid.ContainerWidget{e2e, flowme}, &gowid.ContainerWidget{dv1, flowme}, &gowid.ContainerWidget{mt, flowme}, &gowid.ContainerWidget{dv1, flowme}, &gowid.ContainerWidget{xt2, flowme}, }) twi := styled.New(text.New(" widgets1 "), gowid.MakePaletteRef("magenta")) params := framed.Options{ TitleWidget: twi, } fw1 := framed.New(pw, params) fw := framed.NewUnicode(fw1) pw2 := vpadding.New(fw, gowid.VAlignMiddle{}, gowid.RenderFlow{}) app, err := gowid.NewApp(gowid.AppArgs{ View: pw2, Palette: &styles, Log: log.StandardLogger(), }) examples.ExitOnErr(err) app.MainLoop(handler{}) } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: gowid-1.4.0/examples/gowid-widgets2/000077500000000000000000000000001426234454000173065ustar00rootroot00000000000000gowid-1.4.0/examples/gowid-widgets2/widgets2.go000066400000000000000000000046511426234454000213730ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source code is governed by the MIT license // that can be found in the LICENSE file. // A gowid test app which exercises the columns, checkbox, edit and styled widgets. package main import ( "github.com/gcla/gowid" "github.com/gcla/gowid/examples" "github.com/gcla/gowid/widgets/checkbox" "github.com/gcla/gowid/widgets/columns" "github.com/gcla/gowid/widgets/edit" "github.com/gcla/gowid/widgets/fill" "github.com/gcla/gowid/widgets/selectable" "github.com/gcla/gowid/widgets/styled" "github.com/gcla/gowid/widgets/text" log "github.com/sirupsen/logrus" ) //====================================================================== func main() { f := examples.RedirectLogger("widgets2.log") defer f.Close() palette := gowid.Palette{ "regular": gowid.MakePaletteEntry(gowid.ColorDefault, gowid.ColorDefault), "focus": gowid.MakePaletteEntry(gowid.ColorBlack, gowid.ColorWhite), } text1 := text.New("hello1") text2 := text.New("hello2 yahoo foo another one bar will it wrap") text3 := text.New("cols==7") text4 := styled.NewExt(text.New("len4"), gowid.MakePaletteRef("regular"), gowid.MakePaletteRef("focus")) text5 := selectable.New(text4) edit2 := edit.New(edit.Options{Caption: "Pass:", Text: "foobar", Mask: edit.MakeMask('*')}) edit3 := edit.New(edit.Options{Caption: "E3:", Text: "something"}) cb1 := checkbox.New(true) div1 := fill.New('|') fixed := gowid.RenderFixed{} units7 := gowid.RenderWithUnits{U: 7} weight1 := gowid.RenderWithWeight{1} units1 := gowid.RenderWithUnits{U: 1} c1 := columns.New([]gowid.IContainerWidget{ &gowid.ContainerWidget{text1, weight1}, &gowid.ContainerWidget{div1, units1}, &gowid.ContainerWidget{cb1, fixed}, &gowid.ContainerWidget{div1, units1}, &gowid.ContainerWidget{edit2, units7}, &gowid.ContainerWidget{div1, units1}, &gowid.ContainerWidget{text2, weight1}, &gowid.ContainerWidget{div1, units1}, &gowid.ContainerWidget{edit3, weight1}, &gowid.ContainerWidget{div1, units1}, &gowid.ContainerWidget{text5, fixed}, &gowid.ContainerWidget{div1, units1}, &gowid.ContainerWidget{text3, units7}, }) app, err := gowid.NewApp(gowid.AppArgs{ View: c1, Palette: &palette, Log: log.StandardLogger(), }) examples.ExitOnErr(err) app.SimpleMainLoop() } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: gowid-1.4.0/examples/gowid-widgets3/000077500000000000000000000000001426234454000173075ustar00rootroot00000000000000gowid-1.4.0/examples/gowid-widgets3/widgets3.go000066400000000000000000000122671426234454000213770ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source code is governed by the MIT license // that can be found in the LICENSE file. // A gowid test app which exercises the button, grid, progress and radio widgets. package main import ( "github.com/gcla/gowid" "github.com/gcla/gowid/examples" "github.com/gcla/gowid/widgets/button" "github.com/gcla/gowid/widgets/checkbox" "github.com/gcla/gowid/widgets/clicktracker" "github.com/gcla/gowid/widgets/columns" "github.com/gcla/gowid/widgets/divider" "github.com/gcla/gowid/widgets/grid" "github.com/gcla/gowid/widgets/holder" "github.com/gcla/gowid/widgets/pile" "github.com/gcla/gowid/widgets/radio" "github.com/gcla/gowid/widgets/styled" "github.com/gcla/gowid/widgets/text" "github.com/gcla/gowid/widgets/vpadding" log "github.com/sirupsen/logrus" ) //====================================================================== func main() { f := examples.RedirectLogger("widgets3.log") defer f.Close() styles := gowid.Palette{ "streak": gowid.MakePaletteEntry(gowid.ColorBlack, gowid.ColorRed), "test1focus": gowid.MakePaletteEntry(gowid.ColorBlue, gowid.ColorBlack), "test1notfocus": gowid.MakePaletteEntry(gowid.ColorGreen, gowid.ColorBlack), "test2focus": gowid.MakePaletteEntry(gowid.ColorMagenta, gowid.ColorBlack), "test2notfocus": gowid.MakePaletteEntry(gowid.ColorCyan, gowid.ColorBlack), } text1 := text.New("something") text2 := styled.NewWithRanges(text1, []styled.AttributeRange{styled.AttributeRange{0, 2, gowid.MakePaletteRef("test1notfocus")}}, []styled.AttributeRange{styled.AttributeRange{0, -1, gowid.MakePaletteRef("test1focus")}}) text3 := styled.NewWithRanges(text2, []styled.AttributeRange{styled.AttributeRange{0, 4, gowid.MakePaletteRef("test2notfocus")}}, []styled.AttributeRange{styled.AttributeRange{0, -1, gowid.MakePaletteRef("test2focus")}}) dv1 := divider.NewAscii() bw1 := clicktracker.New( button.NewDecorated( text3, button.Decoration{"[==", "==]"}, ), ) bw2 := vpadding.New(bw1, gowid.VAlignMiddle{}, gowid.RenderWithUnits{U: 10}) fixed := gowid.RenderFixed{} flow := gowid.RenderFlow{} cb1 := checkbox.NewDecorated(false, checkbox.Decoration{button.Decoration{"[[", "]]"}, " X "}) cbt1 := text.New(" Are you sure?") cols1 := columns.New([]gowid.IContainerWidget{ &gowid.ContainerWidget{cb1, fixed}, &gowid.ContainerWidget{cbt1, fixed}, }) cb1.OnClick(gowid.WidgetCallback{"cb", func(app gowid.IApp, w gowid.IWidget) { if _, ok := w.(*checkbox.Widget); !ok { panic("Widget was unexpected type!") } log.Infof("Checkbox clicked!") }}) rbgroup := make([]radio.IWidget, 0) rb1 := radio.New(&rbgroup) rbt1 := text.New(" option1 ") rb2 := radio.New(&rbgroup) rbt2 := text.New(" option2 ") rb3 := radio.New(&rbgroup) rbt3 := text.New(" option3 ") c2cols := []gowid.IContainerWidget{ &gowid.ContainerWidget{rb1, fixed}, &gowid.ContainerWidget{rbt1, fixed}, &gowid.ContainerWidget{rb2, fixed}, &gowid.ContainerWidget{rbt2, fixed}, &gowid.ContainerWidget{rb3, fixed}, &gowid.ContainerWidget{rbt3, fixed}, } cols2 := columns.New(c2cols) text4 := text.New("abcde") text4h := holder.New(text4) text4s := styled.NewWithRanges(text4h, []styled.AttributeRange{styled.AttributeRange{0, -1, gowid.MakePaletteRef("test1notfocus")}}, []styled.AttributeRange{styled.AttributeRange{0, -1, gowid.MakePaletteRef("streak")}}) text4btn := button.New(text4s) gfwids := []gowid.IWidget{text4btn, text4btn, text4btn, text4btn, text4btn, text4btn, text4btn, text4btn} grid1 := grid.New(gfwids, 20, 3, 1, gowid.HAlignMiddle{}) bw1.OnClick(gowid.WidgetCallback{"cb", func(app gowid.IApp, w gowid.IWidget) { m := text1.Content() m.AddAt(m.Length(), text.StringContent("x")) rb := radio.New(&rbgroup) cols2.SetSubWidgets(append(cols2.SubWidgets(), &gowid.ContainerWidget{ IWidget: rb, D: fixed, }), app) }}) text4btn.OnClick(gowid.WidgetCallback{"cb", func(app gowid.IApp, w gowid.IWidget) { text4h.IWidget = text.New("edcba") }}) rb1.OnClick(gowid.WidgetCallback{"cb", func(app gowid.IApp, w gowid.IWidget) { if _, ok := w.(*radio.Widget); !ok { panic("Widget was unexpected type!") } log.Infof("Radio button 1 checked/unchecked!") }}) rb3.OnClick(gowid.WidgetCallback{"cb", func(app gowid.IApp, w gowid.IWidget) { if _, ok := w.(*radio.Widget); !ok { panic("Widget was unexpected type!") } log.Infof("Radio button 3 checked/unchecked!") }}) pw := pile.New([]gowid.IContainerWidget{ &gowid.ContainerWidget{text3, fixed}, &gowid.ContainerWidget{dv1, flow}, &gowid.ContainerWidget{bw2, fixed}, &gowid.ContainerWidget{dv1, flow}, &gowid.ContainerWidget{cols1, fixed}, &gowid.ContainerWidget{dv1, flow}, &gowid.ContainerWidget{cols2, fixed}, &gowid.ContainerWidget{dv1, flow}, &gowid.ContainerWidget{grid1, flow}, &gowid.ContainerWidget{dv1, flow}, }) pw2 := vpadding.New(pw, gowid.VAlignMiddle{}, gowid.RenderFlow{}) app, err := gowid.NewApp(gowid.AppArgs{ View: pw2, Palette: &styles, Log: log.StandardLogger(), }) examples.ExitOnErr(err) app.SimpleMainLoop() } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: gowid-1.4.0/examples/gowid-widgets4/000077500000000000000000000000001426234454000173105ustar00rootroot00000000000000gowid-1.4.0/examples/gowid-widgets4/widgets4.go000066400000000000000000000070011426234454000213670ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source code is governed by the MIT license // that can be found in the LICENSE file. // A gowid test app which exercises the columns, list and framed widgets. package main import ( "fmt" "github.com/gcla/gowid" "github.com/gcla/gowid/examples" "github.com/gcla/gowid/widgets/columns" "github.com/gcla/gowid/widgets/framed" "github.com/gcla/gowid/widgets/list" "github.com/gcla/gowid/widgets/palettemap" "github.com/gcla/gowid/widgets/selectable" "github.com/gcla/gowid/widgets/text" "github.com/gcla/gowid/widgets/vpadding" log "github.com/sirupsen/logrus" ) //====================================================================== func main() { f := examples.RedirectLogger("widgets4.log") defer f.Close() styles := gowid.Palette{ "red": gowid.MakePaletteEntry(gowid.ColorRed, gowid.ColorBlack), "invred": gowid.MakePaletteEntry(gowid.ColorBlack, gowid.ColorRed), } widgets := make([]gowid.IWidget, 0) widgets2 := make([]gowid.IWidget, 0) widgets3 := make([]gowid.IWidget, 0) widgets4 := make([]gowid.IWidget, 0) wid8 := gowid.RenderWithUnits{U: 8} wid10 := gowid.RenderWithUnits{U: 10} wid12 := gowid.RenderWithUnits{U: 12} wid14 := gowid.RenderWithUnits{U: 14} nl := gowid.MakePaletteRef for i := 0; i < 23; i++ { t := text.NewContent([]text.ContentSegment{ text.StyledContent(fmt.Sprintf("abc%dd", i), nl("invred")), }) mt := text.NewFromContent(t) mta := selectable.New(palettemap.New(mt, palettemap.Map{}, palettemap.Map{"invred": "red"})) widgets = append(widgets, mta) t2 := text.NewContent([]text.ContentSegment{ text.StyledContent(fmt.Sprintf("abc%ddefghi", i), nl("invred")), }) mt2 := text.NewFromContent(t2) mta2 := selectable.New(palettemap.New(mt2, palettemap.Map{}, palettemap.Map{"invred": "red"})) widgets2 = append(widgets2, mta2) t3 := text.NewContent([]text.ContentSegment{ text.StyledContent(fmt.Sprintf("%d%d%d-1-2-3-4-5-6-7-8-9-10-11-12-13-14-abcdefghijklmn", i, i, i), nl("invred")), }) mt3 := text.NewFromContent(t3) mta3 := selectable.New(palettemap.New(mt3, palettemap.Map{}, palettemap.Map{"invred": "red"})) widgets3 = append(widgets3, mta3) t4 := text.NewContent([]text.ContentSegment{ text.StyledContent(fmt.Sprintf("%d%d%d-1-2-3-4-5-6-7-8-9-10-11-12-13-14-abcdefghijklmn", i, i, i), nl("invred")), }) mt4 := text.NewFromContent(t4) mta4 := selectable.New(palettemap.New(mt4, palettemap.Map{}, palettemap.Map{"invred": "red"})) widgets4 = append(widgets4, mta4) } walker := list.NewSimpleListWalker(widgets) lb := list.New(walker) lbb := vpadding.NewBox(lb, 7) fr := framed.New(lbb) walker2 := list.NewSimpleListWalker(widgets2) lb2 := list.New(walker2) lbb2 := vpadding.NewBox(lb2, 7) fr2 := framed.New(lbb2) walker3 := list.NewSimpleListWalker(widgets3) lb3 := list.New(walker3) lbb3 := vpadding.NewBox(lb3, 7) fr3 := framed.New(lbb3) walker4 := list.NewSimpleListWalker(widgets4) lb4 := list.New(walker4) lbb4 := vpadding.NewBox(lb4, 7) fr4 := framed.New(lbb4) c1 := columns.New([]gowid.IContainerWidget{ &gowid.ContainerWidget{fr, wid10}, &gowid.ContainerWidget{fr2, wid12}, &gowid.ContainerWidget{fr3, wid14}, &gowid.ContainerWidget{fr4, wid8}, }) app, err := gowid.NewApp(gowid.AppArgs{ View: c1, Palette: &styles, Log: log.StandardLogger(), }) examples.ExitOnErr(err) app.SimpleMainLoop() } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: gowid-1.4.0/examples/gowid-widgets5/000077500000000000000000000000001426234454000173115ustar00rootroot00000000000000gowid-1.4.0/examples/gowid-widgets5/widgets5.go000066400000000000000000000030151426234454000213720ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source code is governed by the MIT license // that can be found in the LICENSE file. // A gowid test app which exercises the edit and vpadding widgets. package main import ( "github.com/gcla/gowid" "github.com/gcla/gowid/examples" "github.com/gcla/gowid/widgets/edit" "github.com/gcla/gowid/widgets/vpadding" log "github.com/sirupsen/logrus" ) //====================================================================== func main() { f := examples.RedirectLogger("widgets5.log") defer f.Close() palette := gowid.Palette{} e1e := edit.New(edit.Options{Caption: "Name:", Text: "(1)abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzab(2)CDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCD(3)efghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdef(4)&^&^&^&^&GHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZ(5)sskdjfhskajfhskajfhksjadfhksjdfhksahdfksahdfkjsdhfkjsdhfkjshadfkshdf(6)87267823687268276382638263826382638263(7)xyxyxyxyxyxyxyxyxyxyxyxyxyxyxyxyxyxyxyxyxyxyxyx(8)ewewewewewewewewewewewewewewewewewewewew"}) e1 := vpadding.New(e1e, gowid.VAlignTop{}, gowid.RenderWithUnits{U: 4}) app, err := gowid.NewApp(gowid.AppArgs{ View: e1, Palette: &palette, Log: log.StandardLogger(), }) examples.ExitOnErr(err) app.SimpleMainLoop() } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: gowid-1.4.0/examples/gowid-widgets6/000077500000000000000000000000001426234454000173125ustar00rootroot00000000000000gowid-1.4.0/examples/gowid-widgets6/widgets6.go000066400000000000000000000037501426234454000214020ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source code is governed by the MIT license // that can be found in the LICENSE file. // A gowid test app which exercises the list, edit, columns and styled widgets. package main import ( "fmt" "github.com/gcla/gowid" "github.com/gcla/gowid/examples" "github.com/gcla/gowid/widgets/checkbox" "github.com/gcla/gowid/widgets/columns" "github.com/gcla/gowid/widgets/edit" "github.com/gcla/gowid/widgets/framed" "github.com/gcla/gowid/widgets/list" "github.com/gcla/gowid/widgets/styled" "github.com/gcla/gowid/widgets/vpadding" log "github.com/sirupsen/logrus" ) //====================================================================== func main() { f := examples.RedirectLogger("widgets6.log") defer f.Close() palette := gowid.Palette{ "body": gowid.MakePaletteEntry(gowid.ColorBlack, gowid.ColorCyan), "fbody": gowid.MakePaletteEntry(gowid.ColorWhite, gowid.ColorBlack), } edits := make([]gowid.IWidget, 0) for i := 0; i < 5; i++ { w11 := edit.New(edit.Options{Caption: fmt.Sprintf("Cap%d:", i+1), Text: "abcde"}) w1 := styled.NewExt(w11, gowid.MakePaletteRef("body"), gowid.MakePaletteRef("fbody")) w22 := checkbox.New(false) w2 := styled.NewExt(w22, gowid.MakePaletteRef("body"), gowid.MakePaletteRef("fbody")) colwids := make([]gowid.IContainerWidget, 0) colwids = append(colwids, &gowid.ContainerWidget{w1, gowid.RenderWithWeight{50}}) colwids = append(colwids, &gowid.ContainerWidget{w2, gowid.RenderFixed{}}) cols1 := columns.New(colwids) edits = append(edits, cols1) } walker := list.NewSimpleListWalker(edits) lbox := list.New(walker) lbox2 := vpadding.NewBox(lbox, 5) fr := framed.New(lbox2) app, err := gowid.NewApp(gowid.AppArgs{ View: fr, Palette: &palette, Log: log.StandardLogger(), }) examples.ExitOnErr(err) app.SimpleMainLoop() } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: gowid-1.4.0/examples/gowid-widgets7/000077500000000000000000000000001426234454000173135ustar00rootroot00000000000000gowid-1.4.0/examples/gowid-widgets7/widgets7.go000066400000000000000000000023431426234454000214010ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source code is governed by the MIT license // that can be found in the LICENSE file. // A gowid test app which exercises the list, edit and framed widgets. package main import ( "fmt" "github.com/gcla/gowid" "github.com/gcla/gowid/examples" "github.com/gcla/gowid/widgets/edit" "github.com/gcla/gowid/widgets/framed" "github.com/gcla/gowid/widgets/list" log "github.com/sirupsen/logrus" ) //====================================================================== func main() { f := examples.RedirectLogger("widgets7.log") defer f.Close() palette := gowid.Palette{} edits := make([]gowid.IWidget, 0) for i := 0; i < 40; i++ { edits = append(edits, edit.New(edit.Options{Caption: fmt.Sprintf("Cap%d:", i+1), Text: "abcde1111111222222222222222223333333333444444444"})) } walker := list.NewSimpleListWalker(edits) lb := list.New(walker) fr := framed.New(lb) app, err := gowid.NewApp(gowid.AppArgs{ View: fr, Palette: &palette, Log: log.StandardLogger(), }) examples.ExitOnErr(err) app.SimpleMainLoop() } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: gowid-1.4.0/examples/gowid-widgets8/000077500000000000000000000000001426234454000173145ustar00rootroot00000000000000gowid-1.4.0/examples/gowid-widgets8/widgets8.go000066400000000000000000000024571426234454000214110ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source code is governed by the MIT license // that can be found in the LICENSE file. // A gowid test app which exercises the checkbox, columns and hpadding widgets. package main import ( "github.com/gcla/gowid" "github.com/gcla/gowid/examples" "github.com/gcla/gowid/widgets/checkbox" "github.com/gcla/gowid/widgets/columns" "github.com/gcla/gowid/widgets/hpadding" log "github.com/sirupsen/logrus" ) //====================================================================== func main() { f := examples.RedirectLogger("widgets8.log") defer f.Close() palette := gowid.Palette{} fixed := gowid.RenderFixed{} cb1 := checkbox.New(false) cp1 := hpadding.New(cb1, gowid.HAlignLeft{}, fixed) cb2 := checkbox.New(false) cp2 := hpadding.New(cb2, gowid.HAlignLeft{}, fixed) view := columns.New([]gowid.IContainerWidget{ &gowid.ContainerWidget{cp1, gowid.RenderWithWeight{10}}, &gowid.ContainerWidget{cp2, gowid.RenderWithWeight{10}}, }) app, err := gowid.NewApp(gowid.AppArgs{ View: view, Palette: &palette, Log: log.StandardLogger(), }) examples.ExitOnErr(err) app.SimpleMainLoop() } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: gowid-1.4.0/examples/utils.go000066400000000000000000000016571426234454000161470ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source code is governed by the MIT license // that can be found in the LICENSE file. package examples import ( "fmt" "os" log "github.com/sirupsen/logrus" ) // RedirectLogger sets the global logger to write to a file in append mode, the filename // specified by the argument to the function. This is a convenience method used in a few // of the example programs to avoid polluting the tty used to display the application. func RedirectLogger(path string) *os.File { f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666) if err != nil { log.Fatalf("Error opening log file: %v", err) } log.SetOutput(f) return f } func ExitOnErr(err error) { if err != nil { fmt.Printf("Error: %v\n", err) os.Exit(1) } } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: gowid-1.4.0/go.mod000066400000000000000000000014431426234454000137410ustar00rootroot00000000000000module github.com/gcla/gowid go 1.13 require ( github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc // indirect github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf // indirect github.com/araddon/dateparse v0.0.0-20210207001429-0eec95c9db7e github.com/creack/pty v1.1.15 github.com/gdamore/tcell/v2 v2.5.0 github.com/go-test/deep v1.0.1 github.com/guptarohit/asciigraph v0.4.1 github.com/hashicorp/golang-lru v0.5.1 github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 github.com/mattn/go-runewidth v0.0.13 github.com/pkg/errors v0.8.1 github.com/rakyll/statik v0.1.6 github.com/sirupsen/logrus v1.4.2 github.com/stretchr/testify v1.7.0 golang.org/x/text v0.3.7 gopkg.in/alecthomas/kingpin.v2 v2.2.6 ) gowid-1.4.0/go.sum000066400000000000000000000133111426234454000137630ustar00rootroot00000000000000github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc h1:cAKDfWh5VpdgMhJosfJnn5/FoN2SRZ4p7fJNX58YPaU= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf h1:qet1QNfXsQxTZqLG4oE62mJzwPIB8+Tee4RNCL9ulrY= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/araddon/dateparse v0.0.0-20210207001429-0eec95c9db7e h1:OjdSMCht0ZVX7IH0nTdf00xEustvbtUGRgMh3gbdmOg= github.com/araddon/dateparse v0.0.0-20210207001429-0eec95c9db7e/go.mod h1:DCaWoUhZrYW9p1lxo/cm8EmUOOzAPSEZNGF2DK1dJgw= github.com/creack/pty v1.1.15 h1:cKRCLMj3Ddm54bKSpemfQ8AtYFBhAI2MPmdys22fBdc= github.com/creack/pty v1.1.15/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko= github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg= github.com/gdamore/tcell/v2 v2.5.0 h1:/LA5f/wqTP5mWT79czngibKVVx5wOgdFTIXPQ68fMO8= github.com/gdamore/tcell/v2 v2.5.0/go.mod h1:wSkrPaXoiIWZqW/g7Px4xc79di6FTcpB8tvaKJ6uGBo= github.com/go-test/deep v1.0.1 h1:UQhStjbkDClarlmv0am7OXXO4/GaPdCGiUiMTvi28sg= github.com/go-test/deep v1.0.1/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= github.com/guptarohit/asciigraph v0.4.1 h1:YHmCMN8VH81BIUIgTg2Fs3B52QDxNZw2RQ6j5pGoSxo= github.com/guptarohit/asciigraph v0.4.1/go.mod h1:9fYEfE5IGJGxlP1B+w8wHFy7sNZMhPtn59f0RLtpRFM= github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s= github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rakyll/statik v0.1.6 h1:uICcfUXpgqtw2VopbIncslhAmE5hwc4g20TEyEENBNs= github.com/rakyll/statik v0.1.6/go.mod h1:OEi9wJV/fMUAGx1eNjq75DKDsJVuEv1U0oYdX6GX8Zs= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/scylladb/termtables v0.0.0-20191203121021-c4c0b6d42ff4/go.mod h1:C1a7PQSMz9NShzorzCiG2fk9+xuCgLkPeCvMHYR2OWg= github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20220318055525-2edf467146b5 h1:saXMvIOKvRFwbOMicHXr0B1uwoxq9dGmLe5ExMES6c4= golang.org/x/sys v0.0.0-20220318055525-2edf467146b5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf h1:MZ2shdL+ZM/XzY3ZGOnh4Nlpnxz5GSOhOmtHo3iPU6M= golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gowid-1.4.0/gwtest/000077500000000000000000000000001426234454000141465ustar00rootroot00000000000000gowid-1.4.0/gwtest/support_test.go000066400000000000000000000044501426234454000172530ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source code is governed by the MIT license // that can be found in the LICENSE file. package gwtest import ( "testing" "github.com/gcla/gowid" "github.com/gcla/gowid/widgets/columns" "github.com/gcla/gowid/widgets/edit" "github.com/gcla/gowid/widgets/pile" "github.com/gcla/gowid/widgets/styled" "github.com/stretchr/testify/assert" ) func Test1(t *testing.T) { fx := gowid.RenderFixed{} t1 := edit.New(edit.Options{Text: "foo"}) ct1 := &gowid.ContainerWidget{IWidget: t1, D: fx} c1 := columns.New([]gowid.IContainerWidget{ct1, ct1, ct1}) cc1 := &gowid.ContainerWidget{IWidget: c1, D: fx} c1.SetFocus(D, 2) assert.Equal(t, 2, c1.Focus()) c2 := columns.New([]gowid.IContainerWidget{ct1, ct1}) c2.SetFocus(D, 1) assert.Equal(t, 1, c2.Focus()) w3 := styled.New(c2, gowid.MakeForeground(gowid.ColorBlack)) cw3 := &gowid.ContainerWidget{IWidget: w3, D: fx} p1 := pile.New([]gowid.IContainerWidget{ct1, cc1, cw3}) assert.Equal(t, 0, p1.Focus()) assert.Equal(t, gowid.FocusPath(p1), []interface{}{0}) p1.SetFocus(D, 1) assert.Equal(t, 1, p1.Focus()) assert.Equal(t, gowid.FocusPath(p1), []interface{}{1, 2}) p1.SetFocus(D, 2) assert.Equal(t, 2, p1.Focus()) assert.Equal(t, gowid.FocusPath(p1), []interface{}{2, 1}) r := gowid.SetFocusPath(p1, []interface{}{0, 4, 5}, D) assert.Equal(t, false, r.Succeeded) assert.Equal(t, 1, r.FailedLevel) assert.Equal(t, 0, p1.Focus()) p1.SetFocus(D, 2) assert.Equal(t, 2, p1.Focus()) r = gowid.SetFocusPath(p1, []interface{}{0}, D) assert.Equal(t, true, r.Succeeded) assert.Equal(t, 0, p1.Focus()) c1.SetFocus(D, 2) assert.Equal(t, 2, c1.Focus()) r = gowid.SetFocusPath(p1, []interface{}{1}, D) assert.Equal(t, true, r.Succeeded) assert.Equal(t, 1, p1.Focus()) assert.Equal(t, 2, c1.Focus()) r = gowid.SetFocusPath(p1, []interface{}{1, 0}, D) assert.Equal(t, true, r.Succeeded) assert.Equal(t, 1, p1.Focus()) assert.Equal(t, 0, c1.Focus()) c2.SetFocus(D, 1) assert.Equal(t, 1, c2.Focus()) r = gowid.SetFocusPath(p1, []interface{}{2, 0}, D) assert.Equal(t, true, r.Succeeded) assert.Equal(t, 2, p1.Focus()) assert.Equal(t, 0, c2.Focus()) } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: gowid-1.4.0/gwtest/testutils.go000066400000000000000000000142211426234454000165350ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. // Package gwtest provides utilities for testing gowid widgets. package gwtest import ( "errors" "testing" "github.com/gcla/gowid" tcell "github.com/gdamore/tcell/v2" log "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" ) var testAppData gowid.Palette func init() { testAppData = make(gowid.Palette) testAppData["test1focus"] = gowid.MakePaletteEntry(gowid.ColorRed, gowid.ColorBlack) testAppData["test1notfocus"] = gowid.MakePaletteEntry(gowid.ColorGreen, gowid.ColorBlack) } type testApp struct { doQuit bool gowid.ClickTargets lastMouse gowid.MouseState } func NewTestApp() *testApp { a := &testApp{ ClickTargets: gowid.MakeClickTargets(), } return a } var D *testApp = NewTestApp() func ClearTestApp() { D.DeleteClickTargets(tcell.Button1) D.DeleteClickTargets(tcell.Button2) D.DeleteClickTargets(tcell.Button3) D.DeleteClickTargets(tcell.ButtonNone) } func (d testApp) CellStyler(name string) (gowid.ICellStyler, bool) { x, y := testAppData[name] return x, y } func (d testApp) RangeOverPalette(f func(name string, entry gowid.ICellStyler) bool) { for k, v := range testAppData { if !f(k, v) { break } } } func (d testApp) Quit() { d.doQuit = true } func (d testApp) Run(f gowid.IAfterRenderEvent) error { f.RunThenRenderEvent(&d) return nil } func (d testApp) GetColorMode() gowid.ColorMode { return gowid.Mode256Colors } func (d testApp) GetMouseState() gowid.MouseState { return gowid.MouseState{ MouseLeftClicked: true, MouseMiddleClicked: false, MouseRightClicked: false, } } func (d *testApp) SetLastMouseState(m gowid.MouseState) { d.lastMouse = m } func (d testApp) GetLastMouseState() gowid.MouseState { return d.lastMouse } func (d testApp) InCopyMode(...bool) bool { return false } func (d testApp) Log(lvl log.Level, msg string, fields ...gowid.LogField) { panic(errors.New("Must not call!")) } func (d testApp) CopyModeClaimedBy(...gowid.IIdentity) gowid.IIdentity { panic(errors.New("Must not call!")) } func (d testApp) RefreshCopyMode() { panic(errors.New("Must not call!")) } func (d testApp) CopyLevel(...int) int { panic(errors.New("Must not call!")) } func (d testApp) Clips() []gowid.ICopyResult { panic(errors.New("Must not call!")) } func (d testApp) CopyModeClaimedAt(...int) int { panic(errors.New("Must not call!")) } func (d testApp) RegisterMenu(m gowid.IMenuCompatible) { panic(errors.New("Must not call!")) } func (d testApp) UnregisterMenu(m gowid.IMenuCompatible) bool { panic(errors.New("Must not call!")) } func (d testApp) GetLog() log.StdLogger { panic(errors.New("Must not call!")) } func (d testApp) SetLog(log.StdLogger) { panic(errors.New("Must not call!")) } func (d testApp) ID() interface{} { panic(errors.New("Must not call!")) } func (d testApp) GetScreen() tcell.Screen { panic(errors.New("Must not call!")) } func (d testApp) Redraw() { panic(errors.New("Must not call!")) } func (d testApp) Sync() { panic(errors.New("Must not call!")) } func (d testApp) SetColorMode(gowid.ColorMode) { panic(errors.New("Must not call!")) } func (d testApp) SetSubWidget(gowid.IWidget, gowid.IApp) { panic(errors.New("Must not call!")) } func (d testApp) SubWidget() gowid.IWidget { panic(errors.New("Must not call!")) } //====================================================================== type CheckBoxTester struct { Gotit bool } func (f *CheckBoxTester) Changed(t gowid.IApp, w gowid.IWidget, data ...interface{}) { f.Gotit = true } func (f *CheckBoxTester) ID() interface{} { return "foo" } //====================================================================== type ButtonTester struct { Gotit bool } func (f *ButtonTester) Changed(gowid.IApp, gowid.IWidget, ...interface{}) { f.Gotit = true } func (f *ButtonTester) ID() interface{} { return "foo" } //====================================================================== func RenderBoxManyTimes(t *testing.T, w gowid.IWidget, minX, maxX, minY, maxY int) { for x := minX; x <= maxX; x++ { for y := minY; y <= maxY; y++ { assert.NotPanics(t, func() { w.Render(gowid.RenderBox{C: x, R: y}, gowid.Focused, D) }) c := w.Render(gowid.RenderBox{C: x, R: y}, gowid.Focused, D) if c.BoxRows() > 0 { assert.Equal(t, c.BoxColumns(), x, "foo boxcol=%v boxrows=%v x=%v y=%v", c.BoxColumns(), c.BoxRows(), x, y) } assert.Equal(t, c.BoxRows(), y) } } } func RenderFlowManyTimes(t *testing.T, w gowid.IWidget, minX, maxX int) { for x := minX; x <= maxX; x++ { assert.NotPanics(t, func() { w.Render(gowid.RenderFlowWith{C: x}, gowid.Focused, D) }) c := w.Render(gowid.RenderFlowWith{C: x}, gowid.Focused, D) if c.BoxRows() > 0 { assert.Equal(t, c.BoxColumns(), x) } } } func RenderFixedDoesNotPanic(t *testing.T, w gowid.IWidget) { assert.NotPanics(t, func() { w.Render(gowid.RenderFixed{}, gowid.Focused, D) }) } //====================================================================== //====================================================================== func ClickAt(x, y int) *tcell.EventMouse { return tcell.NewEventMouse(x, y, tcell.Button1, 0) } func ClickUpAt(x, y int) *tcell.EventMouse { return tcell.NewEventMouse(x, y, tcell.ButtonNone, 0) } func KeyEvent(ch rune) *tcell.EventKey { return tcell.NewEventKey(tcell.KeyRune, ch, tcell.ModNone) } func CursorDown() *tcell.EventKey { return tcell.NewEventKey(tcell.KeyDown, 0, tcell.ModNone) } func CursorUp() *tcell.EventKey { return tcell.NewEventKey(tcell.KeyUp, 0, tcell.ModNone) } func CursorLeft() *tcell.EventKey { return tcell.NewEventKey(tcell.KeyLeft, 0, tcell.ModNone) } func CursorRight() *tcell.EventKey { return tcell.NewEventKey(tcell.KeyRight, 0, tcell.ModNone) } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: gowid-1.4.0/gwutil/000077500000000000000000000000001426234454000141445ustar00rootroot00000000000000gowid-1.4.0/gwutil/utils.go000066400000000000000000000173271426234454000156450ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source code is governed by the MIT license // that can be found in the LICENSE file. // Package gwutil provides general-purpose utilities that are not used by // the core of gowid but that have proved useful for several pre-canned // widgets. package gwutil import ( "errors" "fmt" "math" "os" "runtime/pprof" "sort" log "github.com/sirupsen/logrus" ) //====================================================================== // Min returns the smaller of >1 integer arguments. func Min(i int, js ...int) int { res := i for _, j := range js { if j < res { res = j } } return res } // Min returns the larger of >1 integer arguments. func Max(i int, js ...int) int { res := i for _, j := range js { if j > res { res = j } } return res } // LimitTo is a one-liner that uses Min and Max to bound a value. Assumes // a <= b. func LimitTo(a, v, b int) int { if v < a { return a } if v > b { return b } return v } // StringOfLength returns a string consisting of n runes. func StringOfLength(r rune, n int) string { res := make([]rune, n) for i := 0; i < n; i++ { res[i] = r } return string(res) } // Map is the traditional functional map function for strings. func Map(vs []string, f func(string) string) []string { vsm := make([]string, len(vs)) for i, v := range vs { vsm[i] = f(v) } return vsm } // IPow returns a raised to the bth power. func IPow(a, b int) int { var result int = 1 for 0 != b { if 0 != (b & 1) { result *= a } b >>= 1 a *= a } return result } // Sum is a variadic function that returns the sum of its integer arguments. func Sum(input ...int) int { sum := 0 for i := range input { sum += input[i] } return sum } //====================================================================== type fract struct { fp float64 idx int } type fractlist []fract func (slice fractlist) Len() int { return len(slice) } // Note > to skip the reverse func (slice fractlist) Less(i, j int) bool { return slice[i].fp > slice[j].fp } func (slice fractlist) Swap(i, j int) { slice[i], slice[j] = slice[j], slice[i] } // HamiltonAllocation implements the Hamilton Method (Largest remainder method) to calculate // integral ratios. (Like it is used in some elections.) // // This is shamelessly cribbed from https://excess.org/svn/urwid/contrib/trunk/rbreu_scrollbar.py // // counts -- list of integers ('votes per party') // alloc -- total amount to be allocated ('total amount of seats') // func HamiltonAllocation(counts []int, alloc int) []int { totalCounts := Sum(counts...) if totalCounts == 0 { return counts } res := make([]int, len(counts)) quotas := make([]float64, len(counts)) fracts := fractlist(make([]fract, len(counts))) for i, c := range counts { quotas[i] = (float64(c) * float64(alloc)) / float64(totalCounts) } for i, fp := range quotas { _, f := math.Modf(fp) fracts[i] = fract{fp: f, idx: i} } sort.Sort(fracts) for i, fp := range quotas { n, _ := math.Modf(fp) res[i] = int(n) } remainder := alloc - Sum(res...) for i := 0; i < remainder; i++ { res[fracts[i].idx] += 1 } return res } //====================================================================== // LStripByte returns a slice of its first argument which contains all // bytes up to but not including its second argument. func LStripByte(data []byte, s byte) []byte { var i int for i = 0; i < len(data); i++ { if data[i] != s { break } } return data[i:] } //====================================================================== type IOption interface { IsNone() bool Value() interface{} } // For fmt.Stringer func OptionString(opt IOption) string { if opt.IsNone() { return "None" } else { return fmt.Sprintf("%v", opt.Value()) } } //====================================================================== // IntOption is intended to represent an Option[int] type IntOption struct { some bool val int } var _ fmt.Stringer = IntOption{} var _ IOption = IntOption{} func SomeInt(x int) IntOption { return IntOption{true, x} } func NoneInt() IntOption { return IntOption{} } func (i IntOption) IsNone() bool { return !i.some } func (i IntOption) Value() interface{} { return i.Val() } func (i IntOption) Val() int { if i.IsNone() { panic(errors.New("Called Val on empty IntOption")) } return i.val } // For fmt.Stringer func (i IntOption) String() string { return OptionString(i) } //====================================================================== // Int64Option is intended to represent an Option[int] type Int64Option struct { some bool val int64 } var _ fmt.Stringer = Int64Option{} var _ IOption = Int64Option{} func SomeInt64(x int64) Int64Option { return Int64Option{true, x} } func NoneInt64() Int64Option { return Int64Option{} } func (i Int64Option) IsNone() bool { return !i.some } func (i Int64Option) Value() interface{} { return i.Val() } func (i Int64Option) Val() int64 { if i.IsNone() { panic(errors.New("Called Val on empty Int64Option")) } return i.val } // For fmt.Stringer func (i Int64Option) String() string { return OptionString(i) } //====================================================================== // RuneOption is intended to represent an Option[rune] type RuneOption struct { some bool val rune } var _ fmt.Stringer = RuneOption{} var _ IOption = RuneOption{} func SomeRune(x rune) RuneOption { return RuneOption{true, x} } func NoneRune() RuneOption { return RuneOption{} } func (i RuneOption) IsNone() bool { return !i.some } func (i RuneOption) Value() interface{} { return i.Val() } func (i RuneOption) Val() rune { if i.IsNone() { panic(errors.New("Called Val on empty ByteOption")) } return i.val } func (i RuneOption) String() string { return OptionString(i) } //====================================================================== const float64EqualityThreshold = 1e-5 // AlmostEqual returns true if its two arguments are within 1e-5 of each other. func AlmostEqual(a, b float64) bool { return math.Abs(a-b) <= float64EqualityThreshold } // Round returns a float64 representing the closest whole number // to the supplied float64 argument. func Round(f float64) float64 { if f < 0 { return math.Ceil(f - 0.5) } else { return math.Floor(f + 0.5) } } // RoundFloatToInt returns an int representing the closest int to the // supplied float, rounding up or down. func RoundFloatToInt(val float32) int { if val < 0 { return int(val - 0.5) } return int(val + 0.5) } //====================================================================== // If is a convenience function for mimicking a ternary operator e.g. If(x box.BoxRows() { c.Truncate(0, c.BoxRows()-box.BoxRows()) } } } type IAppendBlankLines interface { BoxColumns() int AppendBelow(c IAppendCanvas, doCursor bool, makeCopy bool) } func AppendBlankLines(c IAppendBlankLines, iters int) { for i := 0; i < iters; i++ { line := make([]Cell, c.BoxColumns()) c.AppendBelow(LineCanvas(line), false, false) } } //====================================================================== type ICallbackRunner interface { RunWidgetCallbacks(name interface{}, app IApp, w IWidget) } // IWidgetChangedCallback defines the types that can be used as callbacks // that are issued when widget properties change. It expects a function // Changed() that is called with the current app and the widget that is // issuing the callback. It also expects to conform to IIdentity, so that // one callback instance can be compared to another - this is to allow // callbacks to be removed correctly, if that is required. type IWidgetChangedCallback interface { IIdentity Changed(app IApp, widget IWidget, data ...interface{}) } // WidgetChangedFunction meets the IWidgetChangedCallback interface, for simpler // usage. type WidgetChangedFunction func(app IApp, widget IWidget) func (f WidgetChangedFunction) Changed(app IApp, widget IWidget, data ...interface{}) { f(app, widget) } type WidgetChangedFunctionExt func(app IApp, widget IWidget, data ...interface{}) func (f WidgetChangedFunctionExt) Changed(app IApp, widget IWidget, data ...interface{}) { f(app, widget, data...) } // WidgetCallback is a simple struct with a name field for IIdentity and // that embeds a WidgetChangedFunction to be issued as a callback when a widget // property changes. type WidgetCallback struct { Name interface{} WidgetChangedFunction } func MakeWidgetCallback(name interface{}, fn WidgetChangedFunction) WidgetCallback { return WidgetCallback{ Name: name, WidgetChangedFunction: fn, } } func (f WidgetCallback) ID() interface{} { return f.Name } // WidgetCallbackExt is a simple struct with a name field for IIdentity and // that embeds a WidgetChangedFunction to be issued as a callback when a widget // property changes. type WidgetCallbackExt struct { Name interface{} WidgetChangedFunctionExt } func MakeWidgetCallbackExt(name interface{}, fn WidgetChangedFunctionExt) WidgetCallbackExt { return WidgetCallbackExt{ Name: name, WidgetChangedFunctionExt: fn, } } func (f WidgetCallbackExt) ID() interface{} { return f.Name } func RunWidgetCallbacks(c ICallbacks, name interface{}, app IApp, data ...interface{}) { if c != nil { data2 := append([]interface{}{app}, data...) c.RunCallbacks(name, data2...) } } type widgetChangedCallbackProxy struct { IWidgetChangedCallback } func (p widgetChangedCallbackProxy) Call(args ...interface{}) { t := args[0].(IApp) var w IWidget w, _ = args[1].(IWidget) p.IWidgetChangedCallback.Changed(t, w, args[2:]...) } func AddWidgetCallback(c ICallbacks, name interface{}, cb IWidgetChangedCallback) { c.AddCallback(name, widgetChangedCallbackProxy{cb}) } func RemoveWidgetCallback(c ICallbacks, name interface{}, id IIdentity) { c.RemoveCallback(name, id) } //'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' // Common callbacks // QuitFn can be used to construct a widget callback that terminates your // application. It can be used as the second argument of the // WidgetChangedCallback struct which implements IWidgetChangedCallback. func QuitFn(app IApp, widget IWidget) { app.Quit() } // SubWidgetCallbacks is a convenience struct for embedding in a widget, providing methods // to add and remove callbacks that are executed when the widget's child is modified. type SubWidgetCallbacks struct { CB **Callbacks } func (w *SubWidgetCallbacks) OnSetSubWidget(f IWidgetChangedCallback) { if *w.CB == nil { *w.CB = NewCallbacks() } AddWidgetCallback(*w.CB, SubWidgetCB{}, f) } func (w *SubWidgetCallbacks) RemoveOnSetSubWidget(f IIdentity) { RemoveWidgetCallback(*w.CB, SubWidgetCB{}, f) } //====================================================================== // SubWidgetsCallbacks is a convenience struct for embedding in a widget, providing methods // to add and remove callbacks that are executed when the widget's children are modified. type SubWidgetsCallbacks struct { CB **Callbacks } func (w *SubWidgetsCallbacks) OnSetSubWidgets(f IWidgetChangedCallback) { if *w.CB == nil { *w.CB = NewCallbacks() } AddWidgetCallback(*w.CB, SubWidgetsCB{}, f) } func (w *SubWidgetsCallbacks) RemoveOnSetSubWidgets(f IIdentity) { RemoveWidgetCallback(*w.CB, SubWidgetsCB{}, f) } //====================================================================== // ClickCallbacks is a convenience struct for embedding in a widget, providing methods // to add and remove callbacks that are executed when the widget is "clicked". type ClickCallbacks struct { CB **Callbacks } func (w *ClickCallbacks) OnClick(f IWidgetChangedCallback) { if *w.CB == nil { *w.CB = NewCallbacks() } AddWidgetCallback(*w.CB, ClickCB{}, f) } func (w *ClickCallbacks) RemoveOnClick(f IIdentity) { RemoveWidgetCallback(*w.CB, ClickCB{}, f) } //====================================================================== // KeyPressCallbacks is a convenience struct for embedding in a widget, providing methods // to add and remove callbacks that are executed when the widget is "clicked". type KeyPressCallbacks struct { CB **Callbacks } func (w *KeyPressCallbacks) OnKeyPress(f IWidgetChangedCallback) { if *w.CB == nil { *w.CB = NewCallbacks() } AddWidgetCallback(*w.CB, KeyPressCB{}, f) } func (w *KeyPressCallbacks) RemoveOnKeyPress(f IIdentity) { RemoveWidgetCallback(*w.CB, KeyPressCB{}, f) } //====================================================================== // FocusCallbacks is a convenience struct for embedding in a widget, providing methods // to add and remove callbacks that are executed when the widget's focus widget changes. type FocusCallbacks struct { CB **Callbacks } func (w *FocusCallbacks) OnFocusChanged(f IWidgetChangedCallback) { if *w.CB == nil { *w.CB = NewCallbacks() } AddWidgetCallback(*w.CB, FocusCB{}, f) } func (w *FocusCallbacks) RemoveOnFocusChanged(f IIdentity) { RemoveWidgetCallback(*w.CB, FocusCB{}, f) } //====================================================================== // ICellProcessor is a general interface used by several gowid types for // processing a range of Cell types. For example, a canvas provides a function // to range over its contents, each cell being handed to an ICellProcessor. type ICellProcessor interface { ProcessCell(cell Cell) Cell } // CellRangeFunc is an adaptor for a simple function to implement ICellProcessor. type CellRangeFunc func(cell Cell) Cell // ProcessCell hands over processing to the adapted function. func (f CellRangeFunc) ProcessCell(cell Cell) Cell { return f(cell) } //====================================================================== // IKey represents a keypress. It's a subset of tcell.EventKey because it doesn't // capture the time of the keypress. It can be used by widgets to customize what // keypresses they respond to. type IKey interface { Rune() rune Key() tcell.Key Modifiers() tcell.ModMask } func KeysEqual(k1, k2 IKey) bool { res := true res = res && (k1.Key() == k2.Key()) if k1.Key() == tcell.KeyRune && k1.Key() == tcell.KeyRune { res = res && (k1.Modifiers() == k2.Modifiers()) res = res && (k1.Rune() == k2.Rune()) } return res } // Key is a trivial representation of a keypress, a subset of tcell.Key. Key // implements IKey. This exists as a convenience to widgets looking to // customize keypress responses. type Key struct { mod tcell.ModMask key tcell.Key ch rune } func MakeKey(ch rune) Key { return Key{ch: ch, key: tcell.KeyRune} } func MakeKeyExt(key tcell.Key) Key { return Key{key: key} } func MakeKeyExt2(mod tcell.ModMask, key tcell.Key, ch rune) Key { return Key{ mod: mod, key: key, ch: ch, } } func (k Key) Rune() rune { return k.ch } func (k Key) Key() tcell.Key { return k.key } func (k Key) Modifiers() tcell.ModMask { return k.mod } // Stolen from tcell, but omit the Rune[...] func (k Key) String() string { s := "" m := []string{} if k.mod&tcell.ModShift != 0 { m = append(m, "Shift") } if k.mod&tcell.ModAlt != 0 { m = append(m, "Alt") } if k.mod&tcell.ModMeta != 0 { m = append(m, "Meta") } if k.mod&tcell.ModCtrl != 0 { m = append(m, "Ctrl") } ok := false if s, ok = tcell.KeyNames[k.key]; !ok { if k.key == tcell.KeyRune { s = fmt.Sprintf("%c", k.ch) } else { s = fmt.Sprintf("Key[%d,%d]", k.key, int(k.ch)) } } if len(m) != 0 { if k.mod&tcell.ModCtrl != 0 && strings.HasPrefix(s, "Ctrl-") { s = s[5:] } return fmt.Sprintf("%s+%s", strings.Join(m, "+"), s) } return s } var _ IKey = Key{} var _ fmt.Stringer = Key{} //====================================================================== // ComputeVerticalSubSizeUnsafe calls ComputeVerticalSubSize but returns only // a single value - the IRenderSize. If there is an error the function will // panic. func ComputeVerticalSubSizeUnsafe(size IRenderSize, d IWidgetDimension, maxCol int, advRow int) IRenderSize { subSize, err := ComputeVerticalSubSize(size, d, maxCol, advRow) if err != nil { panic(err) } return subSize } // ComputeVerticalSubSize is used to determine the size with which a child // widget should be rendered given the parent's render size, and an // IWidgetDimension. The function will make adjustments to the size's // number of rows i.e. in the vertical dimension, and as such is used by // vpadding and pile. For example, if the parent render size is // RenderBox{C: 20, R: 5} and the IWidgetDimension argument is // RenderFlow{}, the function will return RenderFlowWith{C: 20}, i.e. it // will transform a RenderBox to a RenderFlow of the same width. Another // example is to transform a RenderBox to a shorter RenderBox if the // IWidgetDimension specifies a RenderWithUnits{} - so it allows widgets // like pile and vpadding to force widgets to be of a certain height, or // to have their height be in a certain ratio to other widgets. func ComputeVerticalSubSize(size IRenderSize, d IWidgetDimension, maxCol int, advRow int) (IRenderSize, error) { var subSize IRenderSize switch sz := size.(type) { case IRenderFixed: switch d2 := d.(type) { case IRenderFixed: subSize = RenderFixed{} case IRenderBox: subSize = RenderBox{C: d2.BoxColumns(), R: d2.BoxRows()} case IRenderFlowWith: subSize = RenderFlowWith{C: d2.FlowColumns()} case IRenderFlow: if maxCol >= 0 { subSize = RenderFlowWith{C: maxCol} } else { return nil, errors.WithStack(DimensionError{Size: size, Dim: d}) } case IRenderWithUnits: subSize = RenderFixed{} // assumes the outer widget will respect the units - in general when we call this // we don't have a way to convert this to something else with a set number of rows. If we wanted to convert // to flow, we should use RenderFlowWith{}. default: return nil, errors.WithStack(DimensionError{Size: size, Dim: d}) } case IRenderBox: switch d2 := d.(type) { case IRenderFixed: subSize = RenderFixed{} case IRenderFlowWith: subSize = RenderFlowWith{C: gwutil.Max(0, gwutil.Min(sz.BoxColumns(), d2.FlowColumns()))} case IRenderFlow: subSize = RenderFlowWith{C: sz.BoxColumns()} case IRenderWithUnits: subSize = RenderBox{C: sz.BoxColumns(), R: gwutil.Max(0, gwutil.Min(sz.BoxRows(), d2.Units()))} case IRenderRelative: subSize = RenderBox{C: sz.BoxColumns(), R: int((d2.Relative() * float64(sz.BoxRows())) + 0.5)} case IRenderWithWeight: if advRow >= 0 { subSize = RenderBox{C: sz.BoxColumns(), R: advRow} } else { return nil, errors.WithStack(DimensionError{Size: size, Dim: d, Row: advRow}) } default: return nil, errors.WithStack(DimensionError{Size: size, Dim: d}) } case IRenderFlowWith: switch d2 := d.(type) { case IRenderFixed: subSize = RenderFixed{} case IRenderFlow: subSize = RenderFlowWith{C: sz.FlowColumns()} case IRenderWithUnits: subSize = RenderBox{C: sz.FlowColumns(), R: d2.Units()} default: return nil, errors.WithStack(DimensionError{Size: size, Dim: d}) } default: return nil, errors.WithStack(DimensionError{Size: size, Dim: d}) } return subSize, nil } //====================================================================== // ComputeHorizontalSubSizeUnsafe calls ComputeHorizontalSubSize but // returns only a single value - the IRenderSize. If there is an error the // function will panic. func ComputeHorizontalSubSizeUnsafe(size IRenderSize, d IWidgetDimension) IRenderSize { subSize, err := ComputeHorizontalSubSize(size, d) if err != nil { panic(err) } return subSize } // ComputeHorizontalSubSize is used to determine the size with which a // child widget should be rendered given the parent's render size, and an // IWidgetDimension. The function will make adjustments to the size's // number of columns i.e. in the horizontal dimension, and as such is used // by hpadding and columns. For example the function can transform a // RenderBox to a narrower RenderBox if the IWidgetDimension specifies a // RenderWithUnits{} - so it allows widgets like columns and hpadding to // force widgets to be of a certain width, or to have their width be in a // certain ratio to other widgets. func ComputeHorizontalSubSize(size IRenderSize, d IWidgetDimension) (IRenderSize, error) { var subSize IRenderSize switch sz := size.(type) { case IRenderFixed: switch w2 := d.(type) { case IRenderFixed: subSize = RenderFixed{} case IRenderBox: subSize = RenderBox{C: w2.BoxColumns(), R: w2.BoxRows()} case IRenderFlowWith: subSize = RenderFlowWith{C: w2.FlowColumns()} case IRenderWithUnits: subSize = RenderFlowWith{C: w2.Units()} default: return nil, errors.WithStack(DimensionError{Size: size, Dim: d}) } case IRenderBox: switch w2 := d.(type) { case IRenderFixed: subSize = RenderFixed{} case IRenderBox: subSize = RenderBox{ C: gwutil.Max(0, gwutil.Min(sz.BoxColumns(), w2.BoxColumns())), R: gwutil.Max(0, gwutil.Min(sz.BoxRows(), w2.BoxRows())), } case IRenderFlowWith: subSize = RenderFlowWith{C: gwutil.Max(0, gwutil.Min(sz.BoxColumns(), w2.FlowColumns()))} case IRenderFlow: subSize = RenderFlowWith{C: gwutil.Max(0, gwutil.Min(sz.BoxColumns(), sz.BoxColumns()))} case IRenderRelative: subSize = RenderBox{C: int((w2.Relative() * float64(sz.BoxColumns())) + 0.5), R: sz.BoxRows()} case IRenderWithUnits: subSize = RenderBox{ C: gwutil.Max(0, gwutil.Min(sz.BoxColumns(), w2.Units())), R: sz.BoxRows(), } default: return nil, errors.WithStack(DimensionError{Size: size, Dim: d}) } case IRenderFlowWith: switch w2 := d.(type) { case IRenderFixed: subSize = RenderFixed{} case IRenderBox: subSize = RenderBox{ C: gwutil.Max(0, gwutil.Min(sz.FlowColumns(), w2.BoxColumns())), R: w2.BoxRows(), } case IRenderFlowWith: subSize = RenderFlowWith{C: gwutil.Max(0, gwutil.Min(sz.FlowColumns(), w2.FlowColumns()))} case IRenderFlow: subSize = RenderFlowWith{C: sz.FlowColumns()} case IRenderRelative: subSize = RenderFlowWith{C: int((w2.Relative() * float64(sz.FlowColumns())) + 0.5)} case IRenderWithUnits: subSize = RenderFlowWith{C: gwutil.Max(0, gwutil.Min(sz.FlowColumns(), w2.Units()))} default: return nil, errors.WithStack(DimensionError{Size: size, Dim: d}) } default: return nil, errors.WithStack(DimensionError{Size: size, Dim: d}) } return subSize, nil } func ComputeSubSizeUnsafe(size IRenderSize, w IWidgetDimension, h IWidgetDimension) IRenderSize { subSize, err := ComputeSubSize(size, w, h) if err != nil { panic(err) } return subSize } // TODO - doc func ComputeSubSize(size IRenderSize, w IWidgetDimension, h IWidgetDimension) (IRenderSize, error) { var subSize IRenderSize maxh := 1000000 if mh, ok := h.(IRenderMaxUnits); ok { maxh = mh.MaxUnits() } maxw := 1000000 if mw, ok := w.(IRenderMaxUnits); ok { maxw = mw.MaxUnits() } switch sz := size.(type) { case IRenderFixed: switch w2 := w.(type) { case IRenderFixed: subSize = RenderFixed{} case IRenderBox: subSize = RenderBox{ C: gwutil.Min(maxw, w2.BoxColumns()), R: gwutil.Min(maxh, w2.BoxRows()), } case IRenderFlowWith: subSize = RenderFlowWith{ C: gwutil.Min(maxw, w2.FlowColumns()), } case IRenderWithUnits: switch h2 := h.(type) { case IRenderWithUnits: subSize = RenderBox{ C: gwutil.Min(maxw, w2.Units()), R: gwutil.Min(maxh, h2.Units()), } default: subSize = RenderFixed{} } default: return nil, errors.WithStack(DimensionError{Size: size, Dim: w}) } case IRenderBox: switch w2 := w.(type) { case IRenderFixed: subSize = RenderFixed{} case IRenderBox: subSize = RenderBox{ C: gwutil.Max(0, gwutil.Min(maxw, sz.BoxColumns(), w2.BoxColumns())), R: gwutil.Max(0, gwutil.Min(maxh, sz.BoxRows(), w2.BoxRows())), } case IRenderFlowWith: subSize = RenderFlowWith{ C: gwutil.Max(0, gwutil.Min(maxw, sz.BoxColumns(), w2.FlowColumns())), } case IRenderFlow: subSize = RenderFlowWith{ C: gwutil.Max(0, gwutil.Min(maxw, sz.BoxColumns(), sz.BoxColumns())), } case IRenderRelative: cols := int((w2.Relative() * float64(sz.BoxColumns())) + 0.5) switch h2 := h.(type) { case IRenderRelative: rows := int((h2.Relative() * float64(sz.BoxRows())) + 0.5) subSize = RenderBox{ C: gwutil.Min(maxw, cols), R: gwutil.Min(maxh, rows), } case IRenderWithUnits: rows := h2.Units() subSize = RenderBox{ C: gwutil.Min(maxw, cols), R: gwutil.Min(maxh, sz.BoxRows(), rows), } case IRenderFlow: subSize = RenderFlowWith{ C: gwutil.Min(maxw, cols), } case IRenderFixed: subSize = RenderFlowWith{ C: gwutil.Min(maxw, cols), } default: return nil, errors.WithStack(DimensionError{Size: size, Dim: w}) } case IRenderWithUnits: cols := gwutil.Min(sz.BoxColumns(), w2.Units()) switch h := h.(type) { case IRenderRelative: rows := int((h.Relative() * float64(sz.BoxRows())) + 0.5) subSize = RenderBox{ C: gwutil.Min(maxw, cols), R: gwutil.Min(maxh, rows), } case IRenderWithUnits: rows := h.Units() subSize = RenderBox{ C: gwutil.Min(maxw, cols), R: gwutil.Max(0, gwutil.Min(maxh, sz.BoxRows(), rows)), } case IRenderFlow: subSize = RenderFlowWith{C: gwutil.Min(maxw, cols)} case IRenderFixed: subSize = RenderFlowWith{C: gwutil.Min(maxw, cols)} default: return nil, errors.WithStack(DimensionError{Size: size, Dim: w}) } default: return nil, errors.WithStack(DimensionError{Size: size, Dim: w}) } case IRenderFlowWith: switch w2 := w.(type) { case IRenderFixed: subSize = RenderFixed{} case IRenderBox: subSize = RenderBox{ C: gwutil.Min(maxw, w2.BoxColumns()), R: gwutil.Max(0, gwutil.Min(maxh, sz.FlowColumns(), w2.BoxRows())), } case IRenderFlowWith: subSize = RenderFlowWith{C: gwutil.Min(maxw, w2.FlowColumns())} case IRenderFlow: subSize = RenderFlowWith{C: gwutil.Min(maxw, sz.FlowColumns())} case IRenderRelative: subSize = RenderFlowWith{ C: gwutil.Min(maxw, int((w2.Relative()*float64(sz.FlowColumns()))+0.5)), } case IRenderWithUnits: subSize = RenderFlowWith{ C: gwutil.Max(0, gwutil.Min(maxw, sz.FlowColumns(), w2.Units())), } default: return nil, errors.WithStack(DimensionError{Size: size, Dim: w}) } default: return nil, errors.WithStack(DimensionError{Size: size, Dim: w}) } return subSize, nil } //====================================================================== // PrefPosition repeatedly unpacks composite widgets until it has to stop. It // looks for a type exports a prefered position API. The widget might be // ContainerWidget/StyledWidget/... func PrefPosition(curw interface{}) gwutil.IntOption { var res gwutil.IntOption for { if ipos, ok := curw.(IPreferedPosition); ok { res = ipos.GetPreferedPosition() break } if curw2, ok2 := curw.(IComposite); ok2 { curw = curw2.SubWidget() } else { break } } return res } func SetPrefPosition(curw interface{}, prefPos int, app IApp) bool { var res bool for { if ipos, ok := curw.(IPreferedPosition); ok { ipos.SetPreferedPosition(prefPos, app) res = true break } if curw2, ok2 := curw.(IComposite); ok2 { curw = curw2.SubWidget() } else { break } } return res } //====================================================================== type WidgetPredicate func(w IWidget) bool // FindInHierarchy starts at w, and applies the supplied predicate function; if it // returns true, w is returned. If not, then the hierarchy is descended. If w has // a child widget, then the predicate is applied to that child. If w has a set of // children with a concept of one with focus, the predicate is applied to the child // in focus. This repeats until a suitable widget is found, or the hierarchy terminates. func FindInHierarchy(w IWidget, includeMe bool, pred WidgetPredicate) IWidget { var res IWidget for { if includeMe && pred(w) { res = w break } includeMe = true if cw, ok := w.(IComposite); ok { w = cw.SubWidget() } else if cw, ok := w.(ICompositeMultipleFocus); ok { f := cw.Focus() if f < 0 { break } w = cw.SubWidgets()[cw.Focus()] } else { break } } return res } type IFocusSelectable interface { IFocus IFindNextSelectable } type ICompositeMultipleFocus interface { IFocus ICompositeMultiple } type IChangeFocus interface { ChangeFocus(dir Direction, wrap bool, app IApp) bool } // ChangeFocus is a general algorithm for applying a change of focus to a type. If the type // supports IChangeFocus, then that method is called directly. If the type supports IFocusSelectable, // then the next widget is found, and set. Otherwise, if the widget has a child or children, the // call is passed to them. func ChangeFocus(w IWidget, dir Direction, wrap bool, app IApp) bool { w = FindInHierarchy(w, true, WidgetPredicate(func(w IWidget) bool { var res bool if _, ok := w.(IChangeFocus); ok { res = true } else if _, ok := w.(IFocusSelectable); ok { res = true } return res })) var res bool if w != nil { if sw, ok := w.(IChangeFocus); ok { res = sw.ChangeFocus(dir, wrap, app) } else if sw, ok := w.(IFocusSelectable); ok { next, ok := sw.FindNextSelectable(dir, wrap) if ok { sw.SetFocus(app, next) res = true } } } return res } type IGetFocus interface { Focus() int } func Focus(w IWidget) int { w = FindInHierarchy(w, true, WidgetPredicate(func(w IWidget) bool { var res bool if _, ok := w.(IGetFocus); ok { res = true } return res })) res := -1 if w != nil { res = w.(IGetFocus).Focus() } return res } //====================================================================== // FocusPath returns a list of positions, each representing the focus // position at that level in the widget hierarchy. The returned list may // be shorter than the focus path through the hierarchy - only widgets // that have more than one option for the focus will contribute. func FocusPath(w IWidget) []interface{} { res := make([]interface{}, 0) includeMe := true for { w = FindInHierarchy(w, includeMe, WidgetPredicate(func(w IWidget) bool { _, ok := w.(IFocus) return ok })) if w == nil { break } includeMe = false wf, _ := w.(IFocus) res = append(res, wf.Focus()) } return res } type FocusPathResult struct { Succeeded bool FailedLevel int } func (f FocusPathResult) Error() string { return fmt.Sprintf("Focus at level %d could not be applied.", f.FailedLevel) } // SetFocusPath takes an array of focus positions, and applies them down the // widget hierarchy starting at the supplied widget, w. If not all positions // can be applied, the result's Succeeded field is set to false, and the // FailedLevel field provides the index in the array of paths that could not // be applied. func SetFocusPath(w IWidget, path []interface{}, app IApp) FocusPathResult { res := FocusPathResult{ Succeeded: true, } includeMe := true for i, v := range path { w = FindInHierarchy(w, includeMe, WidgetPredicate(func(w IWidget) bool { _, ok := w.(IFocus) return ok })) if w == nil { res.Succeeded = false res.FailedLevel = i break } includeMe = false wf, _ := w.(IFocus) wf.SetFocus(app, v.(int)) } return res } //====================================================================== type ICopyModeWidget interface { IComposite IIdentity IClipboard CopyModeLevels() int } // CopyModeUserInput processes copy mode events in a typical fashion - a widget that wraps one // with potentially copyable information could defer to this implementation of UserInput. func CopyModeUserInput(w ICopyModeWidget, ev interface{}, size IRenderSize, focus Selector, app IApp) bool { res := false lvls := w.CopyModeLevels() if _, ok := ev.(CopyModeEvent); ok { if app.CopyModeClaimedAt() >= app.CopyLevel() && app.CopyModeClaimedAt() < app.CopyLevel()+lvls { app.CopyModeClaimedBy(w) res = true } else { cl := app.CopyLevel() app.CopyLevel(cl + lvls) // this is how many levels hexdumper will support res = w.SubWidget().UserInput(ev, size, focus, app) app.CopyLevel(cl) if !res { app.CopyModeClaimedAt(app.CopyLevel() + lvls) app.CopyModeClaimedBy(w) } } } else if evc, ok := ev.(CopyModeClipsEvent); ok && (app.CopyModeClaimedAt() >= app.CopyLevel() && app.CopyModeClaimedAt() < app.CopyLevel()+lvls+1) { evc.Action.Collect(w.Clips(app)) res = true } else { res = w.SubWidget().UserInput(ev, size, focus, app) } return res } //====================================================================== // CopyWidgets is a trivial utility to return a copy of the array of widgets supplied. // Note that this is not a deep copy! The array is different, but the IWidgets are not. func CopyWidgets(w []IWidget) []IWidget { res := make([]IWidget, len(w)) copy(res, w) return res } //====================================================================== // Local Variables: // mode: Go // fill-column: 78 // End: gowid-1.4.0/utils.go000066400000000000000000000063741426234454000143320ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source code is governed by the MIT license // that can be found in the LICENSE file. package gowid import ( "fmt" "strings" tcell "github.com/gdamore/tcell/v2" ) //====================================================================== type Direction int const ( Forwards = Direction(1) Backwards = Direction(-1) ) //====================================================================== // Unit is a one-valued type used to send a message over a channel. type Unit struct{} //====================================================================== type InvalidTypeToCompare struct { LHS interface{} RHS interface{} } var _ error = InvalidTypeToCompare{} func (e InvalidTypeToCompare) Error() string { return fmt.Sprintf("Cannot compare RHS %v of type %T with LHS %v of type %T", e.RHS, e.RHS, e.LHS, e.LHS) } //====================================================================== type KeyValueError struct { Base error KeyVals map[string]interface{} } var _ error = KeyValueError{} var _ error = (*KeyValueError)(nil) func (e KeyValueError) Error() string { kvs := make([]string, 0, len(e.KeyVals)) for k, v := range e.KeyVals { kvs = append(kvs, fmt.Sprintf("%v: %v", k, v)) } return fmt.Sprintf("%s [%s]", e.Cause().Error(), strings.Join(kvs, ", ")) } func (e KeyValueError) Cause() error { return e.Base } func (e KeyValueError) Unwrap() error { return e.Base } func WithKVs(err error, kvs map[string]interface{}) KeyValueError { return KeyValueError{ Base: err, KeyVals: kvs, } } //====================================================================== // TranslatedMouseEvent is supplied with a tcell event and an x and y // offset - it returns a tcell mouse event that represents a horizontal and // vertical translation. func TranslatedMouseEvent(ev interface{}, x, y int) interface{} { if ev3, ok := ev.(*tcell.EventMouse); ok { x2, y2 := ev3.Position() evTr := tcell.NewEventMouse(x2+x, y2+y, ev3.Buttons(), ev3.Modifiers()) return evTr } else { return ev } } //====================================================================== func posInMap(value string, m map[string]int) int { i, ok := m[value] if ok { return i } else { return -1 } } //====================================================================== type PrettyModMask tcell.ModMask func (p PrettyModMask) String() string { mods := make([]string, 0) m := int(p) if m == int(tcell.ModNone) { mods = append(mods, "None") } else { if m&int(tcell.ModShift) != 0 { mods = append(mods, "Shift") } if m&int(tcell.ModCtrl) != 0 { mods = append(mods, "Ctrl") } if m&int(tcell.ModAlt) != 0 { mods = append(mods, "Alt") } if m&int(tcell.ModMeta) != 0 { mods = append(mods, "Meta") } } return strings.Join(mods, "|") } type PrettyTcellKey tcell.EventKey func (p *PrettyTcellKey) String() string { k := (*tcell.EventKey)(p) mod := PrettyModMask(k.Modifiers()) switch k.Key() { case tcell.KeyRune: return fmt.Sprintf("", k.Rune(), mod) default: return fmt.Sprintf("", tcell.KeyNames[k.Key()], mod) } } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: gowid-1.4.0/vim/000077500000000000000000000000001426234454000134245ustar00rootroot00000000000000gowid-1.4.0/vim/vim.go000066400000000000000000000255251426234454000145570ustar00rootroot00000000000000// Copyright 2020 Graham Clark. All rights reserved. Use of this source code is governed by the MIT license // that can be found in the LICENSE file. // Package vim provides utilities for parsing and generating vim-like // keystrokes. This is heavily tailored towards compatibility with key // events constructed by tcell, for use in terminals. package vim import ( "fmt" "regexp" "strconv" "strings" "github.com/gcla/gowid" tcell "github.com/gdamore/tcell/v2" ) //====================================================================== var ( DefaultEmacsDownKeys = []KeyPress{KeyCtrl('n')} DefaultEmacsUpKeys = []KeyPress{KeyCtrl('p')} DefaultVimDownKeys = []KeyPress{Key('j')} DefaultVimUpKeys = []KeyPress{Key('k')} DefaultDownKeys = []KeyPress{KeyPressDown} DefaultUpKeys = []KeyPress{KeyPressUp} AllDownKeys = append(DefaultDownKeys, append(DefaultEmacsDownKeys, DefaultVimDownKeys...)...) AllUpKeys = append(DefaultUpKeys, append(DefaultEmacsUpKeys, DefaultVimUpKeys...)...) DefaultEmacsLeftKeys = []KeyPress{KeyCtrl('b')} DefaultEmacsRightKeys = []KeyPress{KeyCtrl('f')} DefaultVimLeftKeys = []KeyPress{Key('h')} DefaultVimRightKeys = []KeyPress{Key('l')} DefaultLeftKeys = []KeyPress{KeyPressLeft} DefaultRightKeys = []KeyPress{KeyPressRight} AllLeftKeys = append(DefaultLeftKeys, append(DefaultEmacsLeftKeys, DefaultVimLeftKeys...)...) AllRightKeys = append(DefaultRightKeys, append(DefaultEmacsRightKeys, DefaultVimRightKeys...)...) ModMapReverse = map[string]tcell.ModMask{ "C": tcell.ModCtrl, "c": tcell.ModCtrl, "A": tcell.ModAlt, "a": tcell.ModAlt, "S": tcell.ModShift, "s": tcell.ModShift, } ModMap = map[tcell.ModMask]string{ tcell.ModCtrl: "C", tcell.ModAlt: "A", tcell.ModShift: "S", } SpecialKeyMapReverse = map[string]tcell.Key{ "": tcell.KeyUp, "": tcell.KeyDown, "": tcell.KeyLeft, "": tcell.KeyRight, "": tcell.KeyEnter, "": tcell.KeyEscape, "": tcell.KeyTab, "": tcell.KeyHome, "": tcell.KeyEnd, "": tcell.KeyPgUp, "": tcell.KeyPgDn, "": tcell.KeyF1, "": tcell.KeyF2, "": tcell.KeyF3, "": tcell.KeyF4, "": tcell.KeyF5, "": tcell.KeyF6, "": tcell.KeyF7, "": tcell.KeyF8, "": tcell.KeyF9, "": tcell.KeyF10, "": tcell.KeyF11, "": tcell.KeyF12, } SpecialKeyMap = map[tcell.Key]string{ tcell.KeyUp: "", tcell.KeyDown: "", tcell.KeyLeft: "", tcell.KeyRight: "", tcell.KeyEnter: "", tcell.KeyEscape: "", tcell.KeyTab: "", tcell.KeyHome: "", tcell.KeyEnd: "", tcell.KeyPgUp: "", tcell.KeyPgDn: "", tcell.KeyF1: "", tcell.KeyF2: "", tcell.KeyF3: "", tcell.KeyF4: "", tcell.KeyF5: "", tcell.KeyF6: "", tcell.KeyF7: "", tcell.KeyF8: "", tcell.KeyF9: "", tcell.KeyF10: "", tcell.KeyF11: "", tcell.KeyF12: "", } KeyPressUp KeyPress = NewKeyPress(tcell.KeyUp, 0, 0) KeyPressDown KeyPress = NewKeyPress(tcell.KeyDown, 0, 0) KeyPressLeft KeyPress = NewKeyPress(tcell.KeyLeft, 0, 0) KeyPressRight KeyPress = NewKeyPress(tcell.KeyRight, 0, 0) KeyPressEnter KeyPress = NewKeyPress(tcell.KeyEnter, 0, 0) KeyPressEscape KeyPress = NewKeyPress(tcell.KeyEscape, 0, 0) KeyPressTab KeyPress = NewKeyPress(tcell.KeyTab, 0, 0) KeyPressHome KeyPress = NewKeyPress(tcell.KeyTab, 0, 0) KeyPressEnd KeyPress = NewKeyPress(tcell.KeyTab, 0, 0) KeyPressPgUp KeyPress = NewKeyPress(tcell.KeyPgUp, 0, 0) KeyPressPgDn KeyPress = NewKeyPress(tcell.KeyPgDn, 0, 0) KeyPressF1 KeyPress = NewKeyPress(tcell.KeyF1, 0, 0) KeyPressF2 KeyPress = NewKeyPress(tcell.KeyF2, 0, 0) KeyPressF3 KeyPress = NewKeyPress(tcell.KeyF3, 0, 0) KeyPressF4 KeyPress = NewKeyPress(tcell.KeyF4, 0, 0) KeyPressF5 KeyPress = NewKeyPress(tcell.KeyF5, 0, 0) KeyPressF6 KeyPress = NewKeyPress(tcell.KeyF6, 0, 0) KeyPressF7 KeyPress = NewKeyPress(tcell.KeyF7, 0, 0) KeyPressF8 KeyPress = NewKeyPress(tcell.KeyF8, 0, 0) KeyPressF9 KeyPress = NewKeyPress(tcell.KeyF9, 0, 0) KeyPressF10 KeyPress = NewKeyPress(tcell.KeyF10, 0, 0) KeyPressF11 KeyPress = NewKeyPress(tcell.KeyF11, 0, 0) KeyPressF12 KeyPress = NewKeyPress(tcell.KeyF12, 0, 0) KeyPressF = []KeyPress{ KeyPressF1, KeyPressF2, KeyPressF3, KeyPressF4, KeyPressF5, KeyPressF6, KeyPressF7, KeyPressF8, KeyPressF9, KeyPressF10, KeyPressF11, KeyPressF12, } ) var keyExp *regexp.Regexp func init() { // This crazy-looking regexp will parse correctly formatted vim keys. See vim_test.go for examples. The groups // allow easy extraction of the key pieces of syntax. keyExp = regexp.MustCompile(`(<(?P[CSAcsa])-((?P[A-Za-z0-9!@#$%^&*()\[\]\/\-_+=~"':;<>,.?|` + "`" + `])|(?P(?i)space(?-i)))>|(?P[A-Za-z0-9!@#$%^&*()\[\]\/\-_+=~"':;>,.?|` + "`" + `])|<(?P(?i)Up(?-i))>|<(?P(?i)Down(?-i))>|<(?P(?i)Left(?-i))>|<(?P(?i)Right(?-i))>|<(?P(?i)Esc(?-i))>|<(?P(?i)CR(?-i))>|<(?P(?i)Return(?-i))>|<(?P(?i)Enter(?-i))>|<(?P(?i)Space(?-i))>|<(?P(?i)lt(?-i))>|<(?P(?i)BS(?-i))>|<(?P(?i)Tab(?-i))>|<(?P(?i)Home(?-i))>|<(?P(?i)End(?-i))>|<(?P(?i)PgUp(?-i))>|<(?P(?i)PgDn(?-i))>|<([fF])(?P[1-9]|(1[0-2]))>)`) } // KeyPress represents a gowid keypress. It's a tcell.EventKey without the time // of the keypress. type KeyPress gowid.Key func KeyCtrl(r rune) KeyPress { return KeyPress(gowid.MakeKeyExt2(tcell.ModCtrl, tcell.KeyRune, r)) } // KeyPressFromTcell converts a *tcell.EventKey to a KeyPress. This can then be // serialized to a vim-style keypress e.g. func KeyPressFromTcell(k *tcell.EventKey) KeyPress { mod := k.Modifiers() tk := k.Key() ch := k.Rune() if tk >= tcell.KeyCtrlA && tk <= tcell.KeyCtrlZ { ch = rune(int(tk) + int('a') - 1) tk = tcell.KeyRune } else { switch tk { case tcell.KeyCtrlSpace: ch = ' ' tk = tcell.KeyRune case tcell.KeyCtrlLeftSq: ch = '[' tk = tcell.KeyRune case tcell.KeyCtrlRightSq: ch = ']' tk = tcell.KeyRune case tcell.KeyCtrlCarat: ch = '^' tk = tcell.KeyRune case tcell.KeyCtrlUnderscore: ch = '_' tk = tcell.KeyRune case tcell.KeyCtrlBackslash: ch = '\\' tk = tcell.KeyRune } } return KeyPress(gowid.MakeKeyExt2(mod, tk, ch)) } func NewSimpleKeyPress(ch rune) KeyPress { return NewKeyPress(tcell.KeyRune, ch, 0) } func Key(ch rune) KeyPress { return NewKeyPress(tcell.KeyRune, ch, 0) } func NewKeyPress(k tcell.Key, ch rune, mod tcell.ModMask) KeyPress { if k == tcell.KeyRune && (ch < ' ' || ch == 0x7f) { // Turn specials into proper key codes. This is for // control characters and the DEL. k = tcell.Key(ch) if mod == tcell.ModNone && ch < ' ' { switch tcell.Key(ch) { case tcell.KeyBackspace, tcell.KeyTab, tcell.KeyEsc, tcell.KeyEnter: // these keys are directly typeable without CTRL default: // most likely entered with a CTRL keypress mod = tcell.ModCtrl } } } return KeyPress(gowid.MakeKeyExt2(mod, k, ch)) } func (k KeyPress) String() string { gk := gowid.Key(k) if gk.Key() == tcell.KeyRune { if mod, ok := ModMap[gk.Modifiers()]; ok { if gk.Rune() == ' ' { return fmt.Sprintf("<%s-space>", mod) } else { return fmt.Sprintf("<%s-%c>", mod, gk.Rune()) } } else { if gk.Rune() == '<' { return "" } else if gk.Rune() == ' ' { return "" } else { return string(gk.Rune()) } } } else if str, ok := SpecialKeyMap[gk.Key()]; ok { return str } else { return "" } } // KeySequence is an array of KeyPress. The KeySequence type allows // the sequence to be serialized the way vim would do it e.g. abc type KeySequence []KeyPress func (ks KeySequence) String() string { var res string for _, kp := range ks { res = res + kp.String() } return res } // VimStringToKeys converts e.g. abc into a sequence of KeyPress func VimStringToKeys(input string) KeySequence { matches := keyExp.FindAllStringSubmatch(input, -1) results := make([]map[string]string, len(matches)) for j, _ := range matches { results[j] = make(map[string]string) for i, name := range keyExp.SubexpNames() { if i != 0 && name != "" { results[j][name] = matches[j][i] } } } res := make(KeySequence, 0) for _, result := range results { if str, ok := result["up"]; ok && str != "" { res = append(res, KeyPressUp) } else if str, ok := result["down"]; ok && str != "" { res = append(res, KeyPressDown) } else if str, ok := result["left"]; ok && str != "" { res = append(res, KeyPressLeft) } else if str, ok := result["right"]; ok && str != "" { res = append(res, KeyPressRight) } else if str, ok := result["cr"]; ok && str != "" { res = append(res, KeyPressEnter) } else if str, ok := result["return"]; ok && str != "" { res = append(res, KeyPressEnter) } else if str, ok := result["enter"]; ok && str != "" { res = append(res, KeyPressEnter) } else if str, ok := result["esc"]; ok && str != "" { res = append(res, KeyPressEscape) } else if str, ok := result["tab"]; ok && str != "" { res = append(res, KeyPressTab) } else if str, ok := result["home"]; ok && str != "" { res = append(res, KeyPressHome) } else if str, ok := result["end"]; ok && str != "" { res = append(res, KeyPressEnd) } else if str, ok := result["pgup"]; ok && str != "" { res = append(res, KeyPressPgUp) } else if str, ok := result["pgdn"]; ok && str != "" { res = append(res, KeyPressPgDn) } else if str, ok := result["f"]; ok && str != "" { i, _ := strconv.Atoi(str) res = append(res, KeyPressF[i-1]) } else if str, ok := result["lt"]; ok && str != "" { res = append(res, NewSimpleKeyPress('<')) } else if str, ok := result["space"]; ok && str != "" { res = append(res, NewSimpleKeyPress(' ')) } else if str, ok := result["char"]; ok && str != "" { res = append(res, NewSimpleKeyPress(rune(str[0]))) } else if str, ok := result["modchar"]; ok && str != "" { // regexp guarantees ModMask lookup is safe res = append(res, NewKeyPress(tcell.KeyRune, rune(str[0]), ModMapReverse[result["mod"]])) } else if str, ok := result["modspecial"]; ok && str != "" { // regexp guarantees ModMask lookup is safe switch strings.ToLower(str) { case "space": res = append(res, NewKeyPress(tcell.KeyRune, ' ', ModMapReverse[result["mod"]])) } } } return res } func KeyIn(k *tcell.EventKey, keys []KeyPress) bool { kp := KeyPressFromTcell(k) for i, _ := range keys { if kp == keys[i] { return true } } return false } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: gowid-1.4.0/vim/vim_test.go000066400000000000000000000030651426234454000156110ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. package vim import ( "fmt" "strings" "testing" tcell "github.com/gdamore/tcell/v2" "github.com/stretchr/testify/assert" ) type keytest struct { str string key KeyPress } func TestVim1(t *testing.T) { for _, kt := range []keytest{ {"", KeyPressUp}, {"", KeyPressDown}, {"Z", NewSimpleKeyPress('Z')}, {"z", NewSimpleKeyPress('z')}, {"", NewKeyPress(tcell.KeyRune, 'f', tcell.ModCtrl)}, {"", NewKeyPress(tcell.KeyRune, '|', tcell.ModAlt)}, {"", NewKeyPress(tcell.KeyRune, '"', tcell.ModShift)}, {"", NewKeyPress(tcell.KeyRune, '`', tcell.ModShift)}, {"`", NewSimpleKeyPress('`')}, {"", NewSimpleKeyPress(' ')}, {"", KeyPressEscape}, {"", KeyPressRight}, {"", KeyPressPgDn}, {"", KeyPressEscape}, {"", KeyPressF4}, {"", KeyPressF12}, {"", NewKeyPress(tcell.KeyRune, '<', 0)}, } { res := VimStringToKeys(kt.str) assert.Equal(t, 1, len(res)) assert.Equal(t, kt.key, res[0]) str := fmt.Sprintf("%v", kt.key) assert.Equal(t, kt.str, str) if strings.Contains(kt.str, "<") { res = VimStringToKeys(strings.ToLower(kt.str)) assert.Equal(t, 1, len(res)) assert.Equal(t, kt.key, res[0]) str = fmt.Sprintf("%v", kt.key) assert.Equal(t, kt.str, str) } } } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: gowid-1.4.0/widgets/000077500000000000000000000000001426234454000142775ustar00rootroot00000000000000gowid-1.4.0/widgets/asciigraph/000077500000000000000000000000001426234454000164115ustar00rootroot00000000000000gowid-1.4.0/widgets/asciigraph/asciigraph.go000066400000000000000000000052641426234454000210610ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. // Package asciigraph provides a simple plotting widget. package asciigraph import ( "strings" "unicode/utf8" "github.com/gcla/gowid" "github.com/gcla/gowid/gwutil" "github.com/gcla/gowid/widgets/fill" "github.com/guptarohit/asciigraph" ) //====================================================================== type IAsciiGraph interface { GetData() []float64 GetConf() []asciigraph.Option } type IWidget interface { gowid.IWidget IAsciiGraph } type Widget struct { Data []float64 Conf []asciigraph.Option gowid.RejectUserInput gowid.NotSelectable } func New(series []float64, config []asciigraph.Option) *Widget { res := &Widget{} res.Data = series res.Conf = config var _ IWidget = res return res } func (w *Widget) String() string { return "asciigraph" } func (w *Widget) GetData() []float64 { return w.Data } func (w *Widget) SetData(data []float64, app gowid.IApp) { w.Data = data } func (w *Widget) GetConf() []asciigraph.Option { return w.Conf } func (w *Widget) SetConf(conf []asciigraph.Option, app gowid.IApp) { w.Conf = conf } func (w *Widget) RenderSize(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.IRenderBox { return RenderSize(w, size, focus, app) } func (w *Widget) Render(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.ICanvas { return Render(w, size, focus, app) } //'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' // TODO: ILineWidget? func RenderSize(w IWidget, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.IRenderBox { return gowid.CalculateRenderSizeFallback(w, size, focus, app) } func Render(w IAsciiGraph, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.ICanvas { grender := strings.Split(asciigraph.Plot(w.GetData(), w.GetConf()...), "\n") rows := len(grender) cols := 0 if rows > 0 { cols = utf8.RuneCountInString(grender[0]) } switch sz := size.(type) { case gowid.IRenderBox: cols = sz.BoxColumns() rows = sz.BoxRows() case gowid.IRenderFlowWith: panic(gowid.WidgetSizeError{Widget: w, Size: size}) } blank := fill.NewEmpty() res := blank.Render(gowid.RenderBox{C: cols, R: rows}, gowid.NotSelected, app) for y := 0; y < gwutil.Min(len(grender), res.BoxRows()); y++ { x := 0 for _, r := range grender[y] { if x >= res.BoxColumns() { break } res.SetCellAt(x, y, res.CellAt(x, y).WithRune(r)) x++ } } return res } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: gowid-1.4.0/widgets/asciigraph/asciigraph_test.go000066400000000000000000000026631426234454000221200ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. package asciigraph import ( "testing" "github.com/gcla/gowid" "github.com/gcla/gowid/gwtest" asc "github.com/guptarohit/asciigraph" "github.com/stretchr/testify/assert" ) func TestSolidAsciigraph1(t *testing.T) { // Stolen from asciigraph_test.go data := []float64{2, 1, 1, 2, -2, 5, 7, 11, 3, 7, 1} conf := []asc.Option{} w := New(data, conf) assert.Panics(t, func() { w.Render(gowid.RenderFlowWith{C: 10}, gowid.Focused, gwtest.D) }) c1 := w.Render(gowid.RenderBox{C: 19, R: 14}, gowid.Focused, gwtest.D) res := ` 11.00 ┤ ╭╮ 10.00 ┤ ││ 9.00 ┼ ││ 8.00 ┤ ││ 7.00 ┤ ╭╯│╭╮ 6.00 ┤ │ │││ 5.00 ┤ ╭╯ │││ 4.00 ┤ │ │││ 3.00 ┤ │ ╰╯│ 2.00 ┼╮ ╭╮│ │ 1.00 ┤╰─╯││ ╰ 0.00 ┤ ││ -1.00 ┤ ││ -2.00 ┤ ╰╯ ` t.Logf("Canvas is\n%v\n", c1.String()) assert.Equal(t, c1.String(), res) c1 = w.Render(gowid.RenderFixed{}, gowid.Focused, gwtest.D) assert.Equal(t, c1.String(), res) gwtest.RenderBoxManyTimes(t, w, 0, 20, 0, 20) } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: gowid-1.4.0/widgets/bargraph/000077500000000000000000000000001426234454000160655ustar00rootroot00000000000000gowid-1.4.0/widgets/bargraph/bargraph.go000066400000000000000000000071631426234454000202110ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. // Package bargraph provides a simple plotting widget. package bargraph import ( "github.com/gcla/gowid" "github.com/gcla/gowid/widgets/columns" "github.com/gcla/gowid/widgets/fill" "github.com/gcla/gowid/widgets/overlay" "github.com/gcla/gowid/widgets/pile" ) //====================================================================== type IBarGraph interface { GetData() [][]int GetAttrs() []gowid.IColor GetMax() int } type IWidget interface { gowid.IWidget IBarGraph } type Widget struct { Data [][]int Max int Attrs []gowid.IColor gowid.RejectUserInput gowid.NotSelectable } func New(atts []gowid.IColor) *Widget { res := &Widget{} res.Data = make([][]int, 0) res.Max = 0 res.Attrs = atts var _ gowid.IWidget = res return res } func (w *Widget) String() string { return "bargraph" } func (w *Widget) GetData() [][]int { return w.Data } func (w *Widget) SetData(l [][]int, max int, app gowid.IApp) { w.Data = l w.Max = max } func (w *Widget) GetAttrs() []gowid.IColor { return w.Attrs } func (w *Widget) GetMax() int { return w.Max } func (w *Widget) RenderSize(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.IRenderBox { return RenderSize(w, size, focus, app) } func (w *Widget) Render(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.ICanvas { return Render(w, size, focus, app) } //'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' func RenderSize(w gowid.IWidget, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.IRenderBox { return gowid.CalculateRenderSizeFallback(w, size, focus, app) } func Render(w IBarGraph, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.ICanvas { _, ok := size.(gowid.IRenderBox) if !ok { panic(gowid.WidgetSizeError{Widget: w, Size: size, Required: "gowid.IRenderBox"}) } weight1 := gowid.RenderWithWeight{1} bgTCellColor := gowid.IColorToTCell(w.GetAttrs()[0], gowid.ColorDefault, app.GetColorMode()) // TODO - check case when data is empty dataIdxLimit := 0 if len(w.GetData()) > 0 { dataIdxLimit = len(w.GetData()[0]) } dataWidgets := make([]*columns.Widget, dataIdxLimit) for dataIdx := 0; dataIdx < dataIdxLimit; dataIdx++ { cols := make([]gowid.IContainerWidget, len(w.GetData())) for i, d := range w.GetData() { datum := d[dataIdx] dataTCellColor := gowid.IColorToTCell(w.GetAttrs()[(i%(len(w.GetAttrs())-1))+1], gowid.ColorDefault, app.GetColorMode()) bar := pile.New([]gowid.IContainerWidget{ &gowid.ContainerWidget{ fill.NewSolidFromCell( gowid.Cell{}, ), gowid.RenderWithWeight{w.GetMax() - datum}, }, // Use fg as background because I'm using spaces &gowid.ContainerWidget{ fill.NewSolidFromCell( gowid.MakeCell( ' ', gowid.ColorNone, dataTCellColor, gowid.StyleNone), ), gowid.RenderWithWeight{datum}}, }) cols[i] = &gowid.ContainerWidget{bar, weight1} } dataWidgets[dataIdx] = columns.New(cols) } var res gowid.IWidget = fill.NewSolidFromCell( gowid.MakeCell( ' ', gowid.ColorNone, bgTCellColor, gowid.StyleNone), ) for _, dataWidget := range dataWidgets { res = overlay.New( dataWidget, res, gowid.VAlignMiddle{}, gowid.RenderWithRatio{R: 1.0}, gowid.HAlignMiddle{}, gowid.RenderWithRatio{R: 1.0}, ) } return res.Render(size, focus, app) } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: gowid-1.4.0/widgets/boxadapter/000077500000000000000000000000001426234454000164305ustar00rootroot00000000000000gowid-1.4.0/widgets/boxadapter/boxadapter.go000066400000000000000000000074031426234454000211140ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source code // is governed by the MIT license that can be found in the LICENSE file. // Package boxadapter provides a widget that will allow a box widget to be used // in a flow context. Based on urwid's BoxAdapter - http://urwid.org/reference/widget.html#boxadapter. package boxadapter import ( "fmt" "github.com/gcla/gowid" tcell "github.com/gdamore/tcell/v2" ) //====================================================================== type Widget struct { gowid.IWidget rows int *gowid.Callbacks gowid.SubWidgetCallbacks } type IBoxAdapter interface { Rows() int } type IBoxAdapterWidget interface { gowid.ICompositeWidget IBoxAdapter } func New(inner gowid.IWidget, rows int) *Widget { res := &Widget{ IWidget: inner, rows: rows, } res.SubWidgetCallbacks = gowid.SubWidgetCallbacks{CB: &res.Callbacks} var _ gowid.IWidget = res var _ gowid.IComposite = res var _ IBoxAdapter = res return res } func (w *Widget) String() string { return fmt.Sprintf("boxadapter[%v]", w.SubWidget()) } func (w *Widget) SubWidget() gowid.IWidget { return w.IWidget } func (w *Widget) SetSubWidget(wi gowid.IWidget, app gowid.IApp) { w.IWidget = wi gowid.RunWidgetCallbacks(w, gowid.SubWidgetCB{}, app, w) } func (w *Widget) Rows() int { return w.rows } func (w *Widget) SetRows(rows int, app gowid.IApp) { w.rows = rows } func (w *Widget) RenderSize(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.IRenderBox { return RenderSize(w, size, focus, app) } func (w *Widget) Render(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.ICanvas { return Render(w, size, focus, app) } func (w *Widget) SubWidgetSize(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.IRenderSize { return SubWidgetSize(w, size, focus, app) } func (w *Widget) UserInput(ev interface{}, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) bool { return UserInput(w, ev, size, focus, app) } //====================================================================== // SubWidgetSize is the same as RenderSize for this widget - the inner widget will // be rendered as a box with the specified number of columns and the widget's // set number of rows. func SubWidgetSize(w IBoxAdapter, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.IRenderSize { return RenderSize(w, size, focus, app) } func RenderSize(w IBoxAdapter, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.IRenderBox { switch sz := size.(type) { case gowid.IRenderFlowWith: return gowid.RenderBox{ C: sz.FlowColumns(), R: w.Rows(), } default: panic(gowid.WidgetSizeError{Widget: w, Size: size, Required: "gowid.IRenderFlow"}) } } func Render(w IBoxAdapterWidget, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.ICanvas { rsize := RenderSize(w, size, focus, app) res := w.SubWidget().Render(rsize, focus, app) return res } // Ensure that a valid mouse interaction with a flow widget will result in a // mouse interaction with the subwidget func UserInput(w IBoxAdapterWidget, ev interface{}, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) bool { if _, ok := size.(gowid.IRenderFlowWith); !ok { panic(gowid.WidgetSizeError{Widget: w, Size: size, Required: "gowid.IRenderFlow"}) } box := RenderSize(w, size, focus, app) if evm, ok := ev.(*tcell.EventMouse); ok { _, my := evm.Position() if my < box.BoxRows() && my >= 0 { return gowid.UserInputIfSelectable(w.SubWidget(), ev, box, focus, app) } } else { return gowid.UserInputIfSelectable(w.SubWidget(), ev, box, focus, app) } return false } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: gowid-1.4.0/widgets/boxadapter/boxadapter_test.go000066400000000000000000000070461426234454000221560ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source code is governed by the MIT license // that can be found in the LICENSE file. package boxadapter import ( "strings" "testing" "github.com/gcla/gowid" "github.com/gcla/gowid/gwtest" "github.com/gcla/gowid/widgets/edit" "github.com/gcla/gowid/widgets/list" tcell "github.com/gdamore/tcell/v2" "github.com/stretchr/testify/assert" ) func TestBoxadapter1(t *testing.T) { w := edit.New(edit.Options{Caption: "", Text: "aaaaaaaaaaaaaaaaaaaa"}) w2 := edit.New(edit.Options{Caption: "", Text: "bbbbbbbbbbbbbbbbbbbb"}) c1 := w.Render(gowid.RenderFixed{}, gowid.Focused, gwtest.D) assert.Equal(t, "aaaaaaaaaaaaaaaaaaaa", c1.String()) c1 = w.Render(gowid.RenderFlowWith{C: 6}, gowid.Focused, gwtest.D) assert.Equal(t, "aaaaaa\naaaaaa\naaaaaa\naa ", c1.String()) walker := list.NewSimpleListWalker([]gowid.IWidget{w, w2}) lb := list.New(walker) c1 = lb.Render(gowid.RenderFlowWith{C: 6}, gowid.Focused, gwtest.D) assert.Equal(t, "aaaaaa\naaaaaa\naaaaaa\naa \nbbbbbb\nbbbbbb\nbbbbbb\nbb ", c1.String()) evx := tcell.NewEventKey(tcell.KeyRune, 'x', tcell.ModNone) evlmx1y0 := tcell.NewEventMouse(1, 0, tcell.Button1, 0) evlmx1y4 := tcell.NewEventMouse(1, 4, tcell.Button1, 0) evlmx2y2 := tcell.NewEventMouse(2, 2, tcell.Button1, 0) evnonex1y0 := tcell.NewEventMouse(1, 0, tcell.ButtonNone, 0) evnonex1y4 := tcell.NewEventMouse(1, 4, tcell.ButtonNone, 0) evnonex2y2 := tcell.NewEventMouse(2, 2, tcell.ButtonNone, 0) w.UserInput(evlmx1y0, gowid.RenderFlowWith{C: 21}, gowid.Focused, gwtest.D) w.UserInput(evnonex1y0, gowid.RenderFlowWith{C: 21}, gowid.Focused, gwtest.D) w.UserInput(evx, gowid.RenderFlowWith{C: 21}, gowid.Focused, gwtest.D) c1 = w.Render(gowid.RenderFlowWith{C: 21}, gowid.Focused, gwtest.D) assert.Equal(t, "axaaaaaaaaaaaaaaaaaaa", c1.String()) sz := gowid.RenderFlowWith{C: 6} c1 = lb.Render(sz, gowid.Focused, gwtest.D) assert.Equal(t, strings.Join([]string{ "axaaaa", "aaaaaa", "aaaaaa", "aaa ", "bbbbbb", "bbbbbb", "bbbbbb", "bb ", }, "\n"), c1.String()) lb.UserInput(evlmx1y4, sz, gowid.Focused, gwtest.D) gwtest.D.SetLastMouseState(gowid.MouseState{true, false, false}) lb.UserInput(evnonex1y4, sz, gowid.Focused, gwtest.D) gwtest.D.SetLastMouseState(gowid.MouseState{false, false, false}) lb.UserInput(evx, sz, gowid.Focused, gwtest.D) c1 = lb.Render(sz, gowid.Focused, gwtest.D) assert.Equal(t, strings.Join([]string{ "axaaaa", "aaaaaa", "aaaaaa", "aaa ", "bxbbbb", "bbbbbb", "bbbbbb", "bbb ", }, "\n"), c1.String()) bw := New(w, 2) bw2 := New(w2, 2) cw := bw.Render(sz, gowid.Focused, gwtest.D) assert.Equal(t, strings.Join([]string{ "axaaaa", "aaaaaa", }, "\n"), cw.String()) walker2 := list.NewSimpleListWalker([]gowid.IWidget{bw, bw2}) lb2 := list.New(walker2) c2 := lb2.Render(sz, gowid.Focused, gwtest.D) assert.Equal(t, strings.Join([]string{ "axaaaa", "aaaaaa", "bxbbbb", "bbbbbb", }, "\n"), c2.String()) lb2.UserInput(evlmx2y2, sz, gowid.Focused, gwtest.D) gwtest.D.SetLastMouseState(gowid.MouseState{true, false, false}) lb2.UserInput(evnonex2y2, sz, gowid.Focused, gwtest.D) gwtest.D.SetLastMouseState(gowid.MouseState{false, false, false}) lb2.UserInput(evx, sz, gowid.Focused, gwtest.D) c2 = lb2.Render(sz, gowid.Focused, gwtest.D) assert.Equal(t, strings.Join([]string{ "axaaaa", "aaaaaa", "bxxbbb", "bbbbbb", }, "\n"), c2.String()) } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: gowid-1.4.0/widgets/button/000077500000000000000000000000001426234454000156125ustar00rootroot00000000000000gowid-1.4.0/widgets/button/button.go000066400000000000000000000164371426234454000174670ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. // Package button provides a clickable widget which can be decorated. package button import ( "fmt" "github.com/gcla/gowid" "github.com/gcla/gowid/gwutil" tcell "github.com/gdamore/tcell/v2" ) //====================================================================== // IDecoratedAround is the interface for any type that provides // "decoration" on its left and right side e.g. for buttons, something like // "<" and ">". type IDecoratedAround interface { LeftDec() string RightDec() string } // IDecoratedMiddle is implemented by any type that provides "decoration" // in the middle of its render, such as a 'x' or a '-' symbol on a checked // button. type IDecoratedMiddle interface { MiddleDec() string } //====================================================================== // Decoration is a simple struct that implements IDecoratedAround. type Decoration struct { Left string Right string } func (b *Decoration) LeftDec() string { return b.Left } func (b *Decoration) RightDec() string { return b.Right } func (w *Decoration) SetLeftDec(dec string, app gowid.IApp) { w.Left = dec } func (w *Decoration) SetRightDec(dec string, app gowid.IApp) { w.Right = dec } var ( BareDecoration = Decoration{Left: "", Right: ""} NormalDecoration = Decoration{Left: "<", Right: ">"} AltDecoration = Decoration{Left: "[", Right: "]"} ) //====================================================================== type ICustomKeys interface { CustomSelectKeys() bool SelectKeys() []gowid.IKey // can't be nil } // IWidget is implemented by any widget that contains exactly one // exposed subwidget (ICompositeWidget) and that is decorated on its left // and right (IDecoratedAround). type IWidget interface { gowid.ICompositeWidget IDecoratedAround } type Options struct { Decoration SelectKeysProvided bool SelectKeys []gowid.IKey } type Widget struct { inner gowid.IWidget opts Options *gowid.Callbacks gowid.SubWidgetCallbacks gowid.ClickCallbacks *Decoration gowid.AddressProvidesID gowid.IsSelectable } func New(inner gowid.IWidget, opts ...Options) *Widget { var opt Options if len(opts) > 0 { opt = opts[0] } else { // Make the default have visible decorators, if none are provided explicitly. opt.Decoration = NormalDecoration } res := &Widget{ inner: inner, opts: opt, } res.SubWidgetCallbacks = gowid.SubWidgetCallbacks{CB: &res.Callbacks} res.ClickCallbacks = gowid.ClickCallbacks{CB: &res.Callbacks} res.Decoration = &res.opts.Decoration var _ gowid.IWidget = res var _ gowid.ICompositeWidget = res var _ IWidget = res var _ ICustomKeys = res return res } func NewAlt(inner gowid.IWidget) *Widget { return New(inner, Options{ Decoration: AltDecoration, }) } func NewBare(inner gowid.IWidget) *Widget { return New(inner, Options{ Decoration: BareDecoration, }) } func NewDecorated(inner gowid.IWidget, decoration Decoration) *Widget { return New(inner, Options{ Decoration: decoration, }) } func (w *Widget) String() string { return fmt.Sprintf("button[%v]", w.SubWidget()) } func (w *Widget) Click(app gowid.IApp) { // No button clicked means a key was pressed if app.GetMouseState().NoButtonClicked() || app.GetMouseState().LeftIsClicked() { gowid.RunWidgetCallbacks(w.Callbacks, gowid.ClickCB{}, app, w) } } func (w *Widget) SubWidget() gowid.IWidget { return w.inner } func (w *Widget) SetSubWidget(wi gowid.IWidget, app gowid.IApp) { w.inner = wi gowid.RunWidgetCallbacks(w.Callbacks, gowid.SubWidgetCB{}, app, w) } func (w *Widget) SetLeftDec(dec string, app gowid.IApp) { w.Decoration.Left = dec } func (w *Widget) SetRightDec(dec string, app gowid.IApp) { w.Decoration.Right = dec } func (w *Widget) SubWidgetSize(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.IRenderSize { return SubWidgetSize(w, size, focus, app) } func (w *Widget) RenderSize(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.IRenderBox { return RenderSize(w, size, focus, app) } func (w *Widget) Render(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.ICanvas { return Render(w, size, focus, app) } func (w *Widget) UserInput(ev interface{}, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) bool { return UserInput(w, ev, size, focus, app) } func (w *Widget) CustomSelectKeys() bool { return w.opts.SelectKeysProvided } func (w *Widget) SelectKeys() []gowid.IKey { return w.opts.SelectKeys } //====================================================================== func SubWidgetSize(w IWidget, size interface{}, focus gowid.Selector, app gowid.IApp) gowid.IRenderSize { cols, haveCols := size.(gowid.IColumns) rows, haveRows := size.(gowid.IRows) switch { case haveCols && haveRows: return gowid.RenderBox{C: gwutil.Max(0, cols.Columns()-(len(w.LeftDec())+len(w.RightDec()))), R: rows.Rows()} case haveCols: return gowid.RenderFlowWith{C: gwutil.Max(0, cols.Columns()-(len(w.LeftDec())+len(w.RightDec())))} default: return gowid.RenderFixed{} } } func RenderSize(w IWidget, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.IRenderBox { innerSize := w.SubWidgetSize(size, focus, app) innerRendered := w.SubWidget().RenderSize(innerSize, focus, app) boxHeight := innerRendered.BoxRows() boxWidth := innerRendered.BoxColumns() + len(w.LeftDec()) + len(w.RightDec()) if bsz, ok := size.(gowid.IColumns); ok { if bsz.Columns() < boxWidth { boxWidth = bsz.Columns() } } return gowid.RenderBox{boxWidth, boxHeight} } type IClickableIdentityWidget interface { gowid.IClickableWidget gowid.IIdentity } func UserInput(w IClickableIdentityWidget, ev interface{}, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) bool { res := false switch ev := ev.(type) { case *tcell.EventMouse: switch ev.Buttons() { case tcell.Button1, tcell.Button2, tcell.Button3: app.SetClickTarget(ev.Buttons(), w) res = true case tcell.ButtonNone: if !app.GetLastMouseState().NoButtonClicked() { clickit := false app.ClickTarget(func(k tcell.ButtonMask, v gowid.IIdentityWidget) { if v != nil && v.ID() == w.ID() { clickit = true } }) if clickit { w.Click(app) res = true } } } case *tcell.EventKey: if wk, ok := w.(ICustomKeys); ok && wk.CustomSelectKeys() { for _, k := range wk.SelectKeys() { if gowid.KeysEqual(k, ev) { w.Click(app) res = true break } } } else { if ev.Key() == tcell.KeyEnter || (ev.Key() == tcell.KeyRune && ev.Rune() == ' ') { w.Click(app) res = true } } default: if wc, ok := w.(gowid.IComposite); ok { res = wc.SubWidget().UserInput(ev, size, focus, app) } } return res } func Render(w IWidget, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.ICanvas { newSize := w.SubWidgetSize(size, focus, app) res := w.SubWidget().Render(newSize, focus, app) leftClicker := gowid.CellsFromString(w.LeftDec()) rightClicker := gowid.CellsFromString(w.RightDec()) res.ExtendLeft(leftClicker) res.ExtendRight(rightClicker) gowid.MakeCanvasRightSize(res, size) return res } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: gowid-1.4.0/widgets/button/button_test.go000066400000000000000000000046631426234454000205240ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source code is governed by the MIT license // that can be found in the LICENSE file. package button import ( "strings" "testing" "github.com/gcla/gowid" "github.com/gcla/gowid/gwtest" "github.com/gcla/gowid/widgets/text" log "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" ) //====================================================================== func TestButton1(t *testing.T) { tw := text.New("click") w := New(tw) ct := &gwtest.ButtonTester{Gotit: false} assert.Equal(t, ct.Gotit, false) w.OnClick(ct) c1 := w.Render(gowid.RenderFixed{}, gowid.Focused, gwtest.D) assert.Equal(t, c1.String(), "") w.Click(gwtest.D) assert.Equal(t, ct.Gotit, true) ct.Gotit = false assert.Equal(t, ct.Gotit, false) w.RemoveOnClick(ct) w.Click(gwtest.D) assert.Equal(t, ct.Gotit, false) gwtest.RenderBoxManyTimes(t, w, 0, 10, 0, 10) gwtest.RenderFlowManyTimes(t, w, 0, 20) } func TestButton2(t *testing.T) { w1a := text.New("1.2") w1 := NewBare(w1a) c1 := w1.Render(gowid.RenderFlowWith{C: 3}, gowid.NotSelected, gwtest.D) assert.Equal(t, strings.Join([]string{"1.2"}, "\n"), c1.String()) } func TestCanvas13(t *testing.T) { widget1a := text.New("hello world") widget1 := New(widget1a) canvas1 := widget1.Render(gowid.RenderFlowWith{C: 13}, gowid.NotSelected, gwtest.D) log.Infof("Widget13 is %v", widget1) log.Infof("Canvas13 is %s", canvas1.String()) res := strings.Join([]string{""}, "\n") if res != canvas1.String() { t.Errorf("Failed") } } func TestCanvas14(t *testing.T) { widget1a := text.New("hello world") widget1 := New(widget1a) canvas1 := widget1.Render(gowid.RenderBox{C: 13, R: 1}, gowid.NotSelected, gwtest.D) log.Infof("Widget14 is %v", widget1) log.Infof("Canvas14 is %s", canvas1.String()) res := strings.Join([]string{""}, "\n") if res != canvas1.String() { t.Errorf("Failed") } } func TestCanvas15(t *testing.T) { widget1a := text.New("helloworld") widget1 := New(widget1a) canvas1 := widget1.Render(gowid.RenderBox{C: 7, R: 2}, gowid.NotSelected, gwtest.D) log.Infof("Widget15 is %v", widget1) log.Infof("Canvas15 is %s", canvas1.String()) res := strings.Join([]string{"", ""}, "\n") if res != canvas1.String() { t.Errorf("Failed") } } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: gowid-1.4.0/widgets/cellmod/000077500000000000000000000000001426234454000157165ustar00rootroot00000000000000gowid-1.4.0/widgets/cellmod/cellmod.go000066400000000000000000000060061426234454000176660ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. // Package cellmod provides a widget that can change the cell data of an inner widget. package cellmod import ( "github.com/gcla/gowid" ) //====================================================================== type ICellMod interface { Transform(gowid.Cell, gowid.Selector) gowid.Cell } type Func func(gowid.Cell, gowid.Selector) gowid.Cell func (f Func) Transform(cell gowid.Cell, focus gowid.Selector) gowid.Cell { return f(cell, focus) } type IWidget interface { gowid.ICompositeWidget ICellMod } // Widget that adjusts the palette used - if the rendering context provides for a foreground // color of red (when focused), this widget can provide a map from red -> green to change its // display type Widget struct { gowid.IWidget mod ICellMod *gowid.Callbacks gowid.SubWidgetCallbacks } func New(inner gowid.IWidget, mod ICellMod) *Widget { res := &Widget{ IWidget: inner, mod: mod, } res.SubWidgetCallbacks = gowid.SubWidgetCallbacks{CB: &res.Callbacks} var _ gowid.IWidget = res var _ gowid.ICompositeWidget = res var _ IWidget = res return res } func Opaque(inner gowid.IWidget) *Widget { return New(inner, Func(func(c gowid.Cell, focus gowid.Selector) gowid.Cell { if !c.HasRune() { c = c.WithRune(' ') } return c })) } func (w *Widget) String() string { return "cellmod" } func (w *Widget) SubWidget() gowid.IWidget { return w.IWidget } func (w *Widget) SetSubWidget(inner gowid.IWidget, app gowid.IApp) { w.IWidget = inner gowid.RunWidgetCallbacks(w, gowid.SubWidgetCB{}, app, w) } func (w *Widget) Mod() ICellMod { return w.mod } func (w *Widget) SetMod(mod ICellMod) { w.mod = mod } func (w *Widget) Transform(c gowid.Cell, focus gowid.Selector) gowid.Cell { return w.Mod().Transform(c, focus) } func (w *Widget) SubWidgetSize(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.IRenderSize { return w.SubWidget().RenderSize(size, focus, app) } func (w *Widget) RenderSize(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.IRenderBox { return w.SubWidget().RenderSize(size, focus, app) } func (w *Widget) Render(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.ICanvas { return Render(w, size, focus, app) } func (w *Widget) UserInput(ev interface{}, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) bool { return gowid.UserInputIfSelectable(w.IWidget, ev, size, focus, app) } //'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' func Render(w IWidget, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.ICanvas { c := w.SubWidget().Render(size, focus, app) gowid.RangeOverCanvas(c, gowid.CellRangeFunc(func(cell gowid.Cell) gowid.Cell { return w.Transform(cell, focus) })) return c } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: gowid-1.4.0/widgets/checkbox/000077500000000000000000000000001426234454000160655ustar00rootroot00000000000000gowid-1.4.0/widgets/checkbox/checkbox.go000066400000000000000000000073311426234454000202060ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. // Package checkbox provides a widget which can be checked or unchecked. package checkbox import ( "fmt" "github.com/gcla/gowid" "github.com/gcla/gowid/gwutil" "github.com/gcla/gowid/widgets/button" ) //====================================================================== type IChecked interface { button.IDecoratedAround button.IDecoratedMiddle IsChecked() bool } type IWidget interface { gowid.IWidget IChecked } //====================================================================== type Decoration struct { button.Decoration Middle string } func (b *Decoration) MiddleDec() string { return b.Middle } func (w *Decoration) SetMiddleDec(dec string, app gowid.IApp) { w.Middle = dec } //====================================================================== type Widget struct { checked bool Callbacks *gowid.Callbacks gowid.ClickCallbacks Decoration gowid.AddressProvidesID gowid.IsSelectable } func New(isChecked bool) *Widget { cb := gowid.NewCallbacks() res := &Widget{ checked: isChecked, Callbacks: cb, ClickCallbacks: gowid.ClickCallbacks{CB: &cb}, Decoration: Decoration{button.Decoration{"[", "]"}, "X"}, } var _ gowid.IWidget = res return res } func NewDecorated(isChecked bool, decoration Decoration) *Widget { cb := gowid.NewCallbacks() res := &Widget{ checked: isChecked, Callbacks: cb, ClickCallbacks: gowid.ClickCallbacks{CB: &cb}, Decoration: decoration, } var _ gowid.IWidget = res return res } func (w *Widget) String() string { return fmt.Sprintf("checkbox[%s]", gwutil.If(w.IsChecked(), "X", " ").(string)) } func (w *Widget) IsChecked() bool { return w.checked } func (w *Widget) SetChecked(app gowid.IApp, val bool) { w.setChecked(app, val) } func (w *Widget) setChecked(app gowid.IApp, val bool) { w.checked = val gowid.RunWidgetCallbacks(*w.CB, gowid.ClickCB{}, app, w) } func (w *Widget) Click(app gowid.IApp) { if app.GetMouseState().NoButtonClicked() || app.GetMouseState().LeftIsClicked() { w.setChecked(app, !w.IsChecked()) } } func (w *Widget) RenderSize(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.IRenderBox { return gowid.RenderBox{C: len(w.LeftDec()) + len(w.MiddleDec()) + len(w.RightDec()), R: 1} } func (w *Widget) Render(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.ICanvas { if _, ok := size.(gowid.IRenderFixed); !ok { panic(gowid.WidgetSizeError{Widget: w, Size: size, Required: "gowid.IRenderFixed"}) } return Render(w, size, focus, app) } func (w *Widget) UserInput(ev interface{}, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) bool { if _, ok := size.(gowid.IRenderFixed); !ok { panic(gowid.WidgetSizeError{Widget: w, Size: size, Required: "gowid.IRenderFixed"}) } return button.UserInput(w, ev, size, focus, app) } //====================================================================== func Render(w IChecked, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.ICanvas { line := make([]gowid.Cell, 0) line = append(line, gowid.CellsFromString(w.LeftDec())...) if w.IsChecked() { line = append(line, gowid.CellsFromString(w.MiddleDec())...) } else { line = append(line, gowid.CellsFromString(gwutil.StringOfLength(' ', len(w.MiddleDec())))...) } line = append(line, gowid.CellsFromString(w.RightDec())...) res := gowid.NewCanvasWithLines([][]gowid.Cell{line}) res.SetCursorCoords(len(w.LeftDec())+(len(w.MiddleDec())/2), 0) return res } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: gowid-1.4.0/widgets/checkbox/checkbox_test.go000066400000000000000000000066511426234454000212510ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source code is governed by the MIT license // that can be found in the LICENSE file. package checkbox import ( "testing" "github.com/gcla/gowid" "github.com/gcla/gowid/gwtest" "github.com/gcla/gowid/widgets/button" "github.com/gcla/gowid/widgets/text" "github.com/stretchr/testify/assert" ) //====================================================================== func TestButton1(t *testing.T) { tw := text.New("click") w := button.New(tw) ct := &gwtest.ButtonTester{Gotit: false} assert.Equal(t, ct.Gotit, false) w.OnClick(ct) c1 := w.Render(gowid.RenderFixed{}, gowid.Focused, gwtest.D) assert.Equal(t, c1.String(), "") w.Click(gwtest.D) assert.Equal(t, ct.Gotit, true) ct.Gotit = false assert.Equal(t, ct.Gotit, false) w.RemoveOnClick(ct) w.Click(gwtest.D) assert.Equal(t, ct.Gotit, false) } func TestCheckbox1(t *testing.T) { w := New(false) ct := &gwtest.CheckBoxTester{Gotit: false} assert.Equal(t, ct.Gotit, false) w.OnClick(ct) c1 := w.Render(gowid.RenderFixed{}, gowid.Focused, gwtest.D) assert.Equal(t, c1.String(), "[ ]") assert.Equal(t, w.IsChecked(), false) w.SetChecked(gwtest.D, true) assert.Equal(t, w.IsChecked(), true) assert.Equal(t, ct.Gotit, true) ct.Gotit = false // WRONG Shouldn't issue another callback since state didn't change w.SetChecked(gwtest.D, true) assert.Equal(t, ct.Gotit, true) assert.Equal(t, w.IsChecked(), true) c2 := w.Render(gowid.RenderFixed{}, gowid.Focused, gwtest.D) assert.Equal(t, c2.String(), "[X]") assert.Equal(t, w.IsChecked(), true) assert.Panics(t, func() { w.Render(gowid.RenderFlowWith{C: 5}, gowid.Focused, gwtest.D) }) assert.Panics(t, func() { w.Render(gowid.RenderBox{C: 3, R: 1}, gowid.Focused, gwtest.D) }) } var ( cb1 int cb2 int ) func testCallback1(app gowid.IApp, w gowid.IWidget) { cb1++ } func testCallback2(app gowid.IApp, w gowid.IWidget) { cb2++ } func TestCallbacks(t *testing.T) { cbs := gowid.NewCallbacks() assert.Equal(t, cb1, 0) assert.Equal(t, cb2, 0) gowid.AddWidgetCallback(cbs, "test", gowid.WidgetCallback{"cb1", testCallback1}) dummy := New(false) gowid.RunWidgetCallbacks(cbs, "test", gwtest.D, dummy) assert.Equal(t, cb1, 1) gowid.RemoveWidgetCallback(cbs, "test", gowid.CallbackID{Name: "cb1"}) gowid.RunWidgetCallbacks(cbs, "test", gwtest.D, dummy) assert.Equal(t, cb1, 1) cb1 = 0 assert.Equal(t, cb1, 0) gowid.AddWidgetCallback(cbs, "test", gowid.WidgetCallback{123, testCallback1}) gowid.AddWidgetCallback(cbs, "test", gowid.WidgetCallback{123, testCallback2}) gowid.RunWidgetCallbacks(cbs, "test2", gwtest.D, dummy) assert.Equal(t, cb1, 0) assert.Equal(t, cb2, 0) gowid.RunWidgetCallbacks(cbs, "test", gwtest.D, dummy) assert.Equal(t, cb1, 1) assert.Equal(t, cb2, 1) gowid.RemoveWidgetCallback(cbs, "test2", gowid.CallbackID{Name: 123}) gowid.RunWidgetCallbacks(cbs, "test", gwtest.D, dummy) assert.Equal(t, cb1, 2) assert.Equal(t, cb2, 2) gowid.RemoveWidgetCallback(cbs, "test", gowid.CallbackID{Name: "xx2"}) gowid.RunWidgetCallbacks(cbs, "test", gwtest.D, dummy) assert.Equal(t, cb1, 3) assert.Equal(t, cb2, 3) gowid.RemoveWidgetCallback(cbs, "test", gowid.CallbackID{Name: 123}) gowid.RunWidgetCallbacks(cbs, "test", gwtest.D, dummy) assert.Equal(t, cb1, 3) assert.Equal(t, cb2, 3) } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: gowid-1.4.0/widgets/clicktracker/000077500000000000000000000000001426234454000167405ustar00rootroot00000000000000gowid-1.4.0/widgets/clicktracker/clicktracker.go000066400000000000000000000072261426234454000217370ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. // Package clicktracker provides a widget that inverts when the mouse is clicked // but not yet released. package clicktracker import ( "fmt" "github.com/gcla/gowid" tcell "github.com/gdamore/tcell/v2" ) //====================================================================== // IWidget is implemented by any widget that contains exactly one // exposed subwidget (ICompositeWidget), that can distinguish itself // from another IWidget (via the ID() function), and that can track // a mouse click prior to a mouse release (simply with a bool flag, in // this case) type IWidget interface { gowid.ICompositeWidget gowid.IClickTracker gowid.IIdentity ClickPending() bool } type Widget struct { inner gowid.IWidget Callbacks *gowid.Callbacks gowid.SubWidgetCallbacks gowid.ClickCallbacks gowid.AddressProvidesID gowid.IsSelectable clickDown bool } func New(inner gowid.IWidget) *Widget { res := &Widget{ inner: inner, } res.SubWidgetCallbacks = gowid.SubWidgetCallbacks{CB: &res.Callbacks} res.ClickCallbacks = gowid.ClickCallbacks{CB: &res.Callbacks} var _ gowid.IWidget = res var _ IWidget = res return res } func (w *Widget) String() string { return fmt.Sprintf("clicktracker[%v]", w.SubWidget()) } func (w *Widget) ClickPending() bool { return w.clickDown } func (w *Widget) SetClickPending(pending bool) { w.clickDown = pending } func (w *Widget) SubWidget() gowid.IWidget { return w.inner } func (w *Widget) SetSubWidget(wi gowid.IWidget, app gowid.IApp) { w.inner = wi gowid.RunWidgetCallbacks(w.Callbacks, gowid.SubWidgetCB{}, app, w) } func (w *Widget) SubWidgetSize(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.IRenderSize { return SubWidgetSize(size, focus, app) } func (w *Widget) RenderSize(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.IRenderBox { return RenderSize(w, size, focus, app) } func (w *Widget) Render(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.ICanvas { return Render(w, size, focus, app) } func (w *Widget) UserInput(ev interface{}, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) bool { return UserInput(w, ev, size, focus, app) } //====================================================================== func SubWidgetSize(size interface{}, focus gowid.Selector, app gowid.IApp) gowid.IRenderSize { return size } func RenderSize(w IWidget, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.IRenderBox { return gowid.RenderSize(w.SubWidget(), size, focus, app) } func UserInput(w IWidget, ev interface{}, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) bool { switch ev := ev.(type) { case *tcell.EventMouse: switch ev.Buttons() { case tcell.Button1, tcell.Button2, tcell.Button3: app.SetClickTarget(ev.Buttons(), w) w.SetClickPending(true) case tcell.ButtonNone: w.SetClickPending(false) } } // Never handle the input, always pass it on return w.SubWidget().UserInput(ev, size, focus, app) } func Render(w IWidget, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.ICanvas { res := w.SubWidget().Render(gowid.SubWidgetSize(w, size, focus, app), focus, app) if w.ClickPending() { gowid.RangeOverCanvas(res, gowid.CellRangeFunc(func(c gowid.Cell) gowid.Cell { f, b := c.ForegroundColor(), c.BackgroundColor() return c.WithBackgroundColor(f).WithForegroundColor(b) })) } return res } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: gowid-1.4.0/widgets/columns/000077500000000000000000000000001426234454000157575ustar00rootroot00000000000000gowid-1.4.0/widgets/columns/columns.go000066400000000000000000000512231426234454000177710ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. // Package columns provides a widget for organizing other widgets in columns. package columns import ( "fmt" "strings" "github.com/gcla/gowid" "github.com/gcla/gowid/gwutil" "github.com/gcla/gowid/vim" "github.com/gcla/gowid/widgets/fill" tcell "github.com/gdamore/tcell/v2" ) //====================================================================== type IWidget interface { gowid.ICompositeMultipleWidget gowid.ISettableDimensions gowid.ISettableSubWidgets gowid.IFindNextSelectable gowid.IPreferedPosition gowid.ISelectChild gowid.IIdentity WidgetWidths(size gowid.IRenderSize, focus gowid.Selector, focusIdx int, app gowid.IApp) []int Wrap() bool KeyIsLeft(*tcell.EventKey) bool KeyIsRight(*tcell.EventKey) bool } type Widget struct { widgets []gowid.IContainerWidget focus int // -1 means nothing selectable prefCol int // caches the last set prefered col. Passes it on if widget hasn't changed focus widthHelper []bool // optimizations to save frequent array allocations during use widthHelper2 []bool opt Options *gowid.Callbacks gowid.AddressProvidesID gowid.SubWidgetsCallbacks gowid.FocusCallbacks } type Options struct { StartColumn int // column that gets initial focus Wrap bool // whether or not to wrap from last column to first with movement operations DoNotSetSelected bool // Whether or not to set the focus.Selected field for the selected child LeftKeys []vim.KeyPress RightKeys []vim.KeyPress } func New(widgets []gowid.IContainerWidget, opts ...Options) *Widget { var opt Options if len(opts) > 0 { opt = opts[0] } else { opt = Options{ StartColumn: -1, } } if opt.LeftKeys == nil { opt.LeftKeys = vim.AllLeftKeys } if opt.RightKeys == nil { opt.RightKeys = vim.AllRightKeys } res := &Widget{ widgets: widgets, focus: -1, prefCol: -1, widthHelper: make([]bool, len(widgets)), widthHelper2: make([]bool, len(widgets)), opt: opt, } res.SubWidgetsCallbacks = gowid.SubWidgetsCallbacks{CB: &res.Callbacks} res.FocusCallbacks = gowid.FocusCallbacks{CB: &res.Callbacks} if opt.StartColumn >= 0 { res.focus = gwutil.Min(opt.StartColumn, len(widgets)-1) } else { res.focus, _ = res.FindNextSelectable(1, res.Wrap()) } var _ gowid.IWidget = res var _ IWidget = res var _ gowid.ICompositeMultipleDimensions = res var _ gowid.ICompositeMultipleWidget = res return res } //func Simple(ws ...gowid.IWidget) *Widget { func NewFlow(ws ...interface{}) *Widget { return NewWithDim(gowid.RenderFlow{}, ws...) } func NewFixed(ws ...interface{}) *Widget { return NewWithDim(gowid.RenderFixed{}, ws...) } func NewWithDim(method gowid.IWidgetDimension, ws ...interface{}) *Widget { cws := make([]gowid.IContainerWidget, len(ws)) for i := 0; i < len(ws); i++ { if cw, ok := ws[i].(gowid.IContainerWidget); ok { cws[i] = cw } else { cws[i] = &gowid.ContainerWidget{ IWidget: ws[i].(gowid.IWidget), D: method, } } } return New(cws) } func (w *Widget) SelectChild(f gowid.Selector) bool { return !w.opt.DoNotSetSelected && f.Selected } func (w *Widget) String() string { cols := make([]string, len(w.widgets)) for i := 0; i < len(cols); i++ { cols[i] = fmt.Sprintf("%v", w.widgets[i]) } return fmt.Sprintf("columns[%s]", strings.Join(cols, ",")) } func (w *Widget) SetFocus(app gowid.IApp, i int) { old := w.focus w.focus = gwutil.Min(gwutil.Max(i, 0), len(w.widgets)-1) w.prefCol = -1 // moved, so pass on real focus from now on if old != w.focus { gowid.RunWidgetCallbacks(w.Callbacks, gowid.FocusCB{}, app, w) } } func (w *Widget) Wrap() bool { return w.opt.Wrap } func (w *Widget) Focus() int { return w.focus } func (w *Widget) SubWidgets() []gowid.IWidget { res := make([]gowid.IWidget, len(w.widgets)) for i, iw := range w.widgets { res[i] = iw } return res } func (w *Widget) SetSubWidgets(widgets []gowid.IWidget, app gowid.IApp) { ws := make([]gowid.IContainerWidget, len(widgets)) for i, iw := range widgets { if iwc, ok := iw.(gowid.IContainerWidget); ok { ws[i] = iwc } else { ws[i] = &gowid.ContainerWidget{IWidget: iw, D: gowid.RenderFlow{}} } } w.widthHelper = make([]bool, len(widgets)) w.widthHelper2 = make([]bool, len(widgets)) oldFocus := w.Focus() w.widgets = ws w.SetFocus(app, oldFocus) gowid.RunWidgetCallbacks(w.Callbacks, gowid.SubWidgetsCB{}, app, w) } func (w *Widget) Dimensions() []gowid.IWidgetDimension { res := make([]gowid.IWidgetDimension, len(w.widgets)) for i, iw := range w.widgets { res[i] = iw.Dimension() } return res } func (w *Widget) SetDimensions(dimensions []gowid.IWidgetDimension, app gowid.IApp) { for i, id := range dimensions { w.widgets[i].SetDimension(id) } gowid.RunWidgetCallbacks(w.Callbacks, gowid.DimensionsCB{}, app, w) } func (w *Widget) Selectable() bool { return gowid.SelectableIfAnySubWidgetsAre(w) } func (w *Widget) FindNextSelectable(dir gowid.Direction, wrap bool) (int, bool) { return gowid.FindNextSelectableFrom(w, w.Focus(), dir, wrap) } func (w *Widget) UserInput(ev interface{}, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) bool { return UserInput(w, ev, size, focus, app) } // RenderSize computes the size of this widget when it renders. This is // done by computing the sizes of each subwidget, then arranging them the // same way that Render() does. // func (w *Widget) RenderSize(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.IRenderBox { return RenderSize(w, size, focus, app) } func (w *Widget) Render(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.ICanvas { return Render(w, size, focus, app) } func (w *Widget) RenderSubWidgets(size gowid.IRenderSize, focus gowid.Selector, focusIdx int, app gowid.IApp) []gowid.ICanvas { return RenderSubWidgets(w, size, focus, focusIdx, app) } func (w *Widget) RenderedSubWidgetsSizes(size gowid.IRenderSize, focus gowid.Selector, focusIdx int, app gowid.IApp) []gowid.IRenderBox { return RenderedSubWidgetsSizes(w, size, focus, focusIdx, app) } // Return a slice of ints representing the width in columns for each of the subwidgets to be rendered // in this context given the size argument. // func (w *Widget) WidgetWidths(size gowid.IRenderSize, focus gowid.Selector, focusIdx int, app gowid.IApp) []int { return WidgetWidths(w, size, focus, focusIdx, app) } // Construct the context in which each subwidget will be rendered. It's important to // preserve the type of context e.g. a subwidget may only support being rendered in a // fixed context. The newX parameter is the width the subwidget will have within the // context of the Columns widget. // func (w *Widget) SubWidgetSize(size gowid.IRenderSize, newX int, sub gowid.IWidget, dim gowid.IWidgetDimension) gowid.IRenderSize { return SubWidgetSize(size, newX, dim) } func (w *Widget) GetPreferedPosition() gwutil.IntOption { f := w.prefCol if f == -1 { f = w.Focus() } if f == -1 { return gwutil.NoneInt() } else { return gwutil.SomeInt(f) } } func (w *Widget) SetPreferedPosition(cols int, app gowid.IApp) { col := gwutil.Min(gwutil.Max(cols, 0), len(w.widgets)-1) pref := col colLeft := col - 1 colRight := col for colLeft >= 0 || colRight < len(w.widgets) { if colRight < len(w.widgets) && w.widgets[colRight].Selectable() { w.SetFocus(app, colRight) break } else { colRight++ } if colLeft >= 0 && w.widgets[colLeft].Selectable() { w.SetFocus(app, colLeft) break } else { colLeft-- } } w.prefCol = pref // Save it. Pass it on if widget doesn't change col before losing focus. } func (w *Widget) KeyIsLeft(evk *tcell.EventKey) bool { return vim.KeyIn(evk, w.opt.LeftKeys) } func (w *Widget) KeyIsRight(evk *tcell.EventKey) bool { return vim.KeyIn(evk, w.opt.RightKeys) } type IWidthHelper interface { WidthHelpers() ([]bool, []bool) } var _ IWidthHelper = (*Widget)(nil) func (w *Widget) WidthHelpers() ([]bool, []bool) { return w.widthHelper, w.widthHelper2 } //'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' func SubWidgetSize(size gowid.IRenderSize, newX int, dim gowid.IWidgetDimension) gowid.IRenderSize { var subSize gowid.IRenderSize switch sz := size.(type) { case gowid.IRenderFixed: switch dim.(type) { case gowid.IRenderBox: subSize = dim default: subSize = gowid.RenderFixed{} } case gowid.IRenderBox: switch dim.(type) { case gowid.IRenderFixed: subSize = gowid.RenderFixed{} case gowid.IRenderFlow: subSize = gowid.RenderFlowWith{C: newX} case gowid.IRenderWithUnits, gowid.IRenderWithWeight: subSize = gowid.RenderBox{C: newX, R: sz.BoxRows()} default: subSize = gowid.RenderBox{C: newX, R: sz.BoxRows()} } case gowid.IRenderFlowWith: switch dim.(type) { case gowid.IRenderFixed: subSize = gowid.RenderFixed{} case gowid.IRenderFlow, gowid.IRenderWithUnits, gowid.IRenderWithWeight, gowid.IRenderRelative: // The newX argument is already computed to be the right number of cols for the subwidget subSize = gowid.RenderFlowWith{C: newX} default: panic(gowid.DimensionError{Size: size, Dim: dim}) } default: panic(gowid.DimensionError{Size: size, Dim: dim}) } return subSize } func UserInput(w IWidget, ev interface{}, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) bool { res := false subfocus := w.Focus() subSizes := w.WidgetWidths(size, focus, subfocus, app) dims := w.Dimensions() subs := w.SubWidgets() forChild := false if subfocus != -1 { res = true if evm, ok := ev.(*tcell.EventMouse); ok { curX := 0 mx, _ := evm.Position() Loop: for i, c := range subSizes { if mx < curX+c && mx >= curX { subSize := w.SubWidgetSize(size, c, subs[i], dims[i]) forChild = subs[i].UserInput(gowid.TranslatedMouseEvent(ev, -curX, 0), subSize, focus.SelectIf(w.SelectChild(focus) && i == subfocus), app) // Give the child focus if (a) it's selectable, and (b) if this is the click up corresponding // to a previous click down on this columns widget. switch evm.Buttons() { case tcell.Button1, tcell.Button2, tcell.Button3: app.SetClickTarget(evm.Buttons(), w) case tcell.ButtonNone: if !app.GetLastMouseState().NoButtonClicked() { if subs[i].Selectable() { clickit := false app.ClickTarget(func(k tcell.ButtonMask, v gowid.IIdentityWidget) { if v != nil && v.ID() == w.ID() { clickit = true } }) if clickit { w.SetFocus(app, i) } } } break Loop } break } curX += c } } else { subC := subSizes[subfocus] // guaranteed to be a box subSize := w.SubWidgetSize(size, subC, subs[subfocus], dims[subfocus]) forChild = gowid.UserInputIfSelectable(subs[w.Focus()], ev, subSize, focus, app) } } if !forChild && w.Focus() != -1 { res = false if evk, ok := ev.(*tcell.EventKey); ok { curw := subs[w.Focus()] prefPos := gowid.PrefPosition(curw) switch { case w.KeyIsRight(evk): res = Scroll(w, 1, w.Wrap(), app) case w.KeyIsLeft(evk): res = Scroll(w, -1, w.Wrap(), app) } if !prefPos.IsNone() { // New focus widget curw = subs[w.Focus()] gowid.SetPrefPosition(curw, prefPos.Val(), app) } } } return res } type IFocusSelectable interface { gowid.IFocus gowid.IFindNextSelectable } func Scroll(w IFocusSelectable, dir gowid.Direction, wrap bool, app gowid.IApp) bool { res := false next, ok := w.FindNextSelectable(dir, wrap) if ok { w.SetFocus(app, next) res = true } return res } type ICompositeMultipleDimensionsExt interface { gowid.ICompositeMultipleDimensions gowid.ISelectChild } func WidgetWidths(w ICompositeMultipleDimensionsExt, size gowid.IRenderSize, focus gowid.Selector, focusIdx int, app gowid.IApp) []int { return widgetWidthsExt(w, w.SubWidgets(), w.Dimensions(), size, focus, focusIdx, app) } // Precompute dims and subs func widgetWidthsExt(w gowid.ISelectChild, subs []gowid.IWidget, dims []gowid.IWidgetDimension, size gowid.IRenderSize, focus gowid.Selector, focusIdx int, app gowid.IApp) []int { lenw := len(subs) res := make([]int, lenw) var widthHelper []bool var widthHelper2 []bool if w, ok := w.(IWidthHelper); ok { // Save some allocations widthHelper, widthHelper2 = w.WidthHelpers() defer func() { for i := 0; i < len(widthHelper); i++ { widthHelper[i] = false widthHelper2[i] = false } }() } else { widthHelper = make([]bool, lenw) widthHelper2 = make([]bool, lenw) } haveColsTotal := false var colsTotal int if _, ok := size.(gowid.IRenderFixed); !ok { cols, ok := size.(gowid.IColumns) if !ok { panic(gowid.WidgetSizeError{Widget: w, Size: size, Required: "gowid.IColumns"}) } colsTotal = cols.Columns() haveColsTotal = true } colsUsed := 0 totalWeight := 0 trunc := func(x *int) { if haveColsTotal && colsUsed+*x > colsTotal { *x = colsTotal - colsUsed } } // First, render the widgets whose width is known for i := 0; i < lenw; i++ { // This doesn't support IRenderFlow. That type comes with an associated width e.g. // "Flow with 25 columns". We don't have any way to apportion those columns amongst // the overall width for the widget. switch w2 := dims[i].(type) { case gowid.IRenderFixed: c := gowid.RenderSize(subs[i], gowid.RenderFixed{}, focus.SelectIf(w.SelectChild(focus) && i == focusIdx), app) res[i] = c.BoxColumns() trunc(&res[i]) colsUsed += res[i] widthHelper[i] = true widthHelper2[i] = true case gowid.IRenderBox: res[i] = w2.BoxColumns() trunc(&res[i]) colsUsed += res[i] widthHelper[i] = true widthHelper2[i] = true case gowid.IRenderFlowWith: res[i] = w2.FlowColumns() trunc(&res[i]) colsUsed += res[i] widthHelper[i] = true widthHelper2[i] = true case gowid.IRenderWithUnits: res[i] = w2.Units() trunc(&res[i]) colsUsed += res[i] widthHelper[i] = true widthHelper2[i] = true case gowid.IRenderRelative: cols, ok := size.(gowid.IColumns) if !ok { panic(gowid.WidgetSizeError{Widget: w, Size: size, Required: "gowid.IColumns"}) } res[i] = int((w2.Relative() * float64(cols.Columns())) + 0.5) trunc(&res[i]) colsUsed += res[i] widthHelper[i] = true widthHelper2[i] = true case gowid.IRenderWithWeight: // widget must be weighted totalWeight += w2.Weight() widthHelper[i] = false widthHelper2[i] = false default: panic(gowid.DimensionError{Size: size, Dim: w2}) } } var colsLeft int var colsToDivideUp int if haveColsTotal { colsToDivideUp = colsTotal - colsUsed colsLeft = colsToDivideUp } // Now, divide up the remaining space among the weight columns lasti := -1 maxedOut := false for { if colsLeft == 0 { break } doneone := false totalWeight = 0 for i := 0; i < lenw; i++ { if w2, ok := dims[i].(gowid.IRenderWithWeight); ok && !widthHelper[i] { totalWeight += w2.Weight() } } colsToDivideUp = colsLeft for i := 0; i < lenw; i++ { // Can only be weight here if !helper[i] ; but not sufficient for it to be eligible if !widthHelper[i] { cols := int(((float32(dims[i].(gowid.IRenderWithWeight).Weight()) / float32(totalWeight)) * float32(colsToDivideUp)) + 0.5) if !maxedOut { if max, ok := dims[i].(gowid.IRenderMaxUnits); ok { if cols >= max.MaxUnits() { cols = max.MaxUnits() widthHelper[i] = true // this one is done } } } if cols > colsLeft { cols = colsLeft } if cols > 0 { if res[i] == -1 { res[i] = 0 } res[i] += cols colsLeft -= cols lasti = gwutil.Max(i, lasti) doneone = true } } } if !doneone { // We used up all our extra space, after all weighted columns were maxed out. So // we're done. Any extra space (should be just 1 unit at most) goes on the last columns. if maxedOut { break } // All the weighted columns have been assigned, and all were maxed out. We still // have space to assign. So now grow the weighted columns, even though they don't need // any more space for a full render - what else to do with the space! maxedOut = true // Reset; all false indices will be indices of weighted columns again for i := 0; i < len(widthHelper); i++ { widthHelper[i] = widthHelper2[i] } } } if lasti != -1 && colsLeft > 0 { res[lasti] += colsLeft } return res } func RenderSize(w gowid.ICompositeMultipleWidget, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.IRenderBox { subfocus := w.Focus() sizes := w.RenderedSubWidgetsSizes(size, focus, subfocus, app) maxcol, maxrow := 0, 0 for _, sz := range sizes { maxcol += sz.BoxColumns() maxrow = gwutil.Max(maxrow, sz.BoxRows()) } if cols, ok := size.(gowid.IColumns); ok { maxcol = cols.Columns() if rows, ok2 := size.(gowid.IRows); ok2 { maxrow = rows.Rows() } } return gowid.RenderBox{maxcol, maxrow} } func Render(w IWidget, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.ICanvas { res := gowid.NewCanvas() subfocus := w.Focus() canvases := w.RenderSubWidgets(size, focus, subfocus, app) subs := w.SubWidgets() // Assemble subcanvases into final canvas for i := 0; i < len(subs); i++ { diff := res.BoxRows() - canvases[i].BoxRows() if diff > 0 { fill := fill.NewEmpty() fc := fill.Render(gowid.RenderBox{canvases[i].BoxColumns(), diff}, gowid.NotSelected, app) canvases[i].AppendBelow(fc, false, false) } else if diff < 0 { fill := fill.NewEmpty() fc := fill.Render(gowid.RenderBox{res.BoxColumns(), -diff}, gowid.NotSelected, app) res.AppendBelow(fc, false, false) } res.AppendRight(canvases[i], i == subfocus) } if cols, ok := size.(gowid.IColumns); ok { res.ExtendRight(gowid.EmptyLine(cols.Columns() - res.BoxColumns())) if rows, ok2 := size.(gowid.IRenderBox); ok2 && res.BoxRows() < rows.BoxRows() { gowid.AppendBlankLines(res, rows.BoxRows()-res.BoxRows()) } } gowid.MakeCanvasRightSize(res, size) return res } var AllChildrenMaxDimension = fmt.Errorf("All columns widgets were rendered Max, so there is no max height to use.") // RenderSubWidgets returns an array of canvases for each of the subwidgets, rendering them // with in the context of a column with the provided size and focus. func RenderSubWidgets(w IWidget, size gowid.IRenderSize, focus gowid.Selector, focusIdx int, app gowid.IApp) []gowid.ICanvas { subs := w.SubWidgets() dims := w.Dimensions() l := len(subs) canvases := make([]gowid.ICanvas, l) if l == 0 { return canvases } weights := w.WidgetWidths(size, focus, focusIdx, app) maxes := make([]int, 0, l) ssizes := make([]gowid.IRenderSize, 0, l) curMax := -1 for i := 0; i < l; i++ { subSize := w.SubWidgetSize(size, weights[i], subs[i], dims[i]) if _, ok := dims[i].(gowid.IRenderMax); ok { maxes = append(maxes, i) ssizes = append(ssizes, subSize) } else { canvases[i] = subs[i].Render(subSize, focus.SelectIf(w.SelectChild(focus) && i == focusIdx), app) if canvases[i].BoxRows() > curMax { curMax = canvases[i].BoxRows() } } } if curMax == -1 { panic(AllChildrenMaxDimension) } for j := 0; j < len(maxes); j++ { i := maxes[j] var mss gowid.IRenderSize = ssizes[j] switch css := mss.(type) { case gowid.IRenderFlowWith: mss = gowid.MakeRenderBox(css.FlowColumns(), curMax) case gowid.IRenderBox: mss = gowid.MakeRenderBox(css.BoxColumns(), curMax) default: } canvases[i] = subs[i].Render(mss, focus.SelectIf(w.SelectChild(focus) && i == focusIdx), app) } return canvases } // RenderedSubWidgetsSizes returns an array of boxes that bound each of the subwidgets as they // would be rendered with the given size and focus. func RenderedSubWidgetsSizes(w IWidget, size gowid.IRenderSize, focus gowid.Selector, focusIdx int, app gowid.IApp) []gowid.IRenderBox { subs := w.SubWidgets() dims := w.Dimensions() l := len(subs) res := make([]gowid.IRenderBox, l) weights := w.WidgetWidths(size, focus, focusIdx, app) maxes := make([]int, 0, l) ssizes := make([]int, 0, l) curMax := -1 for i := 0; i < l; i++ { subSize := w.SubWidgetSize(size, weights[i], subs[i], dims[i]) if _, ok := dims[i].(gowid.IRenderMax); ok { maxes = append(maxes, i) ssizes = append(ssizes, weights[i]) } else { c := subs[i].RenderSize(subSize, focus.SelectIf(w.SelectChild(focus) && i == focusIdx), app) res[i] = gowid.RenderBox{weights[i], c.BoxRows()} if res[i].BoxRows() > curMax { curMax = res[i].BoxRows() } } } if curMax == -1 { panic(AllChildrenMaxDimension) } for j := 0; j < len(maxes); j++ { res[maxes[j]] = gowid.RenderBox{ssizes[j], curMax} } return res } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: gowid-1.4.0/widgets/columns/columns_test.go000066400000000000000000000217511426234454000210330ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. package columns import ( "testing" "github.com/gcla/gowid" "github.com/gcla/gowid/gwtest" "github.com/gcla/gowid/widgets/button" "github.com/gcla/gowid/widgets/checkbox" "github.com/gcla/gowid/widgets/fill" "github.com/gcla/gowid/widgets/selectable" "github.com/gcla/gowid/widgets/text" tcell "github.com/gdamore/tcell/v2" "github.com/stretchr/testify/assert" ) func TestInterfaces1(t *testing.T) { var _ gowid.IWidget = (*Widget)(nil) var _ IWidget = (*Widget)(nil) var _ gowid.ICompositeMultipleDimensions = (*Widget)(nil) var _ gowid.ICompositeMultipleWidget = (*Widget)(nil) } func TestColumns1(t *testing.T) { w1 := New([]gowid.IContainerWidget{ &gowid.ContainerWidget{fill.New('x'), gowid.RenderWithUnits{U: 2}}, &gowid.ContainerWidget{fill.New('y'), gowid.RenderWithUnits{U: 2}}, }) c1 := w1.Render(gowid.RenderBox{C: 4, R: 3}, gowid.Focused, gwtest.D) assert.Equal(t, c1.String(), "xxyy\nxxyy\nxxyy") w2 := New([]gowid.IContainerWidget{ &gowid.ContainerWidget{fill.New('x'), gowid.RenderWithWeight{6}}, &gowid.ContainerWidget{fill.New('y'), gowid.RenderWithWeight{2}}, }) c2 := w2.Render(gowid.RenderBox{C: 4, R: 3}, gowid.Focused, gwtest.D) assert.Equal(t, c2.String(), "xxxy\nxxxy\nxxxy") w3 := New([]gowid.IContainerWidget{ &gowid.ContainerWidget{fill.New('x'), gowid.RenderWithRatio{0.75}}, &gowid.ContainerWidget{fill.New('y'), gowid.RenderWithRatio{0.35}}, }) c3 := w3.Render(gowid.RenderBox{C: 4, R: 3}, gowid.Focused, gwtest.D) assert.Equal(t, c3.String(), "xxxy\nxxxy\nxxxy") w4 := New([]gowid.IContainerWidget{ &gowid.ContainerWidget{fill.New('x'), gowid.RenderWithRatio{0.5}}, &gowid.ContainerWidget{fill.New('y'), gowid.RenderWithWeight{10}}, &gowid.ContainerWidget{fill.New('z'), gowid.RenderWithWeight{5}}, }) c4 := w4.Render(gowid.RenderBox{C: 6, R: 3}, gowid.Focused, gwtest.D) assert.Equal(t, c4.String(), "xxxyyz\nxxxyyz\nxxxyyz") w5 := New([]gowid.IContainerWidget{ &gowid.ContainerWidget{checkbox.New(false), gowid.RenderFixed{}}, &gowid.ContainerWidget{checkbox.New(false), gowid.RenderFixed{}}, &gowid.ContainerWidget{checkbox.New(false), gowid.RenderFixed{}}, }) idx := -1 w5.OnFocusChanged(gowid.WidgetCallback{"cb", func(app gowid.IApp, w gowid.IWidget) { w2 := w.(*Widget) idx = w2.Focus() }}) assert.Equal(t, w5.Focus(), 0) assert.Equal(t, idx, -1) w5.SetFocus(gwtest.D, 1) assert.Equal(t, w5.Focus(), 1) assert.Equal(t, idx, 1) w5.SetFocus(gwtest.D, 100) assert.Equal(t, w5.Focus(), 2) for _, w := range []gowid.IWidget{w1, w2, w3, w4} { gwtest.RenderBoxManyTimes(t, w, 0, 10, 0, 10) gwtest.RenderFlowManyTimes(t, w, 0, 10) } gwtest.RenderFixedDoesNotPanic(t, w5) } func TestColumns2(t *testing.T) { w1 := New([]gowid.IContainerWidget{ &gowid.ContainerWidget{&text.Widget1{0}, gowid.RenderWithUnits{U: 2}}, &gowid.ContainerWidget{&text.Widget1{1}, gowid.RenderWithUnits{U: 2}}, &gowid.ContainerWidget{&text.Widget1{2}, gowid.RenderWithUnits{U: 2}}, }) sz := gowid.RenderBox{C: 6, R: 1} c1 := w1.Render(sz, gowid.Focused, gwtest.D) assert.Equal(t, "0f1 2 ", c1.String()) assert.Equal(t, 0, w1.Focus()) evright := gwtest.CursorRight() w1.UserInput(evright, sz, gowid.Focused, gwtest.D) c1 = w1.Render(sz, gowid.Focused, gwtest.D) assert.Equal(t, "0 1f2 ", c1.String()) assert.Equal(t, 1, w1.Focus()) evlmx0y0 := tcell.NewEventMouse(0, 0, tcell.Button1, 0) evnonex0y0 := tcell.NewEventMouse(0, 0, tcell.ButtonNone, 0) w1.UserInput(evlmx0y0, sz, gowid.Focused, gwtest.D) gwtest.D.SetLastMouseState(gowid.MouseState{true, false, false}) w1.UserInput(evnonex0y0, sz, gowid.Focused, gwtest.D) gwtest.D.SetLastMouseState(gowid.MouseState{false, false, false}) c1 = w1.Render(sz, gowid.Focused, gwtest.D) assert.Equal(t, "0f1 2 ", c1.String()) } func makec3(txt string) gowid.IWidget { return selectable.New(text.New(txt)) } func makec3fixed(txt string) gowid.IContainerWidget { return &gowid.ContainerWidget{ IWidget: makec3(txt), D: gowid.RenderFixed{}, } } func TestColumns3(t *testing.T) { w := NewFixed(makec3("111"), makec3("222"), makec3("333")) c := w.Render(gowid.RenderFixed{}, gowid.Focused, gwtest.D) assert.Equal(t, "111222333", c.String()) assert.Equal(t, 0, w.Focus()) w.SetFocus(gwtest.D, 2) assert.Equal(t, 2, w.Focus()) w.SetSubWidgets([]gowid.IWidget{ makec3fixed("aaaa"), makec3fixed("bbbbb"), }, gwtest.D, ) c = w.Render(gowid.RenderFixed{}, gowid.Focused, gwtest.D) assert.Equal(t, "aaaabbbbb", c.String()) assert.Equal(t, 1, w.Focus()) } type renderWeightUpTo struct { gowid.RenderWithWeight max int } func (s renderWeightUpTo) MaxUnits() int { return s.max } func weightupto(w int, max int) renderWeightUpTo { return renderWeightUpTo{gowid.RenderWithWeight{W: w}, max} } func weight(w int) gowid.RenderWithWeight { return gowid.RenderWithWeight{W: w} } func makep(c rune) gowid.IWidget { return selectable.New(fill.New(c)) } func TestColumns4(t *testing.T) { subs := []gowid.IContainerWidget{ &gowid.ContainerWidget{makep('x'), gowid.RenderWithWeight{W: 1}}, &gowid.ContainerWidget{makep('y'), gowid.RenderWithWeight{W: 1}}, &gowid.ContainerWidget{makep('z'), gowid.RenderWithWeight{W: 1}}, } w := New(subs) c := w.Render(gowid.RenderBox{C: 12, R: 1}, gowid.Focused, gwtest.D) assert.Equal(t, "xxxxyyyyzzzz", c.String()) subs[2] = &gowid.ContainerWidget{makep('z'), renderWeightUpTo{gowid.RenderWithWeight{W: 1}, 2}} w = New(subs) c = w.Render(gowid.RenderBox{C: 12, R: 1}, gowid.Focused, gwtest.D) assert.Equal(t, "xxxxxyyyyyzz", c.String()) } func TestColumns5(t *testing.T) { // None are selectable subs := []gowid.IContainerWidget{ &gowid.ContainerWidget{text.New("x"), gowid.RenderWithWeight{W: 1}}, &gowid.ContainerWidget{text.New("y"), gowid.RenderWithWeight{W: 1}}, } w := New(subs) sz := gowid.RenderBox{C: 2, R: 1} c := w.Render(sz, gowid.Focused, gwtest.D) assert.Equal(t, "xy", c.String()) evright := gwtest.CursorRight() acc := w.UserInput(evright, sz, gowid.Focused, gwtest.D) // Nothing in here should accept the input, so it should bubble back up assert.False(t, acc) } type renderWithUnitsMax struct { gowid.RenderWithUnits gowid.RenderMax } func TestColumns6(t *testing.T) { h := renderWithUnitsMax{ RenderWithUnits: gowid.RenderWithUnits{1}, } f := fill.New(' ') subs := []gowid.IContainerWidget{ &gowid.ContainerWidget{f, h}, &gowid.ContainerWidget{button.NewBare(text.New("1")), gowid.RenderWithWeight{W: 4}}, &gowid.ContainerWidget{f, h}, &gowid.ContainerWidget{button.NewBare(text.New("0.000000")), gowid.RenderWithWeight{W: 8}}, &gowid.ContainerWidget{f, h}, &gowid.ContainerWidget{button.NewBare(text.New("192.168.44.123")), gowid.RenderWithWeight{W: 14}}, &gowid.ContainerWidget{f, h}, &gowid.ContainerWidget{button.NewBare(text.New("192.168.44.213")), gowid.RenderWithWeight{W: 14}}, &gowid.ContainerWidget{f, h}, &gowid.ContainerWidget{button.NewBare(text.New("TFTP")), gowid.RenderWithWeight{W: 6}}, &gowid.ContainerWidget{f, h}, &gowid.ContainerWidget{button.NewBare(text.New("77")), gowid.RenderWithWeight{W: 7}}, &gowid.ContainerWidget{f, h}, &gowid.ContainerWidget{button.NewBare(text.New("Read Request, File: C:\\IBMTCPIP\\lccm.1, Transfer type: octet")), gowid.RenderWithWeight{W: 60}}, &gowid.ContainerWidget{f, h}, } w := New(subs) sz := gowid.RenderFlowWith{C: 158} c := w.Render(sz, gowid.Focused, gwtest.D) assert.Equal(t, " 1 0.000000 192.168.44.123 192.168.44.213 TFTP 77 Read Request, File: C:\\IBMTCPIP\\lccm.1, Transfer type: octet ", c.String()) } func TestColumns7(t *testing.T) { d2 := weightupto(1, 2) // weight 1, max 2 d3 := weightupto(1, 3) // weight 1, max 3 d4 := weightupto(1, 4) // weight 1, max 4 sz4 := gowid.RenderFlowWith{C: 4} sz6 := gowid.RenderFlowWith{C: 6} subs := []gowid.IContainerWidget{ &gowid.ContainerWidget{text.New("aa"), d2}, &gowid.ContainerWidget{text.New("bb"), d2}, } w := New(subs) c := w.Render(sz4, gowid.Focused, gwtest.D) assert.Equal(t, "aabb", c.String()) subs = []gowid.IContainerWidget{ &gowid.ContainerWidget{text.New("aa"), d3}, &gowid.ContainerWidget{text.New("bb"), d3}, } w = New(subs) c = w.Render(sz4, gowid.Focused, gwtest.D) assert.Equal(t, "aabb", c.String()) c = w.Render(sz6, gowid.Focused, gwtest.D) assert.Equal(t, "aa bb ", c.String()) subs = []gowid.IContainerWidget{ &gowid.ContainerWidget{text.New("aaaa"), d4}, &gowid.ContainerWidget{text.New("bb"), d2}, } w = New(subs) c = w.Render(sz6, gowid.Focused, gwtest.D) assert.Equal(t, "aaaabb", c.String()) subs = []gowid.IContainerWidget{ &gowid.ContainerWidget{text.New("aaaa"), weight(1)}, &gowid.ContainerWidget{text.New("bb"), weight(1)}, } w = New(subs) c = w.Render(sz6, gowid.Focused, gwtest.D) assert.Equal(t, "aaabb \na ", c.String()) } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: gowid-1.4.0/widgets/dialog/000077500000000000000000000000001426234454000155365ustar00rootroot00000000000000gowid-1.4.0/widgets/dialog/dialog.go000066400000000000000000000304411426234454000173260ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. // Package dialog provides a modal dialog widget with support for ok/cancel. package dialog import ( "fmt" "github.com/gcla/gowid" "github.com/gcla/gowid/widgets/button" "github.com/gcla/gowid/widgets/cellmod" "github.com/gcla/gowid/widgets/columns" "github.com/gcla/gowid/widgets/divider" "github.com/gcla/gowid/widgets/framed" "github.com/gcla/gowid/widgets/hpadding" "github.com/gcla/gowid/widgets/overlay" "github.com/gcla/gowid/widgets/pile" "github.com/gcla/gowid/widgets/shadow" "github.com/gcla/gowid/widgets/styled" "github.com/gcla/gowid/widgets/text" tcell "github.com/gdamore/tcell/v2" ) //====================================================================== type IWidget interface { gowid.IWidget gowid.ISettableComposite // Not ICompositeWidget - no SubWidgetSize GetNoFunction() gowid.IWidgetChangedCallback EscapeCloses() bool IsOpen() bool SetOpen(open bool, app gowid.IApp) SavedSubWidget() gowid.IWidget SetSavedSubWidget(w gowid.IWidget, app gowid.IApp) SavedContainer() gowid.ISettableComposite SetSavedContainer(c gowid.ISettableComposite, app gowid.IApp) Width() gowid.IWidgetDimension SetWidth(gowid.IWidgetDimension, gowid.IApp) Height() gowid.IWidgetDimension SetHeight(gowid.IWidgetDimension, gowid.IApp) } type ISwitchFocus interface { IsSwitchFocus() bool SwitchFocus(gowid.IApp) } type IModal interface { IsModal() bool } type IMaximizer interface { IsMaxed() bool Maximize(gowid.IApp) Unmaximize(gowid.IApp) } // Widget - represents a modal dialog. The bottom widget is rendered // without the focus at full size. The bottom widget is rendered between a // horizontal and vertical padding widget set up with the sizes provided. // type Widget struct { gowid.IWidget Options Options savedSubWidgetWidget gowid.IWidget savedContainer gowid.ISettableComposite content *pile.Widget contentWrapper *gowid.ContainerWidget open bool maxer Maximizer NoFunction gowid.IWidgetChangedCallback Callbacks *gowid.Callbacks } var _ gowid.IWidget = (*Widget)(nil) var _ IWidget = (*Widget)(nil) var _ IMaximizer = (*Widget)(nil) var _ ISwitchFocus = (*Widget)(nil) var _ IModal = (*Widget)(nil) type Options struct { Buttons []Button NoShadow bool NoEscapeClose bool ButtonStyle gowid.ICellStyler BackgroundStyle gowid.ICellStyler BorderStyle gowid.ICellStyler FocusOnWidget bool NoFrame bool Modal bool TabToButtons bool StartIdx int } type Button struct { Msg string Action gowid.IWidgetChangedCallback } var Quit, Exit, CloseD, Cancel Button var OkCancel, ExitCancel, CloseOnly, NoButtons []Button func init() { Quit = Button{ Msg: "Quit", Action: gowid.MakeWidgetCallback("quit", gowid.WidgetChangedFunction(gowid.QuitFn)), } Exit = Button{ Msg: "Exit", Action: gowid.MakeWidgetCallback("exit", gowid.WidgetChangedFunction(gowid.QuitFn)), } CloseD = Button{ Msg: "Close", } Cancel = Button{ Msg: "Cancel", } OkCancel = []Button{ Button{ Msg: "Ok", Action: gowid.MakeWidgetCallback("okcancel", gowid.WidgetChangedFunction(gowid.QuitFn)), }, Cancel, } ExitCancel = []Button{Exit, Cancel} CloseOnly = []Button{CloseD} } type SolidFunction func(gowid.Cell, gowid.Selector) gowid.Cell func (f SolidFunction) Transform(c gowid.Cell, focus gowid.Selector) gowid.Cell { return f(c, focus) } // For callback registration type OpenCloseCB struct{} type SavedSubWidget struct{} type SavedContainer struct{} var ( DefaultBackground = gowid.NewUrwidColor("white") DefaultButton = gowid.NewUrwidColor("dark blue") DefaultButtonText = gowid.NewUrwidColor("yellow") DefaultText = gowid.NewUrwidColor("black") ) func New(content gowid.IWidget, opts ...Options) *Widget { var res *Widget var opt Options if len(opts) > 0 { opt = opts[0] } buttonStyle, backgroundStyle, borderStyle := opt.ButtonStyle, opt.BackgroundStyle, opt.BorderStyle if buttonStyle == nil { buttonStyle = gowid.MakeStyledPaletteEntry(DefaultButtonText, DefaultButton, gowid.StyleNone) } if backgroundStyle == nil { backgroundStyle = gowid.MakeStyledPaletteEntry(DefaultText, DefaultBackground, gowid.StyleNone) } if borderStyle == nil { borderStyle = gowid.MakeStyledPaletteEntry(DefaultButton, DefaultBackground, gowid.StyleNone) } colsW := make([]gowid.IContainerWidget, 0) pileW := make([]interface{}, 0) wrapper := &gowid.ContainerWidget{content, gowid.RenderWithWeight{W: 1}} pileW = append(pileW, wrapper) if len(opts) > 0 { for i, b := range opts[0].Buttons { bw := button.New(text.New(b.Msg)) if b.Action == nil { bw.OnClick(gowid.WidgetCallback{fmt.Sprintf("cb-%d", i), func(app gowid.IApp, widget gowid.IWidget) { res.Close(app) }}) } else { bw.OnClick(b.Action) } colsW = append(colsW, &gowid.ContainerWidget{ hpadding.New( styled.NewExt(bw, backgroundStyle, buttonStyle), gowid.HAlignMiddle{}, gowid.RenderFixed{}, ), gowid.RenderWithWeight{W: 1}, }, ) } } if len(colsW) > 0 { cols := columns.New(colsW, columns.Options{ StartColumn: opt.StartIdx * 2, }) pileW = append(pileW, styled.New( divider.NewUnicodeAlt(), borderStyle, ), cols, ) } dialogContent := pile.NewFlow(pileW...) var d gowid.IWidget = dialogContent if !opt.NoFrame { frameOpts := framed.Options{ Frame: framed.UnicodeAltFrame, Style: borderStyle, } d = framed.New(d, frameOpts) } d = cellmod.Opaque( styled.New( d, backgroundStyle, ), ) if !opt.NoShadow { d = shadow.New(d, 1) } res = &Widget{ IWidget: d, contentWrapper: wrapper, content: dialogContent, Options: opt, Callbacks: gowid.NewCallbacks(), } if !opt.FocusOnWidget { res.FocusOnButtons(nil) } return res } func (w *Widget) String() string { return fmt.Sprintf("dialog") } func (w *Widget) SubWidget() gowid.IWidget { return w.IWidget } func (w *Widget) SetSubWidget(inner gowid.IWidget, app gowid.IApp) { w.IWidget = inner } func (w *Widget) GetNoFunction() gowid.IWidgetChangedCallback { return gowid.WidgetCallback{"no", func(app gowid.IApp, widget gowid.IWidget) { w.Close(app) }} } func (w *Widget) EscapeCloses() bool { return !w.Options.NoEscapeClose } func (w *Widget) IsModal() bool { return w.Options.Modal } func (w *Widget) SwitchFocus(app gowid.IApp) { f := w.content.Focus() if f == 0 { w.FocusOnButtons(app) } else { w.FocusOnContent(app) } } func (w *Widget) IsSwitchFocus() bool { return w.Options.TabToButtons } func (w *Widget) IsOpen() bool { return w.open } func (w *Widget) SetOpen(open bool, app gowid.IApp) { prev := w.open w.open = open if prev != w.open { gowid.RunWidgetCallbacks(w.Callbacks, OpenCloseCB{}, app, w) } } func (w *Widget) IsMaxed() bool { return w.maxer.Maxed } func (w *Widget) Maximize(app gowid.IApp) { w.maxer.Maximize(w, app) } func (w *Widget) Unmaximize(app gowid.IApp) { w.maxer.Unmaximize(w, app) } func (w *Widget) SavedSubWidget() gowid.IWidget { return w.savedSubWidgetWidget } func (w *Widget) SetSavedSubWidget(w2 gowid.IWidget, app gowid.IApp) { w.savedSubWidgetWidget = w2 gowid.RunWidgetCallbacks(w.Callbacks, SavedSubWidget{}, app, w) } func (w *Widget) SavedContainer() gowid.ISettableComposite { return w.savedContainer } func (w *Widget) SetSavedContainer(c gowid.ISettableComposite, app gowid.IApp) { w.savedContainer = c gowid.RunWidgetCallbacks(w.Callbacks, SavedContainer{}, app, w) } func (w *Widget) OnOpenClose(f gowid.IWidgetChangedCallback) { gowid.AddWidgetCallback(w.Callbacks, OpenCloseCB{}, f) } func (w *Widget) RemoveOnOpenClose(f gowid.IIdentity) { gowid.RemoveWidgetCallback(w.Callbacks, OpenCloseCB{}, f) } func (w *Widget) UserInput(ev interface{}, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) bool { return UserInput(w, ev, size, focus, app) } func (w *Widget) Open(container gowid.ISettableComposite, width gowid.IWidgetDimension, app gowid.IApp) { Open(w, container, width, app) } func (w *Widget) Close(app gowid.IApp) { Close(w, app) } // Open the dialog at the top-level of the widget hierarchy which is the App - it itself // is an IComposite // func (w *Widget) OpenGlobally(width gowid.IWidgetDimension, app gowid.IApp) { w.Open(app, width, app) } func (w *Widget) Width() gowid.IWidgetDimension { return w.SavedContainer().SubWidget().(*overlay.Widget).Width() } func (w *Widget) SetWidth(d gowid.IWidgetDimension, app gowid.IApp) { w.SavedContainer().SubWidget().(*overlay.Widget).SetWidth(d, app) } func (w *Widget) Height() gowid.IWidgetDimension { return w.SavedContainer().SubWidget().(*overlay.Widget).Height() } func (w *Widget) SetHeight(d gowid.IWidgetDimension, app gowid.IApp) { w.SavedContainer().SubWidget().(*overlay.Widget).SetHeight(d, app) } func (w *Widget) SetContentWidth(d gowid.IWidgetDimension, app gowid.IApp) { w.contentWrapper.D = d } func (w *Widget) FocusOnButtons(app gowid.IApp) { w.content.SetFocus(app, len(w.content.SubWidgets())-1) } func (w *Widget) FocusOnContent(app gowid.IApp) { w.content.SetFocus(app, 0) } //'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' func Close(w IWidget, app gowid.IApp) { w.SavedContainer().SetSubWidget(w.SavedSubWidget(), app) w.SetOpen(false, app) } func Open(w IOpenExt, container gowid.ISettableComposite, width gowid.IWidgetDimension, app gowid.IApp) { OpenExt(w, container, width, gowid.RenderFlow{}, app) } type IOpenExt interface { gowid.IWidget SetSavedSubWidget(w gowid.IWidget, app gowid.IApp) SetSavedContainer(c gowid.ISettableComposite, app gowid.IApp) SetOpen(open bool, app gowid.IApp) SetContentWidth(w gowid.IWidgetDimension, app gowid.IApp) } func OpenExt(w IOpenExt, container gowid.ISettableComposite, width gowid.IWidgetDimension, height gowid.IWidgetDimension, app gowid.IApp) { ov := overlay.New(w, container.SubWidget(), gowid.VAlignMiddle{}, height, // Intended to mean use as much vertical space as you need gowid.HAlignMiddle{}, width, overlay.Options{ IgnoreLowerStyle: true, }) if _, ok := width.(gowid.IRenderFixed); ok { w.SetContentWidth(gowid.RenderFixed{}, app) // fixed or weight:1, ratio:0.5 } else { w.SetContentWidth(gowid.RenderWithWeight{W: 1}, app) // fixed or weight:1, ratio:0.5 } w.SetSavedSubWidget(container.SubWidget(), app) w.SetSavedContainer(container, app) container.SetSubWidget(ov, app) w.SetOpen(true, app) } func UserInput(w IWidget, ev interface{}, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) bool { var res bool if w.IsOpen() { if evk, ok := ev.(*tcell.EventKey); ok { switch { case evk.Key() == tcell.KeyCtrlC || evk.Key() == tcell.KeyEsc: if w.EscapeCloses() { w.GetNoFunction().Changed(app, w) res = true } } } if !res { res = gowid.UserInputIfSelectable(w.SubWidget(), ev, size, focus, app) } if !res { if evk, ok := ev.(*tcell.EventKey); ok { switch { case evk.Key() == tcell.KeyRune && evk.Rune() == 'z': if w, ok := w.(IMaximizer); ok { if w.IsMaxed() { w.Unmaximize(app) } else { w.Maximize(app) } res = true } case evk.Key() == tcell.KeyTab: if w, ok := w.(ISwitchFocus); ok { w.SwitchFocus(app) res = true } } } } if w, ok := w.(IModal); ok { if w.IsModal() { res = true } } } else { res = gowid.UserInputIfSelectable(w.SubWidget(), ev, size, focus, app) } return res } //====================================================================== type Maximizer struct { Maxed bool Width gowid.IWidgetDimension Height gowid.IWidgetDimension } func (m *Maximizer) Maximize(w IWidget, app gowid.IApp) bool { if m.Maxed { return false } m.Width = w.Width() m.Height = w.Height() w.SetWidth(gowid.RenderWithRatio{R: 1.0}, app) w.SetHeight(gowid.RenderWithRatio{R: 1.0}, app) m.Maxed = true return true } func (m *Maximizer) Unmaximize(w IWidget, app gowid.IApp) bool { if !m.Maxed { return false } w.SetWidth(m.Width, app) w.SetHeight(m.Height, app) m.Maxed = false return true } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: gowid-1.4.0/widgets/disable/000077500000000000000000000000001426234454000157025ustar00rootroot00000000000000gowid-1.4.0/widgets/disable/disable.go000066400000000000000000000040741426234454000176410ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. // Package disable provides a widget that forces its inner widget to be disable (or enabled). package disable import ( "fmt" "github.com/gcla/gowid" ) //====================================================================== // If you would like a non-selectable widget like TextWidget to be selectable // in some context, wrap it in Widget // type Widget struct { gowid.IWidget *gowid.Callbacks gowid.SubWidgetCallbacks isDisabled bool } func New(w gowid.IWidget) *Widget { return NewWith(w, true) } func NewDisabled(w gowid.IWidget) *Widget { return NewWith(w, true) } func NewEnabled(w gowid.IWidget) *Widget { return NewWith(w, false) } func NewWith(w gowid.IWidget, isDisabled bool) *Widget { res := &Widget{ IWidget: w, isDisabled: isDisabled, } res.SubWidgetCallbacks = gowid.SubWidgetCallbacks{CB: &res.Callbacks} var _ gowid.ICompositeWidget = res return res } func (w *Widget) Enable() { w.isDisabled = false } func (w *Widget) Disable() { w.isDisabled = true } func (w *Widget) Set(val bool) { w.isDisabled = val } func (w *Widget) String() string { return fmt.Sprintf("disabled[d=%v,%v]", w.isDisabled, w.SubWidget()) } func (w *Widget) SubWidget() gowid.IWidget { return w.IWidget } func (w *Widget) SetSubWidget(wi gowid.IWidget, app gowid.IApp) { w.IWidget = wi gowid.RunWidgetCallbacks(w.Callbacks, gowid.SubWidgetCB{}, app, w) } func (w *Widget) SubWidgetSize(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.IRenderSize { return size } func (w *Widget) Selectable() bool { return !w.isDisabled && w.SubWidget().Selectable() } func (w *Widget) UserInput(ev interface{}, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) bool { if w.isDisabled { return false } return w.SubWidget().UserInput(ev, size, focus, app) } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: gowid-1.4.0/widgets/divider/000077500000000000000000000000001426234454000157255ustar00rootroot00000000000000gowid-1.4.0/widgets/divider/divider.go000066400000000000000000000057171426234454000177140ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. // Package divider provides a widget that draws a dividing line between other widgets. package divider import ( "fmt" "runtime" "github.com/gcla/gowid" ) //====================================================================== var ( HorizontalLine = '━' AltHorizontalLine = '▀' ) func init() { switch runtime.GOOS { case "windows": HorizontalLine = '─' AltHorizontalLine = HorizontalLine case "darwin": AltHorizontalLine = HorizontalLine } } type IDivider interface { Opts() Options } type IWidget interface { gowid.IWidget IDivider } type Widget struct { opts Options gowid.RejectUserInput gowid.NotSelectable } type Options struct { Chr rune Above, Below int } func New(opts ...Options) *Widget { var opt Options if len(opts) == 0 { opt = Options{ Chr: '-', } } else { opt = opts[0] } res := &Widget{ opts: opt, } var _ IWidget = res return res } func NewAscii() *Widget { return New(Options{ Chr: '-', }) } func NewBlank() *Widget { return New(Options{ Chr: ' ', }) } func NewUnicode() *Widget { return New(Options{ Chr: HorizontalLine, }) } func NewUnicodeAlt() *Widget { return New(Options{ Chr: AltHorizontalLine, }) } func (w *Widget) String() string { return fmt.Sprintf("div[%c]", w.Opts().Chr) } func (w *Widget) Opts() Options { return w.opts } func (w *Widget) RenderSize(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.IRenderBox { return RenderSize(w, size, focus, app) } func (w *Widget) Render(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.ICanvas { return Render(w, size, focus, app) } //'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' func RenderSize(w IDivider, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.IRenderBox { if flow, ok := size.(gowid.IRenderFlowWith); !ok { panic(gowid.WidgetSizeError{Widget: w, Size: size, Required: "gowid.IRenderFlowWith"}) } else { return gowid.RenderBox{C: flow.FlowColumns(), R: w.Opts().Above + w.Opts().Below + 1} } } func Render(w IDivider, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.ICanvas { flow, ok := size.(gowid.IRenderFlowWith) if !ok { panic(gowid.WidgetSizeError{Widget: w, Size: size, Required: "gowid.IRenderFlowWith"}) } div := gowid.CellFromRune(w.Opts().Chr) divArr := make([]gowid.Cell, flow.FlowColumns()) for i := 0; i < flow.FlowColumns(); i++ { divArr[i] = div } res := gowid.NewCanvas() for i := 0; i < w.Opts().Above; i++ { res.AppendLine([]gowid.Cell{}, false) } res.AppendLine(divArr, false) for i := 0; i < w.Opts().Below; i++ { res.AppendLine([]gowid.Cell{}, false) } res.AlignRight() return res } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: gowid-1.4.0/widgets/divider/divider_test.go000066400000000000000000000026231426234454000207440ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source code is governed by the MIT license // that can be found in the LICENSE file. package divider import ( "strings" "testing" "github.com/gcla/gowid" "github.com/gcla/gowid/gwtest" log "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" ) func TestDivider1(t *testing.T) { w := New(Options{Chr: '-', Above: 0, Below: 0}) c1 := w.Render(gowid.RenderFlowWith{C: 5}, gowid.Focused, gwtest.D) assert.Equal(t, c1.String(), "-----") w2 := New(Options{Chr: 'x', Above: 1, Below: 2}) c2 := w2.Render(gowid.RenderFlowWith{C: 5}, gowid.Focused, gwtest.D) assert.Equal(t, c2.String(), " \nxxxxx\n \n ") assert.Panics(t, func() { w.Render(gowid.RenderBox{C: 5, R: 3}, gowid.Focused, gwtest.D) }) assert.Panics(t, func() { w.Render(gowid.RenderFixed{}, gowid.Focused, gwtest.D) }) } func TestCanvas20(t *testing.T) { widget1 := New(Options{Chr: '-', Above: 1, Below: 2}) canvas1 := widget1.Render(gowid.RenderFlowWith{C: 6}, gowid.NotSelected, gwtest.D) log.Infof("Widget20 is %v", widget1) log.Infof("Canvas20 is %s", canvas1.String()) res := strings.Join([]string{" ", "------", " ", " "}, "\n") if res != canvas1.String() { t.Errorf("Failed") } } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: gowid-1.4.0/widgets/edit/000077500000000000000000000000001426234454000152245ustar00rootroot00000000000000gowid-1.4.0/widgets/edit/edit.go000066400000000000000000000403661426234454000165110ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. // Package edit provides an editable text field widget with support for password hiding. package edit import ( "fmt" "io" "unicode" "unicode/utf8" "github.com/gcla/gowid" "github.com/gcla/gowid/gwutil" "github.com/gcla/gowid/widgets/text" tcell "github.com/gdamore/tcell/v2" "github.com/pkg/errors" ) //====================================================================== // IEdit is an interface to be implemented by a text editing widget. A suitable implementation // will be able to defer to RenderEdit() in its Render() function. type IEdit interface { text.ISimple IMask text.ICursor Caption() string MakeText() text.IWidget } type IMask interface { UseMask() bool MaskChr() rune } type IPaste interface { PasteState(...bool) bool AddKey(*tcell.EventKey) GetKeys() []*tcell.EventKey } type Mask struct { Chr rune Enable bool } // For callback registration type Text struct{} type Caption struct{} type Cursor struct{} func DisabledMask() Mask { return Mask{Chr: 'x', Enable: false} } func MakeMask(chr rune) Mask { return Mask{Chr: chr, Enable: true} } func (m Mask) UseMask() bool { return m.Enable } func (m Mask) MaskChr() rune { return m.Chr } type IWidget interface { IEdit text.IChrAt LinesFromTop() int SetLinesFromTop(int, gowid.IApp) UpLines(size gowid.IRenderSize, doPage bool, app gowid.IApp) bool DownLines(size gowid.IRenderSize, doPage bool, app gowid.IApp) bool } type IReadOnly interface { IsReadOnly() bool } type Widget struct { IMask caption string text string paste bool readonly bool pastedKeys []*tcell.EventKey cursorPos int linesFromTop int Callbacks *gowid.Callbacks gowid.IsSelectable } var _ fmt.Stringer = (*Widget)(nil) var _ io.Reader = (*Widget)(nil) var _ gowid.IWidget = (*Widget)(nil) var _ IPaste = (*Widget)(nil) var _ IReadOnly = (*Widget)(nil) // Writer embeds an EditWidget and provides the io.Writer interface. An gowid.IApp needs to // be provided too because the widget's SetText() function requires it in order to issue // callbacks when the text changes. type Writer struct { *Widget gowid.IApp } type Options struct { Caption string Text string Mask IMask ReadOnly bool } func New(args ...Options) *Widget { var opt Options if len(args) > 0 { opt = args[0] } if opt.Mask == nil { opt.Mask = DisabledMask() } res := &Widget{ IMask: opt.Mask, caption: opt.Caption, text: opt.Text, readonly: opt.ReadOnly, cursorPos: len(opt.Text), pastedKeys: make([]*tcell.EventKey, 0, 100), linesFromTop: 0, Callbacks: gowid.NewCallbacks(), } return res } func (w *Widget) String() string { return fmt.Sprintf("edit") } func (w *Widget) IsReadOnly() bool { return w.readonly } func (w *Widget) SetReadOnly(v bool, _ gowid.IApp) { w.readonly = v } // Set content from array func (w *Writer) Write(p []byte) (n int, err error) { w.SetText(string(p), w.IApp) w.cursorPos = 0 w.linesFromTop = 0 return len(p), nil } // Set array from widget content func (w *Widget) Read(p []byte) (n int, err error) { pl := len(p) num := copy(p, w.text[:]) if num < pl { return num, io.EOF } else { return num, nil } } func (w *Widget) Text() string { return w.text } var InvalidRuneIndex error = errors.New("Invalid rune index for string") // TODO - this isn't ideal- if called in a loop, it would be quadratic. func (w *Widget) ChrAt(i int) rune { j := 0 for _, char := range w.caption { if j == i { return char } j++ } for _, char := range w.text { if j == i { return char } j++ } panic(errors.WithStack(gowid.WithKVs(InvalidRuneIndex, map[string]interface{}{"index": i, "text": w.text}))) } func (w *Widget) SetText(text string, app gowid.IApp) { w.text = text wid := utf8.RuneCountInString(w.text) if w.cursorPos > wid { w.SetCursorPos(wid, app) } gowid.RunWidgetCallbacks(w.Callbacks, Text{}, app, w) } func (w *Widget) LinesFromTop() int { return w.linesFromTop } func (w *Widget) SetLinesFromTop(l int, app gowid.IApp) { w.linesFromTop = l } func (w *Widget) Caption() string { return w.caption } func (w *Widget) SetCaption(text string, app gowid.IApp) { w.caption = text gowid.RunWidgetCallbacks(w.Callbacks, Caption{}, app, w) } //func (w *Widget) PasteState(b ...bool) []*tcell.EventKey { func (w *Widget) PasteState(b ...bool) bool { if len(b) > 0 { w.paste = b[0] //if w.paste { w.pastedKeys = w.pastedKeys[:0] //} } return w.paste } func (w *Widget) AddKey(ev *tcell.EventKey) { w.pastedKeys = append(w.pastedKeys, ev) } func (w *Widget) GetKeys() []*tcell.EventKey { return w.pastedKeys } func (w *Widget) CursorEnabled() bool { return w.cursorPos != -1 } func (w *Widget) SetCursorDisabled() { w.cursorPos = -1 } // TODO - weird that you could call set to 0, then get and it would be > 0... func (w *Widget) CursorPos() int { if !w.CursorEnabled() { panic(errors.New("Cursor is disabled, cannot return!")) } return w.cursorPos } func (w *Widget) SetCursorPos(pos int, app gowid.IApp) { pos = gwutil.Min(pos, utf8.RuneCountInString(w.Text())) w.cursorPos = pos gowid.RunWidgetCallbacks(w.Callbacks, Cursor{}, app, w) } func (w *Widget) OnTextSet(cb gowid.IWidgetChangedCallback) { gowid.AddWidgetCallback(w.Callbacks, Text{}, cb) } func (w *Widget) RemoveOnTextSet(cb gowid.IIdentity) { gowid.RemoveWidgetCallback(w.Callbacks, Text{}, cb) } func (w *Widget) OnCaptionSet(cb gowid.IWidgetChangedCallback) { gowid.AddWidgetCallback(w.Callbacks, Caption{}, cb) } func (w *Widget) RemoveOnCaptionSet(cb gowid.IIdentity) { gowid.RemoveWidgetCallback(w.Callbacks, Caption{}, cb) } func (w *Widget) OnCursorPosSet(cb gowid.IWidgetChangedCallback) { gowid.AddWidgetCallback(w.Callbacks, Cursor{}, cb) } func (w *Widget) RemoveOnCursorPosSet(cb gowid.IIdentity) { gowid.RemoveWidgetCallback(w.Callbacks, Cursor{}, cb) } func (w *Widget) RenderSize(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.IRenderBox { return gowid.CalculateRenderSizeFallback(w, size, focus, app) } func (w *Widget) Render(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.ICanvas { return Render(w, size, focus, app) } func (w *Widget) MakeText() text.IWidget { return MakeText(w) } func (w *Widget) UserInput(ev interface{}, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) bool { return UserInput(w, ev, size, focus, app) } func (w *Widget) DownLines(size gowid.IRenderSize, doPage bool, app gowid.IApp) bool { return DownLines(w, size, doPage, app) } func (w *Widget) UpLines(size gowid.IRenderSize, doPage bool, app gowid.IApp) bool { return UpLines(w, size, doPage, app) } func (w *Widget) CalculateTopMiddleBottom(size gowid.IRenderSize) (int, int, int) { return CalculateTopMiddleBottom(w, size) } //'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' func Render(w IWidget, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.ICanvas { twc := w.MakeText() c := twc.Render(size, focus, app) return c } func MakeText(w IWidget) text.IWidget { var txt string if w.UseMask() { arr := make([]rune, len(w.Text())) for i := 0; i < len(arr); i++ { arr[i] = w.MaskChr() } txt = string(arr) } else { txt = w.Text() } //txt = w.Caption() + "\u00A0" + txt txt = w.Caption() + txt tw := text.New(txt) tw.SetLinesFromTop(w.LinesFromTop(), nil) cu := &text.SimpleCursor{-1} cu.SetCursorPos(w.CursorPos()+utf8.RuneCountInString(w.Caption()), nil) twc := &text.WidgetWithCursor{tw, cu} return twc } func CalculateTopMiddleBottom(w IWidget, size gowid.IRenderSize) (int, int, int) { twc := w.MakeText() return text.CalculateTopMiddleBottom(twc, size) } // Return true if done func DownLines(w IWidget, size gowid.IRenderSize, doPage bool, app gowid.IApp) bool { prev := w.CursorPos() twc := w.MakeText() caplen := utf8.RuneCountInString(w.Caption()) // This incorporates the caption too cols, ok := size.(gowid.IColumns) if !ok { panic(gowid.WidgetSizeError{Widget: w, Size: size, Required: "gowid.IColumns"}) } layout := text.MakeTextLayout(twc.Content(), cols.Columns(), text.WrapAny, gowid.HAlignLeft{}) ccol, crow := text.GetCoordsFromCursorPos(w.CursorPos()+caplen, cols.Columns(), layout, w) offset := 1 if rows, ok := size.(gowid.IRows); ok && doPage { if crow < w.LinesFromTop()+rows.Rows()-1 { // if the cursor is in the middle somewhere, jump to the bottom offset = w.LinesFromTop() + rows.Rows() - (crow + 1) } else { // otherwise jump a "page" worth offset = rows.Rows() } } targetRow := crow + offset newCursorPos := text.GetCursorPosFromCoords(ccol, targetRow, layout, w) - caplen if newCursorPos < 0 { return false } else { w.SetCursorPos(newCursorPos, app) // TODO - ugly to check for render type like this if box, ok := size.(gowid.IRenderBox); ok && (targetRow >= box.BoxRows()+w.LinesFromTop()) { // assumes we render fixed not flow w.SetLinesFromTop(w.LinesFromTop()+offset, app) } //w.linesFromTop += 1 return (prev != w.CursorPos()) } } // Return true if done func UpLines(w IWidget, size gowid.IRenderSize, doPage bool, app gowid.IApp) bool { caplen := utf8.RuneCountInString(w.Caption()) prev := w.CursorPos() twc := w.MakeText() cols, isColumns := size.(gowid.IColumns) if !isColumns { panic(gowid.WidgetSizeError{Widget: w, Size: size, Required: "gowid.IColumns"}) } layout := text.MakeTextLayout(twc.Content(), cols.Columns(), text.WrapAny, gowid.HAlignLeft{}) ccol, crow := text.GetCoordsFromCursorPos(w.CursorPos()+caplen, cols.Columns(), layout, w) if crow <= 0 { return false } else { offset := 1 if rows, ok := size.(gowid.IRows); ok && doPage { if crow == w.LinesFromTop() { offset = rows.Rows() } else { offset = crow - w.LinesFromTop() } } targetCol := gwutil.Max(crow-offset, 0) newCursorPos := text.GetCursorPosFromCoords(ccol, targetCol, layout, w) - caplen if newCursorPos < 0 { return false } else { w.SetCursorPos(newCursorPos, app) if targetCol < w.LinesFromTop() { w.SetLinesFromTop(targetCol, app) } return (prev != w.CursorPos()) } } } func keyIsPasteable(ev *tcell.EventKey) bool { switch ev.Key() { case tcell.KeyEnter, tcell.Key(' '), tcell.KeyRune: return true default: return false } } func isReadOnly(w interface{}) bool { readOnly := false if ro, ok := w.(IReadOnly); ok { readOnly = ro.IsReadOnly() } return readOnly } func pasteableKeyInput(w IWidget, ev *tcell.EventKey, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) bool { if isReadOnly(w) { return false } handled := true switch ev.Key() { case tcell.KeyEnter: r := []rune(w.Text()) w.SetText(string(r[0:w.CursorPos()])+string('\n')+string(r[w.CursorPos():]), app) w.SetCursorPos(w.CursorPos()+1, app) case tcell.Key(' '): r := []rune(w.Text()) w.SetText(string(r[0:w.CursorPos()])+" "+string(r[w.CursorPos():]), app) w.SetCursorPos(w.CursorPos()+1, app) case tcell.KeyRune: // TODO: this is lame. Inserting a character is O(n) where n is length // of text. I should switch this to use the two stack model for edited // text. txt := w.Text() r := []rune(txt) cpos := w.CursorPos() rhs := make([]rune, len(r)-cpos) copy(rhs, r[cpos:]) w.SetText(string(append(append(r[:cpos], ev.Rune()), rhs...)), app) w.SetCursorPos(w.CursorPos()+1, app) default: handled = false } return handled } func UserInput(w IWidget, ev interface{}, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) bool { handled := true doup := false dodown := false recalcLinesFromTop := false readOnly := isReadOnly(w) switch ev := ev.(type) { case *tcell.EventMouse: switch ev.Buttons() { case tcell.WheelUp: doup = true case tcell.WheelDown: dodown = true case tcell.Button1: twc := w.MakeText() cols, isColumns := size.(gowid.IColumns) if !isColumns { panic(gowid.WidgetSizeError{Widget: w, Size: size, Required: "gowid.IColumns"}) } layout := text.MakeTextLayout(twc.Content(), cols.Columns(), text.WrapAny, gowid.HAlignLeft{}) mx, my := ev.Position() cursorPos := text.GetCursorPosFromCoords(mx, my+w.LinesFromTop(), layout, w) - (utf8.RuneCountInString(w.Caption())) if cursorPos < 0 { handled = false } else { w.SetCursorPos(cursorPos, app) handled = true } default: handled = false } case *tcell.EventPaste: if wp, ok := w.(IPaste); ok { if ev.Start() { wp.PasteState(true) } else { evs := wp.GetKeys() wp.PasteState(false) for _, ev := range evs { pasteableKeyInput(w, ev, size, focus, app) } } } case *tcell.EventKey: handled = false if wp, ok := w.(IPaste); ok { if wp.PasteState() && keyIsPasteable(ev) && !readOnly { wp.AddKey(ev) handled = true } } if !handled { handled = pasteableKeyInput(w, ev, size, focus, app) } if !handled { handled = true switch ev.Key() { case tcell.KeyPgUp: handled = w.UpLines(size, true, app) case tcell.KeyUp, tcell.KeyCtrlP: doup = true case tcell.KeyDown, tcell.KeyCtrlN: dodown = true case tcell.KeyPgDn: handled = w.DownLines(size, true, app) case tcell.KeyLeft, tcell.KeyCtrlB: if w.CursorPos() > 0 { w.SetCursorPos(w.CursorPos()-1, app) } else { handled = false } case tcell.KeyRight, tcell.KeyCtrlF: if w.CursorPos() < utf8.RuneCountInString(w.Text()) { w.SetCursorPos(w.CursorPos()+1, app) } else { handled = false } case tcell.KeyBackspace, tcell.KeyBackspace2: if !readOnly { if w.CursorPos() > 0 { pos := w.CursorPos() w.SetCursorPos(w.CursorPos()-1, app) r := []rune(w.Text()) w.SetText(string(r[0:pos-1])+string(r[pos:]), app) } } case tcell.KeyDelete, tcell.KeyCtrlD: if !readOnly { if w.CursorPos() < utf8.RuneCountInString(w.Text()) { r := []rune(w.Text()) w.SetText(string(r[0:w.CursorPos()])+string(r[w.CursorPos()+1:]), app) } } case tcell.KeyCtrlK: if !readOnly { r := []rune(w.Text()) w.SetText(string(r[0:w.CursorPos()]), app) } case tcell.KeyCtrlU: if !readOnly { r := []rune(w.Text()) w.SetText(string(r[w.CursorPos():]), app) w.SetCursorPos(0, app) } case tcell.KeyHome: w.SetCursorPos(0, app) w.SetLinesFromTop(0, app) case tcell.KeyCtrlW: if !readOnly { txt := []rune(w.Text()) origcp := w.CursorPos() cp := origcp for cp > 0 && unicode.IsSpace(txt[cp-1]) { cp-- } for cp > 0 && !unicode.IsSpace(txt[cp-1]) { cp-- } if cp != origcp { w.SetText(string(txt[0:cp])+string(txt[origcp:]), app) w.SetCursorPos(cp, app) } } case tcell.KeyCtrlA: // Would be nice to use a slice here, something that doesn't copy // TODO: terrible O(n) behavior :-( txt := w.Text() i := w.CursorPos() j := 0 lastnl := false curstart := 0 for _, ch := range txt { if lastnl { curstart = j } lastnl = (ch == '\n') if i == j { break } j += 1 } w.SetCursorPos(curstart, app) recalcLinesFromTop = true case tcell.KeyEnd: w.SetCursorPos(utf8.RuneCountInString(w.Text()), app) recalcLinesFromTop = true case tcell.KeyCtrlE: // TODO: terrible O(n) behavior :-( txt := w.Text() i := w.CursorPos() j := 0 checknl := false for _, ch := range txt { if i == j { checknl = true } j += 1 if checknl { if ch == '\n' { break } i += 1 } } w.SetCursorPos(i, app) recalcLinesFromTop = true default: handled = false } } } if doup { handled = w.UpLines(size, false, app) } if dodown { handled = w.DownLines(size, false, app) } box, ok := size.(gowid.IRenderBox) if recalcLinesFromTop && ok { twc := w.MakeText() caplen := utf8.RuneCountInString(w.Caption()) layout := text.MakeTextLayout(twc.Content(), box.BoxColumns(), text.WrapAny, gowid.HAlignLeft{}) _, crow := text.GetCoordsFromCursorPos(w.CursorPos()+caplen, box.BoxColumns(), layout, w) w.SetLinesFromTop(gwutil.Max(0, crow-(box.BoxRows()-1)), app) } return handled } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: gowid-1.4.0/widgets/edit/edit_test.go000066400000000000000000000142711426234454000175440ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source code is governed by the MIT license // that can be found in the LICENSE file. package edit import ( "io" "strings" "testing" "unicode/utf8" "github.com/gcla/gowid" "github.com/gcla/gowid/gwtest" tcell "github.com/gdamore/tcell/v2" "github.com/stretchr/testify/assert" ) func evclick(x, y int) *tcell.EventMouse { return tcell.NewEventMouse(x, y, tcell.Button1, 0) } func evunclick(x, y int) *tcell.EventMouse { return tcell.NewEventMouse(x, y, tcell.ButtonNone, 0) } func TestType1(t *testing.T) { w := New(Options{Caption: "", Text: "hi: 现在 abc"}) sz := gowid.RenderFlowWith{C: 15} c1 := w.Render(sz, gowid.Focused, gwtest.D) assert.Equal(t, "hi: 现在 abc ", c1.String()) evq := tcell.NewEventKey(tcell.KeyRune, 'q', tcell.ModNone) evdel := tcell.NewEventKey(tcell.KeyDelete, 'X', tcell.ModNone) w.SetCursorPos(0, gwtest.D) w.UserInput(evq, sz, gowid.Focused, gwtest.D) c1 = w.Render(sz, gowid.Focused, gwtest.D) assert.Equal(t, "qhi: 现在 abc ", c1.String()) w.SetCursorPos(6, gwtest.D) w.UserInput(evq, sz, gowid.Focused, gwtest.D) c1 = w.Render(sz, gowid.Focused, gwtest.D) assert.Equal(t, "qhi: 现q在 abc ", c1.String()) w.UserInput(evq, sz, gowid.Focused, gwtest.D) c1 = w.Render(sz, gowid.Focused, gwtest.D) assert.Equal(t, "qhi: 现qq在 abc", c1.String()) w.UserInput(evdel, sz, gowid.Focused, gwtest.D) c1 = w.Render(sz, gowid.Focused, gwtest.D) assert.Equal(t, "qhi: 现qq abc ", c1.String()) w.SetReadOnly(true, gwtest.D) w.UserInput(evq, sz, gowid.Focused, gwtest.D) c1 = w.Render(sz, gowid.Focused, gwtest.D) assert.Equal(t, "qhi: 现qq abc ", c1.String()) w.UserInput(evdel, sz, gowid.Focused, gwtest.D) c1 = w.Render(sz, gowid.Focused, gwtest.D) assert.Equal(t, "qhi: 现qq abc ", c1.String()) w.SetReadOnly(false, gwtest.D) w.UserInput(evq, sz, gowid.Focused, gwtest.D) c1 = w.Render(sz, gowid.Focused, gwtest.D) assert.Equal(t, "qhi: 现qqq abc ", c1.String()) } func TestRender1(t *testing.T) { w := New(Options{Caption: "", Text: "abcde现fgh"}) sz := gowid.RenderFlowWith{C: 6} c1 := w.Render(sz, gowid.Focused, gwtest.D) assert.Equal(t, "abcde \n现fgh ", c1.String()) } func TestType2(t *testing.T) { w := New(Options{Caption: "", Text: "hi: abc"}) sz := gowid.RenderFlowWith{C: 15} c1 := w.Render(sz, gowid.Focused, gwtest.D) assert.Equal(t, "hi: abc ", c1.String()) evq := tcell.NewEventKey(tcell.KeyRune, 'q', tcell.ModNone) w.SetCursorPos(0, gwtest.D) w.UserInput(evq, sz, gowid.Focused, gwtest.D) c1 = w.Render(sz, gowid.Focused, gwtest.D) assert.Equal(t, "qhi: abc ", c1.String()) } func TestMove1(t *testing.T) { w := New(Options{Caption: "hi: ", Text: "now\n\nis the time"}) sz := gowid.RenderFlowWith{C: 12} c1 := w.Render(sz, gowid.Focused, gwtest.D) assert.Equal(t, "hi: now \n \nis the time ", c1.String()) w.SetCursorPos(0, gwtest.D) assert.Equal(t, 0, w.CursorPos()) w.UserInput(gwtest.CursorDown(), sz, gowid.Focused, gwtest.D) assert.Equal(t, 4, w.CursorPos()) w.UserInput(gwtest.CursorRight(), sz, gowid.Focused, gwtest.D) assert.Equal(t, 5, w.CursorPos()) w.UserInput(gwtest.CursorLeft(), sz, gowid.Focused, gwtest.D) assert.Equal(t, 4, w.CursorPos()) w.UserInput(gwtest.CursorDown(), sz, gowid.Focused, gwtest.D) assert.Equal(t, 5, w.CursorPos()) } func TestLong1(t *testing.T) { w := New(Options{Caption: "现: ", Text: "现在是hetimeforallgoodmentocometotheaid\n\nofthe"}) sz := gowid.RenderFlowWith{C: 12} c1 := w.Render(sz, gowid.Focused, gwtest.D) assert.Equal(t, "现: 现在是he\ntimeforallgo\nodmentocomet\notheaid \n \nofthe ", c1.String()) clickat := func(x, y int) { w.UserInput(evclick(x, y), sz, gowid.Focused, gwtest.D) gwtest.D.SetLastMouseState(gowid.MouseState{true, false, false}) w.UserInput(evunclick(x, y), sz, gowid.Focused, gwtest.D) gwtest.D.SetLastMouseState(gowid.MouseState{false, false, false}) } clickat(4, 0) assert.Equal(t, 0, w.CursorPos()) w.SetCursorPos(1, gwtest.D) assert.Equal(t, 1, w.CursorPos()) x := utf8.RuneCountInString(w.Text()) w.SetCursorPos(500, gwtest.D) assert.Equal(t, x, w.CursorPos()) clickat(11, 0) assert.Equal(t, 4, w.CursorPos()) clickat(0, 1) assert.Equal(t, 5, w.CursorPos()) w.UserInput(gwtest.CursorLeft(), sz, gowid.Focused, gwtest.D) assert.Equal(t, 4, w.CursorPos()) w.UserInput(gwtest.CursorDown(), sz, gowid.Focused, gwtest.D) assert.Equal(t, 16, w.CursorPos()) } func TestEdit1(t *testing.T) { w := New(Options{Caption: "hi: ", Text: "hello world"}) sz := gowid.RenderFlowWith{C: 20} c1 := w.Render(sz, gowid.Focused, gwtest.D) assert.Equal(t, c1.String(), "hi: hello world ") tset := false w.OnTextSet(gowid.WidgetCallback{"cb", func(app gowid.IApp, w gowid.IWidget) { tset = true }}) cset := false w.OnCaptionSet(gowid.WidgetCallback{"cb", func(app gowid.IApp, w gowid.IWidget) { cset = true }}) pset := false w.OnCursorPosSet(gowid.WidgetCallback{"cb", func(app gowid.IApp, w gowid.IWidget) { pset = true }}) _, err := io.Copy(&Writer{w, gwtest.D}, strings.NewReader("goodbye everyone")) assert.NoError(t, err) assert.Equal(t, tset, true) tset = false c2 := w.Render(sz, gowid.Focused, gwtest.D) assert.Equal(t, c2.String(), "hi: goodbye everyone") assert.Equal(t, w.CursorEnabled(), true) assert.Equal(t, c2.CursorEnabled(), true) _, err = io.Copy(&Writer{w, gwtest.D}, strings.NewReader("multi\nline\ntest")) assert.NoError(t, err) assert.Equal(t, tset, true) tset = false c3 := w.Render(gowid.RenderFlowWith{C: 10}, gowid.Focused, gwtest.D) assert.Equal(t, c3.String(), "hi: multi \nline \ntest ") w.SetCaption("bye", gwtest.D) assert.Equal(t, w.Caption(), "bye") assert.Equal(t, cset, true) assert.Equal(t, w.CursorPos(), 0) w.SetCursorPos(1, gwtest.D) assert.Equal(t, w.CursorPos(), 1) assert.Equal(t, pset, true) x := len(w.Text()) w.SetCursorPos(500, gwtest.D) assert.Equal(t, w.CursorPos(), x) // Make sure no crashes! for i := 0; i < 100; i++ { w.UserInput(gwtest.CursorDown(), gowid.RenderBox{C: 20, R: 5}, gowid.Focused, gwtest.D) } } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: gowid-1.4.0/widgets/fill/000077500000000000000000000000001426234454000152255ustar00rootroot00000000000000gowid-1.4.0/widgets/fill/fill.go000066400000000000000000000053231426234454000165050ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. // Package fill provides a widget that can be filled with a styled rune. package fill import ( "fmt" "github.com/gcla/gowid" ) //====================================================================== type ISolidFill interface { Cell() gowid.Cell } type IWidget interface { gowid.IWidget ISolidFill } type Widget struct { cell gowid.Cell gowid.RejectUserInput gowid.NotSelectable } func New(chr rune) *Widget { res := &Widget{cell: gowid.CellFromRune(chr)} var _ gowid.IWidget = res return res } func NewEmpty() *Widget { return NewSolidFromCell(gowid.Cell{}) } func NewSolidFromCell(cell gowid.Cell) *Widget { res := &Widget{cell: cell} var _ gowid.IWidget = res return res } func (w *Widget) String() string { r := ' ' if w.Cell().HasRune() { r = w.Cell().Rune() } return fmt.Sprintf("fill[%c]", r) } func (w *Widget) Cell() gowid.Cell { return w.cell } func (w *Widget) SetCell(c gowid.Cell, app gowid.IApp) { w.cell = c } func (w *Widget) RenderSize(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.IRenderBox { return RenderSize(w, size, focus, app) } func (w *Widget) Render(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.ICanvas { return Render(w, size, focus, app) } //'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' func RenderSize(w interface{}, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.IRenderBox { cols, haveCols := size.(gowid.IColumns) rows, haveRows := size.(gowid.IRows) switch { case haveCols && haveRows: return gowid.RenderBox{C: cols.Columns(), R: rows.Rows()} case haveCols: return gowid.RenderBox{C: cols.Columns(), R: 1} default: panic(gowid.WidgetSizeError{Widget: w, Size: size}) } } func Render(w ISolidFill, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.ICanvas { // If a col and row not provided, what can I choose?? cols2, ok := size.(gowid.IColumns) if !ok { panic(gowid.WidgetSizeError{Widget: w, Size: size, Required: "gowid.IColumns"}) } cols := cols2.Columns() rows := 1 if irows, ok2 := size.(gowid.IRows); ok2 { rows = irows.Rows() } fill := w.Cell() fillArr := make([]gowid.Cell, cols) for i := 0; i < cols; i++ { fillArr[i] = fill } res := gowid.NewCanvas() if rows > 0 { res.AppendLine(fillArr, false) for i := 0; i < rows-1; i++ { res.Lines = append(res.Lines, make([]gowid.Cell, 0, 120)) } } res.AlignRightWith(w.Cell()) return res } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: gowid-1.4.0/widgets/fill/fill_test.go000066400000000000000000000037451426234454000175520ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. package fill import ( "strings" "testing" "github.com/gcla/gowid" "github.com/gcla/gowid/gwtest" log "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" ) func TestSolidFill1(t *testing.T) { w := New('G') c1 := w.Render(gowid.RenderFlowWith{C: 5}, gowid.Focused, gwtest.D) assert.Equal(t, c1.String(), "GGGGG") c2 := w.Render(gowid.RenderBox{C: 3, R: 3}, gowid.Focused, gwtest.D) assert.Equal(t, c2.String(), "GGG\nGGG\nGGG") assert.Panics(t, func() { w.Render(gowid.RenderFixed{}, gowid.Focused, gwtest.D) }) gwtest.RenderBoxManyTimes(t, w, 0, 10, 0, 10) } func TestCanvas21(t *testing.T) { widget1 := New('x') canvas1 := widget1.Render(gowid.RenderFlowWith{C: 6}, gowid.NotSelected, gwtest.D) log.Infof("Widget is %v", widget1) log.Infof("Canvas is %s", canvas1.String()) res := strings.Join([]string{"xxxxxx"}, "\n") if res != canvas1.String() { t.Errorf("Failed") } } func TestCanvas22(t *testing.T) { widget1 := New('x') canvas1 := widget1.Render(gowid.RenderBox{C: 6, R: 3}, gowid.NotSelected, gwtest.D) log.Infof("Widget is %v", widget1) log.Infof("Canvas is %s", canvas1.String()) res := strings.Join([]string{"xxxxxx", "xxxxxx", "xxxxxx"}, "\n") if res != canvas1.String() { t.Errorf("Failed") } } func TestCanvas29(t *testing.T) { widget1 := New('x') canvas1 := widget1.Render(gowid.RenderFlowWith{C: 6}, gowid.NotSelected, gwtest.D) canvas2 := gowid.NewCanvasOfSize(6, 1) canvas2.Lines[0][1] = canvas2.Lines[0][1].WithRune('#') canvas1.MergeUnder(canvas2, 0, 0, false) log.Infof("Widget is %v", widget1) log.Infof("Canvas is %s", canvas1.String()) res := strings.Join([]string{"x#xxxx"}, "\n") if res != canvas1.String() { t.Errorf("Failed") } } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: gowid-1.4.0/widgets/fixedadapter/000077500000000000000000000000001426234454000167375ustar00rootroot00000000000000gowid-1.4.0/widgets/fixedadapter/fixedadapter.go000066400000000000000000000070641426234454000217350ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source code is governed by the MIT license // that can be found in the LICENSE file. // Package fixedadaptor provides a widget that will render a fixed widget when // supplied with a box context. package fixedadapter import ( "fmt" "github.com/gcla/gowid" tcell "github.com/gdamore/tcell/v2" ) //====================================================================== // Wraps a Fixed widget and turns it into a Box widget. If rendered in a Fixed // context, render as normal. If rendered in a Box context, render as a Fixed // widget, then either truncate or grow the resulting canvas to meet the // box size requirement. // type Widget struct { gowid.IWidget *gowid.Callbacks gowid.SubWidgetCallbacks } func New(inner gowid.IWidget) *Widget { res := &Widget{ IWidget: inner, } res.SubWidgetCallbacks = gowid.SubWidgetCallbacks{CB: &res.Callbacks} var _ gowid.IWidget = res var _ gowid.IComposite = res return res } func (w *Widget) String() string { return fmt.Sprintf("fixedadapter[%v]", w.SubWidget()) } func (w *Widget) SubWidget() gowid.IWidget { return w.IWidget } func (w *Widget) SetSubWidget(wi gowid.IWidget, app gowid.IApp) { w.IWidget = wi gowid.RunWidgetCallbacks(w, gowid.SubWidgetCB{}, app, w) } func (w *Widget) SubWidgetSize(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.IRenderSize { return SubWidgetSize(w, size, focus, app) } func (w *Widget) RenderSize(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.IRenderBox { return RenderSize(w, size, focus, app) } func (w *Widget) Render(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.ICanvas { return Render(w, size, focus, app) } func (w *Widget) UserInput(ev interface{}, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) bool { return UserInput(w, ev, size, focus, app) } //'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' func RenderSize(w gowid.IWidget, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.IRenderBox { return gowid.CalculateRenderSizeFallback(w, size, focus, app) } func SubWidgetSize(w interface{}, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.IRenderSize { return gowid.RenderFixed{} } func Render(w gowid.IComposite, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.ICanvas { res := w.SubWidget().Render(SubWidgetSize(w, size, focus, app), focus, app) cols, ok := size.(gowid.IColumns) if !ok { panic(gowid.WidgetSizeError{Widget: w, Size: size, Required: "gowid.IColumns"}) } // Make sure that if we're rendered as a box, we have enough rows. gowid.FixCanvasHeight(res, size) res.ExtendRight(gowid.EmptyLine(cols.Columns() - res.BoxColumns())) return res } // Ensure that a valid mouse interaction with a flow widget will result in a // mouse interaction with the subwidget func UserInput(w gowid.ICompositeWidget, ev interface{}, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) bool { if evm, ok := ev.(*tcell.EventMouse); ok { box := RenderSize(w, size, focus, app) mx, my := evm.Position() if (my < box.BoxRows() && my >= 0) && (mx < box.BoxColumns() && mx >= 0) { return gowid.UserInputIfSelectable(w.SubWidget(), ev, SubWidgetSize(w, size, focus, app), focus, app) } } else { return gowid.UserInputIfSelectable(w.SubWidget(), ev, SubWidgetSize(w, size, focus, app), focus, app) } return false } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: gowid-1.4.0/widgets/fixedadapter/fixedadapter_test.go000066400000000000000000000014061426234454000227660ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source code is governed by the MIT license // that can be found in the LICENSE file. package fixedadapter import ( "testing" "github.com/gcla/gowid" "github.com/gcla/gowid/gwtest" "github.com/gcla/gowid/widgets/checkbox" "github.com/stretchr/testify/assert" ) func TestBoxify1(t *testing.T) { w := checkbox.New(false) c1 := w.Render(gowid.RenderFixed{}, gowid.Focused, gwtest.D) assert.Equal(t, "[ ]", c1.String()) w2 := New(w) c2 := w2.Render(gowid.RenderBox{C: 5, R: 3}, gowid.Focused, gwtest.D) assert.Equal(t, "[ ] \n \n ", c2.String()) } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: gowid-1.4.0/widgets/framed/000077500000000000000000000000001426234454000155355ustar00rootroot00000000000000gowid-1.4.0/widgets/framed/framed.go000066400000000000000000000227551426234454000173350ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. // Package framed provides a widget that draws a frame around an inner widget. package framed import ( "fmt" "runtime" "github.com/gcla/gowid" "github.com/gcla/gowid/gwutil" "github.com/gcla/gowid/widgets/text" tcell "github.com/gdamore/tcell/v2" "github.com/mattn/go-runewidth" ) //====================================================================== type FrameRunes struct { Tl, Tr, Bl, Br rune T, B, L, R rune } var ( AsciiFrame = FrameRunes{'-', '-', '-', '-', '-', '-', '|', '|'} UnicodeFrame = FrameRunes{'┏', '┓', '┗', '┛', '━', '━', '┃', '┃'} UnicodeAltFrame = FrameRunes{'▛', '▜', '▙', '▟', '▀', '▄', '▌', '▐'} UnicodeAlt2Frame = FrameRunes{'╔', '╗', '╚', '╝', '═', '═', '║', '║'} SpaceFrame = FrameRunes{' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '} nullFrame = FrameRunes{} ) func init() { switch runtime.GOOS { case "windows": UnicodeFrame = FrameRunes{'┌', '┐', '└', '┘', '─', '─', '│', '│'} UnicodeAltFrame = UnicodeFrame case "darwin": UnicodeAltFrame = UnicodeFrame } } type IFramed interface { Opts() Options } type IWidget interface { gowid.ICompositeWidget IFramed } type Widget struct { gowid.IWidget // Embed for Selectable method Params Options *gowid.Callbacks gowid.SubWidgetCallbacks } type Options struct { Frame FrameRunes Title string TitleWidget gowid.IWidget Style gowid.ICellStyler } // For callback identification type Title struct{} func New(inner gowid.IWidget, opts ...Options) *Widget { var opt Options if len(opts) == 0 { opt = Options{ Frame: AsciiFrame, } } else { opt = opts[0] } // Likely means nil value was used for 8-field struct - so this is a heuristic if opt.Frame == nullFrame { opt.Frame = AsciiFrame } res := &Widget{ IWidget: inner, Params: opt, } res.SubWidgetCallbacks = gowid.SubWidgetCallbacks{CB: &res.Callbacks} var _ gowid.IWidget = res var _ IWidget = res return res } func NewUnicode(inner gowid.IWidget) *Widget { params := Options{ Frame: UnicodeFrame, } return New(inner, params) } func NewUnicodeAlt(inner gowid.IWidget) *Widget { params := Options{ Frame: UnicodeAltFrame, } return New(inner, params) } func NewUnicodeAlt2(inner gowid.IWidget) *Widget { params := Options{ Frame: UnicodeAlt2Frame, } return New(inner, params) } func NewSpace(inner gowid.IWidget) *Widget { params := Options{ Frame: SpaceFrame, } return New(inner, params) } func (w *Widget) String() string { return fmt.Sprintf("framed[%v]", w.SubWidget()) } func (w *Widget) SubWidget() gowid.IWidget { return w.IWidget } func (w *Widget) SetSubWidget(wi gowid.IWidget, app gowid.IApp) { w.IWidget = wi gowid.RunWidgetCallbacks(w, gowid.SubWidgetCB{}, app, w) } func (w *Widget) OnSetTitle(f gowid.IWidgetChangedCallback) { gowid.AddWidgetCallback(w, Title{}, f) } func (w *Widget) RemoveOnSetAlign(f gowid.IIdentity) { gowid.RemoveWidgetCallback(w, Title{}, f) } // Call from Render thread func (w *Widget) SetTitle(title string, app gowid.IApp) { w.Params.Title = title w.Params.TitleWidget = nil gowid.RunWidgetCallbacks(w, Title{}, app, w) } func (w *Widget) GetTitle() string { return w.Params.Title } func (w *Widget) SetTitleWidget(widget gowid.IWidget, app gowid.IApp) { w.Params.TitleWidget = widget gowid.RunWidgetCallbacks(w, Title{}, app, w) } func (w *Widget) GetTitleWidget() gowid.IWidget { return w.Params.TitleWidget } func (w *Widget) Opts() Options { return w.Params } func (w *Widget) SubWidgetSize(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.IRenderSize { return SubWidgetSize(w, size, focus, app) } func (w *Widget) RenderSize(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.IRenderBox { return RenderSize(w, size, focus, app) } func (w *Widget) UserInput(ev interface{}, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) bool { return UserInput(w, ev, size, focus, app) } func (w *Widget) Render(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.ICanvas { return Render(w, size, focus, app) } //====================================================================== func frameWidth(w IFramed) int { return runewidth.RuneWidth(w.Opts().Frame.L) + runewidth.RuneWidth(w.Opts().Frame.R) } func RenderSize(w IWidget, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.IRenderBox { ss := w.SubWidgetSize(size, focus, app) sdim := w.SubWidget().RenderSize(ss, focus, app) extraRows := 0 if w.Opts().Frame.T != 0 { extraRows++ } if w.Opts().Frame.B != 0 { extraRows++ } return gowid.RenderBox{C: sdim.BoxColumns() + frameWidth(w), R: sdim.BoxRows() + extraRows} } func SubWidgetSize(w IFramed, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.IRenderSize { var newSize gowid.IRenderSize switch sz := size.(type) { case gowid.IRenderFixed: newSize = gowid.RenderFixed{} case gowid.IRenderBox: // Note - this assumes wid(Bl) == wid(L) and so on... yuck extraRows := 0 if w.Opts().Frame.T != 0 { extraRows++ } if w.Opts().Frame.B != 0 { extraRows++ } newSize = gowid.RenderBox{C: gwutil.Max(sz.BoxColumns()-frameWidth(w), 0), R: gwutil.Max(sz.BoxRows()-extraRows, 0)} case gowid.IRenderFlowWith: newSize = gowid.RenderFlowWith{C: gwutil.Max(sz.FlowColumns()-frameWidth(w), 0)} default: panic(gowid.WidgetSizeError{Widget: w, Size: size}) } return newSize } func Render(w IWidget, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.ICanvas { res := gowid.NewCanvas() tmp := gowid.NewCanvas() newSize := w.SubWidgetSize(size, focus, app) innerCanvas := w.SubWidget().Render(newSize, focus, app) innerLines := innerCanvas.BoxRows() maxCol := innerCanvas.BoxColumns() frame := w.Opts().Frame empty := FrameRunes{} if frame == empty { frame = AsciiFrame } var tophor, bottomhor, leftver, rightver gowid.Cell dummy := gowid.CellFromRune(' ') tophor = gowid.CellFromRune(frame.T) bottomhor = gowid.CellFromRune(frame.B) leftver = gowid.CellFromRune(frame.L) rightver = gowid.CellFromRune(frame.R) if w.Opts().Style != nil { f, _, _ := w.Opts().Style.GetStyle(app) fc := gowid.IColorToTCell(f, gowid.ColorNone, app.GetColorMode()) tophor = tophor.WithForegroundColor(fc) bottomhor = bottomhor.WithForegroundColor(fc) leftver = leftver.WithForegroundColor(fc) rightver = rightver.WithForegroundColor(fc) } titleWidget := w.Opts().TitleWidget if titleWidget == nil && w.Opts().Title != "" { titleWidget = text.New(" " + w.Opts().Title + " ") } leftverCanvas := gowid.NewCanvas() rightverCanvas := gowid.NewCanvas() leftverLine := make([]gowid.Cell, 0) rightverLine := make([]gowid.Cell, 0) leftverLine = append(leftverLine, leftver) wid := runewidth.RuneWidth(leftver.Rune()) for i := 1; i < wid; i++ { leftverLine = append(leftverLine, dummy) } rightverLine = append(rightverLine, rightver) wid = runewidth.RuneWidth(rightver.Rune()) for i := 1; i < wid; i++ { rightverLine = append(rightverLine, dummy) } for i := 0; i < innerLines; i++ { leftverCanvas.AppendLine(leftverLine, false) rightverCanvas.AppendLine(rightverLine, false) tmp.AppendLine(make([]gowid.Cell, 0), false) } tophorArr := make([]gowid.Cell, 0) bottomhorArr := make([]gowid.Cell, 0) for i := 0; i < maxCol+frameWidth(w); i++ { tophorArr = append(tophorArr, tophor) bottomhorArr = append(bottomhorArr, bottomhor) } tmp.AppendRight(leftverCanvas, false) tmp.AppendRight(innerCanvas, true) tmp.AppendRight(rightverCanvas, false) if w.Opts().Frame.T != 0 { res.AppendLine(tophorArr, false) } res.AppendBelow(tmp, true, false) if w.Opts().Frame.B != 0 { res.AppendLine(bottomhorArr, false) } if w.Opts().Frame.T != 0 { res.Lines[0][0] = res.Lines[0][0].WithRune(frame.Tl) wid = runewidth.RuneWidth(frame.Tr) res.Lines[0][len(res.Lines[0])-wid] = res.Lines[0][len(res.Lines[0])-wid].WithRune(frame.Tr) } if w.Opts().Frame.B != 0 { resl := res.BoxRows() res.Lines[resl-1][0] = res.Lines[resl-1][0].WithRune(frame.Bl) wid = runewidth.RuneWidth(frame.Br) res.Lines[resl-1][len(res.Lines[0])-wid] = res.Lines[resl-1][len(res.Lines[0])-wid].WithRune(frame.Br) if titleWidget != nil { titleCanvas := titleWidget.Render(gowid.RenderFixed{}, gowid.NotSelected, app) res.MergeUnder(titleCanvas, 2, 0, false) } } return res } func UserInput(w IWidget, ev interface{}, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) bool { subSize := w.SubWidgetSize(size, focus, app) newev := gowid.TranslatedMouseEvent(ev, -1, -1) if _, ok := ev.(*tcell.EventMouse); ok { ss := w.SubWidget().RenderSize(subSize, focus, app) newev2, _ := newev.(*tcell.EventMouse) // gcla tcell todo - clumsy mx, my := newev2.Position() if my < ss.BoxRows() && my >= 0 && mx < ss.BoxColumns() && mx >= 0 { return gowid.UserInputIfSelectable(w.SubWidget(), newev, subSize, focus, app) } } else { return gowid.UserInputIfSelectable(w.SubWidget(), newev, subSize, focus, app) } return false } //====================================================================== type FrameIfSelectedForCopy struct{} var _ gowid.IClipboardSelected = FrameIfSelectedForCopy{} func (r FrameIfSelectedForCopy) AlterWidget(w gowid.IWidget, app gowid.IApp) gowid.IWidget { return New(w) } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: gowid-1.4.0/widgets/framed/framed_test.go000066400000000000000000000034701426234454000203650ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source code is governed by the MIT license // that can be found in the LICENSE file. package framed import ( "strings" "testing" "github.com/gcla/gowid" "github.com/gcla/gowid/gwtest" "github.com/gcla/gowid/widgets/text" log "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" ) //====================================================================== func TestCanvas8(t *testing.T) { widget1 := text.New("hello") opts := Options{ Frame: FrameRunes{'你', '你', '你', '你', '-', '-', '你', '你'}, } fwidget1 := New(widget1, opts) canvas1 := fwidget1.Render(gowid.RenderFixed{}, gowid.NotSelected, gwtest.D) res := strings.Join([]string{"你-----你", "你hello你", "你-----你"}, "\n") assert.Equal(t, res, canvas1.String()) } func TestCanvas9(t *testing.T) { widget1 := text.New("hello transubstantiation good") fwidget1 := New(widget1) canvas1 := fwidget1.Render(gowid.RenderFlowWith{C: 11}, gowid.NotSelected, gwtest.D) log.Infof("Widget9 is %v", fwidget1) log.Infof("Canvas9 is %s", canvas1.String()) res := strings.Join([]string{"-----------", "|hello tra|", "|nsubstant|", "|iation go|", "|od |", "-----------"}, "\n") assert.Equal(t, res, canvas1.String()) } func TestCanvas10(t *testing.T) { widget1 := text.New("hello transubstantiation good") fwidget1 := New(widget1) canvas1 := fwidget1.Render(gowid.RenderBox{C: 7, R: 5}, gowid.NotSelected, gwtest.D) log.Infof("Widget10 is %v", fwidget1) log.Infof("Canvas10 is %s", canvas1.String()) res := strings.Join([]string{"-------", "|hello|", "| tran|", "|subst|", "-------"}, "\n") assert.Equal(t, res, canvas1.String()) } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: gowid-1.4.0/widgets/grid/000077500000000000000000000000001426234454000152245ustar00rootroot00000000000000gowid-1.4.0/widgets/grid/grid.go000066400000000000000000000270041426234454000165030ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. // Package grid allows widgets to be arranged in rows and columns. package grid import ( "errors" "fmt" "strings" "github.com/gcla/gowid" "github.com/gcla/gowid/gwutil" "github.com/gcla/gowid/vim" "github.com/gcla/gowid/widgets/columns" "github.com/gcla/gowid/widgets/hpadding" "github.com/gcla/gowid/widgets/pile" "github.com/gcla/gowid/widgets/text" "github.com/gcla/gowid/widgets/vpadding" tcell "github.com/gdamore/tcell/v2" ) //====================================================================== type IGrid interface { gowid.IFindNextSelectable gowid.IFocus SubWidgets() []gowid.IWidget GenerateWidgets(size gowid.IRenderSize, attrs gowid.IRenderContext) (pile.IWidget, int) Width() int HSep() int VSep() int HAlign() gowid.IHAlignment Wrap() bool KeyIsUp(*tcell.EventKey) bool KeyIsDown(*tcell.EventKey) bool KeyIsLeft(*tcell.EventKey) bool KeyIsRight(*tcell.EventKey) bool } type IWidget interface { gowid.IWidget IGrid } type Width struct{} type Align struct{} type VSepCB struct{} type HSepCB struct{} // align sets the alignment of the group within the leftover space in the row. type Widget struct { widgets []gowid.IWidget width int hSep int vSep int align gowid.IHAlignment focus int // -1 means nothing selectable wrap bool options Options *gowid.Callbacks gowid.SubWidgetsCallbacks gowid.FocusCallbacks } type Options struct { StartPos int Wrap bool DownKeys []vim.KeyPress UpKeys []vim.KeyPress LeftKeys []vim.KeyPress RightKeys []vim.KeyPress } func New(widgets []gowid.IWidget, width int, hSep int, vSep int, align gowid.IHAlignment, opts ...Options) *Widget { var opt Options if len(opts) > 0 { opt = opts[0] } else { opt = Options{ StartPos: -1, } } if opt.DownKeys == nil { opt.DownKeys = vim.AllDownKeys } if opt.UpKeys == nil { opt.UpKeys = vim.AllUpKeys } if opt.LeftKeys == nil { opt.LeftKeys = vim.AllLeftKeys } if opt.RightKeys == nil { opt.RightKeys = vim.AllRightKeys } res := &Widget{ widgets: widgets, width: width, hSep: hSep, vSep: vSep, align: align, focus: -1, options: opt, } res.SubWidgetsCallbacks = gowid.SubWidgetsCallbacks{CB: &res.Callbacks} res.FocusCallbacks = gowid.FocusCallbacks{CB: &res.Callbacks} if opt.StartPos >= 0 { res.focus = gwutil.Min(opt.StartPos, len(widgets)-1) } else { res.focus, _ = res.FindNextSelectable(1, res.Wrap()) } var _ gowid.IWidget = res var _ gowid.ICompositeMultiple = res var _ IWidget = res return res } func (w *Widget) String() string { cols := make([]string, len(w.widgets)) for i := 0; i < len(cols); i++ { cols[i] = fmt.Sprintf("%v", w.widgets[i]) } return fmt.Sprintf("grid[%s]", strings.Join(cols, ",")) } func (w *Widget) Width() int { return w.width } func (w *Widget) SetWidth(i int, app gowid.IApp) { w.width = i gowid.RunWidgetCallbacks(w.Callbacks, Width{}, app, w) } func (w *Widget) HSep() int { return w.hSep } func (w *Widget) SetHSep(i int, app gowid.IApp) { w.hSep = i gowid.RunWidgetCallbacks(w.Callbacks, HSepCB{}, app, w) } func (w *Widget) VSep() int { return w.vSep } func (w *Widget) SetVSep(i int, app gowid.IApp) { w.vSep = i gowid.RunWidgetCallbacks(w.Callbacks, VSepCB{}, app, w) } func (w *Widget) HAlign() gowid.IHAlignment { return w.align } func (w *Widget) SetHAlign(i gowid.IHAlignment, app gowid.IApp) { w.align = i gowid.RunWidgetCallbacks(w.Callbacks, Align{}, app, w) } func (w *Widget) SubWidgets() []gowid.IWidget { return gowid.CopyWidgets(w.widgets) } func (w *Widget) SetSubWidgets(widgets []gowid.IWidget, app gowid.IApp) { w.widgets = widgets gowid.RunWidgetCallbacks(w.Callbacks, gowid.SubWidgetsCB{}, app, w) } // Tries to set at required index, will choose first selectable from there func (w *Widget) SetFocus(app gowid.IApp, i int) { old := w.focus w.focus = gwutil.Min(gwutil.Max(i, 0), len(w.widgets)-1) if old != w.focus { gowid.RunWidgetCallbacks(w.Callbacks, gowid.FocusCB{}, app, w) } } func (w *Widget) Wrap() bool { return w.options.Wrap } // Tries to set at required index, will choose first selectable from there func (w *Widget) Focus() int { return w.focus } func (w *Widget) Selectable() bool { for _, widget := range w.widgets { if widget.Selectable() { return true } } return false } func (w *Widget) FindNextSelectable(dir gowid.Direction, wrap bool) (int, bool) { return gowid.FindNextSelectableWidget(w.widgets, w.focus, dir, wrap) } func (w *Widget) UserInput(ev interface{}, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) bool { return UserInput(w, ev, size, focus, app) } func (w *Widget) GenerateWidgets(size gowid.IRenderSize, attrs gowid.IRenderContext) (pile.IWidget, int) { return GenerateWidgets(w, size, attrs) } func (w *Widget) RenderSize(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.IRenderBox { return gowid.CalculateRenderSizeFallback(w, size, focus, app) } func (w *Widget) Render(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.ICanvas { return Render(w, size, focus, app) } func (w *Widget) KeyIsUp(evk *tcell.EventKey) bool { return vim.KeyIn(evk, w.options.UpKeys) } func (w *Widget) KeyIsDown(evk *tcell.EventKey) bool { return vim.KeyIn(evk, w.options.DownKeys) } func (w *Widget) KeyIsLeft(evk *tcell.EventKey) bool { return vim.KeyIn(evk, w.options.LeftKeys) } func (w *Widget) KeyIsRight(evk *tcell.EventKey) bool { return vim.KeyIn(evk, w.options.RightKeys) } //'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' func Render(w IWidget, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.ICanvas { pile, _ := w.GenerateWidgets(size, app) res := pile.Render(size, focus, app) return res } // Scroll sequentially through the widgets on mouse scroll events or key up/down func UserInput(w IGrid, ev interface{}, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) bool { subfocus := w.Focus() if !focus.Focus { subfocus = -1 } forChild := false var gen pile.IWidget var cols int scrollDown := false scrollUp := false scrollRight := false scrollLeft := false if evm, ok := ev.(*tcell.EventMouse); ok { // If it's a scroll event, then handle it in the grid, not by delegating to the // construction of piles and columns if evm.Buttons()&(tcell.WheelDown|tcell.WheelUp|tcell.WheelLeft|tcell.WheelRight) == 0 { gen, cols = w.GenerateWidgets(size, app) forChild = gowid.UserInputIfSelectable(gen, ev, size, focus, app) if evm.Buttons() == tcell.Button1 && forChild { pileidx := gen.Focus() if pileidx != -1 { // if ForChild, then it can't be any of the in between text widgets used to pad // things, so don't worry about checking the casts rowx, _ := gen.SubWidgets()[pileidx].(*gowid.ContainerWidget) rowy, _ := rowx.SubWidget().(*hpadding.Widget) row, _ := rowy.IWidget.(*columns.Widget) colidx := row.Focus() w.SetFocus(app, (((pileidx+1)/2)*cols)+((colidx+1)/2)) } } } } else { if subfocus != -1 { forChild = gowid.UserInputIfSelectable(w.SubWidgets()[w.Focus()], ev, size, focus, app) } } if forChild { return true } else { newfocus := -1 if evk, ok := ev.(*tcell.EventKey); ok { switch { case w.KeyIsRight(evk): next, ok := w.FindNextSelectable(1, w.Wrap()) if ok { w.SetFocus(app, next) return true } case w.KeyIsLeft(evk): next, ok := w.FindNextSelectable(-1, w.Wrap()) if ok { w.SetFocus(app, next) return true } case w.KeyIsDown(evk): scrollDown = true case w.KeyIsUp(evk): scrollUp = true } } else if ev2, ok := ev.(*tcell.EventMouse); ok { switch ev2.Buttons() { case tcell.WheelDown: scrollDown = true case tcell.WheelUp: scrollUp = true case tcell.WheelRight: scrollRight = true case tcell.WheelLeft: scrollLeft = true } } if scrollDown || scrollUp || scrollRight || scrollLeft { if gen == nil { _, cols = w.GenerateWidgets(size, app) } if scrollDown { for i := w.Focus() + cols; i < len(w.SubWidgets()); i += cols { if w.SubWidgets()[i].Selectable() { newfocus = i break } } } if scrollUp { for i := w.Focus() - cols; i >= 0; i -= cols { if w.SubWidgets()[i].Selectable() { newfocus = i break } } } if scrollRight { i := ((w.Focus() / cols) * cols) + gwutil.Min(w.Focus()+1, cols-1) j := ((w.Focus() / cols) * cols) + (cols - 1) for ; i <= j; i++ { if w.SubWidgets()[i].Selectable() { newfocus = i break } } } if scrollLeft { i := ((w.Focus() / cols) * cols) + gwutil.Max((w.Focus()%cols)-1, 0) j := ((w.Focus() / cols) * cols) for ; i >= j; i-- { if w.SubWidgets()[i].Selectable() { newfocus = i break } } } if newfocus != -1 { w.SetFocus(app, newfocus) return true } } return false } } // Can't support RenderFixed{} because I need to know how many columns so I can roll over widgets // to the next line. // func GenerateWidgets(w IGrid, size gowid.IRenderSize, attrs gowid.IRenderContext) (pile.IWidget, int) { focusIdx := w.Focus() cols2, isColumns := size.(gowid.IColumns) if !isColumns { panic(errors.New("GridFlow widget must not be rendered in Fixed mode.")) } cols := cols2.Columns() // TODO - what about when cols < vsep? numInRow := (cols - w.HSep()) / (w.Width() + w.HSep()) wWidth := numInRow * w.Width() if numInRow > 0 { wWidth += (numInRow - 1) * w.HSep() } pileWidgets := make([]gowid.IContainerWidget, 0) curInRow := 0 hSepWidget := &gowid.ContainerWidget{text.New(gwutil.StringOfLength(' ', w.HSep())), gowid.RenderWithUnits{U: w.HSep()}} hBlankWidget := &gowid.ContainerWidget{text.New(gwutil.StringOfLength(' ', w.Width())), gowid.RenderWithUnits{U: w.Width()}} vBlank := text.New("") vBlankWidget := vpadding.New(vBlank, gowid.VAlignTop{}, gowid.RenderWithUnits{U: w.VSep()}) curRow := make([]gowid.IContainerWidget, 0) todo := 0 rowFocusIdx := -1 pileFocusIdx := -1 var fakeApp gowid.IApp if len(w.SubWidgets()) > 0 { todo = (((len(w.SubWidgets()) - 1) / numInRow) + 1) * numInRow } firstRow := true for i := 0; i < todo; i++ { if i >= len(w.SubWidgets()) { curRow = append(curRow, hBlankWidget) } else { if i == focusIdx { rowFocusIdx = len(curRow) } curRow = append(curRow, &gowid.ContainerWidget{w.SubWidgets()[i], gowid.RenderWithUnits{U: w.Width()}}) } curInRow += 1 if curInRow == numInRow { cols := columns.New(curRow) alignedColumns := hpadding.New(cols, w.HAlign(), gowid.RenderWithUnits{U: wWidth}) if !firstRow { pileWidgets = append(pileWidgets, &gowid.ContainerWidget{vBlankWidget, gowid.RenderFlow{}}) } pileWidgets = append(pileWidgets, &gowid.ContainerWidget{alignedColumns, gowid.RenderFlow{}}) if rowFocusIdx != -1 { // TODO: wrong wrong wrong // It's not necessarily an IApp e.g. when called from Render - but it's ok because I haven't set // any callbacks cols.SetFocus(fakeApp, rowFocusIdx) rowFocusIdx = -1 pileFocusIdx = len(pileWidgets) - 1 } firstRow = false curInRow = 0 curRow = make([]gowid.IContainerWidget, 0) } else { curRow = append(curRow, hSepWidget) } } pile := pile.New(pileWidgets) if pileFocusIdx != -1 { pile.SetFocus(fakeApp, pileFocusIdx) } return pile, numInRow } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: gowid-1.4.0/widgets/grid/grid_test.go000066400000000000000000000103211426234454000175340ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. package grid import ( "testing" "github.com/gcla/gowid" "github.com/gcla/gowid/gwtest" "github.com/gcla/gowid/widgets/button" "github.com/gcla/gowid/widgets/text" tcell "github.com/gdamore/tcell/v2" "github.com/stretchr/testify/assert" ) func TestGridFlow(t *testing.T) { btns := make([]gowid.IWidget, 0) clicks := make([]*gwtest.ButtonTester, 0) for i := 0; i < 9; i++ { btn := button.New(text.New("abc")) click := &gwtest.ButtonTester{Gotit: false} btn.OnClick(click) btns = append(btns, btn) clicks = append(clicks, click) } gf := New(btns, 5, 1, 1, gowid.HAlignMiddle{}) st1 := " \n \n \n \n " sz := gowid.RenderBox{C: 20, R: 5} c := gf.Render(sz, gowid.Focused, gwtest.D) assert.Equal(t, c.String(), st1) c2 := gf.Render(gowid.RenderFlowWith{C: 20}, gowid.Focused, gwtest.D) assert.Equal(t, c2.String(), st1) assert.Equal(t, gf.Focus(), 0) evspace := tcell.NewEventKey(tcell.KeyRune, ' ', tcell.ModNone) evmdown := tcell.NewEventMouse(1, 1, tcell.WheelDown, 0) evmup := tcell.NewEventMouse(1, 1, tcell.WheelUp, 0) evmright := tcell.NewEventMouse(1, 1, tcell.WheelRight, 0) evmleft := tcell.NewEventMouse(1, 1, tcell.WheelLeft, 0) cbcalled := false gf.OnFocusChanged(gowid.WidgetCallback{"cb", func(app gowid.IApp, w gowid.IWidget) { assert.Equal(t, w, gf) cbcalled = true }}) gf.UserInput(gwtest.CursorRight(), sz, gowid.Focused, gwtest.D) assert.Equal(t, 1, gf.Focus()) assert.Equal(t, true, cbcalled) cbcalled = false gf.UserInput(evspace, sz, gowid.Focused, gwtest.D) assert.Equal(t, 1, gf.Focus()) assert.Equal(t, clicks[1].Gotit, true) assert.Equal(t, false, cbcalled) cbcalled = false clicks[1].Gotit = false gf.UserInput(evmdown, sz, gowid.Focused, gwtest.D) assert.Equal(t, 4, gf.Focus()) assert.Equal(t, true, cbcalled) cbcalled = false gf.UserInput(evmdown, sz, gowid.Focused, gwtest.D) assert.Equal(t, 7, gf.Focus()) assert.Equal(t, true, cbcalled) cbcalled = false gf.UserInput(evmdown, sz, gowid.Focused, gwtest.D) assert.Equal(t, 7, gf.Focus()) assert.Equal(t, false, cbcalled) cbcalled = false gf.UserInput(evmup, sz, gowid.Focused, gwtest.D) assert.Equal(t, 4, gf.Focus()) assert.Equal(t, true, cbcalled) cbcalled = false gf.UserInput(evmup, sz, gowid.Focused, gwtest.D) assert.Equal(t, 1, gf.Focus()) assert.Equal(t, true, cbcalled) cbcalled = false gf.UserInput(evmup, sz, gowid.Focused, gwtest.D) assert.Equal(t, 1, gf.Focus()) assert.Equal(t, false, cbcalled) cbcalled = false gf.UserInput(evmright, sz, gowid.Focused, gwtest.D) assert.Equal(t, 2, gf.Focus()) assert.Equal(t, true, cbcalled) cbcalled = false gf.UserInput(evmright, sz, gowid.Focused, gwtest.D) assert.Equal(t, 2, gf.Focus()) assert.Equal(t, false, cbcalled) cbcalled = false gf.UserInput(gwtest.CursorDown(), sz, gowid.Focused, gwtest.D) assert.Equal(t, 5, gf.Focus()) assert.Equal(t, true, cbcalled) cbcalled = false gf.UserInput(evmleft, sz, gowid.Focused, gwtest.D) assert.Equal(t, 4, gf.Focus()) assert.Equal(t, true, cbcalled) cbcalled = false gf.UserInput(evmleft, sz, gowid.Focused, gwtest.D) assert.Equal(t, 3, gf.Focus()) assert.Equal(t, true, cbcalled) cbcalled = false gf.UserInput(evmleft, sz, gowid.Focused, gwtest.D) assert.Equal(t, 3, gf.Focus()) assert.Equal(t, false, cbcalled) cbcalled = false gf.UserInput(gwtest.CursorLeft(), sz, gowid.Focused, gwtest.D) assert.Equal(t, 2, gf.Focus()) assert.Equal(t, true, cbcalled) cbcalled = false gf.UserInput(gwtest.CursorLeft(), sz, gowid.Focused, gwtest.D) assert.Equal(t, 1, gf.Focus()) assert.Equal(t, true, cbcalled) cbcalled = false gf.UserInput(gwtest.CursorLeft(), sz, gowid.Focused, gwtest.D) assert.Equal(t, 0, gf.Focus()) assert.Equal(t, true, cbcalled) cbcalled = false gf.UserInput(gwtest.CursorLeft(), sz, gowid.Focused, gwtest.D) assert.Equal(t, 0, gf.Focus()) assert.Equal(t, false, cbcalled) cbcalled = false } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: gowid-1.4.0/widgets/holder/000077500000000000000000000000001426234454000155545ustar00rootroot00000000000000gowid-1.4.0/widgets/holder/holder.go000066400000000000000000000025161426234454000173640ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. // Package holder provides a widget that wraps an inner widget, and allows it to be easily swapped for another. package holder import ( "fmt" "github.com/gcla/gowid" ) //====================================================================== // Widget is the gowid analog of urwid's WidgetWrap. type Widget struct { gowid.IWidget *gowid.Callbacks gowid.SubWidgetCallbacks } func New(w gowid.IWidget) *Widget { res := &Widget{ IWidget: w, } res.SubWidgetCallbacks = gowid.SubWidgetCallbacks{CB: &res.Callbacks} var _ gowid.IWidget = res var _ gowid.ICompositeWidget = res return res } func (w *Widget) String() string { return fmt.Sprintf("holder[%v]", w.SubWidget()) } func (w *Widget) SubWidget() gowid.IWidget { return w.IWidget } func (w *Widget) SetSubWidget(wi gowid.IWidget, app gowid.IApp) { w.IWidget = wi gowid.RunWidgetCallbacks(w, gowid.SubWidgetCB{}, app, w) } func (w *Widget) SubWidgetSize(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.IRenderSize { return w.SubWidget().RenderSize(size, focus, app) } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: gowid-1.4.0/widgets/hpadding/000077500000000000000000000000001426234454000160555ustar00rootroot00000000000000gowid-1.4.0/widgets/hpadding/hpadding.go000066400000000000000000000144151426234454000201670ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. // Package hpadding provides a widget that pads an inner widget on the left and right. package hpadding import ( "fmt" "github.com/gcla/gowid" "github.com/gcla/gowid/gwutil" tcell "github.com/gdamore/tcell/v2" ) //====================================================================== type IWidget interface { gowid.ICompositeWidget Align() gowid.IHAlignment Width() gowid.IWidgetDimension } // Widget renders the wrapped widget with the provided // width; if the wrapped widget is a box, or the wrapped widget is to be // packed to a width smaller than specified, the wrapped widget can be // aligned in the middle, left or right type Widget struct { gowid.IWidget alignment gowid.IHAlignment width gowid.IWidgetDimension *gowid.Callbacks gowid.FocusCallbacks gowid.SubWidgetCallbacks } func New(inner gowid.IWidget, alignment gowid.IHAlignment, width gowid.IWidgetDimension) *Widget { res := &Widget{ IWidget: inner, alignment: alignment, width: width, } res.FocusCallbacks = gowid.FocusCallbacks{CB: &res.Callbacks} res.SubWidgetCallbacks = gowid.SubWidgetCallbacks{CB: &res.Callbacks} var _ gowid.IWidget = res return res } func (w *Widget) String() string { return fmt.Sprintf("hpad[%v]", w.SubWidget()) } func (w *Widget) SubWidget() gowid.IWidget { return w.IWidget } func (w *Widget) SetSubWidget(wi gowid.IWidget, app gowid.IApp) { w.IWidget = wi gowid.RunWidgetCallbacks(w.Callbacks, gowid.SubWidgetCB{}, app, w) } func (w *Widget) OnSetAlign(f gowid.IWidgetChangedCallback) { gowid.AddWidgetCallback(w.Callbacks, gowid.HAlignCB{}, f) } func (w *Widget) RemoveOnSetAlign(f gowid.IIdentity) { gowid.RemoveWidgetCallback(w.Callbacks, gowid.HAlignCB{}, f) } func (w *Widget) Align() gowid.IHAlignment { return w.alignment } func (w *Widget) SetAlign(i gowid.IHAlignment, app gowid.IApp) { w.alignment = i gowid.RunWidgetCallbacks(w.Callbacks, gowid.HAlignCB{}, app, w) } func (w *Widget) OnSetHeight(f gowid.IWidgetChangedCallback) { gowid.AddWidgetCallback(w.Callbacks, gowid.WidthCB{}, f) } func (w *Widget) RemoveOnSetHeight(f gowid.IIdentity) { gowid.RemoveWidgetCallback(w.Callbacks, gowid.WidthCB{}, f) } func (w *Widget) Width() gowid.IWidgetDimension { return w.width } func (w *Widget) SetWidth(i gowid.IWidgetDimension, app gowid.IApp) { w.width = i gowid.RunWidgetCallbacks(w.Callbacks, gowid.WidthCB{}, app, w) } func (w *Widget) SubWidgetSize(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.IRenderSize { return SubWidgetSize(w, size, focus, app) } func (w *Widget) RenderSize(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.IRenderBox { return gowid.CalculateRenderSizeFallback(w, size, focus, app) } func (w *Widget) Render(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.ICanvas { return Render(w, size, focus, app) } func (w *Widget) UserInput(ev interface{}, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) bool { return UserInput(w, ev, size, focus, app) } //'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' func SubWidgetSize(w IWidget, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.IRenderSize { size2 := size // If there is a horizontal offset specified, the relative features should reduce the size of the // supplied size i.e. it should be relative to the reduced screen size switch al := w.Align().(type) { case gowid.HAlignLeft: switch s := size.(type) { case gowid.IRenderBox: size2 = gowid.RenderBox{C: s.BoxColumns() - (al.Margin + al.MarginRight), R: s.BoxRows()} case gowid.IRenderFlowWith: size2 = gowid.RenderFlowWith{C: s.FlowColumns() - (al.Margin + al.MarginRight)} default: } default: } return gowid.ComputeHorizontalSubSizeUnsafe(size2, w.Width()) } func Render(w IWidget, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.ICanvas { subSize := w.SubWidgetSize(size, focus, app) c := w.SubWidget().Render(subSize, focus, app) subWidgetMaxColumn := c.BoxColumns() var myCols int if cols, ok := size.(gowid.IColumns); ok { myCols = cols.Columns() } else { myCols = subWidgetMaxColumn } if myCols < subWidgetMaxColumn { // TODO - bad, mandates trimming on right c.TrimRight(myCols) } else if myCols > subWidgetMaxColumn { switch al := w.Align().(type) { case gowid.HAlignRight: c.ExtendLeft(gowid.EmptyLine(myCols - subWidgetMaxColumn)) case gowid.HAlignLeft: l := gwutil.Min(al.Margin, myCols-subWidgetMaxColumn) r := myCols - (l + subWidgetMaxColumn) c.ExtendRight(gowid.EmptyLine(r)) c.ExtendLeft(gowid.EmptyLine(l)) default: // middle r := (myCols - subWidgetMaxColumn) / 2 l := myCols - (subWidgetMaxColumn + r) c.ExtendRight(gowid.EmptyLine(r)) c.ExtendLeft(gowid.EmptyLine(l)) } } gowid.MakeCanvasRightSize(c, size) return c } // UserInput will adjust the input event's x coordinate depending on the input size // and widget alignment. If the input is e.g. IRenderFixed, then no adjustment is // made. func UserInput(w IWidget, ev interface{}, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) bool { subSize := w.SubWidgetSize(size, focus, app) ss := w.SubWidget().RenderSize(subSize, focus, app) cols := ss.BoxColumns() cols2, ok := size.(gowid.IColumns) var xd int if ok { switch al := w.Align().(type) { case gowid.HAlignRight: xd = -(cols2.Columns() - cols) case gowid.HAlignMiddle: r := (cols2.Columns() - cols) / 2 l := cols2.Columns() - (cols + r) xd = -l case gowid.HAlignLeft: if al.Margin+cols <= cols2.Columns() { xd = -al.Margin } else { xd = -gwutil.Max(0, cols2.Columns()-cols) } } } newev := gowid.TranslatedMouseEvent(ev, xd, 0) // TODO - don't need to translate event for keyboard event... if evm, ok := newev.(*tcell.EventMouse); ok { mx, _ := evm.Position() if mx >= 0 && mx < cols { return gowid.UserInputIfSelectable(w.SubWidget(), newev, subSize, focus, app) } } else { return gowid.UserInputIfSelectable(w.SubWidget(), newev, subSize, focus, app) } return false } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: gowid-1.4.0/widgets/hpadding/hpadding_test.go000066400000000000000000000226421426234454000212270ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. package hpadding import ( "strings" "testing" "github.com/gcla/gowid" "github.com/gcla/gowid/gwtest" "github.com/gcla/gowid/widgets/checkbox" "github.com/gcla/gowid/widgets/fill" "github.com/gcla/gowid/widgets/text" tcell "github.com/gdamore/tcell/v2" log "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" ) func TestCanvas26(t *testing.T) { widget1i := text.New("1234567890") widget1 := New(widget1i, gowid.HAlignLeft{}, gowid.RenderWithUnits{U: 5}) canvas1 := widget1.Render(gowid.RenderFlowWith{C: 8}, gowid.NotSelected, gwtest.D) log.Infof("Widget is %v", widget1) log.Infof("Canvas is '%s'", canvas1.String()) res := strings.Join([]string{"12345 ", "67890 "}, "\n") if res != canvas1.String() { t.Errorf("Failed") } } func TestHorizontal1(t *testing.T) { w := text.New("abcde") h := New(w, gowid.HAlignLeft{}, gowid.RenderFixed{}) c1 := h.Render(gowid.RenderFlowWith{C: 10}, gowid.Focused, gwtest.D) assert.Equal(t, "abcde ", c1.String()) h2 := New(w, gowid.HAlignRight{}, gowid.RenderFixed{}) c2 := h2.Render(gowid.RenderFlowWith{C: 10}, gowid.Focused, gwtest.D) assert.Equal(t, " abcde", c2.String()) h3 := New(w, gowid.HAlignRight{}, gowid.RenderWithUnits{U: 10}) c3 := h3.Render(gowid.RenderFlowWith{C: 20}, gowid.Focused, gwtest.D) assert.Equal(t, " abcde ", c3.String()) h4 := New(w, gowid.HAlignMiddle{}, gowid.RenderWithUnits{U: 10}) c4 := h4.Render(gowid.RenderFlowWith{C: 20}, gowid.Focused, gwtest.D) assert.Equal(t, " abcde ", c4.String()) h5 := New(w, gowid.HAlignLeft{}, gowid.RenderWithUnits{U: 3}) c5 := h5.Render(gowid.RenderFlowWith{C: 10}, gowid.Focused, gwtest.D) assert.Equal(t, "abc \nde ", c5.String()) } func TestHorizontal2(t *testing.T) { w := text.New("abcde") var c gowid.ICanvas = w.Render(gowid.RenderFixed{}, gowid.Focused, gwtest.D) assert.Equal(t, c.String(), "abcde") c = w.Render(gowid.RenderFlowWith{C: 5}, gowid.Focused, gwtest.D) assert.Equal(t, c.String(), "abcde") c = w.Render(gowid.RenderBox{C: 5, R: 1}, gowid.Focused, gwtest.D) assert.Equal(t, c.String(), "abcde") w2 := New(w, gowid.HAlignMiddle{}, gowid.RenderFixed{}) c = w2.Render(gowid.RenderFixed{}, gowid.Focused, gwtest.D) assert.Equal(t, c.String(), "abcde") c = w2.Render(gowid.RenderFlowWith{C: 7}, gowid.Focused, gwtest.D) assert.Equal(t, c.String(), " abcde ") c = w2.Render(gowid.RenderBox{C: 7, R: 2}, gowid.Focused, gwtest.D) assert.Equal(t, c.String(), " abcde \n ") w3 := New(w, gowid.HAlignRight{}, gowid.RenderFlowWith{C: 7}) c = w3.Render(gowid.RenderFixed{}, gowid.Focused, gwtest.D) // Will render with space on the right, because subwidget doesn't inherit alignment assert.Equal(t, c.String(), "abcde ") c = w3.Render(gowid.RenderFlowWith{C: 7}, gowid.Focused, gwtest.D) assert.Equal(t, c.String(), "abcde ") c = w3.Render(gowid.RenderBox{C: 7, R: 2}, gowid.Focused, gwtest.D) assert.Equal(t, c.String(), "abcde \n ") c = w3.Render(gowid.RenderBox{C: 7, R: 2}, gowid.Focused, gwtest.D) assert.Equal(t, c.String(), "abcde \n ") w4 := New(w, gowid.HAlignRight{}, gowid.RenderFlowWith{C: 3}) c = w4.Render(gowid.RenderFixed{}, gowid.Focused, gwtest.D) assert.Equal(t, c.String(), "abc\nde ") c = w4.Render(gowid.RenderFlowWith{C: 7}, gowid.Focused, gwtest.D) assert.Equal(t, c.String(), " abc\n de ") c = w4.Render(gowid.RenderBox{C: 7, R: 1}, gowid.Focused, gwtest.D) assert.Equal(t, c.String(), " abc") c = w4.Render(gowid.RenderBox{C: 7, R: 3}, gowid.Focused, gwtest.D) assert.Equal(t, c.String(), " abc\n de \n ") w5 := New(w, gowid.HAlignLeft{}, gowid.RenderWithUnits{U: 7}) c = w5.Render(gowid.RenderFixed{}, gowid.Focused, gwtest.D) assert.Equal(t, c.String(), "abcde ") c = w5.Render(gowid.RenderFlowWith{C: 3}, gowid.Focused, gwtest.D) assert.Equal(t, c.String(), "abc\nde ") c = w5.Render(gowid.RenderBox{C: 6, R: 1}, gowid.Focused, gwtest.D) assert.Equal(t, c.String(), "abcde ") c = w5.Render(gowid.RenderBox{C: 3, R: 3}, gowid.Focused, gwtest.D) assert.Equal(t, c.String(), "abc\nde \n ") c = w5.Render(gowid.RenderBox{C: 7, R: 3}, gowid.Focused, gwtest.D) assert.Equal(t, c.String(), "abcde \n \n ") w6 := New(w, gowid.HAlignLeft{}, gowid.RenderWithUnits{U: 3}) c = w6.Render(gowid.RenderBox{C: 5, R: 3}, gowid.Focused, gwtest.D) assert.Equal(t, c.String(), "abc \nde \n ") } func TestCheckbox2(t *testing.T) { ct := &gwtest.CheckBoxTester{Gotit: false} assert.Equal(t, ct.Gotit, false) w := checkbox.New(false) w.OnClick(ct) ev := tcell.NewEventKey(tcell.KeyRune, ' ', tcell.ModNone) w.UserInput(ev, gowid.RenderFixed{}, gowid.Focused, gwtest.D) assert.Equal(t, ct.Gotit, true) ct.Gotit = false assert.Equal(t, ct.Gotit, false) evlmx1y0 := tcell.NewEventMouse(1, 0, tcell.Button1, 0) evnonex1y0 := tcell.NewEventMouse(1, 0, tcell.ButtonNone, 0) w.UserInput(evlmx1y0, gowid.RenderFixed{}, gowid.Focused, gwtest.D) gwtest.D.SetLastMouseState(gowid.MouseState{true, false, false}) w.UserInput(evnonex1y0, gowid.RenderFixed{}, gowid.Focused, gwtest.D) gwtest.D.SetLastMouseState(gowid.MouseState{}) assert.Equal(t, ct.Gotit, true) w2 := New(w, gowid.HAlignLeft{}, gowid.RenderFixed{}) ct.Gotit = false assert.Equal(t, ct.Gotit, false) w2.UserInput(evlmx1y0, gowid.RenderBox{C: 10, R: 1}, gowid.Focused, gwtest.D) gwtest.D.SetLastMouseState(gowid.MouseState{true, false, false}) w2.UserInput(evnonex1y0, gowid.RenderBox{C: 10, R: 1}, gowid.Focused, gwtest.D) gwtest.D.SetLastMouseState(gowid.MouseState{}) assert.Equal(t, ct.Gotit, true) w3 := New(w, gowid.HAlignLeft{Margin: 4}, gowid.RenderFixed{}) ct.Gotit = false assert.Equal(t, ct.Gotit, false) w3.UserInput(evlmx1y0, gowid.RenderBox{C: 10, R: 1}, gowid.Focused, gwtest.D) w3.UserInput(evnonex1y0, gowid.RenderBox{C: 10, R: 1}, gowid.Focused, gwtest.D) assert.Equal(t, ct.Gotit, false) evlmx5y0 := tcell.NewEventMouse(5, 0, tcell.ButtonNone, 0) evnonex5y0 := tcell.NewEventMouse(5, 0, tcell.ButtonNone, 0) w3.UserInput(evlmx5y0, gowid.RenderBox{C: 10, R: 1}, gowid.Focused, gwtest.D) gwtest.D.SetLastMouseState(gowid.MouseState{true, false, false}) w3.UserInput(evnonex5y0, gowid.RenderBox{C: 10, R: 1}, gowid.Focused, gwtest.D) gwtest.D.SetLastMouseState(gowid.MouseState{}) assert.Equal(t, ct.Gotit, true) } func TestHorizontalPadding1(t *testing.T) { w1 := New(fill.New('x'), gowid.HAlignLeft{}, gowid.RenderWithUnits{U: 1}) c1 := w1.Render(gowid.RenderBox{C: 3, R: 4}, gowid.Focused, gwtest.D) assert.Equal(t, c1.String(), "x \nx \nx \nx ") w2 := New(fill.New('x'), gowid.HAlignLeft{}, gowid.RenderWithUnits{U: 2}) c2 := w2.Render(gowid.RenderBox{C: 3, R: 4}, gowid.Focused, gwtest.D) assert.Equal(t, c2.String(), "xx \nxx \nxx \nxx ") w3 := New(fill.New('x'), gowid.HAlignMiddle{}, gowid.RenderWithUnits{U: 1}) c3 := w3.Render(gowid.RenderBox{C: 3, R: 4}, gowid.Focused, gwtest.D) assert.Equal(t, c3.String(), " x \n x \n x \n x ") w4 := New(fill.New('x'), gowid.HAlignRight{}, gowid.RenderWithUnits{U: 1}) c4 := w4.Render(gowid.RenderBox{C: 3, R: 4}, gowid.Focused, gwtest.D) assert.Equal(t, c4.String(), " x\n x\n x\n x") w5 := New(fill.New('x'), gowid.HAlignRight{}, gowid.RenderWithUnits{U: 2}) c5 := w5.Render(gowid.RenderBox{C: 3, R: 4}, gowid.Focused, gwtest.D) assert.Equal(t, c5.String(), " xx\n xx\n xx\n xx") w6 := New(fill.New('x'), gowid.HAlignRight{}, gowid.RenderWithRatio{0.3}) c6 := w6.Render(gowid.RenderBox{C: 3, R: 4}, gowid.Focused, gwtest.D) assert.Equal(t, c6.String(), " x\n x\n x\n x") w7 := New(fill.New('x'), gowid.HAlignRight{}, gowid.RenderWithRatio{0.6}) c7 := w7.Render(gowid.RenderBox{C: 3, R: 4}, gowid.Focused, gwtest.D) assert.Equal(t, c7.String(), " xx\n xx\n xx\n xx") w8 := New(fill.New('x'), gowid.HAlignRight{}, gowid.RenderWithRatio{8.1}) c8 := w8.Render(gowid.RenderBox{C: 3, R: 4}, gowid.Focused, gwtest.D) assert.Equal(t, c8.String(), "xxx\nxxx\nxxx\nxxx") w9 := New(fill.New('x'), gowid.HAlignLeft{Margin: 1}, gowid.RenderWithUnits{U: 3}) c9 := w9.Render(gowid.RenderBox{C: 5, R: 2}, gowid.Focused, gwtest.D) assert.Equal(t, c9.String(), " xxx \n xxx ") w10 := New(fill.New('x'), gowid.HAlignLeft{Margin: 1}, gowid.RenderWithUnits{U: 6}) c10 := w10.Render(gowid.RenderBox{C: 5, R: 2}, gowid.Focused, gwtest.D) assert.Equal(t, c10.String(), " xxxx\n xxxx") w11 := New(fill.New('x'), gowid.HAlignLeft{Margin: 1}, gowid.RenderWithUnits{U: 4}) c11 := w11.Render(gowid.RenderBox{C: 5, R: 2}, gowid.Focused, gwtest.D) assert.Equal(t, c11.String(), " xxxx\n xxxx") w12 := New(checkbox.New(false), gowid.HAlignLeft{Margin: 4}, gowid.RenderFixed{}) c12 := w12.Render(gowid.RenderBox{C: 8, R: 1}, gowid.Focused, gwtest.D) assert.Equal(t, c12.String(), " [ ] ") w13 := New(checkbox.New(false), gowid.HAlignLeft{Margin: 7}, gowid.RenderFixed{}) c13 := w13.Render(gowid.RenderBox{C: 8, R: 1}, gowid.Focused, gwtest.D) assert.Equal(t, c13.String(), " [ ]") for _, w := range []gowid.IWidget{w1, w2, w3, w4, w5, w6, w7, w8, w9, w10, w11, w12, w13} { gwtest.RenderBoxManyTimes(t, w, 0, 10, 0, 10) } for _, w := range []gowid.IWidget{w1, w2, w3, w4, w5, w6, w7, w8, w9, w10, w11, w12, w13} { gwtest.RenderFlowManyTimes(t, w, 0, 10) } } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: gowid-1.4.0/widgets/isselected/000077500000000000000000000000001426234454000164235ustar00rootroot00000000000000gowid-1.4.0/widgets/isselected/isselected.go000066400000000000000000000047461426234454000211110ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. // Package isselected provides a widget that acts differently if selected (or focused) package isselected import ( "fmt" "github.com/gcla/gowid" ) //====================================================================== type Widget struct { Not gowid.IWidget // Must not be nil Selected gowid.IWidget // If nil, then Not used Focused gowid.IWidget // If nil, then Selected used } var _ gowid.IWidget = (*Widget)(nil) func New(w1, w2, w3 gowid.IWidget) *Widget { return &Widget{ Not: w1, Selected: w2, Focused: w3, } } func (w *Widget) pick(focus gowid.Selector) gowid.IWidget { if focus.Focus { if w.Focused == nil && w.Selected == nil { return w.Not } else if w.Focused == nil { return w.Selected } else { return w.Focused } } else if focus.Selected { if w.Selected == nil { return w.Not } else { return w.Selected } } else { return w.Not } } func (w *Widget) RenderSize(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.IRenderBox { return gowid.RenderSize(w.pick(focus), size, focus, app) } func (w *Widget) Render(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.ICanvas { return w.pick(focus).Render(size, focus, app) } func (w *Widget) UserInput(ev interface{}, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) bool { return w.pick(focus).UserInput(ev, size, focus, app) } // TODO - this isn't right. Should Selectable be conditioned on focus? func (w *Widget) Selectable() bool { return w.Not.Selectable() } func (w *Widget) String() string { return fmt.Sprintf("issel[%v#%v#%v]", w.Not, w.Selected, w.Focused) } //====================================================================== // For uses that require IComposite type WidgetExt struct { *Widget } func NewExt(w1, w2, w3 gowid.IWidget) *WidgetExt { return &WidgetExt{New(w1, w2, w3)} } var _ gowid.IWidget = (*WidgetExt)(nil) var _ gowid.IComposite = (*WidgetExt)(nil) // Return Focused because UserInput operations that change state // will apply when the widget is in focus - so this is likely the // one we want. But this looks like a rich source of bugs... func (w *WidgetExt) SubWidget() gowid.IWidget { return w.pick(gowid.Focused) } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: gowid-1.4.0/widgets/keypress/000077500000000000000000000000001426234454000161445ustar00rootroot00000000000000gowid-1.4.0/widgets/keypress/keypress.go000066400000000000000000000104521426234454000203420ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. // Package keypress provides a widget which responds to keyboard input. package keypress import ( "fmt" "github.com/gcla/gowid" tcell "github.com/gdamore/tcell/v2" ) //====================================================================== type ICustomKeys interface { CustomSelectKeys() bool SelectKeys() []gowid.IKey // can't be nil } type KeyPressFunction func(app gowid.IApp, widget gowid.IWidget, key gowid.IKey) func (f KeyPressFunction) Changed(app gowid.IApp, widget gowid.IWidget, data ...interface{}) { k := data[0].(gowid.IKey) f(app, widget, k) } // WidgetCallback is a simple struct with a name field for IIdentity and // that embeds a WidgetChangedFunction to be issued as a callback when a widget // property changes. type WidgetCallback struct { Name interface{} KeyPressFunction } func MakeCallback(name interface{}, fn KeyPressFunction) WidgetCallback { return WidgetCallback{ Name: name, KeyPressFunction: fn, } } func (f WidgetCallback) ID() interface{} { return f.Name } // IWidget is implemented by any widget that contains exactly one // exposed subwidget (ICompositeWidget) and that is decorated on its left // and right (IDecoratedAround). type IWidget interface { gowid.ICompositeWidget } type Options struct { Keys []gowid.IKey } type Widget struct { inner gowid.IWidget opts Options *gowid.Callbacks gowid.SubWidgetCallbacks gowid.KeyPressCallbacks gowid.IsSelectable } func New(inner gowid.IWidget, opts ...Options) *Widget { var opt Options if len(opts) > 0 { opt = opts[0] } res := &Widget{ inner: inner, opts: opt, } res.SubWidgetCallbacks = gowid.SubWidgetCallbacks{CB: &res.Callbacks} res.KeyPressCallbacks = gowid.KeyPressCallbacks{CB: &res.Callbacks} var _ gowid.IWidget = res var _ gowid.ICompositeWidget = res var _ IWidget = res var _ ICustomKeys = res return res } func (w *Widget) String() string { return fmt.Sprintf("keypress[%v]", w.SubWidget()) } func (w *Widget) KeyPress(key gowid.IKey, app gowid.IApp) { gowid.RunWidgetCallbacks(w.Callbacks, gowid.KeyPressCB{}, app, w, key) } func (w *Widget) SubWidget() gowid.IWidget { return w.inner } func (w *Widget) SetSubWidget(wi gowid.IWidget, app gowid.IApp) { w.inner = wi gowid.RunWidgetCallbacks(w.Callbacks, gowid.SubWidgetCB{}, app, w) } func (w *Widget) SubWidgetSize(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.IRenderSize { return SubWidgetSize(w, size, focus, app) } func (w *Widget) RenderSize(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.IRenderBox { return RenderSize(w, size, focus, app) } func (w *Widget) Render(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.ICanvas { return Render(w, size, focus, app) } func (w *Widget) UserInput(ev interface{}, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) bool { return UserInput(w, ev, size, focus, app) } func (w *Widget) CustomSelectKeys() bool { return true } func (w *Widget) SelectKeys() []gowid.IKey { return w.opts.Keys } //====================================================================== func SubWidgetSize(w gowid.ICompositeWidget, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.IRenderSize { return size } func RenderSize(w IWidget, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.IRenderBox { return gowid.RenderSize(w.SubWidget(), size, focus, app) } type IKeyPresser interface { gowid.IKeyPress ICustomKeys gowid.IComposite } func UserInput(w IKeyPresser, ev interface{}, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) bool { res := false switch ev := ev.(type) { case *tcell.EventKey: if w.CustomSelectKeys() { for _, k := range w.SelectKeys() { if gowid.KeysEqual(k, ev) { w.KeyPress(ev, app) res = true break } } } } if !res { res = w.SubWidget().UserInput(ev, size, focus, app) } return res } func Render(w IWidget, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.ICanvas { return w.SubWidget().Render(size, focus, app) } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: gowid-1.4.0/widgets/keypress/keypress_test.go000066400000000000000000000044721426234454000214060ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source code is governed by the MIT license // that can be found in the LICENSE file. package keypress import ( "testing" "github.com/gcla/gowid" "github.com/gcla/gowid/gwtest" "github.com/gcla/gowid/widgets/text" "github.com/stretchr/testify/assert" ) //====================================================================== type KeyPressTester struct { Gotit bool } func (f *KeyPressTester) Changed(gowid.IApp, gowid.IWidget, ...interface{}) { f.Gotit = true } func (f *KeyPressTester) ID() interface{} { return "foo" } //====================================================================== func TestKey1(t *testing.T) { tw := text.New("hitq") w := New(tw, Options{ Keys: []gowid.IKey{gowid.MakeKey('q')}, }) ct := &KeyPressTester{Gotit: false} assert.Equal(t, ct.Gotit, false) w.OnKeyPress(ct) c1 := w.Render(gowid.RenderFixed{}, gowid.Focused, gwtest.D) assert.Equal(t, c1.String(), "hitq") UserInput(w, gwtest.KeyEvent('q'), gowid.RenderFixed{}, gowid.Focused, gwtest.D) // w.Click(gwtest.D) assert.Equal(t, ct.Gotit, true) ct.Gotit = false // most widgets will funnel user input to the "focus" widget - so this should // never be necessary. But if you built something that passed input to both // widgets when only one had focus, you'd presumably want this to fire UserInput(w, gwtest.KeyEvent('q'), gowid.RenderFixed{}, gowid.NotSelected, gwtest.D) assert.Equal(t, ct.Gotit, true) ct.Gotit = false UserInput(w, gwtest.KeyEvent('r'), gowid.RenderFixed{}, gowid.Focused, gwtest.D) assert.Equal(t, ct.Gotit, false) ct.Gotit = false cbCalled := false w.OnKeyPress(WidgetCallback{"cb", func(app gowid.IApp, w gowid.IWidget, k gowid.IKey) { assert.Equal(t, true, gowid.KeysEqual(k, gowid.MakeKey('q'))) cbCalled = true }}) UserInput(w, gwtest.KeyEvent('q'), gowid.RenderFixed{}, gowid.Focused, gwtest.D) assert.Equal(t, ct.Gotit, true) assert.Equal(t, true, cbCalled) ct.Gotit = false // assert.Equal(t, ct.Gotit, false) // w.RemoveOnClick(ct) // w.Click(gwtest.D) // assert.Equal(t, ct.Gotit, false) gwtest.RenderBoxManyTimes(t, w, 0, 10, 0, 10) gwtest.RenderFlowManyTimes(t, w, 0, 20) } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: gowid-1.4.0/widgets/list/000077500000000000000000000000001426234454000152525ustar00rootroot00000000000000gowid-1.4.0/widgets/list/list.go000066400000000000000000001027541426234454000165650ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. // Package list provides a widget displaying a vertical list of widgets with one in focus and support for previous and next. package list import ( "fmt" "github.com/gcla/gowid" "github.com/gcla/gowid/gwutil" "github.com/gcla/gowid/vim" tcell "github.com/gdamore/tcell/v2" "github.com/pkg/errors" ) //====================================================================== // IWalkerPosition is satisfied by any struct with an Equal() method that can // determine whether it's at the "same" position as another or is more advanced // than another. type IWalkerPosition interface { Equal(IWalkerPosition) bool GreaterThan(IWalkerPosition) bool } // For most simple uses type IBoundedWalkerPosition interface { IWalkerPosition ToInt() int } // WalkerMoved is a simple struct used to hold the result of a Next() or // Previous() call on an IWalker. I find its use more convenient than // returning multiple values because you can use it inline with other // expressions. type WalkerIndex struct { Widget gowid.IWidget Pos IWalkerPosition } // Remove HaveFocus because the Next and Previous APIs have to // return an gowid.IWidget anyway - so just standardize on assuming a nil interface // means invalid type IWalker interface { At(pos IWalkerPosition) gowid.IWidget Focus() IWalkerPosition SetFocus(pos IWalkerPosition, app gowid.IApp) Next(pos IWalkerPosition) IWalkerPosition Previous(pos IWalkerPosition) IWalkerPosition } // IBoundedWalker is implemented by an IWalker type that knows the maximum length // of its underlying data set. It must return IBoundedWalkerPosition rather than only // IWalkerPosition. type IBoundedWalker interface { IWalker Length() int } // IWalkerHome is any type that supports being able to provide a first position. type IWalkerHome interface { First() IWalkerPosition // nil possible if empty } // IWalkerEnd is any type that supports being able to provide a last position. This // is used to support the End key on the keyboard. type IWalkerEnd interface { Last() IWalkerPosition // nil possible if empty } //====================================================================== type WidgetIsUnboundedError struct { Type interface{} } var _ error = WidgetIsUnboundedError{} func (e WidgetIsUnboundedError) Error() string { return fmt.Sprintf("Widget does not use IBoundedWalker - %v of type %T", e.Type, e.Type) } var BadState = fmt.Errorf("Broken state in list widget") //====================================================================== type ListPos int func (l ListPos) ToInt() int { return int(l) } func (l ListPos) Equal(other IWalkerPosition) bool { switch o := other.(type) { case ListPos: return o == l default: panic(gowid.InvalidTypeToCompare{LHS: l, RHS: other}) } } func (l ListPos) GreaterThan(other IWalkerPosition) bool { switch o := other.(type) { case ListPos: return l > o default: panic(gowid.InvalidTypeToCompare{LHS: l, RHS: other}) } } type SimpleListWalker struct { Widgets []gowid.IWidget focus ListPos } var _ IBoundedWalker = (*SimpleListWalker)(nil) var _ IWalkerHome = (*SimpleListWalker)(nil) func NewSimpleListWalker(widgets []gowid.IWidget) *SimpleListWalker { res := &SimpleListWalker{ Widgets: widgets, focus: -1, } pos, _ := gowid.FindNextSelectableWidget(widgets, -1, 1, false) res.focus = ListPos(pos) // If nothing is selectable, choose the first, and we'll scroll like a browser if res.focus == -1 && len(widgets) > 0 { res.focus = 0 } return res } func (w *SimpleListWalker) First() IWalkerPosition { if len(w.Widgets) == 0 { return nil } return ListPos(0) } func (w *SimpleListWalker) Last() IWalkerPosition { if len(w.Widgets) == 0 { return nil } return ListPos(len(w.Widgets) - 1) } func (w *SimpleListWalker) Length() int { return len(w.Widgets) } func (w *SimpleListWalker) At(pos IWalkerPosition) gowid.IWidget { var res gowid.IWidget ipos := int(pos.(ListPos)) if ipos >= 0 && ipos < len(w.Widgets) { res = w.Widgets[ipos] } return res } func (w *SimpleListWalker) Focus() IWalkerPosition { return w.focus } func (w *SimpleListWalker) SetFocus(focus IWalkerPosition, app gowid.IApp) { w.focus = focus.(ListPos) } func (w *SimpleListWalker) Next(ipos IWalkerPosition) IWalkerPosition { pos := ipos.(ListPos) if int(pos) == len(w.Widgets)-1 { return ListPos(-1) } else { return pos + 1 } } func (w *SimpleListWalker) Previous(ipos IWalkerPosition) IWalkerPosition { pos := ipos.(ListPos) if pos-1 == -1 { return ListPos(-1) } else { return pos - 1 } } //====================================================================== type IListFns interface { RenderSubwidgets(gowid.IRenderSize, gowid.Selector, gowid.IApp) ([]SubRenders, SubRenders, []SubRenders) Walker() IWalker SetWalker(IWalker, gowid.IApp) } type IWidget interface { gowid.IWidget IListFns } type Widget struct { walker IWalker // This says how many lines to cut from the top of the widget rendered at the top of the listbox. // It might be too big to be rendered fully in the space. st state options Options gowid.AddressProvidesID *gowid.Callbacks gowid.FocusCallbacks gowid.IsSelectable } type Options struct { //SelectedStyle gowid.ICellStyler // apply a style to the selected widget - orthogonal to focus styling DownKeys []vim.KeyPress UpKeys []vim.KeyPress DoNotSetSelected bool // Whether or not to set the focus.Selected field for the selected child } type IndexedWidget struct { *Widget walker IBoundedWalker } type state struct { linesOffTop int // used only if focus widget has more lines than can be displayed topToBottomRatio float32 topToBottomRatioValid bool // Means denominator is 0 if true i.e. at the bottom } func New(walker IWalker, opts ...Options) *Widget { var opt Options if len(opts) > 0 { opt = opts[0] } if opt.DownKeys == nil { opt.DownKeys = vim.AllDownKeys } if opt.UpKeys == nil { opt.UpKeys = vim.AllUpKeys } res := &Widget{ walker: walker, options: opt, } res.FocusCallbacks = gowid.FocusCallbacks{CB: &res.Callbacks} res.goToTop() var _ gowid.IWidget = res return res } func NewBounded(walker IBoundedWalker, opts ...Options) *IndexedWidget { res := New(walker, opts...) return &IndexedWidget{ Widget: res, walker: walker, } } func (w *Widget) String() string { cur := w.Walker().Focus() return fmt.Sprintf("list[pos=%v,f=%v]", cur, w.walker.At(cur)) } func (w *Widget) Walker() IWalker { return w.walker } func (w *IndexedWidget) Walker() IWalker { return w.walker } func (w *Widget) SetWalker(l IWalker, app gowid.IApp) { w.walker = l } func (w *IndexedWidget) SetWalker(l IWalker, app gowid.IApp) { w.walker = l.(IBoundedWalker) w.Widget.SetWalker(l, app) } func (w *Widget) State() interface{} { return w.st } func (w *Widget) SetState(st interface{}, app gowid.IApp) { if state, ok := st.(state); !ok { panic(BadState) } else { w.st = state } } func (w *Widget) GoToTop(app gowid.IApp) { w.goToTop() } func (w *Widget) goToTop() { w.st.topToBottomRatioValid = true w.st.topToBottomRatio = 0 w.st.linesOffTop = 0 } func (w *Widget) GoToBottom(app gowid.IApp) { w.st.topToBottomRatioValid = false } func (w *Widget) GoToMiddle(app gowid.IApp) { w.st.topToBottomRatioValid = true w.st.topToBottomRatio = 0.5 w.st.linesOffTop = 0 } func (w *Widget) AtTop() bool { return w.st.topToBottomRatioValid && gwutil.AlmostEqual(float64(w.st.topToBottomRatio), 0.0) } func (w *Widget) AtBottom() bool { return !w.st.topToBottomRatioValid } func (w *Widget) InMiddle() bool { return w.st.topToBottomRatioValid && gwutil.AlmostEqual(float64(w.st.topToBottomRatio), 0.5) } func (w *Widget) RenderSize(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.IRenderBox { return gowid.CalculateRenderSizeFallback(w, size, focus, app) } func (w *Widget) Render(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.ICanvas { return Render(w, size, focus, app) } func (w *Widget) SubWidgetSize(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.IRenderSize { return SubWidgetSize(w, size, focus, app) } func (w *Widget) CalculateOnScreen(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) (int, int, int, error) { return CalculateOnScreen(w, size, focus, app) } func (w *Widget) SelectChild(f gowid.Selector) bool { return !w.options.DoNotSetSelected && f.Selected } //'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' type SubRenders struct { Widget gowid.IWidget Position IWalkerPosition Canvas gowid.ICanvas FullCanvasLines int } // IsChopped is a utility function for a SubRender struct that returns true if the canvas returned for this // widget is smaller than the full size rendered (i.e. that it has been adjusted vertically) func (r *SubRenders) IsChopped() bool { return r.Canvas.BoxRows() < r.FullCanvasLines } // RenderSubWidgets starts at the focus widget, rendering it, and returning the result as middle, a SubRenders // struct. This tells the caller information about the widget rendered, including the full number of lines // that would've been used if the provided render size had been large enough (this information tells the // caller that the whole widget isn't displayed). After rendering the middle widget, the function renders // Previous and Next widgets until the space above the middle widget and the space below the middle widget is // filled. func (w *Widget) RenderSubwidgets(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) (top []SubRenders, middle SubRenders, bottom []SubRenders) { cols, haveCols := size.(gowid.IColumns) rows, haveRows := size.(gowid.IRows) top = make([]SubRenders, 0) bottom = make([]SubRenders, 0) cur := w.Walker().Focus() curPos := cur curWidget := w.walker.At(curPos) if curWidget == nil { middle = SubRenders{nil, nil, gowid.NewCanvas(), 0} } else { var linesNeeded int haveLinesNeeded := haveRows if haveLinesNeeded { linesNeeded = rows.Rows() } var c gowid.ICanvas //foobar := styled.New(curWidget, gowid.MakeStyledAs(gowid.StyleReverse)) var curToRender gowid.IWidget = curWidget if haveCols { c = curToRender.Render(gowid.RenderFlowWith{C: cols.Columns()}, focus.SelectIf(w.SelectChild(focus)), app) } else { c = curToRender.Render(gowid.RenderFixed{}, focus.SelectIf(w.SelectChild(focus)), app) } creallines := c.BoxRows() middle = SubRenders{curWidget, curPos, c, creallines} // If the focus widget just rendered has more rows than the required size provided, then... if haveLinesNeeded && (c.BoxRows() > linesNeeded) { chopOffTop := w.st.linesOffTop // We don't chop off so much that it brings the next widget into view if c.BoxRows()-chopOffTop < linesNeeded { chopOffTop = c.BoxRows() - linesNeeded } c.Truncate(chopOffTop, c.BoxRows()-(linesNeeded+chopOffTop)) middle = SubRenders{curWidget, curPos, c, creallines} } else { middle = SubRenders{curWidget, curPos, c, c.BoxRows()} upPos := curPos downPos := curPos var topLinesNeeded, bottomLinesNeeded int if haveLinesNeeded { if w.st.topToBottomRatioValid { topLinesNeeded = gwutil.RoundFloatToInt(float32(linesNeeded) * w.st.topToBottomRatio) bottomLinesNeeded = linesNeeded - (topLinesNeeded + c.BoxRows()) if bottomLinesNeeded < 0 { topLinesNeeded -= -bottomLinesNeeded // take away from the top enough to bring the current widget into full display if possible bottomLinesNeeded = 0 if topLinesNeeded < 0 { topLinesNeeded = 0 } } } else { topLinesNeeded = linesNeeded - c.BoxRows() } } var upWidget, downWidget gowid.IWidget for { if haveLinesNeeded && (topLinesNeeded <= 0) { break } up := w.Walker().Previous(upPos) upPos = up upWidget = w.Walker().At(upPos) //upWidget, upPos = up.Widget, up.Pos if upWidget == nil { bottomLinesNeeded += topLinesNeeded break } else { var upC gowid.ICanvas if haveCols { upC = upWidget.Render(gowid.RenderFlowWith{C: cols.Columns()}, gowid.NotSelected, app) } else { upC = upWidget.Render(gowid.RenderFixed{}, gowid.NotSelected, app) } upreallines := upC.BoxRows() if haveLinesNeeded { if upreallines > topLinesNeeded { upC.Truncate(upreallines-topLinesNeeded, 0) } topLinesNeeded -= upC.BoxRows() } top = append(top, SubRenders{upWidget, upPos, upC, upreallines}) } } for { if haveLinesNeeded && (bottomLinesNeeded <= 0) { break } down := w.Walker().Next(downPos) downPos = down downWidget = w.Walker().At(downPos) //downWidget, downPos = down.Widget, down.Pos if downWidget == nil { break } else { var downC gowid.ICanvas if haveCols { downC = downWidget.Render(gowid.RenderFlowWith{C: cols.Columns()}, gowid.NotSelected, app) } else { downC = downWidget.Render(gowid.RenderFixed{}, gowid.NotSelected, app) } downreallines := downC.BoxRows() if haveLinesNeeded { if downreallines > bottomLinesNeeded { downC.Truncate(0, downreallines-bottomLinesNeeded) } bottomLinesNeeded -= downC.BoxRows() } bottom = append(bottom, SubRenders{downWidget, downPos, downC, downreallines}) } } } } return } func SubWidgetSize(w IWidget, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.IRenderSize { switch sz := size.(type) { case gowid.IRenderBox: return gowid.RenderFlowWith{C: sz.BoxColumns()} case gowid.IRenderFlowWith: return sz default: panic(gowid.WidgetSizeError{Widget: w, Size: size}) } } func CalculateOnScreen(w IListFns, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) (int, int, int, error) { aboveMiddle, middle, belowMiddle := w.RenderSubwidgets(size, focus, app) mc := 0 tc := 0 bc := 0 if len(aboveMiddle) > 0 { if pos, ok := aboveMiddle[len(aboveMiddle)-1].Position.(IBoundedWalkerPosition); ok { tc = pos.ToInt() // mc must not be nil, because if we rendered widgets above, then certainly there is a focus widget mc = len(aboveMiddle) + len(belowMiddle) + 1 } else { return -1, -1, -1, errors.WithStack(WidgetIsUnboundedError{Type: aboveMiddle[len(aboveMiddle)-1].Position}) } } else { // e.g. focus is at the top of the screen, so nothing to render above if middle.Widget != nil { // If == nil, this could be because there is no focus widget currently e.g. an empty list if pos, ok := middle.Position.(IBoundedWalkerPosition); ok { tc = pos.ToInt() mc = len(belowMiddle) + 1 } else { return -1, -1, -1, errors.WithStack(WidgetIsUnboundedError{Type: middle.Position}) } } } //allif len(bottom) > 0 { if i, ok := w.Walker().(IBoundedWalker); ok { bc = i.Length() - (mc + tc) } else { return -1, -1, -1, errors.WithStack(WidgetIsUnboundedError{Type: w.Walker()}) } //} return tc, mc, bc, nil } func Render(w IWidget, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.ICanvas { rows, haveRows := size.(gowid.IRows) top, middle, bottom := w.RenderSubwidgets(size, focus, app) topC := gowid.NewCanvas() bottomC := gowid.NewCanvas() for i := len(top); i > 0; i-- { topC.AppendBelow(top[i-1].Canvas, false, false) } for _, ic := range bottom { bottomC.AppendBelow(ic.Canvas, false, false) } topC.AppendBelow(middle.Canvas, true, false) topC.AppendBelow(bottomC, false, false) if haveRows && (topC.BoxRows() < rows.Rows()) { gowid.AppendBlankLines(topC, rows.Rows()-topC.BoxRows()) } return topC } func calcPrefPosition(curw gowid.IWidget) gwutil.IntOption { // Repeatedly unpack composite widgets until I have to stop. Look as I unpack for something that // exports a prefered column API. The widget might be ContainerWidget/StyledWidget/... var prefCol gwutil.IntOption for { if icol, ok := curw.(gowid.IPreferedPosition); ok { prefCol = icol.GetPreferedPosition() break } if curw2, ok2 := curw.(gowid.IComposite); ok2 { curw = curw2.SubWidget() } else { break } } return prefCol } func setPrefPosition(app gowid.IApp, curw gowid.IWidget, prefCol int) { for { if icol, ok := curw.(gowid.IPreferedPosition); ok { icol.SetPreferedPosition(prefCol, app) break } if curw2, ok2 := curw.(gowid.IComposite); ok2 { curw = curw2.SubWidget() } else { break } } } func (w *Widget) UserInput(ev interface{}, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) bool { res := false rows, haveRows := size.(gowid.IRows) cols, haveCols := size.(gowid.IColumns) var numLinesToUse, numColumnsToUse int if haveRows && haveCols { numLinesToUse = rows.Rows() numColumnsToUse = cols.Columns() } sumSubRenders := func(renders []SubRenders) int { res := 0 for _, r := range renders { res += r.FullCanvasLines } return res } var top, bottom, all []SubRenders var middle SubRenders initTMB := false initTopMiddleBottom := func() { if !initTMB { top, middle, bottom = w.RenderSubwidgets(size, focus, app) initTMB = true } } initLSR := false midIndex := -1 initListOfSubRenders := func() int { if !initLSR { all = make([]SubRenders, len(top)+len(bottom)+1) j := 0 for i := len(top); i > 0; i, j = i-1, j+1 { all[j] = top[i-1] } // Remember which one has focus midIndex = j all[j] = middle j++ for i := 0; i < len(bottom); i, j = i+1, j+1 { all[j] = bottom[i] } initLSR = true } return midIndex } calculateScreenLines := func() { if numLinesToUse == 0 { for _, r := range all { numLinesToUse += r.Canvas.BoxRows() numColumnsToUse = gwutil.Max(numColumnsToUse, r.Canvas.BoxColumns()) } } } userInputSize := func() gowid.IRenderSize { var sizeForInput gowid.IRenderSize if !haveRows && !haveCols { sizeForInput = gowid.RenderFixed{} } else { sizeForInput = gowid.RenderFlowWith{C: numColumnsToUse} } return sizeForInput } var subRenderSize gowid.IRenderSize if haveCols { subRenderSize = gowid.RenderFlowWith{C: cols.Columns()} } else { subRenderSize = gowid.RenderFixed{} } forChild := false childSelectable := false curi := w.Walker().Focus() position := curi cur := w.Walker().At(position) if cur == nil { return false } startPosition := position dirMoved := 0 if evm, ok := ev.(*tcell.EventMouse); ok { initTopMiddleBottom() initListOfSubRenders() // If the render size provided didn't specify a number of rows, then // add up all the rows rendered and use that. This will cause problems with // infinite listboxes... calculateScreenLines() _, my := evm.Position() curY := 0 for i, widgetRender := range all { if my < curY+widgetRender.Canvas.BoxRows() && my >= curY { for j := 0; !widgetRender.Position.Equal(position); j++ { // If we use w.Walker().Next() here, we don't update the state that tracks how far // down the screen we are. And regardless of whether we accept the input for this // event or not, we are going to change focus (if we didn't click on the focus // list item). So change focus as we go. if i > midIndex { // Need to walk forwards position = w.Walker().Next(position) dirMoved = 1 } else { // Need to walk backwards position = w.Walker().Previous(position) dirMoved = -1 } } sizeForInput := userInputSize() forChild = gowid.UserInputIfSelectable(widgetRender.Widget, gowid.TranslatedMouseEvent(ev, 0, -curY), sizeForInput, gowid.Focused, app) childSelectable = widgetRender.Widget.Selectable() break } curY += widgetRender.Canvas.BoxRows() } } else { if position != ListPos(-1) { sizeForInput := userInputSize() forChild = gowid.UserInputIfSelectable(cur, ev, sizeForInput, focus, app) } } scrollDown := false scrollUp := false pgDown := false pgUp := false toHome := false toEnd := false // If the child takes the user input, and its a key, then the list will never // handle it if evk, ok := ev.(*tcell.EventKey); !forChild && ok { k := evk.Key() switch { case k == tcell.KeyCtrlL: if !w.AtBottom() { if w.AtTop() { w.GoToBottom(app) } else { if w.InMiddle() { w.GoToTop(app) } else { w.GoToMiddle(app) } } } else { w.GoToMiddle(app) } res = true case k == tcell.KeyHome: toHome = true case k == tcell.KeyEnd: toEnd = true case vim.KeyIn(evk, w.options.DownKeys): scrollDown = true case vim.KeyIn(evk, w.options.UpKeys): scrollUp = true case k == tcell.KeyPgDn: pgDown = true case k == tcell.KeyPgUp: pgUp = true default: } // But if the input is from the mouse, the list can handle it as well as any subwidget. For example, // if the list holds checkbox widgets, a left mouse click might check the subwidget, but it can // also change the focus list item. } else if ev2, ok := ev.(*tcell.EventMouse); ok { switch ev2.Buttons() { case tcell.WheelDown: if !forChild { scrollDown = true } case tcell.WheelUp: if !forChild { scrollUp = true } case tcell.Button1: app.SetClickTarget(ev2.Buttons(), w) res = true case tcell.ButtonNone: if childSelectable { // tcell will report ButtonNone for mouse events which are simply pointer movements // (at least in my terminal). To distinguish this from a mouse release event, we track // the prior input's mouse state. If the last state was a mouse click, then this event // is processed as a mouse button release. if !app.GetLastMouseState().NoButtonClicked() { clickit := false app.ClickTarget(func(k tcell.ButtonMask, v gowid.IIdentityWidget) { if v != nil && v.ID() == w.ID() { clickit = true } }) if clickit { // This means the mouse button was released over widget w, after earlier having // been clicked on widget w curPosition := startPosition saveState := w.st for { if curPosition.Equal(position) { res = true break } else if dirMoved > 0 && curPosition.GreaterThan(position) { res = false w.st = saveState w.Walker().SetFocus(startPosition, app) break } else if dirMoved < 0 && position.GreaterThan(curPosition) { res = false w.st = saveState w.Walker().SetFocus(startPosition, app) break } if dirMoved > 0 { res, curPosition = w.MoveToNextFocus(subRenderSize, focus, numLinesToUse, app) } else if dirMoved < 0 { res, curPosition = w.MoveToPreviousFocus(subRenderSize, focus, numLinesToUse, app) } else { panic(BadState) } if !res { w.st = saveState w.Walker().SetFocus(startPosition, app) break } } //res = true } } } } } var prefCol gwutil.IntOption if pgDown || pgUp { prefCol = calcPrefPosition(w.Walker().At(w.Walker().Focus())) if pgDown { // This means we're not at rendered at the bottom, so move // down until we are the bottom widget (no lines at bottom) startedAtBottom := w.AtBottom() cur := w.Walker().Next(position) curw := w.Walker().At(cur) if curw != nil { // We scrolled at least once, therefore we accepted the input. res = true // We need to move at least one widget on page down. This is it candidate := cur oldpos := candidate var curLines int var curBoundingBox gowid.IRenderBox topLines := make([]int, 0, 120) if !startedAtBottom { // test this widget with focus curBoundingBox = gowid.RenderSize(curw, subRenderSize, gowid.NotSelected, app) initTopMiddleBottom() topLines = append(topLines, sumSubRenders(top)+curBoundingBox.BoxRows()) } // TODO: factor if !haveRows { initTopMiddleBottom() initListOfSubRenders() calculateScreenLines() } Loop1: for { // test this widget with focus curBoundingBox = gowid.RenderSize(curw, subRenderSize, focus, app) curLines = curBoundingBox.BoxRows() // if this widget would need more lines than we have left, if gwutil.Sum(topLines...)+curLines > numLinesToUse { // we stop here, use candidate break } // We can move again if this widget, rendered without focus, still doesn't // take us over the limit. If it does, then we have to stop curBoundingBox = gowid.RenderSize(curw, subRenderSize, gowid.NotSelected, app) curLines = curBoundingBox.BoxRows() // if this widget would need more lines than we have left, if gwutil.Sum(topLines...)+curLines > numLinesToUse { // we stop here, use candidate break } candidate = cur for { cur = w.Walker().Next(cur) w2 := w.Walker().At(cur) if w2 == nil { break Loop1 } curLines := gowid.RenderSize(w2, subRenderSize, gowid.NotSelected, app).BoxRows() topLines = append(topLines, curLines) if w2.Selectable() { break } } } // At end of loop, invariant holds - candidate.Pos is the position for the widget w.Walker().SetFocus(candidate, app) if !oldpos.Equal(candidate) { gowid.RunWidgetCallbacks(w, gowid.FocusCB{}, app, w.Walker().At(candidate)) } if !startedAtBottom { w.GoToBottom(app) } } } if pgUp { // This means we're not at rendered at the top, so move // down until we are the bottom widget (no lines at bottom) startedAtTop := !w.AtBottom() && gwutil.AlmostEqual(float64(w.st.topToBottomRatio), 0.0) cur := w.Walker().Previous(position) curw := w.Walker().At(cur) if curw != nil { // We scrolled at least once, therefore we accepted the input. res = true // We need to move at least one widget on page down. This is it candidate := cur oldpos := cur var curLines int var curBoundingBox gowid.IRenderBox bottomLines := make([]int, 0, 120) if !startedAtTop { // test this widget with focus curBoundingBox = gowid.RenderSize(curw, subRenderSize, gowid.NotSelected, app) initTopMiddleBottom() bottomLines = append(bottomLines, sumSubRenders(bottom)+curBoundingBox.BoxRows()) } // TODO: factor if !haveRows { initTopMiddleBottom() initListOfSubRenders() calculateScreenLines() } Loop2: for { // test this widget with focus curBoundingBox = gowid.RenderSize(curw, subRenderSize, focus, app) curLines = curBoundingBox.BoxRows() // if this widget would need more lines than we have left, if gwutil.Sum(bottomLines...)+curLines > numLinesToUse { // we stop here, use candidate break } // We can move again if this widget, rendered without focus, still doesn't // take us over the limit. If it does, then we have to stop curBoundingBox = gowid.RenderSize(curw, subRenderSize, gowid.NotSelected, app) curLines = curBoundingBox.BoxRows() // if this widget would need more lines than we have left, if gwutil.Sum(bottomLines...)+curLines > numLinesToUse { // we stop here, use candidate break } candidate = cur for { cur = w.Walker().Previous(cur) w2 := w.Walker().At(cur) if w2 == nil { break Loop2 } curLines := gowid.RenderSize(w2, subRenderSize, gowid.NotSelected, app).BoxRows() // We are moving down one more widget, so track how far we are from the top bottomLines = append(bottomLines, curLines) if w2.Selectable() { break } } } // At end of loop, invariant holds - candidate.Pos is the position for the widget w.Walker().SetFocus(candidate, app) if !oldpos.Equal(candidate) { gowid.RunWidgetCallbacks(w, gowid.FocusCB{}, app, w.Walker().At(candidate)) } if !startedAtTop { w.GoToTop(app) } } } if res && !prefCol.IsNone() { setPrefPosition(app, w.Walker().At(w.Walker().Focus()), prefCol.Val()) } } if scrollDown || scrollUp { initTopMiddleBottom() res = true prefCol = calcPrefPosition(middle.Widget) if scrollDown { // This means that the middle widget could not fit entirely in the screen provided, and that // we have not scrolled to the bottom of the middle widget yet if middle.IsChopped() && (middle.Canvas.BoxRows()+w.st.linesOffTop < middle.FullCanvasLines) { w.st.linesOffTop += 1 } else { res, _ = w.MoveToNextFocus(subRenderSize, focus, numLinesToUse, app) } } if scrollUp { // If the current widget itself is chopped, and is missing lines at the top, then reduce the number of missing lines if middle.IsChopped() && (w.st.linesOffTop > 0) { w.st.linesOffTop -= 1 } else { res, _ = w.MoveToPreviousFocus(subRenderSize, focus, numLinesToUse, app) } } if res && !prefCol.IsNone() { setPrefPosition(app, w.Walker().At(w.Walker().Focus()), prefCol.Val()) } } if toHome || toEnd { prefCol = calcPrefPosition(w.Walker().At(w.Walker().Focus())) oldpos := w.Walker().Focus() if toHome { if homer, ok := w.Walker().(IWalkerHome); ok { pos := homer.First() if pos != nil { w.Walker().SetFocus(pos, app) w.GoToTop(app) res = true } } } if toEnd { if ender, ok := w.Walker().(IWalkerEnd); ok { pos := ender.Last() if pos != nil { w.Walker().SetFocus(pos, app) w.GoToBottom(app) res = true } } } if res && !prefCol.IsNone() { setPrefPosition(app, w.Walker().At(w.Walker().Focus()), prefCol.Val()) } newwandpos := w.Walker().Focus() if !oldpos.Equal(newwandpos) { gowid.RunWidgetCallbacks(w, gowid.FocusCB{}, app, w.Walker().At(newwandpos)) } } return forChild || res } func (w *Widget) MoveToNextFocus(subRenderSize gowid.IRenderSize, focus gowid.Selector, screenLines int, app gowid.IApp) (bool, IWalkerPosition) { cur := w.Walker().Focus() curw := w.Walker().At(cur) if curw == nil { return false, cur } oldPos := cur curLinesNoFocus := gowid.RenderSize(curw, subRenderSize, gowid.NotSelected, app).BoxRows() // from that, get the next widget and next position. The nextw is used to run callbacks. var next IWalkerPosition var nextw gowid.IWidget for { next = w.Walker().Next(cur) nextw = w.Walker().At(next) if nextw == nil { return false, nil } if nextw.Selectable() { break } curLinesNoFocus += gowid.RenderSize(nextw, subRenderSize, gowid.NotSelected, app).BoxRows() cur = next } w.Walker().SetFocus(next, app) nextLines := gowid.RenderSize(nextw, subRenderSize, focus, app).BoxRows() // curWidgetLines has the number of lines used by the current focus widget when rendered. Compute how // many line)s should be above it, and how many below it. var computedLinesAbove, computedLinesBelow int if !w.AtBottom() { computedLinesAbove = gwutil.RoundFloatToInt(float32(gwutil.Max(0, screenLines)) * w.st.topToBottomRatio) computedLinesAbove += curLinesNoFocus computedLinesBelow = screenLines - (computedLinesAbove + nextLines) if computedLinesBelow <= 0 { w.GoToBottom(app) } else { w.st.topToBottomRatioValid = true w.st.topToBottomRatio = float32(computedLinesAbove) / float32(screenLines) } } w.st.linesOffTop = 0 // Do this at the end in case the focus callback wants to save the list state too. if !next.Equal(oldPos) { gowid.RunWidgetCallbacks(w, gowid.FocusCB{}, app, nextw) } return true, next } func (w *Widget) MoveToPreviousFocus(subRenderSize gowid.IRenderSize, focus gowid.Selector, screenLines int, app gowid.IApp) (bool, IWalkerPosition) { wasAtBottom := w.AtBottom() cur := w.Walker().Focus() curw := w.Walker().At(cur) oldpos := cur curLinesFocus := gowid.RenderSize(curw, subRenderSize, focus, app).BoxRows() betweenNoFocus := 0 // from that, get the next widget and next position. The nextw is used to run callbacks. var prev IWalkerPosition var prevw gowid.IWidget for { prev = w.Walker().Previous(cur) prevw = w.Walker().At(prev) if prevw == nil { return false, nil } if prevw.Selectable() { break } betweenNoFocus += gowid.RenderSize(prevw, subRenderSize, gowid.NotSelected, app).BoxRows() cur = prev } w.Walker().SetFocus(prev, app) prevLinesNoFocus := gowid.RenderSize(prevw, subRenderSize, gowid.NotSelected, app).BoxRows() // curWidgetLines has the number of lines used by the current focus widget when rendered. Compute how // many lines should be above it, and how many below it. var computedLinesAbove int if wasAtBottom { computedLinesAbove = gwutil.Max(0, screenLines) - (curLinesFocus + betweenNoFocus + prevLinesNoFocus) } else { computedLinesAbove = gwutil.RoundFloatToInt(float32(gwutil.Max(0, screenLines)) * w.st.topToBottomRatio) // Preserve lines *above* focus - it feels the most natural when scrolling. So if the // previous widget (below) takes 3 lines to render with focus, but only 1 without, then add just // one because that widget will only contribute 1 when it's no longer current. computedLinesAbove -= (prevLinesNoFocus + betweenNoFocus) } if computedLinesAbove <= 0 { prevLinesFocus := gowid.RenderSize(prevw, subRenderSize, focus, app).BoxRows() w.GoToTop(app) // widget is logically top, but might have lines cut if too big (see next line) w.st.linesOffTop = gwutil.Max(0, prevLinesFocus-screenLines) // in case prev doesn't fit, start at bottom } else { w.st.topToBottomRatioValid = true w.st.topToBottomRatio = float32(computedLinesAbove) / float32(screenLines) } // Do this at the end in case the focus callback wants to save the list state too. if !prev.Equal(oldpos) { gowid.RunWidgetCallbacks(w, gowid.FocusCB{}, app, prevw) } return true, prev } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: gowid-1.4.0/widgets/list/list_test.go000066400000000000000000000251411426234454000176160ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source code is governed by the MIT license // that can be found in the LICENSE file. package list import ( "fmt" "strings" "testing" "github.com/gcla/gowid" "github.com/gcla/gowid/gwtest" "github.com/gcla/gowid/widgets/checkbox" "github.com/gcla/gowid/widgets/disable" "github.com/gcla/gowid/widgets/fixedadapter" "github.com/gcla/gowid/widgets/isselected" "github.com/gcla/gowid/widgets/pile" "github.com/gcla/gowid/widgets/selectable" "github.com/gcla/gowid/widgets/text" tcell "github.com/gdamore/tcell/v2" log "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" ) //====================================================================== func TestCanvasListBox1(t *testing.T) { widget1 := text.New("a") widget2 := text.New("b") widget3 := text.New("c") walker := NewSimpleListWalker([]gowid.IWidget{widget1, widget2, widget3}) lb := New(walker) canvas1 := lb.Render(gowid.RenderBox{C: 1, R: 3}, gowid.NotSelected, gwtest.D) log.Infof("Widget is %v", widget1) log.Infof("Canvas is '%s'", canvas1.String()) // res := strings.Join([]string{""}, "\n") // if res != canvas1.ToString() { // t.Errorf("Failed") // } } func TestListBox2(t *testing.T) { defer gwtest.ClearTestApp() ct := &gwtest.CheckBoxTester{Gotit: false} assert.Equal(t, ct.Gotit, false) widget1 := checkbox.New(false) widget2 := checkbox.New(false) widget3 := checkbox.New(false) widget2.OnClick(ct) walker := NewSimpleListWalker([]gowid.IWidget{ fixedadapter.New(widget1), fixedadapter.New(widget2), fixedadapter.New(widget3), }) sz := gowid.RenderBox{C: 6, R: 4} lb := New(walker) c1 := lb.Render(sz, gowid.NotSelected, gwtest.D) assert.Equal(t, c1.String(), "[ ] \n[ ] \n[ ] \n ") evsp := tcell.NewEventKey(tcell.KeyRune, ' ', tcell.ModNone) evlm := tcell.NewEventMouse(1, 1, tcell.Button1, 0) evnone := tcell.NewEventMouse(1, 1, tcell.ButtonNone, 0) ct.Gotit = false lb.UserInput(evsp, sz, gowid.Focused, gwtest.D) c1 = lb.Render(sz, gowid.NotSelected, gwtest.D) assert.Equal(t, "[X] \n[ ] \n[ ] \n ", c1.String()) assert.Equal(t, ct.Gotit, false) ct.Gotit = false log.Infof("Sending left mouse down at %d,%d", 1, 1) lb.UserInput(evlm, sz, gowid.Focused, gwtest.D) gwtest.D.SetLastMouseState(gowid.MouseState{true, false, false}) log.Infof("Sending left mouse up at %d,%d", 1, 1) lb.UserInput(evnone, sz, gowid.Focused, gwtest.D) gwtest.D.SetLastMouseState(gowid.MouseState{}) c1 = lb.Render(sz, gowid.NotSelected, gwtest.D) assert.Equal(t, "[X] \n[X] \n[ ] \n ", c1.String()) assert.Equal(t, ct.Gotit, true) ct.Gotit = false lb.UserInput(evsp, sz, gowid.Focused, gwtest.D) c1 = lb.Render(sz, gowid.NotSelected, gwtest.D) assert.Equal(t, "[X] \n[ ] \n[ ] \n ", c1.String()) assert.Equal(t, ct.Gotit, true) } func TestListBox3(t *testing.T) { defer gwtest.ClearTestApp() ct := &gwtest.CheckBoxTester{Gotit: false} assert.Equal(t, ct.Gotit, false) widget1 := checkbox.New(false) widget2 := checkbox.New(false) widget3 := checkbox.New(false) widget1.OnClick(ct) walker := NewSimpleListWalker([]gowid.IWidget{ widget1, widget2, widget3, }) lb := New(walker) c1 := lb.Render(gowid.RenderFixed{}, gowid.NotSelected, gwtest.D) assert.Equal(t, c1.String(), "[ ]\n[ ]\n[ ]") evlm := tcell.NewEventMouse(1, 0, tcell.Button1, 0) evnone := tcell.NewEventMouse(1, 0, tcell.ButtonNone, 0) ct.Gotit = false lb.UserInput(evlm, gowid.RenderFixed{}, gowid.Focused, gwtest.D) gwtest.D.SetLastMouseState(gowid.MouseState{true, false, false}) lb.UserInput(evnone, gowid.RenderFixed{}, gowid.Focused, gwtest.D) gwtest.D.SetLastMouseState(gowid.MouseState{}) assert.Equal(t, ct.Gotit, true) } type focusWidget struct { focus gowid.IWidget notfocus gowid.IWidget } func (w *focusWidget) RenderSize(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.IRenderBox { if focus.Focus { return w.focus.RenderSize(size, focus, app) } else { return w.notfocus.RenderSize(size, focus, app) } } func (w *focusWidget) Render(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.ICanvas { if focus.Focus { return w.focus.Render(size, focus, app) } else { return w.notfocus.Render(size, focus, app) } } func (w *focusWidget) UserInput(ev interface{}, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) bool { if focus.Focus { return w.focus.UserInput(ev, size, focus, app) } else { return w.notfocus.UserInput(ev, size, focus, app) } } func (w *focusWidget) Selectable() bool { return true } func TestListBox4(t *testing.T) { defer gwtest.ClearTestApp() lws := make([]gowid.IWidget, 5) for i := 0; i < len(lws); i++ { w := text.New(fmt.Sprintf("%d", i)) lws[i] = &focusWidget{pile.NewFixed(w, w), w} } lw := NewSimpleListWalker(lws) lb := New(lw) c1 := lb.Render(gowid.RenderFixed{}, gowid.Focused, gwtest.D) assert.Equal(t, "0\n0\n1\n2\n3\n4", c1.String()) evpgup := tcell.NewEventKey(tcell.KeyPgUp, ' ', tcell.ModNone) evpgdn := tcell.NewEventKey(tcell.KeyPgDn, ' ', tcell.ModNone) // ct.Gotit = false lb.UserInput(gwtest.CursorDown(), gowid.RenderFixed{}, gowid.Focused, gwtest.D) c1 = lb.Render(gowid.RenderFixed{}, gowid.Focused, gwtest.D) assert.Equal(t, "0\n1\n1\n2\n3\n4", c1.String()) for i := 0; i < len(lws); i++ { res := lb.UserInput(gwtest.CursorDown(), gowid.RenderFixed{}, gowid.Focused, gwtest.D) if i < 3 { // TODO: bug - assert.Equal(t, true, res) } else { assert.Equal(t, false, res) } } c1 = lb.Render(gowid.RenderFixed{}, gowid.Focused, gwtest.D) assert.Equal(t, "0\n1\n2\n3\n4\n4", c1.String()) lb.UserInput(evpgup, gowid.RenderFixed{}, gowid.Focused, gwtest.D) c1 = lb.Render(gowid.RenderFixed{}, gowid.Focused, gwtest.D) assert.Equal(t, "0\n0\n1\n2\n3\n4", c1.String()) lb.UserInput(evpgdn, gowid.RenderFixed{}, gowid.Focused, gwtest.D) c1 = lb.Render(gowid.RenderFixed{}, gowid.Focused, gwtest.D) assert.Equal(t, "0\n1\n2\n3\n4\n4", c1.String()) } func TestListBox5(t *testing.T) { defer gwtest.ClearTestApp() lws := make([]gowid.IWidget, 0) for i := 0; i < 5; i++ { lws = append(lws, &text.Widget1{i}) lws = append(lws, text.New("-")) } lw := NewSimpleListWalker(lws) lb := New(lw) c1 := lb.Render(gowid.RenderFixed{}, gowid.Focused, gwtest.D) assert.Equal(t, "0f\n- \n1 \n- \n2 \n- \n3 \n- \n4 \n- ", c1.String()) // ct.Gotit = false lb.UserInput(gwtest.CursorDown(), gowid.RenderFixed{}, gowid.Focused, gwtest.D) lb.UserInput(gwtest.CursorDown(), gowid.RenderFixed{}, gowid.Focused, gwtest.D) c1 = lb.Render(gowid.RenderFixed{}, gowid.Focused, gwtest.D) assert.Equal(t, "0 \n- \n1 \n- \n2f\n- \n3 \n- \n4 \n- ", c1.String()) // Click on a selectable widget, should move lb.UserInput(gwtest.ClickAt(1, 0), gowid.RenderFixed{}, gowid.Focused, gwtest.D) gwtest.D.SetLastMouseState(gowid.MouseState{true, false, false}) lb.UserInput(gwtest.ClickUpAt(1, 0), gowid.RenderFixed{}, gowid.Focused, gwtest.D) gwtest.D.SetLastMouseState(gowid.MouseState{false, false, false}) c1 = lb.Render(gowid.RenderFixed{}, gowid.Focused, gwtest.D) assert.Equal(t, "0f\n- \n1 \n- \n2 \n- \n3 \n- \n4 \n- ", c1.String()) // Click on an unselectable widget, should preserve current state - so no change lb.UserInput(gwtest.ClickAt(1, 3), gowid.RenderFixed{}, gowid.Focused, gwtest.D) gwtest.D.SetLastMouseState(gowid.MouseState{true, false, false}) lb.UserInput(gwtest.ClickUpAt(1, 3), gowid.RenderFixed{}, gowid.Focused, gwtest.D) gwtest.D.SetLastMouseState(gowid.MouseState{false, false, false}) c1 = lb.Render(gowid.RenderFixed{}, gowid.Focused, gwtest.D) assert.Equal(t, "0f\n- \n1 \n- \n2 \n- \n3 \n- \n4 \n- ", c1.String()) } func TestEmptyListBox1(t *testing.T) { defer gwtest.ClearTestApp() lws := make([]gowid.IWidget, 0) lw := NewSimpleListWalker(lws) lb := New(lw) c1 := lb.Render(gowid.RenderFixed{}, gowid.Focused, gwtest.D) assert.Equal(t, "", c1.String()) // ct.Gotit = false lb.UserInput(gwtest.CursorDown(), gowid.RenderFixed{}, gowid.Focused, gwtest.D) c1 = lb.Render(gowid.RenderFixed{}, gowid.Focused, gwtest.D) assert.Equal(t, "", c1.String()) f := lw.Focus() assert.Equal(t, nil, lw.At(f)) } func TestDisabled1(t *testing.T) { defer gwtest.ClearTestApp() fixed := gowid.RenderFixed{} foc := make([]gowid.IWidget, 0) notfoc := make([]gowid.IWidget, 0) for i := 0; i < 3; i++ { txtf := text.New(fmt.Sprintf("f%d", i)) txtn := text.New(fmt.Sprintf("a%d", i)) foc = append(foc, txtf) notfoc = append(notfoc, txtn) } lws := make([]gowid.IWidget, 0) for i := 0; i < len(foc); i++ { lws = append(lws, selectable.New( isselected.New(notfoc[i], nil, foc[i]), ), ) } lw := NewSimpleListWalker(lws) lb := New(lw) c1 := lb.Render(fixed, gowid.Focused, gwtest.D) assert.Equal(t, strings.Join([]string{ "f0", "a1", "a2", }, "\n"), c1.String()) lb.UserInput(gwtest.CursorDown(), fixed, gowid.Focused, gwtest.D) c1 = lb.Render(fixed, gowid.Focused, gwtest.D) assert.Equal(t, strings.Join([]string{ "a0", "f1", "a2", }, "\n"), c1.String()) dis := disable.New(text.New("dd")) lws2 := append(lws[0:2], append([]gowid.IWidget{dis}, lws[2:]...)...) lw2 := NewSimpleListWalker(lws2) lb.SetWalker(lw2, gwtest.D) lb.UserInput(gwtest.CursorDown(), fixed, gowid.Focused, gwtest.D) c1 = lb.Render(fixed, gowid.Focused, gwtest.D) assert.Equal(t, strings.Join([]string{ "a0", "f1", "dd", "a2", }, "\n"), c1.String()) clickat := func(x, y int) { evlm := tcell.NewEventMouse(x, y, tcell.Button1, 0) evnone := tcell.NewEventMouse(x, y, tcell.ButtonNone, 0) lb.UserInput(evlm, fixed, gowid.Focused, gwtest.D) gwtest.D.SetLastMouseState(gowid.MouseState{true, false, false}) lb.UserInput(evnone, fixed, gowid.Focused, gwtest.D) gwtest.D.SetLastMouseState(gowid.MouseState{}) } clickat(1, 1) c1 = lb.Render(fixed, gowid.Focused, gwtest.D) assert.Equal(t, strings.Join([]string{ "a0", "f1", "dd", "a2", }, "\n"), c1.String()) clickat(1, 2) c1 = lb.Render(fixed, gowid.Focused, gwtest.D) assert.Equal(t, strings.Join([]string{ "a0", "f1", "dd", "a2", }, "\n"), c1.String()) clickat(1, 3) c1 = lb.Render(fixed, gowid.Focused, gwtest.D) assert.Equal(t, strings.Join([]string{ "a0", "a1", "dd", "f2", }, "\n"), c1.String()) fpos := -1 lb.OnFocusChanged(gowid.WidgetCallback{Name: "cb", WidgetChangedFunction: func(app gowid.IApp, w gowid.IWidget) { fpos = lb.Walker().Focus().(ListPos).ToInt() }}) clickat(1, 1) assert.Equal(t, 1, fpos) clickat(1, 2) assert.Equal(t, 1, fpos) clickat(1, 3) assert.Equal(t, 3, fpos) } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: gowid-1.4.0/widgets/menu/000077500000000000000000000000001426234454000152435ustar00rootroot00000000000000gowid-1.4.0/widgets/menu/menu.go000066400000000000000000000277741426234454000165570ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. // Package menu is a widget that presents a drop-down menu. package menu import ( "fmt" "github.com/gcla/gowid" "github.com/gcla/gowid/widgets/holder" "github.com/gcla/gowid/widgets/null" "github.com/gcla/gowid/widgets/overlay" tcell "github.com/gdamore/tcell/v2" ) //====================================================================== type IWidget interface { gowid.ICompositeWidget Overlay() overlay.IWidgetSettable Open(ISite, gowid.IApp) Close(gowid.IApp) IsOpen() bool CloseKeys() []gowid.IKey // Keys that should close the current submenu (e.g. left arrow) IgnoreKeys() []gowid.IKey // Keys that shouldn't close submenu but should be passed back to main app AutoClose() bool SetAutoClose(bool, gowid.IApp) Width() gowid.IWidgetDimension SetWidth(gowid.IWidgetDimension, gowid.IApp) Name() string } type IOpener interface { OpenMenu(*Widget, ISite, gowid.IApp) bool CloseMenu(*Widget, gowid.IApp) } type Options struct { CloseKeysProvided bool CloseKeys []gowid.IKey IgnoreKeysProvided bool IgnoreKeys []gowid.IKey NoAutoClose bool Modal bool OpenCloser IOpener } var ( DefaultIgnoreKeys = []gowid.IKey{ gowid.MakeKeyExt(tcell.KeyLeft), gowid.MakeKeyExt(tcell.KeyRight), gowid.MakeKeyExt(tcell.KeyUp), gowid.MakeKeyExt(tcell.KeyDown), } DefaultCloseKeys = []gowid.IKey{ gowid.MakeKeyExt(tcell.KeyLeft), gowid.MakeKeyExt(tcell.KeyEscape), } ) // Widget overlays one widget on top of another. The bottom widget // is rendered without the focus at full size. The bottom widget is // rendered between a horizontal and vertical padding widget set up with // the sizes provided. type Widget struct { overlay *overlay.Widget // So that I can set the "top" widget in the overlay to "open" the menu baseHolder *holder.Widget // Holds the actual base widget modal *rejectKeyInput // Allow/disallow keys to lower when menu is open top *NavWrapperWidget // So that I can reinstate it in the overlay to "open" the menu name string // The name uses for the canvas anchor site ISite // If open, provides the name of the canvas anchor at which the top widget is rendered width gowid.IWidgetDimension // For rendering the top widget autoClose bool // If true, then close the menu if it was open, and another widget takes the input opts Options Callbacks *gowid.Callbacks } type rejectKeyInput struct { gowid.IWidget on bool } // New takes a widget, rather than a menu model, so that I can potentially style // the menu. TODO - consider adding styling to menu model? // func New(name string, menuw gowid.IWidget, width gowid.IWidgetDimension, opts ...Options) *Widget { var opt Options if len(opts) > 0 { opt = opts[0] } if opt.OpenCloser == nil { opt.OpenCloser = OpenerFunc(OpenSimple) } res := &Widget{ name: name, width: width, autoClose: !opt.NoAutoClose, opts: opt, Callbacks: gowid.NewCallbacks(), } var _ IWidget = res var _ ISiteName = res baseHolder := holder.New(null.New()) closerBase := &rejectKeyInput{ IWidget: baseHolder, } // We don't have the base widget at this point base := &AutoCloserWidget{ IWidget: closerBase, menu: res, } // // Makes sure submenu doesn't take keyboard input unless it is "selected" top := &NavWrapperWidget{menuw, res} ov := overlay.New( nil, base, gowid.VAlignTop{0}, gowid.RenderFixed{}, //gowid.RenderWithRatio{1.0}, gowid.HAlignLeft{}, width, overlay.Options{ BottomGetsFocus: true, }, ) res.overlay = ov res.baseHolder = baseHolder res.modal = closerBase res.top = top return res } func (w *Widget) String() string { return fmt.Sprintf("menu[%v]", w.Name()) } func (w *Widget) AutoClose() bool { return w.autoClose } func (w *Widget) SetAutoClose(autoClose bool, app gowid.IApp) { w.autoClose = autoClose } func (w *Widget) Width() gowid.IWidgetDimension { return w.overlay.Width() } func (w *Widget) SetWidth(width gowid.IWidgetDimension, app gowid.IApp) { w.overlay.SetWidth(width, app) } func (w *Widget) Height() gowid.IWidgetDimension { return w.overlay.Height() } func (w *Widget) SetHeight(width gowid.IWidgetDimension, app gowid.IApp) { w.overlay.SetHeight(width, app) } func (w *Widget) Name() string { return w.name } func (w *Widget) Open(site ISite, app gowid.IApp) { w.opts.OpenCloser.OpenMenu(w, site, app) } func (w *Widget) OpenImpl(site ISite, app gowid.IApp) { w.site = site site.SetNamer(w, app) w.overlay.SetTop(w.top, app) if w.opts.Modal { w.modal.on = true } } func (w *Widget) Close(app gowid.IApp) { w.opts.OpenCloser.CloseMenu(w, app) } func (w *Widget) CloseImpl(app gowid.IApp) { // protect against case where it's closed already if w.site != nil { w.site.SetNamer(nil, app) w.site = nil } w.overlay.SetTop(nil, app) w.modal.on = false } func (w *Widget) Overlay() overlay.IWidgetSettable { return w.overlay } func (w *Widget) SetSubWidget(widget gowid.IWidget, app gowid.IApp) { w.baseHolder.IWidget = widget } func (w *Widget) SubWidget() gowid.IWidget { return w.baseHolder.IWidget } func (w *Widget) SubWidgetSize(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.IRenderSize { return w.RenderSize(size, focus, app) } func (w *Widget) IsOpen() bool { return w.overlay.Top() != nil } func (w *Widget) CloseKeys() []gowid.IKey { closeKeys := w.opts.CloseKeys if !w.opts.CloseKeysProvided && len(w.opts.CloseKeys) == 0 { closeKeys = DefaultCloseKeys } return closeKeys } func (w *Widget) IgnoreKeys() []gowid.IKey { ignoreKeys := w.opts.IgnoreKeys if !w.opts.IgnoreKeysProvided && len(w.opts.IgnoreKeys) == 0 { ignoreKeys = DefaultIgnoreKeys } return ignoreKeys } func (w *Widget) RenderSize(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.IRenderBox { return gowid.CalculateRenderSizeFallback(w, size, focus, app) } func (w *Widget) Selectable() bool { return true } func (w *Widget) UserInput(ev interface{}, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) bool { return UserInput(w, ev, size, focus, app) } func (w *Widget) Render(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.ICanvas { return Render(w, size, focus, app) } func (w *rejectKeyInput) UserInput(ev interface{}, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) bool { if _, ok := ev.(*tcell.EventKey); ok && w.on { return false } if _, ok := ev.(*tcell.EventPaste); ok && w.on { return false } return w.IWidget.UserInput(ev, size, focus, app) } //====================================================================== type CachedWidget struct { gowid.IWidget c gowid.ICanvas } func (w *CachedWidget) Render(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.ICanvas { return w.c } type CachedOverlay struct { overlay.IWidget c gowid.ICanvas } func (w *CachedOverlay) Bottom() gowid.IWidget { return &CachedWidget{w.IWidget.Bottom(), w.c} } func (w *CachedOverlay) Render(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.ICanvas { return overlay.Render(w, size, focus, app) } //====================================================================== func UserInput(w IWidget, ev interface{}, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) bool { return gowid.UserInputIfSelectable(w.Overlay(), ev, size, focus, app) } func Render(w IWidget, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.ICanvas { bfocus := focus.And(w.Overlay().BottomGetsFocus()) bottomC := w.Overlay().Bottom().Render(size, bfocus, app) off, ok := bottomC.GetMark(w.Name()) if !ok { // Means menu is closed return bottomC } w.Overlay().SetVAlign(gowid.VAlignTop{off.Y}, app) w.Overlay().SetHAlign(gowid.HAlignLeft{Margin: off.X}, app) // So we don't need to render the bottom canvas twice fakeOverlay := &CachedOverlay{w.Overlay(), bottomC} return fakeOverlay.Render(size, focus, app) } //====================================================================== type ISiteName interface { Name() string } type ISite interface { Namer() ISiteName SetNamer(ISiteName, gowid.IApp) } // SiteWidget is a zero-width widget which acts as the coordinates at which a submenu will open type SiteWidget struct { gowid.IWidget Options SiteOptions } type SiteOptions struct { Namer ISiteName XOffset int YOffset int } func NewSite(opts ...SiteOptions) *SiteWidget { var opt SiteOptions if len(opts) > 0 { opt = opts[0] } res := &SiteWidget{ IWidget: null.New(), Options: opt, } var _ gowid.IWidget = res var _ ISite = res return res } func (w *SiteWidget) Selectable() bool { return false } func (w *SiteWidget) Namer() ISiteName { return w.Options.Namer } func (w *SiteWidget) SetNamer(m ISiteName, app gowid.IApp) { w.Options.Namer = m } func (w *SiteWidget) Render(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.ICanvas { res := w.IWidget.Render(size, focus, app) if w.Options.Namer != nil { res.SetMark(w.Options.Namer.Name(), w.Options.XOffset, w.Options.YOffset) } return res } func (w *SiteWidget) String() string { return fmt.Sprintf("menusite[->%v]", w.Options.Namer) } //====================================================================== // AutoCloserWidget is used to detect if a given menu is open when a widget responds to user // input. Then some action can be taken after that user input (e.g. closing the menu) type AutoCloserWidget struct { gowid.IWidget menu IWidget } func (w *AutoCloserWidget) UserInput(ev interface{}, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) bool { wasOpen := w.menu.IsOpen() res := w.IWidget.UserInput(ev, size, focus, app) // Close the menu if it was open prior to this input operation (i.e. not just opened) and // if the non-menu part of the UI took the current input - but only if the input was mouse. // This makes it harder to accidentally close the menu by hitting e.g. a right cursor key and // it not being accepted by the menu, instead being accepted by the base widget if w.menu.AutoClose() && wasOpen && res { if _, ok := ev.(*tcell.EventMouse); ok { w.menu.Close(app) } } return res } //====================================================================== // NavWrapperWidget is used to detect if a given menu is open when a widget responds to user // input. Then some action can be taken after that user input (e.g. closing the menu) type NavWrapperWidget struct { gowid.IWidget menu IWidget //index int } func (w *NavWrapperWidget) UserInput(ev interface{}, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) bool { res := false // if _, ok := ev.(*tcell.EventKey); ok { // if w.index != w.menu.Active() { // return res // } // } // Test the subwidget first. It might want to capture certain keys res = w.IWidget.UserInput(ev, size, focus, app) // If the submenu itself didn't claim the input, check the close keys if !res { if evk, ok := ev.(*tcell.EventKey); ok { for _, k := range w.menu.CloseKeys() { if gowid.KeysEqual(k, evk) { w.menu.Close(app) res = true break } } if !res { for _, k := range w.menu.IgnoreKeys() { if gowid.KeysEqual(k, evk) { res = true break } } } } } return res } //====================================================================== // Return false if it was already open type OpenerFunc func(bool, *Widget, ISite, gowid.IApp) bool func (m OpenerFunc) OpenMenu(mu *Widget, site ISite, app gowid.IApp) bool { return m(true, mu, site, app) } func (m OpenerFunc) CloseMenu(mu *Widget, app gowid.IApp) { m(false, mu, nil, app) } func OpenSimple(open bool, mu *Widget, site ISite, app gowid.IApp) bool { if open { mu.OpenImpl(site, app) return true } else { mu.CloseImpl(app) return true } } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: gowid-1.4.0/widgets/null/000077500000000000000000000000001426234454000152515ustar00rootroot00000000000000gowid-1.4.0/widgets/null/null.go000066400000000000000000000026501426234454000165550ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. // Package null provides a widget which does nothing and renders nothing. package null import ( "fmt" "github.com/gcla/gowid" ) //====================================================================== type Widget struct{} func New() *Widget { res := &Widget{} var _ gowid.IWidget = res return res } func (w *Widget) Selectable() bool { return false } func (w *Widget) UserInput(ev interface{}, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) bool { return false } func (w *Widget) RenderSize(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.IRenderBox { cols, haveCols := size.(gowid.IColumns) rows, haveRows := size.(gowid.IRows) switch { case haveCols && haveRows: return gowid.RenderBox{C: cols.Columns(), R: rows.Rows()} case haveCols: return gowid.RenderBox{C: cols.Columns(), R: 0} default: return gowid.RenderBox{C: 0, R: 0} } } func (w *Widget) Render(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.ICanvas { res := gowid.NewCanvasOfSize(0, 0) gowid.MakeCanvasRightSize(res, size) return res } func (w *Widget) String() string { return fmt.Sprintf("null") } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: gowid-1.4.0/widgets/overlay/000077500000000000000000000000001426234454000157605ustar00rootroot00000000000000gowid-1.4.0/widgets/overlay/overlay.go000066400000000000000000000213301426234454000177670ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. // Package overlay is a widget that allows one widget to be overlaid on another. package overlay import ( "fmt" "github.com/gcla/gowid" "github.com/gcla/gowid/widgets/padding" tcell "github.com/gdamore/tcell/v2" ) //====================================================================== // Utility widget, used only to determine if a user input mouse event is within the bounds of // a widget. This is different from whether or not a widget handles an event. In the case of overlay, // an overlaid widget may not handle a mouse event, but if it occludes the widget underneath, that // lower widget should not accept the mouse event (since it ought to be hidden). So the callback // is expected to set a flag in the composite overlay widget to say the click was within bounds of the // upper layer. // type MouseCheckerWidget struct { gowid.IWidget ClickWasInBounds func() } func (w *MouseCheckerWidget) SubWidget() gowid.IWidget { return w.IWidget } func (w *MouseCheckerWidget) SetSubWidget(inner gowid.IWidget, app gowid.IApp) { w.IWidget = inner } func (w *MouseCheckerWidget) SubWidgetSize(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.IRenderSize { return w.SubWidget().RenderSize(size, focus, app) } func NewMouseChecker(inner gowid.IWidget, clickWasInBounds func()) *MouseCheckerWidget { res := &MouseCheckerWidget{inner, clickWasInBounds} var _ gowid.ICompositeWidget = res return res } func (w *MouseCheckerWidget) UserInput(ev interface{}, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) bool { if ev2, ok := ev.(*tcell.EventMouse); ok { mx, my := ev2.Position() ss := w.RenderSize(size, focus, app) if my < ss.BoxRows() && my >= 0 && mx < ss.BoxColumns() && mx >= 0 { w.ClickWasInBounds() } } return gowid.UserInputIfSelectable(w.IWidget, ev, size, focus, app) } //====================================================================== type IOverlay interface { Top() gowid.IWidget Bottom() gowid.IWidget VAlign() gowid.IVAlignment Height() gowid.IWidgetDimension HAlign() gowid.IHAlignment Width() gowid.IWidgetDimension BottomGetsFocus() bool TopGetsFocus() bool BottomGetsCursor() bool } type IIgnoreLowerStyle interface { IgnoreLowerStyle() bool } type IWidget interface { gowid.IWidget IOverlay } type IWidgetSettable interface { IWidget SetTop(gowid.IWidget, gowid.IApp) SetBottom(gowid.IWidget, gowid.IApp) SetVAlign(gowid.IVAlignment, gowid.IApp) SetHeight(gowid.IWidgetDimension, gowid.IApp) SetHAlign(gowid.IHAlignment, gowid.IApp) SetWidth(gowid.IWidgetDimension, gowid.IApp) } // Widget overlays one widget on top of another. The bottom widget // is rendered without the focus at full size. The bottom widget is // rendered between a horizontal and vertical padding widget set up with // the sizes provided. type Widget struct { top gowid.IWidget bottom gowid.IWidget vAlign gowid.IVAlignment height gowid.IWidgetDimension hAlign gowid.IHAlignment width gowid.IWidgetDimension opts Options Callbacks *gowid.Callbacks } var _ IIgnoreLowerStyle = (*Widget)(nil) // For callback registration type Top struct{} type Bottom struct{} type Options struct { BottomGetsFocus bool TopGetsNoFocus bool BottomGetsCursor bool IgnoreLowerStyle bool } func New(top, bottom gowid.IWidget, valign gowid.IVAlignment, height gowid.IWidgetDimension, halign gowid.IHAlignment, width gowid.IWidgetDimension, opts ...Options) *Widget { var opt Options if len(opts) > 0 { opt = opts[0] } res := &Widget{ top: top, bottom: bottom, vAlign: valign, height: height, hAlign: halign, width: width, opts: opt, Callbacks: gowid.NewCallbacks(), } var _ gowid.IWidget = res var _ IWidgetSettable = res return res } func (w *Widget) String() string { return fmt.Sprintf("overlay[t=%v,b=%v]", w.top, w.bottom) } func (w *Widget) BottomGetsCursor() bool { return w.opts.BottomGetsCursor } func (w *Widget) BottomGetsFocus() bool { return w.opts.BottomGetsFocus } func (w *Widget) TopGetsFocus() bool { return !w.opts.TopGetsNoFocus } func (w *Widget) Top() gowid.IWidget { return w.top } func (w *Widget) SetTop(w2 gowid.IWidget, app gowid.IApp) { w.top = w2 gowid.RunWidgetCallbacks(w.Callbacks, Top{}, app, w) } func (w *Widget) Bottom() gowid.IWidget { return w.bottom } func (w *Widget) SetBottom(w2 gowid.IWidget, app gowid.IApp) { w.bottom = w2 gowid.RunWidgetCallbacks(w.Callbacks, Bottom{}, app, w) } func (w *Widget) VAlign() gowid.IVAlignment { return w.vAlign } func (w *Widget) SetVAlign(valign gowid.IVAlignment, app gowid.IApp) { w.vAlign = valign gowid.RunWidgetCallbacks(w.Callbacks, gowid.VAlignCB{}, app, w) } func (w *Widget) Height() gowid.IWidgetDimension { return w.height } func (w *Widget) SetHeight(height gowid.IWidgetDimension, app gowid.IApp) { w.height = height gowid.RunWidgetCallbacks(w.Callbacks, gowid.HeightCB{}, app, w) } func (w *Widget) HAlign() gowid.IHAlignment { return w.hAlign } func (w *Widget) SetHAlign(halign gowid.IHAlignment, app gowid.IApp) { w.hAlign = halign gowid.RunWidgetCallbacks(w.Callbacks, gowid.HAlignCB{}, app, w) } func (w *Widget) Width() gowid.IWidgetDimension { return w.width } func (w *Widget) SetWidth(width gowid.IWidgetDimension, app gowid.IApp) { w.width = width gowid.RunWidgetCallbacks(w.Callbacks, gowid.WidthCB{}, app, w) } func (w *Widget) RenderSize(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.IRenderBox { return w.bottom.RenderSize(size, gowid.NotSelected, app) } func (w *Widget) Selectable() bool { return (w.top != nil && w.top.Selectable()) || w.bottom.Selectable() } func (w *Widget) UserInput(ev interface{}, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) bool { return UserInput(w, ev, size, focus, app) } func (w *Widget) Render(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.ICanvas { return Render(w, size, focus, app) } func (w *Widget) SubWidget() gowid.IWidget { if w.opts.BottomGetsFocus { return w.bottom } else { return w.top } } func (w *Widget) SetSubWidget(inner gowid.IWidget, app gowid.IApp) { if w.opts.BottomGetsFocus { w.bottom = inner } else { w.top = inner } } func (w *Widget) IgnoreLowerStyle() bool { return w.opts.IgnoreLowerStyle } //====================================================================== func UserInput(w IOverlay, ev interface{}, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) bool { res := false notOccluded := true if w.Top() == nil { res = gowid.UserInputIfSelectable(w.Bottom(), ev, size, focus, app) } else { top := NewMouseChecker(w.Top(), func() { notOccluded = false }) p := padding.New(top, w.VAlign(), w.Height(), w.HAlign(), w.Width()) res = gowid.UserInputIfSelectable(p, ev, size, focus, app) if !res { _, ok1 := ev.(*tcell.EventKey) _, ok2 := ev.(*tcell.EventMouse) _, ok3 := ev.(*tcell.EventPaste) if notOccluded && (ok1 || ok2 || ok3) { res = gowid.UserInputIfSelectable(w.Bottom(), ev, size, focus, app) } } } return res } // Merge cells as follows - use upper rune if set, use upper colors if set, // and use upper style only (don't let any lower run style bleed through) func mergeAllExceptUpperStyle(lower gowid.Cell, upper gowid.Cell) gowid.Cell { res := lower if upper.HasRune() { res = res.WithRune(upper.Rune()) } ufg, ubg, _ := upper.GetDisplayAttrs() if ubg != gowid.ColorNone { res = res.WithBackgroundColor(ubg) } if ufg != gowid.ColorNone { res = res.WithForegroundColor(ufg) } res = res.WithStyle(upper.Style()) return res } type iMergeWithFuncCanvas interface { MergeWithFunc(gowid.IMergeCanvas, int, int, gowid.CellMergeFunc, bool) } func Render(w IOverlay, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.ICanvas { bfocus := focus.And(w.BottomGetsFocus()) tfocus := focus.And(w.TopGetsFocus()) bottomC := w.Bottom().Render(size, bfocus, app) if w.Top() == nil { return bottomC } else { bottomC2 := bottomC.Duplicate() p2 := padding.New(w.Top(), w.VAlign(), w.Height(), w.HAlign(), w.Width()) topC := p2.Render(size, tfocus, app) var bottomC2mc iMergeWithFuncCanvas ign := false if wIgn, ok := w.(IIgnoreLowerStyle); ok { if gc, ok := bottomC2.(iMergeWithFuncCanvas); ok { bottomC2mc = gc ign = wIgn.IgnoreLowerStyle() } } if ign { bottomC2mc.MergeWithFunc(topC, 0, 0, mergeAllExceptUpperStyle, w.BottomGetsCursor()) } else { bottomC2.MergeUnder(topC, 0, 0, w.BottomGetsCursor()) } return bottomC2 } } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: gowid-1.4.0/widgets/overlay/overlay_test.go000066400000000000000000000030451426234454000210310ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. package overlay import ( "testing" "github.com/gcla/gowid" "github.com/gcla/gowid/gwtest" "github.com/gcla/gowid/widgets/styled" "github.com/gcla/gowid/widgets/text" "github.com/gdamore/tcell/v2" "github.com/stretchr/testify/assert" ) func TestOverlay1(t *testing.T) { tw := text.New("top") bw := text.New("bottom") ov := New(tw, bw, gowid.VAlignTop{}, gowid.RenderFixed{}, gowid.HAlignLeft{}, gowid.RenderFixed{}) c := ov.Render(gowid.RenderFlowWith{C: 6}, gowid.Focused, gwtest.D) assert.Equal(t, "toptom", c.String()) bwStyled := styled.New(bw, gowid.MakeStyledAs(gowid.StyleBold)) // When the widget is created this way, the style from the lower widget bleeds through ov = New(tw, bwStyled, gowid.VAlignTop{}, gowid.RenderFixed{}, gowid.HAlignLeft{}, gowid.RenderFixed{}) c = ov.Render(gowid.RenderFlowWith{C: 6}, gowid.Focused, gwtest.D) assert.Equal(t, "toptom", c.String()) assert.Equal(t, tcell.AttrBold, c.CellAt(0, 0).Style().OnOff&tcell.AttrBold) // When the widget is created this way, the style from the upper widget is set unilaterally ov = New(tw, bwStyled, gowid.VAlignTop{}, gowid.RenderFixed{}, gowid.HAlignLeft{}, gowid.RenderFixed{}, Options{ IgnoreLowerStyle: true, }) c = ov.Render(gowid.RenderFlowWith{C: 6}, gowid.Focused, gwtest.D) assert.Equal(t, "toptom", c.String()) assert.Equal(t, tcell.AttrMask(0), c.CellAt(0, 0).Style().OnOff&tcell.AttrBold) } gowid-1.4.0/widgets/padding/000077500000000000000000000000001426234454000157055ustar00rootroot00000000000000gowid-1.4.0/widgets/padding/padding.go000066400000000000000000000300071426234454000176420ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. // Package padding provides a widget that pads an inner widget on the sides, above and below package padding import ( "errors" "fmt" "github.com/gcla/gowid" "github.com/gcla/gowid/gwutil" "github.com/gcla/gowid/widgets/fill" tcell "github.com/gdamore/tcell/v2" ) //====================================================================== type IWidget interface { gowid.ICompositeWidget HAlign() gowid.IHAlignment Width() gowid.IWidgetDimension VAlign() gowid.IVAlignment Height() gowid.IWidgetDimension } // Widget renders the wrapped widget with the provided // width; if the wrapped widget is a box, or the wrapped widget is to be // packed to a width smaller than specified, the wrapped widget can be // aligned in the middle, left or right type Widget struct { inner gowid.IWidget vAlign gowid.IVAlignment height gowid.IWidgetDimension hAlign gowid.IHAlignment width gowid.IWidgetDimension opts Options Callbacks *gowid.Callbacks gowid.FocusCallbacks gowid.SubWidgetCallbacks } type Options struct{} func New(inner gowid.IWidget, valign gowid.IVAlignment, height gowid.IWidgetDimension, halign gowid.IHAlignment, width gowid.IWidgetDimension, opts ...Options) *Widget { var opt Options if len(opts) > 0 { opt = opts[0] } res := &Widget{ inner: inner, vAlign: valign, height: height, hAlign: halign, width: width, opts: opt, Callbacks: gowid.NewCallbacks(), } //var _ gowid.IWidget = res //var _ IWidgetSettable = res return res } func (w *Widget) String() string { return fmt.Sprintf("hpad[%v]", w.SubWidget()) } func (w *Widget) Selectable() bool { return w.inner.Selectable() } func (w *Widget) SubWidget() gowid.IWidget { return w.inner } func (w *Widget) SetSubWidget(wi gowid.IWidget, app gowid.IApp) { w.inner = wi gowid.RunWidgetCallbacks(w.Callbacks, gowid.SubWidgetCB{}, app, w) } func (w *Widget) OnSetAlign(f gowid.IWidgetChangedCallback) { gowid.AddWidgetCallback(w.Callbacks, gowid.HAlignCB{}, f) } func (w *Widget) RemoveOnSetAlign(f gowid.IIdentity) { gowid.RemoveWidgetCallback(w.Callbacks, gowid.HAlignCB{}, f) } func (w *Widget) HAlign() gowid.IHAlignment { return w.hAlign } func (w *Widget) SetHAlign(i gowid.IHAlignment, app gowid.IApp) { w.hAlign = i gowid.RunWidgetCallbacks(w.Callbacks, gowid.HAlignCB{}, app, w) } func (w *Widget) VAlign() gowid.IVAlignment { return w.vAlign } func (w *Widget) SetVAlign(i gowid.IVAlignment, app gowid.IApp) { w.vAlign = i gowid.RunWidgetCallbacks(w.Callbacks, gowid.VAlignCB{}, app, w) } func (w *Widget) OnSetHeight(f gowid.IWidgetChangedCallback) { gowid.AddWidgetCallback(w.Callbacks, gowid.WidthCB{}, f) } func (w *Widget) RemoveOnSetHeight(f gowid.IIdentity) { gowid.RemoveWidgetCallback(w.Callbacks, gowid.WidthCB{}, f) } func (w *Widget) Width() gowid.IWidgetDimension { return w.width } func (w *Widget) SetWidth(i gowid.IWidgetDimension, app gowid.IApp) { w.width = i gowid.RunWidgetCallbacks(w.Callbacks, gowid.WidthCB{}, app, w) } func (w *Widget) Height() gowid.IWidgetDimension { return w.height } func (w *Widget) SetHeight(i gowid.IWidgetDimension, app gowid.IApp) { w.height = i gowid.RunWidgetCallbacks(w.Callbacks, gowid.HeightCB{}, app, w) } func (w *Widget) SubWidgetSize(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.IRenderSize { size2 := size // If there is a horizontal offset specified, the relative features should reduce the size of the // supplied size i.e. it should be relative to the reduced screen size switch al := w.HAlign().(type) { case gowid.HAlignLeft: switch s := size.(type) { case gowid.IRenderBox: size2 = gowid.RenderBox{C: s.BoxColumns() - (al.Margin + al.MarginRight), R: s.BoxRows()} case gowid.IRenderFlowWith: size2 = gowid.RenderFlowWith{C: s.FlowColumns() - (al.Margin + al.MarginRight)} default: } default: } switch al := w.VAlign().(type) { case gowid.VAlignTop: switch s := size2.(type) { case gowid.IRenderBox: size2 = gowid.RenderBox{C: s.BoxColumns(), R: s.BoxRows() - al.Margin} } } return gowid.ComputeSubSizeUnsafe(size2, w.Width(), w.Height()) } func (w *Widget) RenderSize(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.IRenderBox { return gowid.CalculateRenderSizeFallback(w, size, focus, app) } func (w *Widget) Render(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.ICanvas { return Render(w, size, focus, app) } func (w *Widget) UserInput(ev interface{}, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) bool { return UserInput(w, ev, size, focus, app) } //'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' // func SubWidgetSize(w IWidget, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.IRenderSize { // size2 := size // // If there is a horizontal offset specified, the relative features should reduce the size of the // // supplied size i.e. it should be relative to the reduced screen size // switch al := w.Align().(type) { // case gowid.HAlignLeft: // switch s := size.(type) { // case gowid.IRenderBox: // size2 = gowid.RenderBox{C: s.BoxColumns() - al.Margin, R: s.BoxRows()} // case gowid.IRenderFlowWith: // size2 = gowid.RenderFlowWith{C: s.FlowColumns() - al.Margin} // default: // } // default: // } // return gowid.ComputeHorizontalSubSizeUnsafe(size2, w.Width()) // } func Render(w IWidget, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.ICanvas { subSize := w.SubWidgetSize(size, focus, app) subWidgetCanvas := w.SubWidget().Render(subSize, focus, app) subWidgetMaxColumn := subWidgetCanvas.BoxColumns() var myCols int if cols, ok := size.(gowid.IColumns); ok { myCols = cols.Columns() } else { myCols = subWidgetMaxColumn } if myCols < subWidgetMaxColumn { // TODO - bad, mandates trimming on right subWidgetCanvas.TrimRight(myCols) } else if myCols > subWidgetMaxColumn { switch al := w.HAlign().(type) { case gowid.HAlignRight: subWidgetCanvas.ExtendLeft(gowid.EmptyLine(myCols - subWidgetMaxColumn)) case gowid.HAlignMiddle: r := (myCols - subWidgetMaxColumn) / 2 l := myCols - (subWidgetMaxColumn + r) subWidgetCanvas.ExtendRight(gowid.EmptyLine(r)) subWidgetCanvas.ExtendLeft(gowid.EmptyLine(l)) case gowid.HAlignLeft: l := gwutil.Min(al.Margin, myCols-subWidgetMaxColumn) r := myCols - (l + subWidgetMaxColumn) subWidgetCanvas.ExtendRight(gowid.EmptyLine(r)) subWidgetCanvas.ExtendLeft(gowid.EmptyLine(l)) default: panic(fmt.Errorf("Invalid horizontal alignment setting %v of type %T", al, al)) } } maxCol := subWidgetCanvas.BoxColumns() subWidgetRows := subWidgetCanvas.BoxRows() fill := fill.NewEmpty() var rowsToUseInResult int switch sz := size.(type) { case gowid.IRenderBox: rowsToUseInResult = sz.BoxRows() case gowid.IRenderFlowWith: switch w.Height().(type) { case gowid.IRenderFlow, gowid.IRenderFixed, gowid.IRenderWithUnits: rowsToUseInResult = subWidgetRows default: panic(fmt.Errorf("Height spec %v cannot be used in flow mode for %T", w.Height(), w)) } case gowid.IRenderFixed: switch w.Height().(type) { case gowid.IRenderFlow, gowid.IRenderFixed: rowsToUseInResult = subWidgetRows switch al := w.VAlign().(type) { case gowid.VAlignTop: rowsToUseInResult += al.Margin } case gowid.IRenderWithUnits: rowsToUseInResult = w.Height().(gowid.IRenderWithUnits).Units() default: panic(fmt.Errorf("This spec %v of type %T cannot be used in flow mode for %T", w.Height(), w.Height(), w)) } default: panic(fmt.Errorf("Unknown size %v", size)) } switch al := w.VAlign().(type) { case gowid.VAlignBottom: if rowsToUseInResult > subWidgetRows+al.Margin { bottoml := al.Margin topl := rowsToUseInResult - (bottoml + subWidgetRows) fc1 := fill.Render(gowid.RenderBox{C: maxCol, R: topl}, gowid.NotSelected, app) fc2 := fill.Render(gowid.RenderBox{C: maxCol, R: bottoml}, gowid.NotSelected, app) fc1.AppendBelow(subWidgetCanvas, true, false) subWidgetCanvas = fc1 subWidgetCanvas.AppendBelow(fc2, false, false) } else if rowsToUseInResult > al.Margin { bottoml := al.Margin topl := subWidgetRows - (rowsToUseInResult + bottoml) subWidgetCanvas.Truncate(topl, 0) fc1 := fill.Render(gowid.RenderBox{C: maxCol, R: bottoml}, gowid.NotSelected, app) fc1.AppendBelow(subWidgetCanvas, true, false) subWidgetCanvas = fc1 } else { subWidgetCanvas = fill.Render(gowid.RenderBox{C: maxCol, R: rowsToUseInResult}, gowid.NotSelected, app) } case gowid.VAlignMiddle: if rowsToUseInResult > subWidgetRows { topl := (rowsToUseInResult - subWidgetRows) / 2 bottoml := rowsToUseInResult - (topl + subWidgetRows) fc1 := fill.Render(gowid.RenderBox{C: maxCol, R: topl}, gowid.NotSelected, app) fc2 := fill.Render(gowid.RenderBox{C: maxCol, R: bottoml}, gowid.NotSelected, app) fc1.AppendBelow(subWidgetCanvas, true, false) subWidgetCanvas = fc1 subWidgetCanvas.AppendBelow(fc2, false, false) } else { topl := (subWidgetRows - rowsToUseInResult) / 2 bottoml := subWidgetRows - (rowsToUseInResult + topl) subWidgetCanvas.Truncate(topl, bottoml) } case gowid.VAlignTop: if rowsToUseInResult > subWidgetRows+al.Margin { topl := al.Margin bottoml := rowsToUseInResult - (topl + subWidgetRows) fc1 := fill.Render(gowid.RenderBox{C: maxCol, R: topl}, gowid.NotSelected, app) fc2 := fill.Render(gowid.RenderBox{C: maxCol, R: bottoml}, gowid.NotSelected, app) fc1.AppendBelow(subWidgetCanvas, true, false) subWidgetCanvas = fc1 subWidgetCanvas.AppendBelow(fc2, false, false) } else if rowsToUseInResult > al.Margin { topl := al.Margin bottoml := subWidgetRows - (rowsToUseInResult - al.Margin) subWidgetCanvas.Truncate(0, bottoml) fc1 := fill.Render(gowid.RenderBox{C: maxCol, R: topl}, gowid.NotSelected, app) fc1.AppendBelow(subWidgetCanvas, true, false) subWidgetCanvas = fc1 } else { topl := rowsToUseInResult subWidgetCanvas = fill.Render(gowid.RenderBox{C: maxCol, R: topl}, gowid.NotSelected, app) } default: panic(errors.New("Invalid vertical alignment setting")) } gowid.MakeCanvasRightSize(subWidgetCanvas, size) return subWidgetCanvas } // UserInput will adjust the input event's x coordinate depending on the input size // and widget alignment. If the input is e.g. IRenderFixed, then no adjustment is // made. func UserInput(w IWidget, ev interface{}, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) bool { //return false rSize := gowid.RenderSize(w, size, focus, app) subSize := w.SubWidgetSize(size, focus, app) ss := w.SubWidget().RenderSize(subSize, focus, app) sCols := ss.BoxColumns() sRows := ss.BoxRows() //cols2, ok := size.(gowid.IColumns) cols2 := rSize.BoxColumns() rows2 := rSize.BoxRows() var xd int //if ok { switch al := w.HAlign().(type) { case gowid.HAlignRight: xd = -(cols2 - sCols) case gowid.HAlignMiddle: r := (cols2 - sCols) / 2 l := cols2 - (sCols + r) xd = -l case gowid.HAlignLeft: if al.Margin+sCols <= cols2 { xd = -al.Margin } else { xd = -gwutil.Max(0, cols2-sCols) } } var yd int switch al := w.VAlign().(type) { case gowid.VAlignBottom: yd = sRows - rows2 case gowid.VAlignMiddle: yd = (sRows - rows2) / 2 case gowid.VAlignTop: if rows2 > sRows+al.Margin { yd = -al.Margin } else if rows2 > al.Margin { yd = -al.Margin } else { yd = -(rows2 - 1) } } //} newev := gowid.TranslatedMouseEvent(ev, xd, yd) // TODO - don't need to translate event for keyboard event... if evm, ok := newev.(*tcell.EventMouse); ok { mx, transY := evm.Position() if mx >= 0 && mx < sCols { if transY < sRows && transY >= 0 { return gowid.UserInputIfSelectable(w.SubWidget(), newev, subSize, focus, app) } } } else { return gowid.UserInputIfSelectable(w.SubWidget(), newev, subSize, focus, app) } return false } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: gowid-1.4.0/widgets/padding/padding_test.go000066400000000000000000000224341426234454000207060ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source code is governed by the MIT license // that can be found in the LICENSE file. package padding import ( "testing" "github.com/gcla/gowid" "github.com/gcla/gowid/gwtest" "github.com/gcla/gowid/widgets/button" "github.com/gcla/gowid/widgets/fill" "github.com/gcla/gowid/widgets/framed" "github.com/gcla/gowid/widgets/text" tcell "github.com/gdamore/tcell/v2" "github.com/stretchr/testify/assert" ) type renderRatioUpTo struct { gowid.RenderWithRatio max int } func (s renderRatioUpTo) MaxUnits() int { return s.max } func ratioupto(w float64, max int) renderRatioUpTo { return renderRatioUpTo{gowid.RenderWithRatio{R: w}, max} } func TestPadding1(t *testing.T) { var w gowid.IWidget var c gowid.ICanvas w = New(fill.New('x'), gowid.VAlignMiddle{}, gowid.RenderWithUnits{U: 2}, gowid.HAlignMiddle{}, gowid.RenderWithUnits{U: 2}) c = w.Render(gowid.RenderBox{C: 4, R: 4}, gowid.Focused, gwtest.D) assert.Equal(t, " \n xx \n xx \n ", c.String()) w = New(fill.New('x'), gowid.VAlignMiddle{}, gowid.RenderWithUnits{U: 2}, gowid.HAlignMiddle{}, gowid.RenderWithRatio{R: 0.5}) c = w.Render(gowid.RenderBox{C: 4, R: 4}, gowid.Focused, gwtest.D) assert.Equal(t, " \n xx \n xx \n ", c.String()) w = New(fill.New('x'), gowid.VAlignMiddle{}, ratioupto(0.5, 1), gowid.HAlignMiddle{}, gowid.RenderWithUnits{U: 2}) c = w.Render(gowid.RenderBox{C: 4, R: 4}, gowid.Focused, gwtest.D) assert.Equal(t, " \n xx \n \n ", c.String()) w = New(fill.New('x'), gowid.VAlignMiddle{}, gowid.RenderWithUnits{U: 2}, gowid.HAlignMiddle{}, ratioupto(0.5, 1)) c = w.Render(gowid.RenderBox{C: 4, R: 4}, gowid.Focused, gwtest.D) assert.Equal(t, " \n x \n x \n ", c.String()) w = New(text.New("foo"), gowid.VAlignMiddle{}, gowid.RenderFixed{}, gowid.HAlignMiddle{}, gowid.RenderFixed{}) c = w.Render(gowid.RenderBox{C: 5, R: 3}, gowid.Focused, gwtest.D) assert.Equal(t, " \n foo \n ", c.String()) w = New(framed.New(text.New("foo")), gowid.VAlignMiddle{}, gowid.RenderFixed{}, gowid.HAlignMiddle{}, gowid.RenderFixed{}) c = w.Render(gowid.RenderBox{C: 7, R: 5}, gowid.Focused, gwtest.D) assert.Equal(t, " \n ----- \n |foo| \n ----- \n ", c.String()) w = New(framed.New(text.New("foo")), gowid.VAlignTop{}, gowid.RenderFixed{}, gowid.HAlignMiddle{}, gowid.RenderFixed{}) c = w.Render(gowid.RenderBox{C: 7, R: 5}, gowid.Focused, gwtest.D) assert.Equal(t, " ----- \n |foo| \n ----- \n \n ", c.String()) w = New(framed.New(text.New("foo")), gowid.VAlignBottom{}, gowid.RenderFixed{}, gowid.HAlignRight{}, gowid.RenderFixed{}) c = w.Render(gowid.RenderBox{C: 7, R: 5}, gowid.Focused, gwtest.D) assert.Equal(t, " \n \n -----\n |foo|\n -----", c.String()) ct := &gwtest.ButtonTester{Gotit: false} assert.Equal(t, ct.Gotit, false) btn := button.NewBare(text.New("foo")) btn.OnClick(ct) w = New(btn, gowid.VAlignMiddle{}, gowid.RenderFixed{}, gowid.HAlignMiddle{}, gowid.RenderFixed{}) c = w.Render(gowid.RenderBox{C: 5, R: 3}, gowid.Focused, gwtest.D) assert.Equal(t, " \n foo \n ", c.String()) btn.Click(gwtest.D) assert.Equal(t, ct.Gotit, true) ct.Gotit = false ev31 := tcell.NewEventMouse(3, 1, tcell.Button1, 0) evnone31 := tcell.NewEventMouse(3, 1, tcell.ButtonNone, 0) w.UserInput(ev31, gowid.RenderBox{C: 5, R: 3}, gowid.Focused, gwtest.D) gwtest.D.SetLastMouseState(gowid.MouseState{true, false, false}) w.UserInput(evnone31, gowid.RenderBox{C: 5, R: 3}, gowid.Focused, gwtest.D) gwtest.D.SetLastMouseState(gowid.MouseState{}) assert.Equal(t, ct.Gotit, true) // w2 := New(fill.New('x'), gowid.VAlignMiddle{}, gowid.RenderWithRatio{0.5}) // c2 := w2.Render(gowid.RenderBox{C: 3, R: 4}, gowid.Focused, gwtest.D) // assert.Equal(t, c2.String(), " \nxxx\nxxx\n ") // w3 := New(fill.New('x'), gowid.VAlignMiddle{}, gowid.RenderWithRatio{0.75}) // c3 := w3.Render(gowid.RenderBox{C: 3, R: 4}, gowid.Focused, gwtest.D) // assert.Equal(t, c3.String(), "xxx\nxxx\nxxx\n ") // w4 := New(fill.New('x'), gowid.VAlignTop{}, gowid.RenderWithRatio{0.75}) // c4 := w4.Render(gowid.RenderBox{C: 3, R: 4}, gowid.Focused, gwtest.D) // assert.Equal(t, c4.String(), "xxx\nxxx\nxxx\n ") // w5 := New(fill.New('x'), gowid.VAlignMiddle{}, gowid.RenderWithRatio{0.8}) // c5 := w5.Render(gowid.RenderBox{C: 3, R: 4}, gowid.Focused, gwtest.D) // assert.Equal(t, c5.String(), "xxx\nxxx\nxxx\n ") // w6 := New(fill.New('x'), gowid.VAlignMiddle{}, gowid.RenderWithRatio{0.88}) // c6 := w6.Render(gowid.RenderBox{C: 3, R: 4}, gowid.Focused, gwtest.D) // assert.Equal(t, c6.String(), "xxx\nxxx\nxxx\nxxx") // w7 := New(fill.New('x'), gowid.VAlignTop{1}, gowid.RenderWithUnits{U: 3}) // c7 := w7.Render(gowid.RenderBox{C: 3, R: 4}, gowid.Focused, gwtest.D) // assert.Equal(t, c7.String(), " \nxxx\nxxx\nxxx") // w8 := New(fill.New('x'), gowid.VAlignTop{2}, gowid.RenderWithUnits{U: 3}) // c8 := w8.Render(gowid.RenderBox{C: 3, R: 4}, gowid.Focused, gwtest.D) // assert.Equal(t, c8.String(), " \n \nxxx\nxxx") // w9 := New(fill.New('x'), gowid.VAlignTop{2}, gowid.RenderWithUnits{U: 3}) // c9 := w9.Render(gowid.RenderBox{C: 3, R: 3}, gowid.Focused, gwtest.D) // assert.Equal(t, c9.String(), " \n \nxxx") // w10 := New(fill.New('x'), gowid.VAlignTop{2}, gowid.RenderWithUnits{U: 4}) // c10 := w10.Render(gowid.RenderBox{C: 3, R: 8}, gowid.Focused, gwtest.D) // assert.Equal(t, c10.String(), " \n \nxxx\nxxx\nxxx\nxxx\n \n ") // for _, w := range []gowid.IWidget{w1, w2, w3, w4, w5, w6, w7, w8, w9, w10} { // gwtest.RenderBoxManyTimes(t, w, 0, 10, 0, 10) // } // for _, w := range []gowid.IWidget{w1, w7, w8, w9, w10} { // gwtest.RenderFlowManyTimes(t, w, 0, 10) // } } // func TestCanvas17(t *testing.T) { // widget1i := text.New("line 1line 2line 3") // widget1 := NewBox(widget1i, 2) // canvas1 := widget1.Render(gowid.RenderFlowWith{C: 6}, gowid.NotSelected, gwtest.D) // log.Infof("Widget17 is %v", widget1) // log.Infof("Canvas17 is %s", canvas1.String()) // res := strings.Join([]string{"line 1", "line 2"}, "\n") // if res != canvas1.String() { // t.Errorf("Failed") // } // } // func TestCanvas18(t *testing.T) { // widget1i := text.New("line 1 line 2 line 3") // widget1 := NewBox(widget1i, 5) // canvas1 := widget1.Render(gowid.RenderFlowWith{C: 6}, gowid.NotSelected, gwtest.D) // log.Infof("Widget18 is %v", widget1) // log.Infof("Canvas18 is %s", canvas1.String()) // res := strings.Join([]string{"line 1", " line ", "2 line", " 3 ", " "}, "\n") // if res != canvas1.String() { // t.Errorf("Failed") // } // gwtest.RenderBoxManyTimes(t, widget1, 0, 10, 0, 10) // gwtest.RenderFlowManyTimes(t, widget1, 0, 10) // gwtest.RenderFixedDoesNotPanic(t, widget1) // } // func TestCheckbox3(t *testing.T) { // ct := &gwtest.CheckBoxTester{Gotit: false} // assert.Equal(t, ct.Gotit, false) // w := checkbox.New(false) // w.OnClick(ct) // ev := tcell.NewEventKey(tcell.KeyRune, ' ', tcell.ModNone) // w.UserInput(ev, gowid.RenderFixed{}, gowid.Focused, gwtest.D) // assert.Equal(t, ct.Gotit, true) // ct.Gotit = false // assert.Equal(t, ct.Gotit, false) // evlmx1y0 := tcell.NewEventMouse(1, 0, tcell.Button1, 0) // evnonex1y0 := tcell.NewEventMouse(1, 0, tcell.ButtonNone, 0) // sz := gowid.RenderBox{C: 5, R: 3} // w2 := New(w, gowid.VAlignTop{}, gowid.RenderFixed{}) // ct.Gotit = false // assert.Equal(t, ct.Gotit, false) // w2.UserInput(evlmx1y0, sz, gowid.Focused, gwtest.D) // gwtest.D.SetLastMouseState(gowid.MouseState{true, false, false}) // w2.UserInput(evnonex1y0, sz, gowid.Focused, gwtest.D) // gwtest.D.SetLastMouseState(gowid.MouseState{}) // assert.Equal(t, ct.Gotit, true) // w3 := New(w, gowid.VAlignBottom{}, gowid.RenderFixed{}) // ct.Gotit = false // assert.Equal(t, ct.Gotit, false) // w3.UserInput(evlmx1y0, sz, gowid.Focused, gwtest.D) // gwtest.D.SetLastMouseState(gowid.MouseState{true, false, false}) // w3.UserInput(evnonex1y0, sz, gowid.Focused, gwtest.D) // gwtest.D.SetLastMouseState(gowid.MouseState{}) // assert.Equal(t, ct.Gotit, false) // evlmx1y2 := tcell.NewEventMouse(1, 2, tcell.ButtonNone, 0) // w3.UserInput(evlmx1y2, gowid.RenderBox{C: 10, R: 1}, gowid.Focused, gwtest.D) // gwtest.D.SetLastMouseState(gowid.MouseState{true, false, false}) // w3.UserInput(evnonex1y0, gowid.RenderBox{C: 10, R: 1}, gowid.Focused, gwtest.D) // gwtest.D.SetLastMouseState(gowid.MouseState{}) // assert.Equal(t, ct.Gotit, true) // w4 := New(w, gowid.VAlignTop{1}, gowid.RenderFixed{}) // ct.Gotit = false // assert.Equal(t, ct.Gotit, false) // w4.UserInput(evlmx1y0, sz, gowid.Focused, gwtest.D) // gwtest.D.SetLastMouseState(gowid.MouseState{true, false, false}) // w4.UserInput(evnonex1y0, sz, gowid.Focused, gwtest.D) // gwtest.D.SetLastMouseState(gowid.MouseState{}) // assert.Equal(t, ct.Gotit, false) // evlmx1y1 := tcell.NewEventMouse(1, 1, tcell.Button1, 0) // evnonex1y1 := tcell.NewEventMouse(1, 1, tcell.ButtonNone, 0) // w4.UserInput(evlmx1y1, sz, gowid.Focused, gwtest.D) // gwtest.D.SetLastMouseState(gowid.MouseState{true, false, false}) // w4.UserInput(evnonex1y1, sz, gowid.Focused, gwtest.D) // gwtest.D.SetLastMouseState(gowid.MouseState{}) // assert.Equal(t, ct.Gotit, true) // } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: gowid-1.4.0/widgets/palettemap/000077500000000000000000000000001426234454000164335ustar00rootroot00000000000000gowid-1.4.0/widgets/palettemap/palettemap.go000066400000000000000000000077331426234454000211300ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. // Package palettemap provides a widget that can change the color and style of an inner widget. package palettemap import ( "fmt" "github.com/gcla/gowid" ) //====================================================================== type IPaletteMapper interface { GetMappedColor(string) (string, bool) } type Map map[string]string func (p Map) GetMappedColor(key string) (x string, y bool) { x, y = p[key] return } type IPaletteMap interface { FocusMap() IPaletteMapper NotFocusMap() IPaletteMapper } type IWidget interface { gowid.ICompositeWidget IPaletteMap } // Widget that adjusts the palette used - if the rendering context provides for a foreground // color of red (when focused), this widget can provide a map from red -> green to change its // display type Widget struct { gowid.IWidget focusMap IPaletteMapper notFocusMap IPaletteMapper *gowid.Callbacks gowid.SubWidgetCallbacks } func New(inner gowid.IWidget, focusMap Map, notFocusMap Map) *Widget { res := &Widget{ IWidget: inner, focusMap: focusMap, notFocusMap: notFocusMap, } res.SubWidgetCallbacks = gowid.SubWidgetCallbacks{CB: &res.Callbacks} var _ gowid.IWidget = res var _ gowid.ICompositeWidget = res var _ IWidget = res return res } func (w *Widget) String() string { return fmt.Sprintf("palettemap[%v]", w.SubWidget()) } func (w *Widget) SubWidget() gowid.IWidget { return w.IWidget } func (w *Widget) SetSubWidget(inner gowid.IWidget, app gowid.IApp) { w.IWidget = inner gowid.RunWidgetCallbacks(w, gowid.SubWidgetCB{}, app, w) } func (w *Widget) FocusMap() IPaletteMapper { return w.focusMap } func (w *Widget) NotFocusMap() IPaletteMapper { return w.notFocusMap } func (w *Widget) SubWidgetSize(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.IRenderSize { return w.SubWidget().RenderSize(size, focus, app) } func (w *Widget) RenderSize(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.IRenderBox { return w.SubWidget().RenderSize(size, focus, app) } func (w *Widget) Render(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.ICanvas { return Render(w, size, focus, app) } func (w *Widget) UserInput(ev interface{}, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) bool { return gowid.UserInputIfSelectable(w.IWidget, ev, size, focus, app) } //'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' func Render(w IWidget, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.ICanvas { newAttrs := make(gowid.Palette) var mapToUse IPaletteMapper if focus.Focus { mapToUse = w.FocusMap() } else { mapToUse = w.NotFocusMap() } // walk through palette, making a copy with changes as dictated by either the focus map or // not-focus map. app.RangeOverPalette(func(k string, v gowid.ICellStyler) bool { done := false if newk, ok := mapToUse.GetMappedColor(k); ok { if newval, ok := app.CellStyler(newk); ok { newAttrs[k] = newval done = true } } if !done { newAttrs[k] = v } return true }) override := NewOverride(app, &newAttrs) res := w.SubWidget().Render(size, focus, override) return res } //====================================================================== type PaletteOverride struct { gowid.IApp Palette gowid.IPalette } func NewOverride(app gowid.IApp, newattrs gowid.IPalette) *PaletteOverride { return &PaletteOverride{ IApp: app, Palette: newattrs, } } func (a *PaletteOverride) CellStyler(name string) (gowid.ICellStyler, bool) { p, ok := a.Palette.CellStyler(name) if ok { return p, ok } return a.IApp.CellStyler(name) } func (a *PaletteOverride) RangeOverPalette(f func(k string, v gowid.ICellStyler) bool) { a.Palette.RangeOverPalette(f) } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: gowid-1.4.0/widgets/paragraph/000077500000000000000000000000001426234454000162445ustar00rootroot00000000000000gowid-1.4.0/widgets/paragraph/paragraph.go000066400000000000000000000062201426234454000205400ustar00rootroot00000000000000// Copyright 2021 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. // Package paragraph provides a simple text widget that neatly splits // over multiple lines. package paragraph import ( "strings" "github.com/gcla/gowid" "github.com/gcla/gowid/gwutil" ) //====================================================================== // Widget can be used to display text on the screen, with words broken // cleanly as the output width changes. type Widget struct { words []string lastcols int lastrows int gowid.RejectUserInput gowid.NotSelectable } var _ gowid.IWidget = (*Widget)(nil) func New(text string) *Widget { return &Widget{ words: strings.Fields(text), } } func NewWithWords(words ...string) *Widget { return &Widget{ words: words, } } func (w *Widget) Render(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.ICanvas { switch size := size.(type) { case gowid.IRenderFlowWith: res := gowid.NewCanvas() var lc gowid.ICanvas var pos int var curWord string i := 0 space := false for { if i >= len(w.words) { if lc != nil { res.AppendBelow(lc, false, false) } break } if curWord == "" { curWord = w.words[i] } if lc == nil { lc = gowid.NewCanvasOfSize(size.FlowColumns(), 1) pos = 0 } if space { if pos < size.FlowColumns()-1 { pos++ } else { if lc != nil { res.AppendBelow(lc, false, false) lc = gowid.NewCanvasOfSize(size.FlowColumns(), 1) pos = 0 } } space = false } else { // No space left in current line if len(curWord) > size.FlowColumns()-pos { // No space on this line, but it will fit on next line. If it // doesn't even fit on a line by itself, well just split it if pos > 0 && lc != nil && len(curWord) <= size.FlowColumns() { res.AppendBelow(lc, false, false) lc = gowid.NewCanvasOfSize(size.FlowColumns(), 1) pos = 0 } take := gwutil.Min(size.FlowColumns()-pos, len(curWord)) for j := 0; j < take; j++ { lc.SetCellAt(pos, 0, gowid.CellFromRune(rune(curWord[j]))) pos++ } if pos >= size.FlowColumns() { res.AppendBelow(lc, false, false) lc = nil } if take == len(curWord) { i++ curWord = "" space = true } else { // Must be less curWord = curWord[take:] } } else { for j := 0; j < len(curWord); j++ { lc.SetCellAt(pos, 0, gowid.CellFromRune(rune(curWord[j]))) pos++ } i++ curWord = "" space = true } } } return res default: panic(gowid.WidgetSizeError{Widget: w, Size: size, Required: "gowid.IRenderFlow"}) } } func (w *Widget) RenderSize(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.IRenderBox { if w.lastcols != 0 { return gowid.RenderBox{C: w.lastcols, R: w.lastrows} } res := gowid.CalculateRenderSizeFallback(w, size, focus, app) w.lastcols = res.C w.lastrows = res.R return res } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: gowid-1.4.0/widgets/paragraph/paragraph_test.go000066400000000000000000000044731426234454000216070ustar00rootroot00000000000000// Copyright 2021 Graham Clark. All rights reserved. Use of this source code is governed by the MIT license // that can be found in the LICENSE file. package paragraph import ( "strings" "testing" "github.com/gcla/gowid" "github.com/gcla/gowid/gwtest" "github.com/stretchr/testify/assert" ) //====================================================================== func Test1(t *testing.T) { w := New("hello world") c := w.Render(gowid.RenderFlowWith{C: 16}, gowid.NotSelected, gwtest.D) res := strings.Join([]string{ "hello world ", }, "\n") assert.Equal(t, res, c.String()) c = w.Render(gowid.RenderFlowWith{C: 6}, gowid.NotSelected, gwtest.D) res = strings.Join([]string{ "hello ", "world ", }, "\n") assert.Equal(t, res, c.String()) c = w.Render(gowid.RenderFlowWith{C: 9}, gowid.NotSelected, gwtest.D) res = strings.Join([]string{ "hello ", "world ", }, "\n") assert.Equal(t, res, c.String()) c = w.Render(gowid.RenderFlowWith{C: 5}, gowid.NotSelected, gwtest.D) res = strings.Join([]string{ "hello", "world", }, "\n") assert.Equal(t, res, c.String()) c = w.Render(gowid.RenderFlowWith{C: 4}, gowid.NotSelected, gwtest.D) res = strings.Join([]string{ "hell", "o wo", "rld ", }, "\n") assert.Equal(t, res, c.String()) w = New("hello worldatlarge") c = w.Render(gowid.RenderFlowWith{C: 6}, gowid.NotSelected, gwtest.D) res = strings.Join([]string{ "hello ", "worlda", "tlarge", }, "\n") assert.Equal(t, res, c.String()) c = w.Render(gowid.RenderFlowWith{C: 8}, gowid.NotSelected, gwtest.D) res = strings.Join([]string{ "hello wo", "rldatlar", "ge ", }, "\n") assert.Equal(t, res, c.String()) c = w.Render(gowid.RenderFlowWith{C: 12}, gowid.NotSelected, gwtest.D) res = strings.Join([]string{ "hello ", "worldatlarge", }, "\n") assert.Equal(t, res, c.String()) c = w.Render(gowid.RenderFlowWith{C: 13}, gowid.NotSelected, gwtest.D) res = strings.Join([]string{ "hello ", "worldatlarge ", }, "\n") assert.Equal(t, res, c.String()) c = w.Render(gowid.RenderFlowWith{C: 18}, gowid.NotSelected, gwtest.D) res = strings.Join([]string{ "hello worldatlarge", }, "\n") assert.Equal(t, res, c.String()) } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: gowid-1.4.0/widgets/pile/000077500000000000000000000000001426234454000152305ustar00rootroot00000000000000gowid-1.4.0/widgets/pile/pile.go000066400000000000000000000463031426234454000165160ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. // Package pile provides a widget for organizing other widgets in a vertical stack. package pile import ( "fmt" "strings" "github.com/gcla/gowid" "github.com/gcla/gowid/gwutil" "github.com/gcla/gowid/vim" tcell "github.com/gdamore/tcell/v2" ) //====================================================================== type IWidget interface { gowid.ICompositeMultipleWidget gowid.ISettableDimensions gowid.ISettableSubWidgets gowid.IFindNextSelectable gowid.IPreferedPosition gowid.ISelectChild gowid.IIdentity RenderBoxMaker(size gowid.IRenderSize, focus gowid.Selector, focusIdx int, app gowid.IApp, fn IPileBoxMaker) ([]gowid.IRenderBox, []gowid.IRenderSize) Wrap() bool KeyIsUp(*tcell.EventKey) bool KeyIsDown(*tcell.EventKey) bool } type Widget struct { widgets []gowid.IContainerWidget focus int // -1 means nothing selectable prefRow int // caches the last set prefered row. Passes it on if widget hasn't changed focus opt Options *gowid.Callbacks gowid.AddressProvidesID gowid.FocusCallbacks gowid.SubWidgetsCallbacks } type Options struct { StartRow int Wrap bool DoNotSetSelected bool // Whether or not to set the focus.Selected field for the selected child DownKeys []vim.KeyPress UpKeys []vim.KeyPress } var _ gowid.IWidget = (*Widget)(nil) var _ IWidget = (*Widget)(nil) var _ gowid.ICompositeMultipleDimensions = (*Widget)(nil) var _ gowid.ICompositeMultipleWidget = (*Widget)(nil) func New(widgets []gowid.IContainerWidget, opts ...Options) *Widget { var opt Options if len(opts) > 0 { opt = opts[0] } else { opt = Options{ StartRow: -1, } } if opt.DownKeys == nil { opt.DownKeys = vim.AllDownKeys } if opt.UpKeys == nil { opt.UpKeys = vim.AllUpKeys } res := &Widget{ widgets: widgets, focus: -1, prefRow: -1, opt: opt, } res.FocusCallbacks = gowid.FocusCallbacks{CB: &res.Callbacks} res.SubWidgetsCallbacks = gowid.SubWidgetsCallbacks{CB: &res.Callbacks} if opt.StartRow >= 0 { res.focus = gwutil.Min(opt.StartRow, len(widgets)-1) } else { res.focus, _ = res.FindNextSelectable(1, res.opt.Wrap) } return res } //func Simple(ws ...gowid.IWidget) *Widget { func NewFlow(ws ...interface{}) *Widget { return NewWithDim(gowid.RenderFlow{}, ws...) } func NewFixed(ws ...interface{}) *Widget { return NewWithDim(gowid.RenderFixed{}, ws...) } func NewWithDim(method gowid.IWidgetDimension, ws ...interface{}) *Widget { cws := make([]gowid.IContainerWidget, len(ws)) for i := 0; i < len(ws); i++ { if cw, ok := ws[i].(gowid.IContainerWidget); ok { cws[i] = cw } else { cws[i] = &gowid.ContainerWidget{ IWidget: ws[i].(gowid.IWidget), D: method, } } } return New(cws) } func (w *Widget) SelectChild(f gowid.Selector) bool { return !w.opt.DoNotSetSelected && f.Selected } func (w *Widget) String() string { rows := make([]string, len(w.widgets)) for i := 0; i < len(rows); i++ { rows[i] = fmt.Sprintf("%v", w.widgets[i]) } return fmt.Sprintf("pile[%s]", strings.Join(rows, ",")) } // Tries to set at required index, will choose first selectable from there func (w *Widget) SetFocus(app gowid.IApp, i int) { oldpos := w.focus w.focus = gwutil.Min(gwutil.Max(i, 0), len(w.widgets)-1) w.prefRow = -1 // moved, so pass on real focus from now on if oldpos != w.focus { gowid.RunWidgetCallbacks(w.Callbacks, gowid.FocusCB{}, app, w) } } func (w *Widget) Wrap() bool { return w.opt.Wrap } // Tries to set at required index, will choose first selectable from there func (w *Widget) Focus() int { return w.focus } func (w *Widget) SubWidgets() []gowid.IWidget { res := make([]gowid.IWidget, len(w.widgets)) for i, iw := range w.widgets { res[i] = iw } return res } func (w *Widget) SetSubWidgets(widgets []gowid.IWidget, app gowid.IApp) { ws := make([]gowid.IContainerWidget, len(widgets)) for i, iw := range widgets { if iwc, ok := iw.(gowid.IContainerWidget); ok { ws[i] = iwc } else { ws[i] = &gowid.ContainerWidget{IWidget: iw, D: gowid.RenderFlow{}} } } oldFocus := w.Focus() w.widgets = ws w.SetFocus(app, oldFocus) gowid.RunWidgetCallbacks(w.Callbacks, gowid.SubWidgetsCB{}, app, w) } func (w *Widget) Dimensions() []gowid.IWidgetDimension { res := make([]gowid.IWidgetDimension, len(w.widgets)) for i, iw := range w.widgets { res[i] = iw.Dimension() } return res } func (w *Widget) SetDimensions(dimensions []gowid.IWidgetDimension, app gowid.IApp) { for i, id := range dimensions { w.widgets[i].SetDimension(id) } gowid.RunWidgetCallbacks(w.Callbacks, gowid.DimensionsCB{}, app, w) } func (w *Widget) Selectable() bool { return gowid.SelectableIfAnySubWidgetsAre(w) } func (w *Widget) FindNextSelectable(dir gowid.Direction, wrap bool) (int, bool) { return gowid.FindNextSelectableFrom(w, w.Focus(), dir, wrap) } func (w *Widget) UserInput(ev interface{}, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) bool { return UserInput(w, ev, size, focus, app) } // TODO - widen each line to same width func (w *Widget) Render(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.ICanvas { return Render(w, size, focus, app) } func (w *Widget) RenderedSubWidgetsSizes(size gowid.IRenderSize, focus gowid.Selector, focusIdx int, app gowid.IApp) []gowid.IRenderBox { res, _ := RenderedChildrenSizes(w, size, focus, focusIdx, app) return res } func (w *Widget) RenderSize(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.IRenderBox { return RenderSize(w, size, focus, app) } func (w *Widget) RenderSubWidgets(size gowid.IRenderSize, focus gowid.Selector, focusIdx int, app gowid.IApp) []gowid.ICanvas { return RenderSubwidgets(w, size, focus, focusIdx, app) } // // TODO - widen each line to same width // gcdoc - the fn argument is used to return either canvases or sizes, depending on whether // the caller is rendering, or rendering subsizes func (w *Widget) RenderBoxMaker(size gowid.IRenderSize, focus gowid.Selector, focusIdx int, app gowid.IApp, fn IPileBoxMaker) ([]gowid.IRenderBox, []gowid.IRenderSize) { return RenderBoxMaker(w, size, focus, focusIdx, app, fn) } // SubWidgetSize is the size that should be used to render a child widget, based on the size used to render the parent. func (w *Widget) SubWidgetSize(size gowid.IRenderSize, newY int, sub gowid.IWidget, dim gowid.IWidgetDimension) gowid.IRenderSize { return gowid.ComputeVerticalSubSizeUnsafe(size, dim, -1, newY) } func (w *Widget) GetPreferedPosition() gwutil.IntOption { f := w.prefRow if f == -1 { f = w.Focus() } if f == -1 { return gwutil.NoneInt() } else { return gwutil.SomeInt(f) } } func (w *Widget) SetPreferedPosition(rows int, app gowid.IApp) { row := gwutil.Min(gwutil.Max(rows, 0), len(w.widgets)-1) pref := row rowLeft := row - 1 rowRight := row for rowLeft >= 0 || rowRight < len(w.widgets) { if rowRight < len(w.widgets) && w.widgets[rowRight].Selectable() { w.SetFocus(app, rowRight) break } else { rowRight++ } if rowLeft >= 0 && w.widgets[rowLeft].Selectable() { w.SetFocus(app, rowLeft) break } else { rowLeft-- } } w.prefRow = pref // Save it. Pass it on if widget doesn't change col before losing focus. } func (w *Widget) KeyIsUp(evk *tcell.EventKey) bool { return vim.KeyIn(evk, w.opt.UpKeys) } func (w *Widget) KeyIsDown(evk *tcell.EventKey) bool { return vim.KeyIn(evk, w.opt.DownKeys) } //'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' func UserInput(w IWidget, ev interface{}, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) bool { subfocus := w.Focus() // An array of IRenderBoxes ss, ss2 := RenderedChildrenSizes(w, size, focus, subfocus, app) forChild := false subs := w.SubWidgets() focusEvent := func(selectable bool) { srows := 0 for i := 0; i < subfocus; i++ { srows += ss[i].BoxRows() } subSize := ss2[subfocus] if selectable { forChild = subs[subfocus].UserInput(gowid.TranslatedMouseEvent(ev, 0, -srows), subSize, focus, app) } else { forChild = gowid.UserInputIfSelectable(subs[subfocus], gowid.TranslatedMouseEvent(ev, 0, -srows), subSize, focus, app) } } if evm, ok := ev.(*tcell.EventMouse); ok { switch evm.Buttons() { case tcell.WheelUp, tcell.WheelDown: // A wheel up/down event applied to a pile should be sent to the focus widget. Consider a pile // which contains a list. If the pile focus isn't the list (say it's the previous widget), then // the wheel event will invoke the scroll-down code below (forChild == false), putting the list // in focus. If the focus is on the list, then forChild == true and the pile won't handle it further, // but the list will scroll as intended. If we scroll back to the top of the list, then the list // will return false for handle (forChild == false) and we'll invoke the scroll-up code below // to scroll to the previous pile focus. if subfocus != -1 { focusEvent(true) } default: // Don't try to compute if subfocus == -1 { break } // A left click sets focus if the widget is selectable and would take the mouse input; but // if I don't filter by click, then moving the mouse over another widget would shift focus // automatically, which is not usually what's wanted. _, my := evm.Position() curY := 0 Loop: for i, c := range ss { if my < curY+c.BoxRows() && my >= curY { subSize := ss2[i] forChild = subs[i].UserInput(gowid.TranslatedMouseEvent(ev, 0, -curY), subSize, focus.SelectIf(w.SelectChild(focus) && i == subfocus), app) // Give the child focus if (a) it's selectable, and (b) if this is the click up corresponding // to a previous click down on this pile widget. switch evm.Buttons() { case tcell.Button1, tcell.Button2, tcell.Button3: app.SetClickTarget(evm.Buttons(), w) case tcell.ButtonNone: if !app.GetLastMouseState().NoButtonClicked() { if subs[i].Selectable() { clickit := false app.ClickTarget(func(k tcell.ButtonMask, v gowid.IIdentityWidget) { if v != nil && v.ID() == w.ID() { clickit = true } }) if clickit { w.SetFocus(app, i) } } } break Loop } } curY += c.BoxRows() } } } else { if subfocus != -1 { focusEvent(false) } } res := forChild if !forChild && w.Focus() != -1 { // e.g. if none of the subwidgets are selectable res = true scrollDown := false scrollUp := false if evk, ok := ev.(*tcell.EventKey); ok { switch { case w.KeyIsDown(evk): scrollDown = true case w.KeyIsUp(evk): scrollUp = true default: res = false } } else if ev2, ok := ev.(*tcell.EventMouse); ok { switch ev2.Buttons() { case tcell.WheelDown: scrollDown = true case tcell.WheelUp: scrollUp = true default: res = false } } else { res = false } if scrollUp || scrollDown { curw := subs[w.Focus()] prefPos := gowid.PrefPosition(curw) if scrollUp { res = gowid.ChangeFocus(w, -1, w.Wrap(), app) } else { res = gowid.ChangeFocus(w, 1, w.Wrap(), app) } if !prefPos.IsNone() { // New focus widget curw = subs[w.Focus()] gowid.SetPrefPosition(curw, prefPos.Val(), app) } } } return res } type IFocusSelectable interface { gowid.IFocus gowid.IFindNextSelectable } func RenderSize(w gowid.ICompositeMultipleWidget, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.IRenderBox { subfocus := w.Focus() sizes := w.RenderedSubWidgetsSizes(size, focus, subfocus, app) maxcol := 0 maxrow := 0 for _, sz := range sizes { maxcol = gwutil.Max(maxcol, sz.BoxColumns()) maxrow += sz.BoxRows() } if sz, ok := size.(gowid.IRenderBox); ok { maxrow = gwutil.Min(maxrow, sz.BoxRows()) } return gowid.RenderBox{maxcol, maxrow} } // Heights can be: // - Pack - use what you need // - Units - fixed number of rows (RenderBox) // - Weight - divide up // // What if height is bigger than rows we have? Then we chop. // What if height is bigger before weights taken into consideration? They are zero // What if the Pile is rendered as a RenderFlow? Then you can't specify any weighted widgets func Render(w IWidget, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.ICanvas { subfocus := w.Focus() // if !focus.Focus { // subfocus = -1 // } canvases := w.RenderSubWidgets(size, focus, subfocus, app) rows, ok := size.(gowid.IRows) haveMaxRow := ok res := gowid.NewCanvas() trim := false for i := 0; i < len(canvases); i++ { // Can be nil if weighted widgets were included but there wasn't enough space if canvases[i] != nil { // make sure each canvas uses up the width it is alloted - so if I render // a pile of width 20, and put it in a column, the next column starts at 21 // TODO - remember which one has focus res.AppendBelow(canvases[i], i == subfocus, false) if haveMaxRow && res.BoxRows() > rows.Rows() { trim = true break } } } if trim { res.Truncate(0, res.BoxRows()-rows.Rows()) } if haveMaxRow && res.BoxRows() < rows.Rows() { gowid.AppendBlankLines(res, rows.Rows()-res.BoxRows()) } if cols, ok := size.(gowid.IColumns); ok { res.ExtendRight(gowid.EmptyLine(cols.Columns() - res.BoxColumns())) } return res } func RenderedChildrenSizes(w IWidget, size gowid.IRenderSize, focus gowid.Selector, focusIdx int, app gowid.IApp) ([]gowid.IRenderBox, []gowid.IRenderSize) { fn2 := BoxMakerFunc(func(w gowid.IWidget, subSize gowid.IRenderSize, focus gowid.Selector, subApp gowid.IApp) gowid.IRenderBox { return w.RenderSize(subSize, focus, subApp) }) res := make([]gowid.IRenderBox, 0) resSS := make([]gowid.IRenderSize, 0) sts, stsSS := w.RenderBoxMaker(size, focus, focusIdx, app, fn2) res = append(res, sts...) resSS = append(resSS, stsSS...) return res, resSS } func RenderSubwidgets(w IWidget, size gowid.IRenderSize, focus gowid.Selector, focusIdx int, app gowid.IApp) []gowid.ICanvas { fn1 := BoxMakerFunc(func(w gowid.IWidget, subSize gowid.IRenderSize, focus gowid.Selector, subApp gowid.IApp) gowid.IRenderBox { return w.Render(subSize, focus, subApp) }) canvases, _ := w.RenderBoxMaker(size, focus, focusIdx, app, fn1) res := make([]gowid.ICanvas, len(canvases)) for i := 0; i < len(canvases); i++ { if canvases[i] != nil { res[i] = canvases[i].(gowid.ICanvas) } } return res } // TODO - make this an interface type IPileBoxMaker interface { MakeBox(gowid.IWidget, gowid.IRenderSize, gowid.Selector, gowid.IApp) gowid.IRenderBox } type BoxMakerFunc func(gowid.IWidget, gowid.IRenderSize, gowid.Selector, gowid.IApp) gowid.IRenderBox func (f BoxMakerFunc) MakeBox(w gowid.IWidget, s gowid.IRenderSize, b gowid.Selector, c gowid.IApp) gowid.IRenderBox { return f(w, s, b, c) } func RenderBoxMaker(w IWidget, size gowid.IRenderSize, focus gowid.Selector, focusIdx int, app gowid.IApp, fn IPileBoxMaker) ([]gowid.IRenderBox, []gowid.IRenderSize) { dims := w.Dimensions() _, ok1 := size.(gowid.IRenderFlowWith) _, ok2 := size.(gowid.IRenderFixed) weightWidgets := 0 if ok1 || ok2 { for _, ww := range dims { if _, ok := ww.(gowid.IRenderWithWeight); ok { weightWidgets++ if weightWidgets > 1 { panic(fmt.Errorf("Pile is rendered as Flow/Fixed %v of type %T so cannot contain more than one Weight widget", size, size)) } } } } subs := w.SubWidgets() wlen := len(subs) res := make([]gowid.IRenderBox, wlen) resSS := make([]gowid.IRenderSize, wlen) heights := make([]int, wlen) ineligible := make([]bool, wlen) // So I know what is initialized later and what isn't (since 0 can be a legit row height) for i := 0; i < wlen; i++ { heights[i] = -1 } rowsUsed := 0 totalWeight := 0 // Render all fixed first. This will determine the maximum width which can then be // supplied as an advisory parameter to unrendered subwidgets. Any that are specified // as RenderFlow{} can then be rendered to the max width maxcol := -1 for i := 0; i < wlen; i++ { subSize, err := gowid.ComputeVerticalSubSize(size, dims[i], -1, -1) if err == nil { if _, ok := subSize.(gowid.IRenderFixed); ok { // only do if subsize is fixed resSS[i] = subSize res[i] = fn.MakeBox(subs[i], subSize, focus.SelectIf(w.SelectChild(focus) && i == focusIdx), app) heights[i] = res[i].BoxRows() rowsUsed += heights[i] if res[i].BoxColumns() > maxcol { maxcol = res[i].BoxColumns() } } } } // // Do the packed and fixed height widgets first. Then after, divvy up the rest // among the weighted widgets // for i := 0; i < wlen; i++ { // TODO - remember which one has focus if res[i] == nil { subSize, err := gowid.ComputeVerticalSubSize(size, dims[i], maxcol, -1) if err == nil { resSS[i] = subSize res[i] = fn.MakeBox(subs[i], subSize, focus.SelectIf(w.SelectChild(focus) && i == focusIdx), app) heights[i] = res[i].BoxRows() rowsUsed += heights[i] } else { if w2, ok := dims[i].(gowid.IRenderWithWeight); !ok { panic(fmt.Errorf("Unsupported dimension %T of type %T for widget %v - %v", dims[i], dims[i], subs[i], err)) } else { // It must be weighted totalWeight += w2.Weight() } } } } // // Now divide up remaining space // // Track the last height row, so I can adjust for floating errors lasti := -1 if box, ok := size.(gowid.IRenderBox); ok { rowsToDivideUp := box.BoxRows() - rowsUsed rowsLeft := rowsToDivideUp for { if rowsLeft == 0 { break } doneone := false totalWeight = 0 for i := 0; i < wlen; i++ { if w2, ok := dims[i].(gowid.IRenderWithWeight); ok && !ineligible[i] { totalWeight += w2.Weight() } } rowsToDivideUp = rowsLeft for i := 0; i < wlen; i++ { if w2, ok := dims[i].(gowid.IRenderWithWeight); ok && !ineligible[i] { rows := int(((float32(w2.Weight()) / float32(totalWeight)) * float32(rowsToDivideUp)) + 0.5) if max, ok := dims[i].(gowid.IRenderMaxUnits); ok { if rows > max.MaxUnits() { rows = max.MaxUnits() ineligible[i] = true // this one is done } } if rows > rowsLeft { rows = rowsLeft } if rows > 0 { if heights[i] == -1 { heights[i] = 0 } heights[i] += rows rowsLeft -= rows lasti = i doneone = true } } } if !doneone { break } } if lasti != -1 && rowsLeft > 0 { heights[lasti] += rowsLeft } // Now actually render for i := 0; i < wlen; i++ { if _, ok := dims[i].(gowid.IRenderWithWeight); ok { ss := gowid.RenderBox{box.BoxColumns(), heights[i]} resSS[i] = ss res[i] = fn.MakeBox(subs[i], ss, focus.SelectIf(w.SelectChild(focus) && i == focusIdx), app) } } } else { // FlowWith and Fixed for i := 0; i < wlen; i++ { // Should only be one! if _, ok := dims[i].(gowid.IRenderWithWeight); ok { resSS[i] = size res[i] = fn.MakeBox(subs[i], size, focus.SelectIf(w.SelectChild(focus) && i == focusIdx), app) } } } zbox := gowid.RenderBox{0, 0} for i := 0; i < wlen; i++ { if res[i] == nil { resSS[i] = zbox res[i] = fn.MakeBox(subs[i], zbox, focus.SelectIf(w.SelectChild(focus) && i == focusIdx), app) } } return res, resSS } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: gowid-1.4.0/widgets/pile/pile_test.go000066400000000000000000000214311426234454000175500ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source code is governed by the MIT license // that can be found in the LICENSE file. package pile import ( "fmt" "strings" "testing" "github.com/gcla/gowid" "github.com/gcla/gowid/gwtest" "github.com/gcla/gowid/widgets/button" "github.com/gcla/gowid/widgets/fill" "github.com/gcla/gowid/widgets/framed" "github.com/gcla/gowid/widgets/list" "github.com/gcla/gowid/widgets/selectable" "github.com/gcla/gowid/widgets/text" tcell "github.com/gdamore/tcell/v2" "github.com/stretchr/testify/assert" ) // Test that a mouse wheel down inside the region of the list within a pile // is correctly translated and passed to the list. func TestPile5(t *testing.T) { bws := make([]gowid.IWidget, 50) for i := 0; i < len(bws); i++ { bws[i] = button.New(text.New(fmt.Sprintf("%03d", i))) } walker := list.NewSimpleListWalker(bws) lb := list.New(walker) // Framed is needed because it validates the mouse y coordinate before passing it on // to its subwidget. flb := framed.New(lb) pws := make([]gowid.IContainerWidget, 3) pws[0] = &gowid.ContainerWidget{button.New(text.New("top ")), gowid.RenderWithUnits{U: 1}} pws[1] = &gowid.ContainerWidget{flb, gowid.RenderWithWeight{W: 1}} // WEIGHT!! pws[2] = &gowid.ContainerWidget{button.New(text.New("bot ")), gowid.RenderWithUnits{U: 1}} pl := New(pws) sta := make([]string, 0) sta = append(sta, "") sta = append(sta, "-------") for i := 0; i < 6; i++ { sta = append(sta, fmt.Sprintf("|<%03d>|", i)) } sta = append(sta, "-------") sta = append(sta, "") csize := gowid.RenderBox{C: 7, R: 10} c := pl.Render(csize, gowid.Focused, gwtest.D) assert.Equal(t, strings.Join(sta, "\n"), c.String()) assert.Equal(t, 0, pl.Focus()) evmdown := tcell.NewEventMouse(1, 4, tcell.WheelDown, 0) pl.UserInput(evmdown, csize, gowid.Focused, gwtest.D) assert.Equal(t, 1, pl.Focus()) // Now at list widget assert.Equal(t, 0, lb.Walker().Focus().(list.ListPos).ToInt()) pl.UserInput(evmdown, csize, gowid.Focused, gwtest.D) assert.Equal(t, 1, pl.Focus()) // Now at list widget assert.Equal(t, 1, lb.Walker().Focus().(list.ListPos).ToInt()) for i := 0; i < 40; i++ { pl.UserInput(evmdown, csize, gowid.Focused, gwtest.D) } assert.Equal(t, 1, pl.Focus()) // Now at list widget assert.Equal(t, 41, lb.Walker().Focus().(list.ListPos).ToInt()) } func TestPile2(t *testing.T) { btns := make([]gowid.IContainerWidget, 0) //clicks := make([]*gwtest.ButtonTester, 0) for i := 0; i < 3; i++ { btn := button.New(text.New("abc")) click := &gwtest.ButtonTester{Gotit: false} btn.OnClick(click) btns = append(btns, &gowid.ContainerWidget{btn, gowid.RenderFixed{}}) //clicks = append(clicks, click) } pl := New(btns) st1 := "\n\n" st2 := " \n \n " c := pl.Render(gowid.RenderFixed{}, gowid.Focused, gwtest.D) assert.Equal(t, c.String(), st1) c2 := pl.Render(gowid.RenderFlowWith{C: 6}, gowid.Focused, gwtest.D) assert.Equal(t, c2.String(), st2) assert.Equal(t, pl.Focus(), 0) // evright := tcell.NewEventKey(tcell.KeyRight, ' ', tcell.ModNone) // evleft := tcell.NewEventKey(tcell.KeyLeft, ' ', tcell.ModNone) // evdown := tcell.NewEventKey(tcell.KeyDown, ' ', tcell.ModNone) // evspace := tcell.NewEventKey(tcell.KeyRune, ' ', tcell.ModNone) evmdown := tcell.NewEventMouse(1, 1, tcell.WheelDown, 0) evmup := tcell.NewEventMouse(1, 1, tcell.WheelUp, 0) // evmright := tcell.NewEventMouse(1, 1, tcell.WheelRight, 0) // evmleft := tcell.NewEventMouse(1, 1, tcell.WheelLeft, 0) cbcalled := false pl.OnFocusChanged(gowid.WidgetCallback{"cb", func(app gowid.IApp, w gowid.IWidget) { assert.Equal(t, w, pl) cbcalled = true }}) pl.UserInput(evmdown, gowid.RenderFixed{}, gowid.Focused, gwtest.D) assert.Equal(t, 1, pl.Focus()) assert.Equal(t, true, cbcalled) cbcalled = false pl.UserInput(evmdown, gowid.RenderFixed{}, gowid.Focused, gwtest.D) assert.Equal(t, 2, pl.Focus()) assert.Equal(t, true, cbcalled) cbcalled = false pl.UserInput(evmdown, gowid.RenderFixed{}, gowid.Focused, gwtest.D) assert.Equal(t, 2, pl.Focus()) assert.Equal(t, false, cbcalled) pl.UserInput(evmup, gowid.RenderFixed{}, gowid.Focused, gwtest.D) assert.Equal(t, 1, pl.Focus()) assert.Equal(t, true, cbcalled) cbcalled = false pl.UserInput(evmup, gowid.RenderFixed{}, gowid.Focused, gwtest.D) assert.Equal(t, 0, pl.Focus()) assert.Equal(t, true, cbcalled) cbcalled = false pl.UserInput(evmup, gowid.RenderFixed{}, gowid.Focused, gwtest.D) assert.Equal(t, 0, pl.Focus()) assert.Equal(t, false, cbcalled) } func TestPile1(t *testing.T) { w1 := New([]gowid.IContainerWidget{ &gowid.ContainerWidget{fill.New('x'), gowid.RenderWithUnits{U: 2}}, &gowid.ContainerWidget{fill.New('y'), gowid.RenderWithUnits{U: 2}}, }) c1 := w1.Render(gowid.RenderBox{C: 3, R: 4}, gowid.Focused, gwtest.D) assert.Equal(t, c1.String(), "xxx\nxxx\nyyy\nyyy") w2 := New([]gowid.IContainerWidget{ &gowid.ContainerWidget{fill.New('x'), gowid.RenderWithUnits{U: 1}}, &gowid.ContainerWidget{fill.New('y'), gowid.RenderWithUnits{U: 2}}, }) c2 := w2.Render(gowid.RenderFlowWith{C: 3}, gowid.Focused, gwtest.D) assert.Equal(t, c2.String(), "xxx\nyyy\nyyy") w3 := New([]gowid.IContainerWidget{ &gowid.ContainerWidget{fill.New('x'), gowid.RenderWithWeight{1}}, &gowid.ContainerWidget{fill.New('y'), gowid.RenderWithWeight{2}}, }) assert.Panics(t, func() { w3.Render(gowid.RenderFlowWith{C: 3}, gowid.Focused, gwtest.D) }) w4 := New([]gowid.IContainerWidget{ &gowid.ContainerWidget{fill.New('x'), gowid.RenderWithRatio{0.25}}, &gowid.ContainerWidget{fill.New('y'), gowid.RenderWithRatio{0.5}}, }) c4 := w4.Render(gowid.RenderBox{C: 3, R: 3}, gowid.Focused, gwtest.D) assert.Equal(t, c4.String(), "xxx\nyyy\nyyy") c41 := w4.Render(gowid.RenderBox{C: 3, R: 4}, gowid.Focused, gwtest.D) assert.Equal(t, c41.String(), "xxx\nyyy\nyyy\n ") for _, w := range []gowid.IWidget{w1, w2, w4} { gwtest.RenderBoxManyTimes(t, w, 0, 10, 0, 10) } gwtest.RenderFlowManyTimes(t, w2, 0, 10) } func TestPile3(t *testing.T) { w1 := New([]gowid.IContainerWidget{ &gowid.ContainerWidget{fill.New('x'), gowid.RenderWithUnits{U: 2}}, &gowid.ContainerWidget{text.New("y"), gowid.RenderFlow{}}, }) // Test that a pile can render in flow mode with a single embedded flow widget c1 := w1.Render(gowid.RenderFlowWith{C: 3}, gowid.Focused, gwtest.D) assert.Equal(t, c1.String(), "xxx\nxxx\ny ") w1 = New([]gowid.IContainerWidget{ &gowid.ContainerWidget{fill.New('x'), gowid.RenderWithUnits{U: 2}}, &gowid.ContainerWidget{text.New("y"), gowid.RenderWithWeight{1}}, }) // Test that a pile can render in flow mode with a single embedded flow widget c1 = w1.Render(gowid.RenderFlowWith{C: 3}, gowid.Focused, gwtest.D) assert.Equal(t, c1.String(), "xxx\nxxx\ny ") w1 = New([]gowid.IContainerWidget{ &gowid.ContainerWidget{fill.New('x'), gowid.RenderWithUnits{U: 2}}, &gowid.ContainerWidget{text.New("y"), gowid.RenderWithWeight{1}}, &gowid.ContainerWidget{text.New("z"), gowid.RenderWithWeight{1}}, }) // Two weight widgets don't work in flow mode, how do you restrict their vertical ratio? assert.Panics(t, func() { w1.Render(gowid.RenderFlowWith{C: 3}, gowid.Focused, gwtest.D) }) } func makep(c rune) gowid.IWidget { return selectable.New(fill.New(c)) } func makepfixed(c rune) gowid.IContainerWidget { return &gowid.ContainerWidget{ IWidget: makep(c), D: gowid.RenderFixed{}, } } type renderWeightUpTo struct { gowid.RenderWithWeight max int } func (s renderWeightUpTo) MaxUnits() int { return s.max } func weightupto(w int, max int) renderWeightUpTo { return renderWeightUpTo{gowid.RenderWithWeight{W: w}, max} } func TestPile4(t *testing.T) { subs := []gowid.IContainerWidget{ &gowid.ContainerWidget{makep('x'), gowid.RenderWithWeight{W: 1}}, &gowid.ContainerWidget{makep('y'), gowid.RenderWithWeight{W: 1}}, &gowid.ContainerWidget{makep('z'), gowid.RenderWithWeight{W: 1}}, } w := New(subs) c := w.Render(gowid.RenderBox{C: 1, R: 12}, gowid.Focused, gwtest.D) assert.Equal(t, ` x x x x y y y y z z z z`[1:], c.String()) subs[2] = &gowid.ContainerWidget{makep('z'), renderWeightUpTo{gowid.RenderWithWeight{W: 1}, 2}} w = New(subs) c = w.Render(gowid.RenderBox{C: 1, R: 12}, gowid.Focused, gwtest.D) assert.Equal(t, ` x x x x x y y y y y z z`[1:], c.String()) } func TestPile6(t *testing.T) { subs := []gowid.IContainerWidget{ &gowid.ContainerWidget{text.New("foo"), gowid.RenderFixed{}}, &gowid.ContainerWidget{text.New("bar"), gowid.RenderWithWeight{W: 1}}, &gowid.ContainerWidget{text.New("baz"), gowid.RenderFixed{}}, } w := New(subs) c := w.Render(gowid.RenderBox{C: 3, R: 5}, gowid.Focused, gwtest.D) assert.Equal(t, ` foo bar baz`[1:], c.String()) } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: gowid-1.4.0/widgets/progress/000077500000000000000000000000001426234454000161435ustar00rootroot00000000000000gowid-1.4.0/widgets/progress/progress.go000066400000000000000000000122141426234454000203360ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. // Package progress provides a simple progress bar. package progress import ( "fmt" "github.com/gcla/gowid" "github.com/gcla/gowid/gwutil" "github.com/gcla/gowid/widgets/hpadding" "github.com/gcla/gowid/widgets/styled" "github.com/gcla/gowid/widgets/text" ) //====================================================================== // IWidget - if your widget implements progress.IWidget, you will be able to render it using the // progress.Render() function. // type IWidget interface { gowid.IWidget // Text should return the string to be displayed inside the progress bar e.g. "50%" Text() string // Progress returns the number of units completed. Progress() int // Target returns the number of units required overall. Target() int // Normal is used to render the incomplete part of the progress bar. Normal() gowid.ICellStyler // Complete is used to render the complete part of the progress bar. Complete() gowid.ICellStyler } // For callback registration type ProgressCB struct{} type TargetCB struct{} // Widget is the concrete type of a progressbar widget. type Widget struct { Current, Done int normal, complete gowid.ICellStyler Callbacks *gowid.Callbacks gowid.RejectUserInput gowid.NotSelectable } // Options is used for passing arguments to the progressbar initializer, New(). type Options struct { Normal, Complete gowid.ICellStyler Target, Current int } // New will return an initialized progressbar Widget/ func New(args Options) *Widget { if args.Target == 0 { args.Target = 100 } res := &Widget{ Current: args.Current, Done: args.Target, normal: args.Normal, complete: args.Complete, Callbacks: gowid.NewCallbacks(), } var _ IWidget = res return res } func (w *Widget) String() string { return fmt.Sprintf("progress") } func (w *Widget) Text() string { var percent int if w.Done == 0 { percent = 100 } else { percent = gwutil.Min(100, gwutil.Max(0, w.Current*100/w.Done)) } return fmt.Sprintf("%d %%", percent) } func (w *Widget) OnSetProgress(f gowid.IWidgetChangedCallback) { gowid.AddWidgetCallback(w.Callbacks, ProgressCB{}, f) } func (w *Widget) RemoveOnSetProgress(f gowid.IIdentity) { gowid.RemoveWidgetCallback(w.Callbacks, ProgressCB{}, f) } func (w *Widget) OnSetTarget(f gowid.IWidgetChangedCallback) { gowid.AddWidgetCallback(w.Callbacks, TargetCB{}, f) } func (w *Widget) RemoveOnSetTarget(f gowid.IIdentity) { gowid.RemoveWidgetCallback(w.Callbacks, TargetCB{}, f) } func (w *Widget) SetProgress(app gowid.IApp, current int) { w.Current = current if w.Current > w.Done { w.Current = w.Done } else if w.Current < 0 { w.Current = 0 } gowid.RunWidgetCallbacks(w.Callbacks, ProgressCB{}, app, w) } func (w *Widget) SetTarget(app gowid.IApp, target int) { w.Done = target if w.Done < 0 { w.Done = 0 } if w.Current > w.Done { w.Current = w.Done } gowid.RunWidgetCallbacks(w.Callbacks, TargetCB{}, app, w) } func (w *Widget) Progress() int { return w.Current } func (w *Widget) Target() int { return w.Done } func (w *Widget) Normal() gowid.ICellStyler { return w.normal } func (w *Widget) Complete() gowid.ICellStyler { return w.complete } func (w *Widget) RenderSize(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.IRenderBox { return gowid.CalculateRenderSizeFallback(w, size, focus, app) } func (w *Widget) Render(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.ICanvas { return Render(w, size, focus, app) } //'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' // Render will render a progressbar IWidget. func Render(w IWidget, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.ICanvas { flow, isFlow := size.(gowid.IRenderFlowWith) if !isFlow { panic(gowid.WidgetSizeError{Widget: w, Size: size, Required: "gowid.IRenderFlowWith"}) } cols := flow.FlowColumns() barCanvas := styled.New( text.New(gwutil.StringOfLength(' ', cols)), w.Normal(), ).Render( gowid.RenderBox{C: cols, R: 1}, gowid.NotSelected, app) fnorm, _, _ := w.Normal().GetStyle(app) percentStyle := gowid.MakePaletteEntry(fnorm, gowid.NoColor{}) fcomp, bcomp, scomp := w.Complete().GetStyle(app) fcompCol := gowid.IColorToTCell(fcomp, gowid.ColorNone, app.GetColorMode()) bcompCol := gowid.IColorToTCell(bcomp, gowid.ColorNone, app.GetColorMode()) cur, done := w.Progress(), w.Target() var cutoff int if done == 0 { cutoff = cols } else { cutoff = (cur * cols) / done } for i := 0; i < cutoff; i++ { barCanvas.SetCellAt(i, 0, barCanvas.CellAt(i, 0).WithForegroundColor(fcompCol).WithBackgroundColor(bcompCol).WithStyle(scomp)) } percent := hpadding.New( styled.New( text.New(w.Text()), percentStyle, ), gowid.HAlignMiddle{}, gowid.RenderFixed{}, ) percentCanvas := percent.Render(gowid.RenderBox{C: cols, R: 1}, gowid.NotSelected, app) barCanvas.MergeUnder(percentCanvas, 0, 0, false) return barCanvas } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: gowid-1.4.0/widgets/progress/progress_test.go000066400000000000000000000046631426234454000214060ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source code is governed by the MIT license // that can be found in the LICENSE file. package progress import ( "strings" "testing" "github.com/gcla/gowid" "github.com/gcla/gowid/gwtest" log "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" ) //====================================================================== var ( pcb1 int ) func testProgressCallback1(app gowid.IApp, w gowid.IWidget) { pcb1++ } func TestCallbacks2(t *testing.T) { widget1 := New(Options{gowid.EmptyPalette{}, gowid.EmptyPalette{}, 100, 0}) widget1.OnSetProgress(gowid.WidgetCallback{"cb", testProgressCallback1}) assert.Equal(t, pcb1, 0) widget1.SetProgress(gwtest.D, 50) assert.Equal(t, pcb1, 1) } func TestCanvas23(t *testing.T) { widget1 := New(Options{gowid.EmptyPalette{}, gowid.EmptyPalette{}, 100, 0}) canvas1 := widget1.Render(gowid.RenderFlowWith{C: 10}, gowid.NotSelected, gwtest.D) log.Infof("Widget is %v", widget1) log.Infof("Canvas is %s", canvas1.String()) res := strings.Join([]string{" 0 % "}, "\n") log.Infof("FOOFOO1: res is '%v'", res) log.Infof("FOOFOO2: c1 is '%v'", canvas1) log.Infof("FOOFOO3: c1s is '%v'", canvas1.String()) if res != canvas1.String() { t.Errorf("Failed") } widget1.SetProgress(gwtest.D, 50) canvas1 = widget1.Render(gowid.RenderFlowWith{C: 10}, gowid.NotSelected, gwtest.D) log.Infof("Widget is %v", widget1) log.Infof("Canvas is %s", canvas1.String()) res = strings.Join([]string{" 50 % "}, "\n") log.Infof("FOOFOO1: res is '%v'", res) log.Infof("FOOFOO2: c1 is '%v'", canvas1) log.Infof("FOOFOO3: c1s is '%v'", canvas1.String()) //res = strings.Join([]string{"█████ "}, "\n") //res = strings.Join([]string{"xxxxx "}, "\n") if res != canvas1.String() { t.Errorf("Failed") } widget1.SetProgress(gwtest.D, 100) canvas1 = widget1.Render(gowid.RenderFlowWith{C: 10}, gowid.NotSelected, gwtest.D) log.Infof("Widget is %v", widget1) log.Infof("Canvas is %s", canvas1.String()) res = strings.Join([]string{" 100 % "}, "\n") log.Infof("FOOFOO1: res is '%v'", res) log.Infof("FOOFOO2: c1 is '%v'", canvas1) log.Infof("FOOFOO3: c1s is '%v'", canvas1.String()) //res = strings.Join([]string{"xxxxx "}, "\n") if res != canvas1.String() { t.Errorf("Failed") } } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: gowid-1.4.0/widgets/radio/000077500000000000000000000000001426234454000153755ustar00rootroot00000000000000gowid-1.4.0/widgets/radio/radio.go000066400000000000000000000076731426234454000170370ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. // Package radio provides radio button widgets where one can be selected among many. package radio import ( "fmt" "github.com/gcla/gowid" "github.com/gcla/gowid/gwutil" "github.com/gcla/gowid/widgets/button" "github.com/gcla/gowid/widgets/checkbox" ) //====================================================================== type IWidget interface { gowid.IWidget gowid.ICallbacks IsChecked() bool Group() *[]IWidget SetStateInternal(selected bool) } type Widget struct { Selected bool group *[]IWidget *gowid.Callbacks gowid.ClickCallbacks checkbox.Decoration gowid.AddressProvidesID gowid.IsSelectable } // If the group supplied is empty, this radio button will be marked as selected, regardless // of the isChecked parameter. func New(group *[]IWidget) *Widget { res := &Widget{ Selected: false, group: group, Decoration: checkbox.Decoration{button.Decoration{"(", ")"}, "X"}, } res.ClickCallbacks = gowid.ClickCallbacks{CB: &res.Callbacks} res.initRadioButton(group) var _ IWidget = res return res } func NewDecorated(group *[]IWidget, decoration checkbox.Decoration) *Widget { res := &Widget{ Selected: false, group: group, Decoration: decoration, } res.ClickCallbacks = gowid.ClickCallbacks{CB: &res.Callbacks} res.initRadioButton(group) var _ gowid.IWidget = res return res } func (w *Widget) initRadioButton(group *[]IWidget) { *group = append(*group, w) if len(*group) == 1 { w.SetStateInternal(true) } } func (w *Widget) String() string { return fmt.Sprintf("radio[%s]", gwutil.If(w.IsChecked(), "X", " ").(string)) } func (w *Widget) Select(app gowid.IApp) { Select(w, app) } func (w *Widget) Group() *[]IWidget { return w.group } // Don't ensure consistency of other widgets, but do issue callbacks for // state change. TODO - need to do callbacks here to capture // losing selection func (w *Widget) SetStateInternal(selected bool) { w.Selected = selected } func (w *Widget) IsChecked() bool { return w.Selected } func (w *Widget) Click(app gowid.IApp) { if app.GetMouseState().NoButtonClicked() || app.GetMouseState().LeftIsClicked() { w.Select(app) } } func (w *Widget) RenderSize(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.IRenderBox { if _, ok := size.(gowid.IRenderFixed); !ok { panic(gowid.WidgetSizeError{Widget: w, Size: size, Required: "gowid.IRenderFixed"}) } return gowid.RenderBox{C: len(w.LeftDec()) + len(w.RightDec()) + len(w.MiddleDec()), R: 1} } func (w *Widget) Render(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.ICanvas { if _, ok := size.(gowid.IRenderFixed); !ok { panic(gowid.WidgetSizeError{Widget: w, Size: size, Required: "gowid.IRenderFixed"}) } res := checkbox.Render(w, size, focus, app) return res } func (w *Widget) UserInput(ev interface{}, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) bool { return button.UserInput(w, ev, size, focus, app) } //'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' func Select(w IWidget, app gowid.IApp) { cur := w.IsChecked() if !cur { for _, w2 := range *w.Group() { if w != w2 && w2.IsChecked() { w2.SetStateInternal(false) gowid.RunWidgetCallbacks(w2, gowid.ClickCB{}, app, w2) break } } w.SetStateInternal(true) gowid.RunWidgetCallbacks(w, gowid.ClickCB{}, app, w) } } //====================================================================== // This is here to avoid import cycles type RadioButtonTester struct { State bool } func (f *RadioButtonTester) Changed(app gowid.IApp, w gowid.IWidget, data ...interface{}) { rb := w.(*Widget) f.State = rb.Selected } func (f *RadioButtonTester) ID() interface{} { return "bar" } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: gowid-1.4.0/widgets/radio/radio_test.go000066400000000000000000000042531426234454000200650ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source code is governed by the MIT license // that can be found in the LICENSE file. package radio import ( "testing" "github.com/gcla/gowid" "github.com/gcla/gowid/gwtest" "github.com/gcla/gowid/widgets/columns" "github.com/stretchr/testify/assert" ) //====================================================================== func TestRadioButton1(t *testing.T) { rbgroup := make([]IWidget, 0) rb1 := New(&rbgroup) rb2 := New(&rbgroup) rb3 := New(&rbgroup) assert.Equal(t, rb1.IsChecked(), true) assert.Equal(t, rb2.IsChecked(), false) assert.Equal(t, rb3.IsChecked(), false) c1 := rb1.Render(gowid.RenderFixed{}, gowid.Focused, gwtest.D) assert.Equal(t, c1.String(), "(X)") c2 := rb2.Render(gowid.RenderFixed{}, gowid.Focused, gwtest.D) assert.Equal(t, c2.String(), "( )") ct1 := &RadioButtonTester{State: true} assert.Equal(t, ct1.State, true) rb1.OnClick(ct1) ct2 := &RadioButtonTester{State: false} assert.Equal(t, ct2.State, false) rb2.OnClick(ct2) rb2.Click(gwtest.D) assert.Equal(t, rb1.IsChecked(), false) assert.Equal(t, ct1.State, false) assert.Equal(t, rb2.IsChecked(), true) assert.Equal(t, ct2.State, true) assert.Equal(t, rb3.IsChecked(), false) fixed := gowid.RenderFixed{} ccols := []gowid.IContainerWidget{ &gowid.ContainerWidget{rb1, fixed}, &gowid.ContainerWidget{rb2, fixed}, &gowid.ContainerWidget{rb3, fixed}, } cols := columns.New(ccols) c3 := cols.Render(gowid.RenderFixed{}, gowid.Focused, gwtest.D) assert.Equal(t, c3.String(), "( )(X)( )") cpos := c3.CursorCoords() cx, cy := cpos.X, cpos.Y assert.Equal(t, c3.CursorEnabled(), true) assert.Equal(t, cx, 1) assert.Equal(t, cy, 0) assert.Equal(t, c3.String(), "( )(X)( )") ev := gwtest.CursorRight() cols.UserInput(ev, gowid.RenderFixed{}, gowid.Focused, gwtest.D) c3 = cols.Render(gowid.RenderFixed{}, gowid.Focused, gwtest.D) cx = c3.CursorCoords().X assert.Equal(t, cx, 4) gwtest.RenderBoxManyTimes(t, cols, 0, 20, 0, 20) gwtest.RenderFlowManyTimes(t, cols, 0, 20) } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: gowid-1.4.0/widgets/selectable/000077500000000000000000000000001426234454000164025ustar00rootroot00000000000000gowid-1.4.0/widgets/selectable/selectable.go000066400000000000000000000032531426234454000210370ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. // Package selectable provides a widget that forces its inner widget to be selectable. package selectable import ( "fmt" "github.com/gcla/gowid" ) //====================================================================== // If you would like a non-selectable widget like TextWidget to be selectable // in some context, wrap it in Widget // type Widget struct { gowid.IWidget *gowid.Callbacks gowid.SubWidgetCallbacks isSelectable bool } func New(w gowid.IWidget) *Widget { return NewWith(w, true) } func NewSelectable(w gowid.IWidget) *Widget { return NewWith(w, true) } func NewUnselectable(w gowid.IWidget) *Widget { return NewWith(w, false) } func NewWith(w gowid.IWidget, isSelectable bool) *Widget { res := &Widget{ IWidget: w, isSelectable: isSelectable, } res.SubWidgetCallbacks = gowid.SubWidgetCallbacks{CB: &res.Callbacks} var _ gowid.ICompositeWidget = res return res } func (w *Widget) String() string { return fmt.Sprintf("selectable[%v]", w.SubWidget()) } func (w *Widget) SubWidget() gowid.IWidget { return w.IWidget } func (w *Widget) SetSubWidget(wi gowid.IWidget, app gowid.IApp) { w.IWidget = wi gowid.RunWidgetCallbacks(w.Callbacks, gowid.SubWidgetCB{}, app, w) } func (w *Widget) SubWidgetSize(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.IRenderSize { return size } func (w *Widget) Selectable() bool { return w.isSelectable } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: gowid-1.4.0/widgets/shadow/000077500000000000000000000000001426234454000155645ustar00rootroot00000000000000gowid-1.4.0/widgets/shadow/shadow.go000066400000000000000000000106331426234454000174030ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. // Package shadow adds a drop shadow effect to a widget. package shadow import ( "fmt" "github.com/gcla/gowid" tcell "github.com/gdamore/tcell/v2" ) //====================================================================== type IOffset interface { Offset() int } type IWidget interface { gowid.ICompositeWidget IOffset } // Widget will render a drop shadow underneath and to the right of the inner widget, // providing a simple 3D effect. // // Offset is the number of lines to extend the drop shadow down - it is // extended right by 2*Offset because terminal cells aren't square. // type Widget struct { gowid.IWidget offset int // Means y offset, x is 2*y because cells are not squares - // we just guess at a reasonable look for a reasonable // aspect ratio *gowid.Callbacks gowid.SubWidgetCallbacks } func New(inner gowid.IWidget, offset int) *Widget { res := &Widget{ IWidget: inner, offset: offset, } res.SubWidgetCallbacks = gowid.SubWidgetCallbacks{CB: &res.Callbacks} var _ gowid.ICompositeWidget = res var _ IWidget = res return res } func (w *Widget) String() string { return fmt.Sprintf("shadow[%v]", w.SubWidget()) } func (w *Widget) SubWidget() gowid.IWidget { return w.IWidget } func (w *Widget) SetSubWidget(wi gowid.IWidget, app gowid.IApp) { w.IWidget = wi gowid.RunWidgetCallbacks(w, gowid.SubWidgetCB{}, app, w) } func (w *Widget) Offset() int { return w.offset } func (w *Widget) SetOffset(x int, app gowid.IApp) { w.offset = x } func (w *Widget) UserInput(ev interface{}, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) bool { return UserInput(w, ev, size, focus, app) } func (w *Widget) Render(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.ICanvas { return Render(w, size, focus, app) } func (w *Widget) SubWidgetSize(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.IRenderSize { return SubWidgetSize(w, size, focus, app) } func (w *Widget) RenderSize(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.IRenderBox { return RenderSize(w, size, focus, app) } //'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' func UserInput(w gowid.ICompositeWidget, ev interface{}, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) bool { subSize := w.SubWidgetSize(size, focus, app) if evm, ok := ev.(*tcell.EventMouse); ok { ss := w.SubWidget().RenderSize(subSize, focus, app) mx, my := evm.Position() if my < ss.BoxRows() && my >= 0 && mx < ss.BoxColumns() && mx >= 0 { return gowid.UserInputIfSelectable(w.SubWidget(), ev, subSize, focus, app) } } else { return gowid.UserInputIfSelectable(w.SubWidget(), ev, subSize, focus, app) } return false } func Render(w IWidget, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.ICanvas { newSize := w.SubWidgetSize(size, focus, app) innerCanvas := w.SubWidget().Render(newSize, focus, app) shadowCanvas := gowid.NewCanvasOfSizeExt(innerCanvas.BoxColumns(), innerCanvas.BoxRows(), gowid.MakeCell(' ', gowid.MakeTCellColorExt(tcell.ColorDefault), gowid.MakeTCellColorExt(tcell.ColorBlack), gowid.StyleNone)) shadowCanvas.ExtendLeft(gowid.EmptyLine(w.Offset() * 2)) res := gowid.NewCanvasOfSize(shadowCanvas.BoxColumns(), w.Offset()) res.AppendBelow(shadowCanvas, false, false) res.MergeUnder(innerCanvas, 0, 0, false) return res } func SubWidgetSize(w IOffset, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.IRenderSize { var newSize gowid.IRenderSize switch sz := size.(type) { case gowid.IRenderFixed: newSize = gowid.RenderFixed{} case gowid.IRenderBox: newSize = gowid.RenderBox{C: sz.BoxColumns() - (w.Offset() * 2), R: sz.BoxRows() - w.Offset()} case gowid.IRenderFlowWith: newSize = gowid.RenderFlowWith{C: sz.FlowColumns() - 2} default: panic(gowid.WidgetSizeError{Widget: w, Size: size}) } return newSize } func RenderSize(w IWidget, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.IRenderBox { ss := w.SubWidgetSize(size, focus, app) sdim := w.SubWidget().RenderSize(ss, focus, app) return gowid.RenderBox{C: sdim.BoxColumns() + (w.Offset() * 2), R: sdim.BoxRows() + w.Offset()} } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: gowid-1.4.0/widgets/spinner/000077500000000000000000000000001426234454000157555ustar00rootroot00000000000000gowid-1.4.0/widgets/spinner/spinner.go000066400000000000000000000100331426234454000177570ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. // Package spinner provides a simple themable spinner. package spinner import ( "fmt" "runtime" "time" "github.com/gcla/gowid" "github.com/gcla/gowid/widgets/styled" "github.com/gcla/gowid/widgets/text" ) //====================================================================== // IWidget - if your widget implements progress.IWidget, you will be able to render it using the // progress.Render() function. // type IWidget interface { gowid.IWidget // Text should return the string to be displayed inside the progress bar e.g. "50%" Text() string // Enabled returns true if spinner is spinning Enabled() bool // SetEnabled enables or disables the animation/spinning SetEnabled(bool, gowid.IApp) // Index returns the index at which to start drawing from the spinner characters Index() int // SpinnerLen returns the length of the set of chars used to draw the spinner SpinnerLen() int // Styler is used to render the incomplete part of the progress bar. Styler() gowid.ICellStyler } // Widget is the concrete type of a progressbar widget. type Widget struct { enabled bool label string idx int ticker *time.Ticker stopChan chan struct{} styler gowid.ICellStyler Callbacks *gowid.Callbacks gowid.RejectUserInput gowid.NotSelectable } type ChangeStateCB struct{} //var wave []rune = []rune("▁▃▄▅▆▇█▇▆▅▄▃") //var wave []rune = []rune("◢■◤") var wave []rune func init() { if runtime.GOOS == "windows" { wave = []rune("▲ ") } else { wave = []rune("◤ ◢") } } // Options is used for passing arguments to the progressbar initializer, New(). type Options struct { Label string Styler gowid.ICellStyler } // New will return an initialized spinner func New(args Options) *Widget { res := &Widget{ label: args.Label, styler: args.Styler, Callbacks: gowid.NewCallbacks(), } var _ IWidget = res return res } func (w *Widget) String() string { return fmt.Sprintf("spinner") } func (w *Widget) Text() string { return w.label } func (w *Widget) Index() int { return w.idx } func (w *Widget) SpinnerLen() int { return len(wave) } func (w *Widget) OnChangeState(f gowid.IWidgetChangedCallback) { gowid.AddWidgetCallback(w.Callbacks, ChangeStateCB{}, f) } func (w *Widget) RemoveOnChangeState(f gowid.IIdentity) { gowid.RemoveWidgetCallback(w.Callbacks, ChangeStateCB{}, f) } func (w *Widget) Enabled() bool { return w.enabled } func (w *Widget) Update() { w.idx -= 1 if w.idx < 0 { w.idx = len(wave) - 1 } } func (w *Widget) SetEnabled(enabled bool, app gowid.IApp) { cur := w.enabled w.enabled = enabled if enabled != cur { gowid.RunWidgetCallbacks(w.Callbacks, ChangeStateCB{}, app, w) } } func (w *Widget) Styler() gowid.ICellStyler { return w.styler } func (w *Widget) RenderSize(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.IRenderBox { return gowid.CalculateRenderSizeFallback(w, size, focus, app) } func (w *Widget) Render(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.ICanvas { return Render(w, size, focus, app) } //'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' // Render will render a progressbar IWidget. func Render(w IWidget, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.ICanvas { flow, isFlow := size.(gowid.IRenderFlowWith) if !isFlow { panic(gowid.WidgetSizeError{Widget: w, Size: size, Required: "gowid.IRenderFlowWith"}) } cols := flow.FlowColumns() display := make([]rune, cols) wi := w.Index() for i := 0; i < cols; i++ { display[i] = wave[wi] wi += 1 if wi == w.SpinnerLen() { wi = 0 } } barCanvas := styled.New( text.New(string(display)), w.Styler(), ).Render( gowid.RenderBox{C: cols, R: 1}, gowid.NotSelected, app) return barCanvas } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: gowid-1.4.0/widgets/styled/000077500000000000000000000000001426234454000156035ustar00rootroot00000000000000gowid-1.4.0/widgets/styled/styled.go000066400000000000000000000132771426234454000174500ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. // Package styled provides a colored styled widget. package styled import ( "fmt" "github.com/gcla/gowid" ) //====================================================================== // TODO - make a constructor to keep these fields unexported type AttributeRange struct { Start int End int Styler gowid.ICellStyler } type Widget struct { gowid.IWidget focusRange []AttributeRange notFocusRange []AttributeRange options Options *gowid.Callbacks gowid.SubWidgetCallbacks } type Options struct { OverWrite bool // If true, then apply the style over any style below; if false, style underneath takes precedence } // Very simple way to color an entire widget func New(inner gowid.IWidget, styler gowid.ICellStyler, opts ...Options) *Widget { res := NewWithRanges( inner, []AttributeRange{AttributeRange{0, -1, styler}}, []AttributeRange{AttributeRange{0, -1, styler}}, opts..., ) var _ gowid.ICompositeWidget = res return res } func NewInvertedFocus(inner gowid.IWidget, styler gowid.ICellStyler, opts ...Options) *Widget { return NewExt(inner, styler, gowid.ColorInverter{styler}, opts...) } func NewFocus(inner gowid.IWidget, styler gowid.ICellStyler, opts ...Options) *Widget { return NewExt(inner, nil, styler, opts...) } func NewNoFocus(inner gowid.IWidget, styler gowid.ICellStyler, opts ...Options) *Widget { return NewExt(inner, styler, nil, opts...) } func NewExt(inner gowid.IWidget, notFocusStyler, focusStyler gowid.ICellStyler, opts ...Options) *Widget { res := NewWithRanges( inner, []AttributeRange{AttributeRange{0, -1, notFocusStyler}}, []AttributeRange{AttributeRange{0, -1, focusStyler}}, opts..., ) var _ gowid.ICompositeWidget = res return res } func NewWithRanges(inner gowid.IWidget, notFocusRange []AttributeRange, focusRange []AttributeRange, opts ...Options) *Widget { var opt Options if len(opts) > 0 { opt = opts[0] } res := &Widget{ IWidget: inner, focusRange: focusRange, notFocusRange: notFocusRange, options: opt, } res.SubWidgetCallbacks = gowid.SubWidgetCallbacks{CB: &res.Callbacks} var _ gowid.IWidget = res return res } func (w *Widget) String() string { return fmt.Sprintf("styler[%v]", w.SubWidget()) } func (w *Widget) SubWidget() gowid.IWidget { return w.IWidget } func (w *Widget) SetSubWidget(inner gowid.IWidget, app gowid.IApp) { w.IWidget = inner gowid.RunWidgetCallbacks(w, gowid.SubWidgetCB{}, app, w) } func (w *Widget) SubWidgetSize(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.IRenderSize { return w.RenderSize(size, focus, app) } func (w *Widget) RenderSize(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.IRenderBox { return w.SubWidget().RenderSize(size, focus, app) } func (w *Widget) UserInput(ev interface{}, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) bool { return gowid.UserInputIfSelectable(w.IWidget, ev, size, focus, app) } func (w *Widget) Render(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.ICanvas { canvas := w.SubWidget().Render(size, focus, app) cols := canvas.BoxColumns() var attrSpecs []AttributeRange var f1 gowid.TCellColor var b1 gowid.TCellColor if focus.Focus { attrSpecs = w.focusRange } else { attrSpecs = w.notFocusRange } x := cols y := canvas.BoxRows() max := x * y if attrSpecs != nil { for _, attr := range attrSpecs { // TODO - bounds checks if attr.Styler != nil { f, b, s := attr.Styler.GetStyle(app) for i := attr.Start; true; i++ { if attr.End != -1 && i == attr.End { break } if i == max { break } col, row := i%cols, i/cols c := canvas.CellAt(col, row) c2 := c if f != nil { f1 = gowid.IColorToTCell(f, gowid.ColorNone, app.GetColorMode()) c = c.WithForegroundColor(f1) } if b != nil { b1 = gowid.IColorToTCell(b, gowid.ColorNone, app.GetColorMode()) c = c.WithBackgroundColor(b1) } if !w.options.OverWrite { c = c.WithStyle(s).MergeDisplayAttrsUnder(c2) } else { c = c2.MergeDisplayAttrsUnder(c.WithStyle(s)) } canvas.SetCellAt(col, row, c) } } } } return canvas } //====================================================================== type ReverseIfSelectedForCopy struct{} var _ gowid.IClipboardSelected = ReverseIfSelectedForCopy{} func (r ReverseIfSelectedForCopy) AlterWidget(w gowid.IWidget, app gowid.IApp) gowid.IWidget { return New(w, gowid.MakeStyledAs(gowid.StyleReverse)) } //====================================================================== type BoldIfSelectedForCopy struct{} var _ gowid.IClipboardSelected = BoldIfSelectedForCopy{} func (r BoldIfSelectedForCopy) AlterWidget(w gowid.IWidget, app gowid.IApp) gowid.IWidget { return New(w, gowid.MakeStyledAs(gowid.StyleBold)) } //====================================================================== type BlinkIfSelectedForCopy struct{} var _ gowid.IClipboardSelected = BlinkIfSelectedForCopy{} func (r BlinkIfSelectedForCopy) AlterWidget(w gowid.IWidget, app gowid.IApp) gowid.IWidget { return New(w, gowid.MakeStyledAs(gowid.StyleBlink)) } //====================================================================== type UsePaletteIfSelectedForCopy struct { Entry string } var _ gowid.IClipboardSelected = UsePaletteIfSelectedForCopy{} func (r UsePaletteIfSelectedForCopy) AlterWidget(w gowid.IWidget, app gowid.IApp) gowid.IWidget { return New(w, gowid.MakePaletteRef(r.Entry)) } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: gowid-1.4.0/widgets/styled/styled_test.go000066400000000000000000000021631426234454000204770ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. package styled import ( "strings" "testing" "github.com/gcla/gowid" "github.com/gcla/gowid/gwtest" "github.com/gcla/gowid/widgets/text" log "github.com/sirupsen/logrus" ) //====================================================================== func TestCanvas12(t *testing.T) { widget1a := text.New("hello transubstantiation boy") widget1 := NewWithRanges(widget1a, []AttributeRange{AttributeRange{0, 5, gowid.MakePaletteRef("test1notfocus")}}, []AttributeRange{AttributeRange{0, 5, gowid.MakePaletteRef("test1focus")}}) canvas1 := widget1.Render(gowid.RenderFlowWith{C: 11}, gowid.NotSelected, gwtest.D) log.Infof("Widget12 is %v", widget1) log.Infof("Canvas12 is %s", canvas1.String()) res := strings.Join([]string{"hello trans", "ubstantiati", "on boy "}, "\n") if res != canvas1.String() { t.Errorf("Failed") } } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: gowid-1.4.0/widgets/table/000077500000000000000000000000001426234454000153665ustar00rootroot00000000000000gowid-1.4.0/widgets/table/simple_model.go000066400000000000000000000224571426234454000204000ustar00rootroot00000000000000//go:generate statik -src=data // Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. // A simple implementation of a CSV table widget. package table import ( "encoding/csv" "io" "sort" "github.com/gcla/gowid" "github.com/gcla/gowid/widgets/button" "github.com/gcla/gowid/widgets/columns" "github.com/gcla/gowid/widgets/divider" "github.com/gcla/gowid/widgets/fill" "github.com/gcla/gowid/widgets/isselected" "github.com/gcla/gowid/widgets/radio" "github.com/gcla/gowid/widgets/styled" "github.com/gcla/gowid/widgets/text" log "github.com/sirupsen/logrus" ) //====================================================================== type LayoutOptions struct { Widths []gowid.IWidgetDimension } type StyleOptions struct { VerticalSeparator gowid.IWidget HorizontalSeparator gowid.IWidget TableSeparator gowid.IWidget HeaderStyleProvided bool HeaderStyleNoFocus gowid.ICellStyler HeaderStyleSelected gowid.ICellStyler HeaderStyleFocus gowid.ICellStyler CellStyleProvided bool CellStyleNoFocus gowid.ICellStyler CellStyleSelected gowid.ICellStyler CellStyleFocus gowid.ICellStyler } type SimpleOptions struct { NoDefaultSorters bool Comparators []ICompare Style StyleOptions Layout LayoutOptions } // SimpleModel implements table.IModel and can be used as a simple model for // table.IWidget. Fill in the headers, the data; initialize the SortOrder array // and provide any styling needed. The resulting struct can then be rendered // as a table. type SimpleModel struct { Headers []string Data [][]string Comparators []ICompare SortOrder []int // table row order as displayed -> table row identifier (RowId) InvSortOrder []int // table row identifier (RowId) -> table row order as displayed Style StyleOptions Layout LayoutOptions } var _ IBoundedModel = (*SimpleModel)(nil) func defaultOptions() SimpleOptions { return SimpleOptions{ Style: StyleOptions{ HorizontalSeparator: divider.NewAscii(), TableSeparator: divider.NewAscii(), VerticalSeparator: fill.New('|'), }, } } // NewCsvModel returns a SimpleTable built from CSV data in the supplied reader. SimpleTable // implements IModel, and so can be used as a source for table.IWidget. func NewCsvModel(csvFile io.Reader, firstLineIsHeaders bool, opts ...SimpleOptions) *SimpleModel { haveHeaders := false reader := csv.NewReader(csvFile) res := make([][]string, 0) var headers []string for { line, err := reader.Read() if err == io.EOF { break } else if err != nil { log.Fatal(err) } if firstLineIsHeaders && !haveHeaders { headers = line haveHeaders = true } else { res = append(res, line) } } return NewSimpleModel(headers, res, opts...) } // NewSimpleModel returns a SimpleTable built from caller-supplied header data and table data. // SimpleTable implements IModel, and so can be used as a source for table.IWidget. func NewSimpleModel(headers []string, res [][]string, opts ...SimpleOptions) *SimpleModel { var opt SimpleOptions if len(opts) > 0 { opt = opts[0] } else { opt = defaultOptions() } sortOrder := make([]int, len(res)) invSortOrder := make([]int, len(res)) for i := 0; i < len(sortOrder); i++ { sortOrder[i] = i invSortOrder[i] = i } tbl := &SimpleModel{ Headers: headers, Data: res, SortOrder: sortOrder, InvSortOrder: invSortOrder, } sorters := opt.Comparators if sorters == nil { var cols int if headers != nil { cols = len(headers) } else if len(res) > 0 { cols = len(res[0]) } sorters = make([]ICompare, cols) } if !opt.NoDefaultSorters { for i := 0; i < len(sorters); i++ { if sorters[i] == nil { sorters[i] = StringCompare{} } } } tbl.Comparators = sorters tbl.Style = opt.Style tbl.Layout = opt.Layout return tbl } func (c *SimpleModel) Columns() int { return len(c.Data[0]) } func (c *SimpleModel) Rows() int { return len(c.Data) } var widthOneHeightMax RenderWithUnitsMax = RenderWithUnitsMax{ RenderWithUnits: gowid.RenderWithUnits{1}, } func (c *SimpleModel) HeaderWidget(ws []gowid.IWidget, focus int) gowid.IWidget { var flowVertDivider *gowid.ContainerWidget if c.VerticalSeparator() != nil { flowVertDivider = &gowid.ContainerWidget{IWidget: c.VerticalSeparator(), D: widthOneHeightMax} } cws := make([]gowid.IContainerWidget, 0) if flowVertDivider != nil { cws = append(cws, flowVertDivider) } for i, w := range ws { var dim gowid.IWidgetDimension = gowid.RenderWithWeight{W: 1} if c.Widths() != nil && i < len(c.Widths()) { dim = c.Widths()[i] } cws = append(cws, &gowid.ContainerWidget{IWidget: w, D: dim}) if flowVertDivider != nil { cws = append(cws, flowVertDivider) } } hw := columns.New(cws, columns.Options{ StartColumn: focus, }) return hw } func (c *SimpleModel) HeaderWidgets() []gowid.IWidget { if c.Headers == nil || len(c.Headers) == 0 { return nil } var res []gowid.IWidget rbgroup := make([]radio.IWidget, 0, len(c.Headers)*2) res = make([]gowid.IWidget, 0, len(c.Headers)) for i, s := range c.Headers { i2 := i var all, label gowid.IWidget label = text.New(s + " ") label = button.NewBare(label) sorters := c.Comparators if sorters != nil { sorteri := sorters[i2] if sorteri != nil { rb1 := radio.New(&rbgroup) rb1.Decoration.Right = "/" rb1.OnClick(gowid.WidgetCallback{"cb", func(app gowid.IApp, widget gowid.IWidget) { sorter := &SimpleTableByColumn{ SimpleModel: c, Column: i2, } sort.Sort(sorter) }}) rb2 := radio.New(&rbgroup) rb2.Decoration.Left = "" rb2.OnClick(gowid.WidgetCallback{"cb", func(app gowid.IApp, widget gowid.IWidget) { sorter := &SimpleTableByColumn{ SimpleModel: c, Column: i2, } sort.Sort(sort.Reverse(sorter)) }}) all = columns.NewFixed(label, rb1, rb2) } } if all == nil { all = text.New(s) } var w gowid.IWidget if c.Style.HeaderStyleProvided { w = isselected.New( styled.New( all, c.GetStyle().HeaderStyleNoFocus, ), styled.New( all, c.GetStyle().HeaderStyleSelected, ), styled.New( all, c.GetStyle().HeaderStyleFocus, ), ) } else { w = styled.NewExt( all, nil, gowid.MakeStyledAs(gowid.StyleReverse), ) } res = append(res, w) } return res } type ISimpleRowProvider interface { GetStyle() StyleOptions } func (c *SimpleModel) GetStyle() StyleOptions { return c.Style } // Provides a "cell" which is stitched together with columns to provide a "row" func SimpleCellWidget(c ISimpleRowProvider, i int, s string) gowid.IWidget { var w gowid.IWidget if c.GetStyle().CellStyleProvided { b := button.NewBare(text.New(s)) w = isselected.New(b, styled.New(b, c.GetStyle().CellStyleSelected), styled.New(b, c.GetStyle().CellStyleFocus)) } else { w = styled.NewExt(button.NewBare(text.New(s)), nil, gowid.MakeStyledAs(gowid.StyleReverse)) } return w } func (c *SimpleModel) CellWidget(i int, s string) gowid.IWidget { return SimpleCellWidget(c, i, s) } type ISimpleDataProvider interface { GetData() [][]string CellWidget(i int, s string) gowid.IWidget } func (c *SimpleModel) GetData() [][]string { return c.Data } func SimpleCellWidgets(c ISimpleDataProvider, row2 RowId) []gowid.IWidget { if int(row2) < len(c.GetData()) { row := int(row2) res := make([]gowid.IWidget, len(c.GetData()[row])) for i, s := range c.GetData()[row] { res[i] = c.CellWidget(i, s) } return res } return nil } func (c *SimpleModel) CellWidgets(rowid RowId) []gowid.IWidget { return SimpleCellWidgets(c, rowid) } func (c *SimpleModel) RowIdentifier(row int) (RowId, bool) { if row < 0 || row >= len(c.SortOrder) { return RowId(-1), false } else { return RowId(c.SortOrder[row]), true } } func (c *SimpleModel) IdentifierToRow(rowid RowId) (int, bool) { if rowid < 0 || int(rowid) >= len(c.InvSortOrder) { return -1, false } else { return c.InvSortOrder[rowid], true } } func (c *SimpleModel) VerticalSeparator() gowid.IWidget { return c.Style.VerticalSeparator } func (c *SimpleModel) HorizontalSeparator() gowid.IWidget { return c.Style.HorizontalSeparator } func (c *SimpleModel) HeaderSeparator() gowid.IWidget { return c.Style.TableSeparator } func (c *SimpleModel) Widths() []gowid.IWidgetDimension { return c.Layout.Widths } //====================================================================== // SimpleTableByColumn is a SimpleTable with a selected column; it's intended // to be sortable, with the values in the selected column being those compared. type SimpleTableByColumn struct { *SimpleModel Column int } func (m *SimpleTableByColumn) Len() int { return len(m.Data) } func (m *SimpleTableByColumn) Less(i, j int) bool { return m.SimpleModel.Comparators[m.Column].Less(m.Data[m.SortOrder[i]][m.Column], m.Data[m.SortOrder[j]][m.Column]) } func (m *SimpleTableByColumn) Swap(i, j int) { invi, invj := m.SortOrder[i], m.SortOrder[j] m.SortOrder[i], m.SortOrder[j] = m.SortOrder[j], m.SortOrder[i] m.InvSortOrder[invi], m.InvSortOrder[invj] = m.InvSortOrder[invj], m.InvSortOrder[invi] } var _ sort.Interface = (*SimpleTableByColumn)(nil) //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: gowid-1.4.0/widgets/table/table.go000066400000000000000000000640151426234454000170120ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. // Package table provides a widget that renders tabular output. package table import ( "fmt" "strconv" "github.com/araddon/dateparse" "github.com/gcla/gowid" "github.com/gcla/gowid/gwutil" "github.com/gcla/gowid/widgets/columns" "github.com/gcla/gowid/widgets/isselected" "github.com/gcla/gowid/widgets/list" "github.com/gcla/gowid/widgets/pile" tcell "github.com/gdamore/tcell/v2" lru "github.com/hashicorp/golang-lru" ) //====================================================================== // RowId is used to uniquely identify a row. The idea here is that a table // row can move in the order of rows rendered, if the table is sorted, but // we would like to preserve the ability to cache the row's widgets (e.g. // the selected column in the row). So a client of an ITable should first // look up a RowId given an actual row to be rendered (1st, 2nd, etc). Then // with the RowId, the client asks for the RowWidgets. Clients of ITable // can then cache the rendered row using the RowId as a lookup field. Even // if that row moves around in the order rendered, it can be found in the // cache. type RowId int // IModel is implemented by any type which can provide arrays of // widgets for a given table row, and optionally header widgets. type IModel interface { Columns() int RowIdentifier(row int) (RowId, bool) // return a unique ID for row CellWidgets(row RowId) []gowid.IWidget // nil means EOD HeaderWidgets() []gowid.IWidget // nil means no headers VerticalSeparator() gowid.IWidget HorizontalSeparator() gowid.IWidget HeaderSeparator() gowid.IWidget Widths() []gowid.IWidgetDimension } type IInvertible interface { IdentifierToRow(rowid RowId) (int, bool) } type IMakeHeader interface { HeaderWidget([]gowid.IWidget, int) gowid.IWidget } // IBoundedTable implements ITable and can also provide the total number of // rows in the table. type IBoundedModel interface { IModel Rows() int } // ICompare is the type of the compare function used when sorting a table's // rows. type ICompare interface { Less(i, j string) bool } //====================================================================== // StringCompare is a unit type that satisfies ICompare, and can be used // for lexicographically comparing strings. type StringCompare struct{} func (s StringCompare) Less(i, j string) bool { return i < j } var _ ICompare = StringCompare{} // IntCompare is a unit type that satisfies ICompare, and can be used // for numerically comparing ints. type IntCompare struct{} func (s IntCompare) Less(i, j string) bool { x, err1 := strconv.Atoi(i) y, err2 := strconv.Atoi(j) if err1 == nil && err2 == nil { return x < y } else { return false } } var _ ICompare = IntCompare{} // FloatCompare is a unit type that satisfies ICompare, and can be used // for numerically comparing float64 values. type FloatCompare struct{} func (s FloatCompare) Less(i, j string) bool { x, err1 := strconv.ParseFloat(i, 64) y, err2 := strconv.ParseFloat(j, 64) if err1 == nil && err2 == nil { return x < y } else { return false } } var _ ICompare = FloatCompare{} // DateTimeCompare is a unit type that satisfies ICompare, and can be used // for numerically comparing date/time values. type DateTimeCompare struct{} func (s DateTimeCompare) Less(i, j string) bool { x, err1 := dateparse.ParseAny(i) y, err2 := dateparse.ParseAny(j) if err1 == nil && err2 == nil { return x.Before(y) } else { return false } } var _ ICompare = DateTimeCompare{} //====================================================================== // ListWithPreferedColumn acts like a list.Widget but also satisfies gowid.IPreferedPosition. // The idea is that if the list rows consist of columns, then moving up and down the list // should preserve the selected column. type ListWithPreferedColumn struct { list.IWidget // so we can use bounded or unbounded lists } var _ gowid.IPreferedPosition = (*ListWithPreferedColumn)(nil) var _ gowid.IComposite = (*ListWithPreferedColumn)(nil) func (l *ListWithPreferedColumn) SubWidget() gowid.IWidget { return l.IWidget } func (l *ListWithPreferedColumn) GetPreferedPosition() gwutil.IntOption { res := gwutil.NoneInt() fpos := l.IWidget.Walker().Focus() w := l.IWidget.Walker().At(fpos) if w != nil { res = gowid.PrefPosition(w) } return res } func (l *ListWithPreferedColumn) SetPreferedPosition(col int, app gowid.IApp) { fpos := l.IWidget.Walker().Focus() w := l.IWidget.Walker().At(fpos) if w != nil { gowid.SetPrefPosition(w, col, app) } } func (w *ListWithPreferedColumn) String() string { return fmt.Sprintf("listc") } //====================================================================== type Position int var _ list.IBoundedWalkerPosition = Position(0) var _ list.IWalkerPosition = Position(0) func (t Position) ToInt() int { return int(t) } func (t Position) Equal(pos list.IWalkerPosition) bool { if t2, ok := pos.(Position); ok { return t == t2 } else { panic(gowid.InvalidTypeToCompare{LHS: t, RHS: pos}) } } func (t Position) GreaterThan(pos list.IWalkerPosition) bool { if t2, ok := pos.(Position); ok { return t > t2 } else { panic(gowid.InvalidTypeToCompare{LHS: t, RHS: pos}) } } //====================================================================== type RenderWithUnitsMax struct { gowid.RenderWithUnits gowid.RenderMax } var _ gowid.IRenderMax = widthOneHeightMax //====================================================================== // Widget wraps a widget and aligns it vertically according to the supplied arguments. The wrapped // widget can be aligned to the top, bottom or middle, and can be provided with a specific height in #lines. // type Widget struct { wrapper *pile.Widget header gowid.IWidget listw *ListWithPreferedColumn model IModel cur int cache *lru.Cache flowHorzDivider *gowid.ContainerWidget flowVertDivider *gowid.ContainerWidget flowTableDivider *gowid.ContainerWidget opt Options *gowid.Callbacks gowid.FocusCallbacks gowid.IsSelectable } var _ gowid.IWidget = (*Widget)(nil) type BoundedWidget struct { *Widget } var _ list.IBoundedWalker = (*BoundedWidget)(nil) var _ list.IWalkerHome = (*BoundedWidget)(nil) var _ list.IWalkerEnd = (*BoundedWidget)(nil) type Options struct { CacheSize int } func New(model IModel, opts ...Options) *Widget { var opt Options if len(opts) > 0 { opt = opts[0] } // Fill in Widget later once constructed. listw := &ListWithPreferedColumn{} // Construct the table first, then set the list later. That's because when the // pile is constructed, it tries to find the first selectable widget; but lw's // embedded list widget is nil at the time. sz := 4096 if opt.CacheSize > 0 { sz = opt.CacheSize } cache, err := lru.New(sz) if err != nil { panic(err) } // res acts as a ListWalker and a widget res := &Widget{ listw: listw, cur: 0, cache: cache, } res.FocusCallbacks = gowid.FocusCallbacks{CB: &res.Callbacks} switch model.(type) { case IBoundedModel: listw.IWidget = list.NewBounded(&BoundedWidget{res}) default: listw.IWidget = list.New(res) } res.update(listw, 0, model, opt) return res } var _ gowid.IWidget = (*Widget)(nil) func (w *Widget) update(listw *ListWithPreferedColumn, row int, model IModel, opt Options) { // Save whether we were in the header or the packet lists pf := -1 hf := -1 // setPileFocus := false if w.wrapper != nil { pf = w.wrapper.Focus() if w.model != nil && len(w.model.HeaderWidgets()) > 0 { // 0th must be header hf = gowid.Focus(w.wrapper.SubWidgets()[0]) } } // //if w.wrapper.Focus() // //} // //if bm, ok := // if w.model != nil { // if bm, ok := w.model.(IBoundedTable); ok && bm.Rows() == 0 { // setPileFocus = true // } // } else { // setPileFocus = true // } // if setPileFocus { // setPileFocus = false // if model != nil { // if bm, ok := model.(IBoundedTable); ok && bm.Rows() > 0 { // setPileFocus = true // } // } // } pileWidgets := make([]gowid.IContainerWidget, 0) var flowHorzDivider *gowid.ContainerWidget var flowVertDivider *gowid.ContainerWidget var flowTableDivider *gowid.ContainerWidget if model.HorizontalSeparator() != nil { flowHorzDivider = &gowid.ContainerWidget{model.HorizontalSeparator(), gowid.RenderFlow{}} } if model.VerticalSeparator() != nil { flowVertDivider = &gowid.ContainerWidget{model.VerticalSeparator(), widthOneHeightMax} } if model.HeaderSeparator() != nil { flowTableDivider = &gowid.ContainerWidget{model.HeaderSeparator(), gowid.RenderFlow{}} } if flowTableDivider != nil { pileWidgets = append(pileWidgets, flowTableDivider) } hws := model.HeaderWidgets() // widgets var hw *columns.Widget if hws != nil && len(hws) > 0 { var hw2 gowid.IWidget if nm, ok := model.(IMakeHeader); ok { hw2 = nm.HeaderWidget(hws, hf) } else { cws := make([]gowid.IContainerWidget, 0) if flowVertDivider != nil { cws = append(cws, flowVertDivider) } for i, w := range hws { var dim gowid.IWidgetDimension = gowid.RenderWithWeight{1} if model.Widths() != nil && i < len(model.Widths()) { dim = model.Widths()[i] } cws = append(cws, &gowid.ContainerWidget{w, dim}) if flowVertDivider != nil { cws = append(cws, flowVertDivider) } } hw = columns.New(cws, columns.Options{ StartColumn: hf, }) hw2 = hw } pileWidgets = append(pileWidgets, &gowid.ContainerWidget{hw2, gowid.RenderFlow{}}) if flowTableDivider != nil { pileWidgets = append(pileWidgets, flowTableDivider) } } // Fill in Widget later once constructed. pileWidgets = append(pileWidgets, &gowid.ContainerWidget{listw, gowid.RenderWithWeight{1}}) // Construct the table first, then set the list later. That's because when the // pile is constructed, it tries to find the first selectable widget; but lw's // embedded list widget is nil at the time. if hw != nil { w.header = hw } w.model = model w.flowHorzDivider = flowHorzDivider w.flowVertDivider = flowVertDivider w.flowTableDivider = flowTableDivider w.opt = opt if pf == -1 { // This is imperfect. If the table model is updated in such a way that // the dividers change, then re-using the previous pile index is wrong - // it needs to be done logically i.e. recalculated w.wrapper = pile.New(pileWidgets) } else { w.wrapper = pile.New(pileWidgets, pile.Options{ StartRow: pf, }) } } func (w *BoundedWidget) First() list.IWalkerPosition { if w.Length() == 0 { return nil } return Position(0) } func (w *BoundedWidget) Last() list.IWalkerPosition { if w.Length() == 0 { return nil } if w.flowHorzDivider != nil { return Position(w.Length()*2 - 2) } else { return Position(w.Length() - 1) } } func (w *BoundedWidget) BoundedWalker() list.IBoundedWalker { return w.listw.Walker().(list.IBoundedWalker) } func (w *BoundedWidget) Pos() int { return w.listw.Walker().(list.IBoundedWalker).Focus().(list.IBoundedWalkerPosition).ToInt() } func (w *BoundedWidget) SetPos(pos list.IBoundedWalkerPosition, app gowid.IApp) { w.listw.Walker().(list.IBoundedWalker).SetFocus(pos, app) } func (w *BoundedWidget) Length() int { return w.Model().(IBoundedModel).Rows() } func (w *Widget) CalculateOnScreen(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) (int, int, int, error) { return list.CalculateOnScreen(w.listw, size, focus, app) } func (w *Widget) SetModel(model IModel, app gowid.IApp) { oldpos, olderr := w.FocusXY() w.cache.Purge() // gcla later todo w.update(w.listw, w.cur, model, w.opt) if olderr == nil { w.SetFocusXY(app, oldpos) // mght not be able to set old focus, if model shape has changed } else { // If we previously had no data and now we do, change focus to the data element // in the pile // if bm, bmok := model.(IBoundedTable); bmok && bm.Rows() > 0 && model.Columns() > 0 { // w.wrapper.SetFocus(app, 1) // } // No focus in old model, so try to set a default one //w.SetFocusXY(app, Coords{0, 0}) // if bm, ok := model.(IBoundedTable); ok && bm.Rows() > 0 && model.Columns() > 0 { // if len(model.HeaderWidgets()) > 0 { // // //w.SetFocusXY(app, Coords{0, 1}) // // w.SetFocusXY(app, Coords{0, 0}) // // } else { // // w.SetFocusXY(app, Coords{0, 0}) // } // } } newpos, newerr := w.FocusXY() if olderr != newerr || oldpos != newpos { gowid.RunWidgetCallbacks(w.Callbacks, gowid.FocusCB{}, app, w) } } func (w *Widget) Lower() *ListWithPreferedColumn { return w.listw } func (w *Widget) SetLower(l *ListWithPreferedColumn) { w.listw = l } func (w *Widget) Cache() *lru.Cache { return w.cache } func (w *Widget) Model() IModel { return w.model } func (w *Widget) CurrentRow() int { return w.cur } func (w *Widget) SetCurrentRow(p Position) { w.cur = int(p) } func (w *Widget) VertDivider() gowid.IContainerWidget { // Do it this way to avoid having a nil value in an interface type that isn't nil if w.flowVertDivider == nil { return nil } else { return w.flowVertDivider } } func (w *Widget) TableDivider() gowid.IContainerWidget { // Do it this way to avoid having a nil value in an interface type that isn't nil if w.flowTableDivider == nil { return nil } else { return w.flowTableDivider } } func (w *Widget) HorzDivider() gowid.IWidget { // Do it this way to avoid having a nil value in an interface type that isn't nil if w.flowHorzDivider == nil { return nil } else { return w.flowHorzDivider } } func (w *Widget) String() string { return fmt.Sprintf("table") } func (w *Widget) RenderSize(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.IRenderBox { return gowid.CalculateRenderSizeFallback(w, size, focus, app) } func (w *Widget) SetFocusOnHeader(app gowid.IApp) { w.wrapper.SetFocus(app, 0) } // SetFocusOnData returns true if there is data to focus on func (w *Widget) SetFocusOnData(app gowid.IApp) bool { cur := w.wrapper.Focus() w.wrapper.SetFocus(app, 1) return cur != w.wrapper.Focus() } func (w *Widget) UserInput(ev interface{}, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) bool { oldpos, olderr := w.FocusXY() res := w.wrapper.UserInput(ev, size, focus, app) newpos, newerr := w.FocusXY() if olderr != newerr || oldpos != newpos { gowid.RunWidgetCallbacks(w.Callbacks, gowid.FocusCB{}, app, w) } return res } func (w *Widget) Render(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.ICanvas { return w.wrapper.Render(size, focus, app) } func (w *Widget) Up(lines int, size gowid.IRenderSize, app gowid.IApp) { for i := 0; i < lines; i++ { w.wrapper.UserInput(tcell.NewEventKey(tcell.KeyUp, ' ', tcell.ModNone), size, gowid.Focused, app) } } func (w *Widget) Down(lines int, size gowid.IRenderSize, app gowid.IApp) { for i := 0; i < lines; i++ { w.wrapper.UserInput(tcell.NewEventKey(tcell.KeyDown, ' ', tcell.ModNone), size, gowid.Focused, app) } } func (w *Widget) UpPage(num int, size gowid.IRenderSize, app gowid.IApp) { for i := 0; i < num; i++ { w.wrapper.UserInput(tcell.NewEventKey(tcell.KeyPgUp, ' ', tcell.ModNone), size, gowid.Focused, app) } } func (w *Widget) DownPage(num int, size gowid.IRenderSize, app gowid.IApp) { for i := 0; i < num; i++ { w.wrapper.UserInput(tcell.NewEventKey(tcell.KeyPgDn, ' ', tcell.ModNone), size, gowid.Focused, app) } } type IRowToWidget interface { VertDivider() gowid.IContainerWidget Model() IModel Cache() *lru.Cache } // propagatePrefPosition will ensure prefered position is set for all candidates in an isselected // widget, so that - if appropriate - a prefered position is rendered whether or not // the widget has focus (or indeed is selected). type propagatePrefPosition struct { *isselected.WidgetExt } var _ gowid.IWidget = (*propagatePrefPosition)(nil) var _ gowid.IComposite = (*propagatePrefPosition)(nil) var _ gowid.IPreferedPosition = (*propagatePrefPosition)(nil) func (w *propagatePrefPosition) SubWidget() gowid.IWidget { return w.WidgetExt } func (w *propagatePrefPosition) GetPreferedPosition() gwutil.IntOption { // Get the Focused one because when this is called, the child widget (which will be columns) // will be in focus. // // This list above this will get the pref position of the current container widget, prior to // effecting user input. e.g. column 2. Then it might move focus to the next row, so a new // column widget. It will then set the prefered column of that new focus widget to 2. so when // querying the current pref col, get the one in focus. A big hack. res := gowid.PrefPosition(w.Focused) return res } func (w *propagatePrefPosition) SetPreferedPosition(col int, app gowid.IApp) { gowid.SetPrefPosition(w.Not, col, app) gowid.SetPrefPosition(w.Selected, col, app) gowid.SetPrefPosition(w.Focused, col, app) } func (t *Widget) RowToWidget(ws []gowid.IWidget) gowid.IWidget { return RowToWidget(t, ws) } func RowToWidget(t IRowToWidget, ws []gowid.IWidget) gowid.IWidget { var res gowid.IWidget if ws != nil { cws := make([]gowid.IContainerWidget, 0) if t.VertDivider() != nil { cws = append(cws, t.VertDivider()) } for i, w := range ws { var dim gowid.IWidgetDimension = gowid.RenderWithWeight{1} if t.Model().Widths() != nil && i < len(t.Model().Widths()) { dim = t.Model().Widths()[i] } cws = append(cws, &gowid.ContainerWidget{w, dim}) if t.VertDivider() != nil { cws = append(cws, t.VertDivider()) } } colsWhenFocusOrSelected := columns.New(cws) colsWhenNotSelected := columns.New(cws, columns.Options{ // Don't let columns set focus.select for selected child DoNotSetSelected: true, }) // This has the following effect: // // - if a row of the table is either selected or in focus, the conditional // widget will render a regular columns. In that case, the columns will // set focus.Selected for the "cell" that is currently selected. Styling // can be applied as appropriate. // // - if a row of the table is not selected (so also not focused), then // the columns widget will not set focus.Selected. That means rows // of the table that are not the current row cannot make decisions // based on which cell is selected. This is likely what is needed - // applying cell-level styling to the selected table row, and not // other table rows (e.g. highlighting the selected column, but not // in rows not in focus) // res = &propagatePrefPosition{ WidgetExt: isselected.NewExt( colsWhenNotSelected, colsWhenFocusOrSelected, colsWhenFocusOrSelected, ), } } return res } type IWidgetAt interface { HorzDivider() gowid.IWidget RowToWidget(ws []gowid.IWidget) gowid.IWidget IRowToWidget } // WidgetAt is used by the type that satisfies list.IWalker - therefore it provides // a row for each line of the list. That means that if the table has dividers, it // provides them too. func (t *Widget) AtRow(pos int) gowid.IWidget { return WidgetAt(t, pos) } // Provide the pos'th "row" widget func WidgetAt(t IWidgetAt, pos int) gowid.IWidget { if pos < 0 { return nil } if t.HorzDivider() != nil { if pos%2 == 1 { return t.HorzDivider() } else { pos = pos / 2 } } var res gowid.IWidget if rid, ok := t.Model().RowIdentifier(pos); ok { if cw, ok := t.Cache().Get(rid); ok { res = cw.(gowid.IWidget) } else { ws := t.Model().CellWidgets(rid) res = t.RowToWidget(ws) if res != nil { t.Cache().Add(rid, res) } return res } } return res } type IFocus interface { IWidgetAt AtRow(int) gowid.IWidget CurrentRow() int } // list.IWalker func (t *Widget) Focus() list.IWalkerPosition { return Focus(t) } // list.IWalker func (t *Widget) At(pos list.IWalkerPosition) gowid.IWidget { return t.AtRow(int(pos.(Position))) } // list.IWalker func Focus(t IFocus) list.IWalkerPosition { return Position(t.CurrentRow()) } type ISetFocus interface { SetCurrentRow(pos Position) } // In order to implement list.IWalker // list.IWalker func (t *Widget) SetFocus(pos list.IWalkerPosition, app gowid.IApp) { SetFocus(t, pos) } // In order to implement list.IWalker func SetFocus(t ISetFocus, pos list.IWalkerPosition) { if t2, ok := pos.(Position); !ok { panic(fmt.Errorf("Invalid position %v passed to SetFocus", pos)) } else { t.SetCurrentRow(t2) } } // In order to implement list.IWalker // list.IWalker func (t *Widget) Next(ipos list.IWalkerPosition) list.IWalkerPosition { if pos, ok := ipos.(Position); !ok { panic(fmt.Errorf("Invalid position %v passed to Next", ipos)) } else { npos := int(pos) + 1 return Position(npos) } } // In order to implement list.IWalker // list.IWalker func (t *Widget) Previous(ipos list.IWalkerPosition) list.IWalkerPosition { if pos, ok := ipos.(Position); !ok { panic(fmt.Errorf("Invalid position %v passed to Prev", ipos)) } else { ppos := int(pos) - 1 return Position(ppos) } } func (t *Widget) GoToFirst(app gowid.IApp) bool { if homer, ok := t.listw.Walker().(list.IWalkerHome); ok { oldpos, olderr := t.FocusXY() defer func() { newpos, newerr := t.FocusXY() if olderr != newerr || oldpos != newpos { gowid.RunWidgetCallbacks(t.Callbacks, gowid.FocusCB{}, app, t) } }() pos := homer.First() if pos != nil { t.listw.Walker().SetFocus(pos, app) t.GoToTop(app) return true } } return false } func (t *Widget) GoToLast(app gowid.IApp) bool { if ender, ok := t.listw.Walker().(list.IWalkerEnd); ok { oldpos, olderr := t.FocusXY() defer func() { newpos, newerr := t.FocusXY() if olderr != newerr || oldpos != newpos { gowid.RunWidgetCallbacks(t.Callbacks, gowid.FocusCB{}, app, t) } }() pos := ender.Last() if pos != nil { t.listw.Walker().SetFocus(pos, app) t.GoToBottom(app) return true } } return false } type ISetPos interface { Length() int SetPos(pos list.IBoundedWalkerPosition, app gowid.IApp) } func (t *Widget) GoToNth(app gowid.IApp, pos int) bool { if walker, ok := t.listw.Walker().(ISetPos); ok { oldpos, olderr := t.FocusXY() defer func() { newpos, newerr := t.FocusXY() if olderr != newerr || oldpos != newpos { gowid.RunWidgetCallbacks(t.Callbacks, gowid.FocusCB{}, app, t) } }() pos = gwutil.LimitTo(0, pos, walker.Length()-1) walker.SetPos(Position(pos), app) t.GoToMiddle(app) return true } return false } type IGoToTop interface { GoToTop(app gowid.IApp) } func (t *Widget) GoToTop(app gowid.IApp) bool { if b, ok := t.listw.IWidget.(IGoToTop); ok { b.GoToTop(app) return true } return false } type IGoToBottom interface { GoToBottom(app gowid.IApp) } func (t *Widget) GoToBottom(app gowid.IApp) bool { if b, ok := t.listw.IWidget.(IGoToBottom); ok { b.GoToBottom(app) return true } return false } type IGoToMiddle interface { GoToMiddle(app gowid.IApp) } func (t *Widget) GoToMiddle(app gowid.IApp) { if b, ok := t.listw.IWidget.(IGoToMiddle); ok { b.GoToMiddle(app) } } type Coords struct { Column int Row int } func (c Coords) String() string { return fmt.Sprintf("(%d,%d)", c.Column, c.Row) } type NoFocus struct{} func (n NoFocus) Error() string { return "No table data" } func findFocus(w gowid.IWidget) gowid.IWidget { w = gowid.FindInHierarchy(w, true, gowid.WidgetPredicate(func(w gowid.IWidget) bool { var res bool if _, ok := w.(gowid.IFocus); ok { res = true } return res })) return w } // FocusXY returns the coordinates of the focus widget in the table, potentially including // the header if one is configured. This is grungy and needs to account for the cell separators // in its arithmetic. func (t *Widget) FocusXY() (Coords, error) { var col, row int addOne := false if t.header != nil { focusedOnHeader := false if t.TableDivider() != nil && t.wrapper.Focus() == 1 { focusedOnHeader = true } else if t.TableDivider() == nil && t.wrapper.Focus() == 0 { focusedOnHeader = true } if focusedOnHeader { row = 0 fw := findFocus(t.header) if fw == nil { return Coords{}, NoFocus{} } col = fw.(gowid.IFocus).Focus() if t.VertDivider() != nil { col = (col - 1) / 2 } return Coords{Column: col, Row: row}, nil } else { addOne = true } } rw := t.listw.Walker().Focus() rww := t.listw.Walker().At(rw) colwi := gowid.FindInHierarchy(rww, true, gowid.WidgetPredicate(func(w gowid.IWidget) bool { _, ok := w.(*columns.Widget) return ok })) if colwi == nil { //panic(fmt.Errorf("Could not find columns widget within table structure")) return Coords{}, NoFocus{} } colw := colwi.(*columns.Widget) col = colw.Focus() if t.VertDivider() != nil { col = (col - 1) / 2 } row = int(rw.(Position)) if t.HorzDivider() != nil { row = row / 2 } if addOne { row++ } return Coords{Column: col, Row: row}, nil } func (t *Widget) SetFocusXY(app gowid.IApp, xy Coords) { oldpos, olderr := t.FocusXY() defer func() { newpos, newerr := t.FocusXY() if olderr != newerr || oldpos != newpos { gowid.RunWidgetCallbacks(t.Callbacks, gowid.FocusCB{}, app, t) } }() if t.header != nil { if xy.Row == 0 { if t.TableDivider() != nil { t.wrapper.SetFocus(app, 1) } else { t.wrapper.SetFocus(app, 0) } fw := findFocus(t.header) if fw == nil { return } fw2 := fw.(gowid.IFocus) if t.VertDivider() != nil { fw2.SetFocus(app, xy.Column*2+1) } else { fw2.SetFocus(app, xy.Column) } return } else { if t.TableDivider() != nil { t.wrapper.SetFocus(app, 3) } else { t.wrapper.SetFocus(app, 1) } xy.Row-- } } // Have to set list walker := t.listw.Walker() if t.HorzDivider() != nil { walker.SetFocus(Position(xy.Row*2), app) } else { walker.SetFocus(Position(xy.Row), app) } colwi := gowid.FindInHierarchy(walker.At(walker.Focus()), true, gowid.WidgetPredicate(func(w gowid.IWidget) bool { _, ok := w.(*columns.Widget) return ok })) if colwi != nil { cols := colwi.(*columns.Widget) //cols := walker.Focus().Widget.(*propagatePrefPosition).Widget.(*isselected.WidgetExt).Not.(*columns.Widget) if t.VertDivider() != nil { cols.SetFocus(app, xy.Column*2+1) } else { cols.SetFocus(app, xy.Column) } } } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: gowid-1.4.0/widgets/table/table_test.go000066400000000000000000000236321426234454000200510ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. package table import ( "sort" "strings" "testing" "github.com/gcla/gowid" "github.com/gcla/gowid/gwtest" "github.com/gcla/gowid/widgets/divider" "github.com/gcla/gowid/widgets/fill" "github.com/gcla/gowid/widgets/selectable" "github.com/gcla/gowid/widgets/text" tcell "github.com/gdamore/tcell/v2" "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" ) type MyTable struct { rows [][]gowid.IWidget hor bool ver bool wid []gowid.IWidgetDimension } type MyHeader struct { widgets []gowid.IWidget } func (t MyTable) Columns() int { return 3 } func (t MyTable) Rows() int { return len(t.rows) } func (t MyTable) HeaderWidgets() []gowid.IWidget { return nil } func (t MyTable) CellWidgets(row RowId) []gowid.IWidget { if row >= 0 && int(row) < t.Rows() { return t.rows[int(row)] } else { return nil } } func (t MyTable) Comparators() []ICompare { return nil } func (t MyTable) RowIdentifier(row int) (RowId, bool) { return RowId(row), true } type MyTableWithHeader struct { MyTable MyHeader } func (t MyTableWithHeader) HeaderWidgets() []gowid.IWidget { return t.MyHeader.widgets } func (t MyTable) VerticalSeparator() gowid.IWidget { if t.ver { return fill.New('|') } return nil } func (t MyTable) HorizontalSeparator() gowid.IWidget { if t.hor { return divider.NewAscii() } return nil } func (t MyTable) HeaderSeparator() gowid.IWidget { if t.hor { return divider.NewAscii() } return nil } func (t MyTable) Widths() []gowid.IWidgetDimension { return t.wid } var _ IModel = MyTable{} //====================================================================== func makew(txt string) *selectable.Widget { return selectable.New(text.New(txt)) } func TestEmptyTable1(t *testing.T) { model := MyTable{ rows: [][]gowid.IWidget{}, ver: true, } sz := gowid.RenderFlowWith{C: 19} w1 := New(model) c1 := w1.Render(sz, gowid.Focused, gwtest.D) assert.Equal(t, "", c1.String()) _, err := w1.FocusXY() assert.Error(t, err) cbcalled := false w1.OnFocusChanged(gowid.WidgetCallback{"cb", func(app gowid.IApp, w gowid.IWidget) { assert.Equal(t, w, w1) cbcalled = true }}) model.rows = append(model.rows, []gowid.IWidget{makew("w1r0"), makew("w2r0"), makew("w3r0")}) _, err = w1.FocusXY() assert.Error(t, err) assert.Equal(t, false, cbcalled) w1.SetModel(model, gwtest.D) xy, err := w1.FocusXY() assert.NoError(t, err) assert.Equal(t, Coords{0, 0}, xy) assert.Equal(t, true, cbcalled) } func TestTable1(t *testing.T) { model := MyTable{ rows: [][]gowid.IWidget{ {makew("w1r0"), makew("w2r0"), makew("w3r0")}, {makew("w1r1"), makew("w2r1"), makew("w3r1")}, }, ver: true, } sz := gowid.RenderFlowWith{C: 19} w1 := New(model) c1 := w1.Render(sz, gowid.Focused, gwtest.D) assert.Equal(t, "|w1r0 |w2r0 |w3r0 |\n|w1r1 |w2r1 |w3r1 |", c1.String()) model.hor = true w1 = New(model) c1 = w1.Render(sz, gowid.Focused, gwtest.D) assert.Equal(t, "-------------------\n|w1r0 |w2r0 |w3r0 |\n-------------------\n|w1r1 |w2r1 |w3r1 |\n-------------------", c1.String()) headers := MyHeader{ widgets: []gowid.IWidget{ makew("col0"), makew("col1"), makew("col2"), }, } modelh := MyTableWithHeader{ MyTable: model, MyHeader: headers, } w1 = New(modelh) c1 = w1.Render(sz, gowid.Focused, gwtest.D) assert.Equal(t, strings.TrimSuffix(` ------------------- |col0 |col1 |col2 | ------------------- |w1r0 |w2r0 |w3r0 | ------------------- |w1r1 |w2r1 |w3r1 | ------------------- `[1:], "\n"), c1.String()) widths := []gowid.IWidgetDimension{ gowid.RenderWithUnits{6}, gowid.RenderWithUnits{5}, gowid.RenderWithUnits{4}, } modelh.wid = widths w1 = New(modelh) c1 = w1.Render(sz, gowid.Focused, gwtest.D) assert.Equal(t, strings.TrimSuffix(` ------------------- |col0 |col1 |col2| ------------------- |w1r0 |w2r0 |w3r0| ------------------- |w1r1 |w2r1 |w3r1| ------------------- `[1:], "\n"), c1.String()) assert.Equal(t, true, w1.Focus().Equal(Position(0))) xy, err := w1.FocusXY() assert.NoError(t, err) assert.Equal(t, 0, xy.Column) assert.Equal(t, 0, xy.Row) evr := gwtest.CursorRight() w1.UserInput(evr, sz, gowid.Focused, gwtest.D) xy, err = w1.FocusXY() assert.NoError(t, err) assert.Equal(t, 1, xy.Column) assert.Equal(t, 0, xy.Row) evd := gwtest.CursorDown() w1.UserInput(evd, sz, gowid.Focused, gwtest.D) xy, err = w1.FocusXY() assert.NoError(t, err) assert.Equal(t, 1, xy.Column) assert.Equal(t, 1, xy.Row) evu := gwtest.CursorUp() w1.UserInput(evr, sz, gowid.Focused, gwtest.D) xy, err = w1.FocusXY() assert.NoError(t, err) assert.Equal(t, 2, xy.Column) assert.Equal(t, 1, xy.Row) w1.UserInput(evu, sz, gowid.Focused, gwtest.D) xy, err = w1.FocusXY() assert.NoError(t, err) assert.Equal(t, 2, xy.Column) assert.Equal(t, 0, xy.Row) cbcalled := false w1.OnFocusChanged(gowid.WidgetCallback{"cb", func(app gowid.IApp, w gowid.IWidget) { assert.Equal(t, w, w1) cbcalled = true }}) evmdown := tcell.NewEventMouse(1, 1, tcell.WheelDown, 0) w1.UserInput(evmdown, sz, gowid.Focused, gwtest.D) assert.Equal(t, true, cbcalled) cbcalled = false w1.UserInput(evmdown, sz, gowid.Focused, gwtest.D) assert.Equal(t, true, cbcalled) cbcalled = false xy, err = w1.FocusXY() assert.NoError(t, err) assert.Equal(t, 2, xy.Column) assert.Equal(t, 2, xy.Row) w1.UserInput(evmdown, sz, gowid.Focused, gwtest.D) assert.Equal(t, false, cbcalled) xy, err = w1.FocusXY() assert.NoError(t, err) assert.Equal(t, 2, xy.Column) assert.Equal(t, 2, xy.Row) w1.SetFocusXY(gwtest.D, Coords{Column: 1, Row: 0}) xy, err = w1.FocusXY() assert.NoError(t, err) assert.Equal(t, 1, xy.Column) assert.Equal(t, 0, xy.Row) w1.SetFocusXY(gwtest.D, Coords{Column: 1, Row: 1}) xy, err = w1.FocusXY() assert.NoError(t, err) assert.Equal(t, 1, xy.Column) assert.Equal(t, 1, xy.Row) w1.SetFocusXY(gwtest.D, Coords{Column: 2, Row: 2}) xy, err = w1.FocusXY() assert.NoError(t, err) assert.Equal(t, 2, xy.Column) assert.Equal(t, 2, xy.Row) evlmx14y5 := tcell.NewEventMouse(14, 5, tcell.Button1, 0) evnonex14y5 := tcell.NewEventMouse(14, 5, tcell.ButtonNone, 0) w1.UserInput(evlmx14y5, sz, gowid.Focused, gwtest.D) gwtest.D.SetLastMouseState(gowid.MouseState{true, false, false}) w1.UserInput(evnonex14y5, sz, gowid.Focused, gwtest.D) gwtest.D.SetLastMouseState(gowid.MouseState{false, false, false}) xy, err = w1.FocusXY() assert.NoError(t, err) assert.Equal(t, 2, xy.Column) assert.Equal(t, 2, xy.Row) evmup := tcell.NewEventMouse(1, 1, tcell.WheelUp, 0) w1.UserInput(evmup, sz, gowid.Focused, gwtest.D) xy, err = w1.FocusXY() assert.NoError(t, err) assert.Equal(t, 2, xy.Column) assert.Equal(t, 1, xy.Row) w1.UserInput(evmdown, sz, gowid.Focused, gwtest.D) xy, err = w1.FocusXY() assert.NoError(t, err) assert.Equal(t, 2, xy.Column) assert.Equal(t, 2, xy.Row) } //====================================================================== func TestTable2(t *testing.T) { csv := strings.TrimSuffix(` 1,c,-2 3,a,1.2 2,b,3.4 `[1:], "\n") sz := gowid.RenderFlowWith{C: 13} // Implements ITable t1 := NewCsvModel(strings.NewReader(csv), false, SimpleOptions{ Style: StyleOptions{ VerticalSeparator: fill.New('|'), }, }) w1 := New(t1) c1 := w1.Render(sz, gowid.Focused, gwtest.D) assert.Equal(t, strings.TrimSuffix(` |1 |c |-2 | |3 |a |1.2| |2 |b |3.4| `[1:], "\n"), c1.String()) assert.Equal(t, []int{0, 1, 2}, t1.SortOrder) assert.Equal(t, []int{0, 1, 2}, t1.InvSortOrder) logrus.Infof("cols is %d", t1.Columns()) logrus.Infof("rows is %d", len(t1.Data)) logrus.Infof("sort order is %v", t1.SortOrder) logrus.Infof("comp is %v", t1.Comparators) sorter := &SimpleTableByColumn{ SimpleModel: t1, Column: 0, } sort.Sort(sorter) c1 = w1.Render(sz, gowid.Focused, gwtest.D) assert.Equal(t, strings.TrimSuffix(` |1 |c |-2 | |2 |b |3.4| |3 |a |1.2| `[1:], "\n"), c1.String()) assert.Equal(t, []int{0, 2, 1}, t1.SortOrder) assert.Equal(t, []int{0, 2, 1}, t1.InvSortOrder) t1.Comparators[2] = FloatCompare{} sorter = &SimpleTableByColumn{ SimpleModel: t1, Column: 2, } sort.Sort(sorter) c1 = w1.Render(sz, gowid.Focused, gwtest.D) assert.Equal(t, strings.TrimSuffix(` |1 |c |-2 | |3 |a |1.2| |2 |b |3.4| `[1:], "\n"), c1.String()) assert.Equal(t, []int{0, 1, 2}, t1.SortOrder) assert.Equal(t, []int{0, 1, 2}, t1.InvSortOrder) sort.Sort(sort.Reverse(sorter)) c1 = w1.Render(sz, gowid.Focused, gwtest.D) assert.Equal(t, strings.TrimSuffix(` |2 |b |3.4| |3 |a |1.2| |1 |c |-2 | `[1:], "\n"), c1.String()) assert.Equal(t, []int{2, 1, 0}, t1.SortOrder) assert.Equal(t, []int{2, 1, 0}, t1.InvSortOrder) t1.Layout.Widths = []gowid.IWidgetDimension{ gowid.RenderWithUnits{1}, gowid.RenderWithUnits{2}, gowid.RenderWithUnits{3}, } w1 = New(t1) c1 = w1.Render(sz, gowid.Focused, gwtest.D) assert.Equal(t, strings.TrimSuffix(` |2|b |3.4| |3|a |1.2| |1|c |-2 | `[1:], "\n"), c1.String()) } //====================================================================== func TestTable3(t *testing.T) { csv := strings.TrimSuffix(` aaaaaaaaaaa,1 bbbbbbbbbbb,2 `[1:], "\n") // Implements ITable t1 := NewCsvModel(strings.NewReader(csv), false, SimpleOptions{ Style: StyleOptions{ VerticalSeparator: fill.New('|'), }, Layout: LayoutOptions{ Widths: []gowid.IWidgetDimension{ gowid.RenderWithWeight{1}, gowid.RenderWithUnits{1}, }, }, }) w1 := New(t1) c1 := w1.Render(gowid.RenderFlowWith{C: 15}, gowid.Focused, gwtest.D) assert.Equal(t, strings.TrimSuffix(` |aaaaaaaaaaa|1| |bbbbbbbbbbb|2| `[1:], "\n"), c1.String()) c1 = w1.Render(gowid.RenderFlowWith{C: 12}, gowid.Focused, gwtest.D) assert.Equal(t, strings.TrimSuffix(` |aaaaaaaa|1| |aaa | | |bbbbbbbb|2| |bbb | | `[1:], "\n"), c1.String()) } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: gowid-1.4.0/widgets/terminal/000077500000000000000000000000001426234454000161125ustar00rootroot00000000000000gowid-1.4.0/widgets/terminal/term_canvas.go000066400000000000000000001141071426234454000207470ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. // Based heavily on vterm.py from urwid package terminal import ( "bytes" "errors" "fmt" "io" "strconv" "strings" "unicode/utf8" "github.com/gcla/gowid" "github.com/gcla/gowid/gwutil" tcell "github.com/gdamore/tcell/v2" "github.com/mattn/go-runewidth" log "github.com/sirupsen/logrus" "golang.org/x/text/encoding/charmap" ) //====================================================================== const ( CharsetDefault = iota CharsetUTF8 = iota ) const ( EscByte byte = 27 ) type LEDSState int const ( LEDSClear LEDSState = 0 LEDSScrollLock LEDSState = 1 LEDSNumLock LEDSState = 2 LEDSCapsLock LEDSState = 3 ) const ( DecSpecialChars = "▮◆▒␉␌␍␊°±␤␋┘┐┌└┼⎺⎻─⎼⎽├┤┴┬│≤≥π≠£·" AltDecSpecialChars = "_`abcdefghijklmnopqrstuvwxyz{|}~" ) type ScrollDir bool const ( ScrollDown ScrollDir = false ScrollUp ScrollDir = true ) type IMouseSupport interface { MouseEnabled() bool MouseIsSgr() bool MouseReportButton() bool MouseReportAny() bool } //====================================================================== // Modes is used to track the state of this terminal - which modes // are enabled, etc. It tracks the mouse state in particular so implements // IMouseSupport. type Modes struct { DisplayCtrl bool Insert bool LfNl bool KeysAutoWrap bool ReverseVideo bool ConstrainScrolling bool DontAutoWrap bool InvisibleCursor bool Charset int VT200Mouse bool // #define SET_VT200_MOUSE 1000 ReportButton bool // #define SET_BTN_EVENT_MOUSE 1002 ReportAny bool // #define SET_ANY_EVENT_MOUSE 1003 SgrModeMouse bool // #define SET_SGR_EXT_MODE_MOUSE 1006 } func (t Modes) MouseEnabled() bool { return t.VT200Mouse } func (t Modes) MouseIsSgr() bool { return t.SgrModeMouse } func (t Modes) MouseReportButton() bool { return t.ReportButton } func (t Modes) MouseReportAny() bool { return t.ReportAny } //====================================================================== type CSIFunction func(canvas *Canvas, args []int, qmark bool) type ICSICommand interface { MinArgs() int FallbackArg() int IsAlias() bool Alias() byte Call(canvas *Canvas, args []int, qmark bool) } type RegularCSICommand struct { minArgs int fallbackArg int fn CSIFunction } func (c RegularCSICommand) MinArgs() int { return c.minArgs } func (c RegularCSICommand) FallbackArg() int { return c.fallbackArg } func (c RegularCSICommand) IsAlias() bool { return false } func (c RegularCSICommand) Alias() byte { panic(errors.New("Do not call")) } func (c RegularCSICommand) Call(canvas *Canvas, args []int, qmark bool) { c.fn(canvas, args, qmark) } type AliasCSICommand struct { alias byte } func (c AliasCSICommand) MinArgs() int { panic(errors.New("Do not call")) } func (c AliasCSICommand) FallbackArg() int { panic(errors.New("Do not call")) } func (c AliasCSICommand) IsAlias() bool { return true } func (c AliasCSICommand) Alias() byte { return c.alias } func (c AliasCSICommand) Call(canvas *Canvas, args []int, qmark bool) { panic(errors.New("Do not call")) } type CSIMap map[byte]ICSICommand // csiMap maps bytes to CSI mode changing functions. This closely follows urwid's structure. var csiMap = CSIMap{ '@': RegularCSICommand{1, 1, func(canvas *Canvas, args []int, qmark bool) { canvas.InsertChars(gwutil.NoneInt(), gwutil.NoneInt(), args[0], gwutil.NoneRune()) }}, 'A': RegularCSICommand{1, 1, func(canvas *Canvas, args []int, qmark bool) { canvas.MoveCursor(0, -int(args[0]), true, false, false) }}, 'B': RegularCSICommand{1, 1, func(canvas *Canvas, args []int, qmark bool) { canvas.MoveCursor(0, int(args[0]), true, false, false) }}, 'C': RegularCSICommand{1, 1, func(canvas *Canvas, args []int, qmark bool) { canvas.MoveCursor(int(args[0]), 0, true, false, false) }}, 'D': RegularCSICommand{1, 1, func(canvas *Canvas, args []int, qmark bool) { canvas.MoveCursor(-int(args[0]), 0, true, false, false) }}, 'E': RegularCSICommand{1, 1, func(canvas *Canvas, args []int, qmark bool) { canvas.MoveCursor(0, int(args[0]), false, false, true) }}, 'F': RegularCSICommand{1, 1, func(canvas *Canvas, args []int, qmark bool) { canvas.MoveCursor(0, -int(args[0]), false, false, true) }}, 'G': RegularCSICommand{1, 1, func(canvas *Canvas, args []int, qmark bool) { canvas.MoveCursor(int(args[0])-1, 0, false, false, true) }}, 'H': RegularCSICommand{2, 1, func(canvas *Canvas, args []int, qmark bool) { canvas.MoveCursor(int(args[1])-1, int(args[0])-1, false, false, false) }}, 'J': RegularCSICommand{1, 0, func(canvas *Canvas, args []int, qmark bool) { canvas.CSIEraseDisplay(args[0]) }}, 'K': RegularCSICommand{1, 0, func(canvas *Canvas, args []int, qmark bool) { canvas.CSIEraseLine(args[0]) }}, 'L': RegularCSICommand{1, 1, func(canvas *Canvas, args []int, qmark bool) { canvas.InsertLines(true, args[0]) }}, 'M': RegularCSICommand{1, 1, func(canvas *Canvas, args []int, qmark bool) { canvas.RemoveLines(true, args[0]) }}, 'P': RegularCSICommand{1, 1, func(canvas *Canvas, args []int, qmark bool) { canvas.RemoveChars(gwutil.NoneInt(), gwutil.NoneInt(), args[0]) }}, 'X': RegularCSICommand{1, 1, func(canvas *Canvas, args []int, qmark bool) { myx, myy := canvas.TermCursor() canvas.Erase(myx, myy, myx+args[0]-1, myy) }}, 'a': AliasCSICommand{alias: 'C'}, 'c': RegularCSICommand{0, 0, func(canvas *Canvas, args []int, qmark bool) { canvas.CSIGetDeviceAttributes(qmark) }}, 'd': RegularCSICommand{1, 1, func(canvas *Canvas, args []int, qmark bool) { canvas.MoveCursor(0, int(args[0])-1, false, true, false) }}, 'e': AliasCSICommand{alias: 'B'}, 'f': AliasCSICommand{alias: 'H'}, 'g': RegularCSICommand{1, 0, func(canvas *Canvas, args []int, qmark bool) { canvas.CSIClearTabstop(args[0]) }}, 'h': RegularCSICommand{1, 0, func(canvas *Canvas, args []int, qmark bool) { canvas.CSISetModes(args, qmark, false) }}, 'l': RegularCSICommand{1, 0, func(canvas *Canvas, args []int, qmark bool) { canvas.CSISetModes(args, qmark, true) }}, 'm': RegularCSICommand{1, 0, func(canvas *Canvas, args []int, qmark bool) { canvas.CSISetAttr(args) }}, 'n': RegularCSICommand{1, 0, func(canvas *Canvas, args []int, qmark bool) { canvas.CSIStatusReport(args[0]) }}, 'q': RegularCSICommand{1, 0, func(canvas *Canvas, args []int, qmark bool) { canvas.CSISetKeyboardLEDs(args[0]) }}, 'r': RegularCSICommand{2, 0, func(canvas *Canvas, args []int, qmark bool) { canvas.CSISetScroll(args[0], args[1]) }}, 's': RegularCSICommand{0, 0, func(canvas *Canvas, args []int, qmark bool) { canvas.SaveCursor(false) }}, 'u': RegularCSICommand{0, 0, func(canvas *Canvas, args []int, qmark bool) { canvas.RestoreCursor(false) }}, '`': AliasCSICommand{alias: 'G'}, } //====================================================================== var charsetMapping = map[string]rune{ "default": 0, "vt100": '0', "ibmpc": 'U', "user": 0, } type Charset struct { SgrMapping bool Active int Current rune Mapping []string } func NewTerminalCharset() *Charset { res := &Charset{} res.Mapping = []string{"default", "vt100"} res.Activate(0) return res } func (t *Charset) Activate(g int) { t.Active = g if val, ok := charsetMapping[t.Mapping[g]]; ok { t.Current = val } else { t.Current = 0 } } func (t *Charset) Define(g int, charset string) { t.Mapping[g] = charset t.Activate(t.Active) } func (t *Charset) SetSgrIbmpc() { t.SgrMapping = true } func (t *Charset) ResetSgrIbmpc() { t.SgrMapping = false t.Activate(t.Active) } func (t *Charset) ApplyMapping(r rune) rune { if t.SgrMapping || t.Mapping[t.Active] == "ibmpc" { decPos := strings.IndexRune(DecSpecialChars, charmap.CodePage437.DecodeByte(byte(r))) if decPos >= 0 { t.Current = '0' return rune(AltDecSpecialChars[decPos]) } else { t.Current = 'U' return r } } else { return r } } //====================================================================== // ViewPortCanvas implements ICanvas by embedding a Canvas pointer, but // reimplementing Line and Cell access APIs relative to an Offset and // a Height. The Height specifies the number of visible rows in the // ViewPortCanvas; the rows that are not visible are logically "above" // the visible rows. If Offset is reduced, the view of the underlying // large Canvas is shifted up. This type is used by the terminal widget // to hold the terminal's scrollback buffer. type ViewPortCanvas struct { *gowid.Canvas Offset int Height int } func NewViewPort(c *gowid.Canvas, offset, height int) *ViewPortCanvas { res := &ViewPortCanvas{ Canvas: c, Offset: offset, Height: height, } return res } func (c *ViewPortCanvas) Duplicate() gowid.ICanvas { res := &ViewPortCanvas{ Canvas: c.Canvas.Duplicate().(*gowid.Canvas), Offset: c.Offset, Height: c.Height, } return res } func (c *ViewPortCanvas) MergeUnder(c2 gowid.IMergeCanvas, leftOffset, topOffset int, bottomGetsCursor bool) { c.Canvas.MergeUnder(c2, leftOffset, topOffset+c.Offset, bottomGetsCursor) } func (v *ViewPortCanvas) BoxRows() int { return v.Height } func (v *ViewPortCanvas) Line(y int, cp gowid.LineCopy) gowid.LineResult { return v.Canvas.Line(y+v.Offset, cp) } func (v *ViewPortCanvas) SetLineAt(row int, line []gowid.Cell) { v.Canvas.SetLineAt(row+v.Offset, line) } func (v *ViewPortCanvas) CellAt(col, row int) gowid.Cell { return v.Canvas.CellAt(col, row+v.Offset) } func (v *ViewPortCanvas) SetCellAt(col, row int, c gowid.Cell) { v.Canvas.SetCellAt(col, row+v.Offset, c) } func (c *ViewPortCanvas) String() string { return gowid.CanvasToString(c) } //====================================================================== type parseState int const ( defaultState parseState = iota csiState oscState nonCsiState ignoreState ) func (p parseState) String() string { switch p { case defaultState: return "default" case csiState: return "csi" case oscState: return "osc" case nonCsiState: return "noncsi" case ignoreState: return "ignore" default: panic(fmt.Errorf("Invalid parse state: %d", int(p))) } } //====================================================================== // Canvas implements gowid.ICanvas and stores the state of the terminal drawing area // associated with a terminal (and TerminalWidget). type Canvas struct { *ViewPortCanvas alternate *ViewPortCanvas alternateActive bool parsestate parseState scrollback int withinEscape bool savedx, savedy gwutil.IntOption savedstyles map[string]bool savedfg, savedbg gwutil.IntOption scrollRegionStart, scrollRegionEnd int terminal ITerminal charset *Charset tcx, tcy int styles map[string]bool tabstops []int isRottenCursor bool escbuf []byte fg, bg gwutil.IntOption utf8Buffer []byte gowid.ICallbacks } func NewCanvasOfSize(cols, rows int, scrollback int, widget ITerminal) *Canvas { res := &Canvas{ ViewPortCanvas: NewViewPort(gowid.NewCanvasOfSize(cols, rows), 0, rows), alternate: NewViewPort(gowid.NewCanvasOfSize(cols, rows), 0, rows), scrollback: scrollback, terminal: widget, utf8Buffer: make([]byte, 0, 4), ICallbacks: gowid.NewCallbacks(), } res.Reset() var _ io.Writer = res return res } // Write is an io.Writer for a terminal canvas, which processes the input as // terminal codes, and writes with respect to the current cursor position. func (c *Canvas) Write(p []byte) (n int, err error) { for _, b := range p { c.ProcessByte(b) } return len(p), nil } func (c *Canvas) Duplicate() gowid.ICanvas { res := &Canvas{} *res = *c res.ViewPortCanvas = c.ViewPortCanvas.Duplicate().(*ViewPortCanvas) res.savedstyles = make(map[string]bool) for k, v := range c.savedstyles { res.savedstyles[k] = v } res.styles = make(map[string]bool) for k, v := range c.styles { res.styles[k] = v } res.tabstops = make([]int, len(c.tabstops)) for i, v := range res.tabstops { res.tabstops[i] = v } res.escbuf = make([]byte, len(c.escbuf)) for i, v := range res.escbuf { res.escbuf[i] = v } res.utf8Buffer = make([]byte, len(c.utf8Buffer)) for i, v := range res.utf8Buffer { res.utf8Buffer[i] = v } return res } func (c *Canvas) Reset() { c.alternateActive = false c.escbuf = make([]byte, 0) c.charset = NewTerminalCharset() c.parsestate = defaultState c.withinEscape = false c.savedx = gwutil.NoneInt() c.savedy = gwutil.NoneInt() c.savedfg = gwutil.NoneInt() c.savedbg = gwutil.NoneInt() c.savedstyles = make(map[string]bool) c.fg = gwutil.NoneInt() c.bg = gwutil.NoneInt() c.styles = make(map[string]bool) *c.terminal.Modes() = Modes{} c.ResetScroll() c.InitTabstops(false) c.Clear(gwutil.SomeInt(0), gwutil.SomeInt(0)) } func (c *Canvas) IsScrollRegionSet() bool { return !((c.scrollRegionStart == 0) && (c.scrollRegionEnd == c.BoxRows()-1)) } func (c *Canvas) ResetScroll() { c.scrollRegionStart = 0 c.scrollRegionEnd = c.BoxRows() - 1 } func (c *Canvas) CarriageReturn() { c.SetTermCursor(gwutil.SomeInt(0), gwutil.SomeInt(c.tcy)) } func (c *Canvas) Tab(tabstop int) { x, y := c.TermCursor() for x < c.BoxColumns()-1 { x += 1 if c.IsTabstop(x) { break } } c.isRottenCursor = false c.SetTermCursor(gwutil.SomeInt(x), gwutil.SomeInt(y)) } func (c *Canvas) InitTabstops(extend bool) { tablen, mod := c.BoxColumns()/8, c.BoxColumns() if mod > 0 { tablen += 1 } if extend { for len(c.tabstops) < tablen { c.tabstops = append(c.tabstops, 1) } } else { c.tabstops = []int{} for i := 0; i < tablen; i++ { c.tabstops = append(c.tabstops, 1) } } } func (c *Canvas) SetTabstop(x2 gwutil.IntOption, remove bool, clear bool) { if clear { for tab := 0; tab < len(c.tabstops); tab++ { c.tabstops[tab] = 0 } } else { var x int if x2.IsNone() { x, _ = c.TermCursor() } else { x = x2.Val() } div, mod := x/8, x%8 if remove { c.tabstops[div] &= ^(1 << uint(mod)) } else { c.tabstops[div] |= 1 << uint(mod) } } } func (c *Canvas) IsTabstop(x int) bool { div, mod := x/8, x%8 return (c.tabstops[div] & (1 << uint(mod))) > 0 } func (c *Canvas) TermCursor() (x, y int) { x, y = c.tcx, c.tcy return } func (c *Canvas) SetTermCursor(x2, y2 gwutil.IntOption) { tx, ty := c.TermCursor() var x, y int if x2.IsNone() { x = tx } else { x = x2.Val() } if y2.IsNone() { y = ty } else { y = y2.Val() } c.tcx, c.tcy = c.ConstrainCoords(x, y, false) if !c.terminal.Modes().InvisibleCursor { c.SetCursorCoords(c.tcx, c.tcy) } else { c.SetCursorCoords(-1, -1) } } func (c *Canvas) ConstrainCoords(x, y int, ignoreScrolling bool) (int, int) { if x >= c.BoxColumns() { x = c.BoxColumns() - 1 } else if x < 0 { x = 0 } if c.terminal.Modes().ConstrainScrolling && !ignoreScrolling { if y > c.scrollRegionEnd { y = c.scrollRegionEnd } else if y < c.scrollRegionStart { y = c.scrollRegionStart } } else { if y >= c.BoxRows() { y = c.BoxRows() - 1 } else if y < 0 { y = 0 } } return x, y } // ScrollBuffer will return the number of lines actually scrolled. func (c *Canvas) ScrollBuffer(dir ScrollDir, reset bool, linesOpt gwutil.IntOption) int { prev := c.Offset if reset { c.Offset = c.Canvas.BoxRows() - c.BoxRows() } else { var lines int if linesOpt.IsNone() { lines = c.BoxRows() / 2 } else { lines = linesOpt.Val() } if dir == ScrollDown { lines = -lines } maxScroll := c.Canvas.BoxRows() - c.BoxRows() c.Offset -= lines if c.Offset < 0 { c.Offset = 0 } else if c.Offset > maxScroll { c.Offset = maxScroll } } c.SetTermCursor(gwutil.NoneInt(), gwutil.NoneInt()) return c.Offset - prev } func (c *Canvas) Scroll(dir ScrollDir) { // reverse means scrolling up towards the top if dir == ScrollDown { // e.g. pgdown if c.IsScrollRegionSet() { start := c.scrollRegionStart + c.Offset end := c.scrollRegionEnd + c.Offset dummy := make([][]gowid.Cell, len(c.ViewPortCanvas.Canvas.Lines)) n := 0 n += copy(dummy[n:], c.ViewPortCanvas.Canvas.Lines[:start]) n += copy(dummy[n:], c.ViewPortCanvas.Canvas.Lines[start+1:end+1]) n += copy(dummy[n:], sliceWithOneEmptyLine(c.BoxColumns())) copy(dummy[n:], c.ViewPortCanvas.Canvas.Lines[end+1:]) c.ViewPortCanvas.Canvas.Lines = dummy } else { chopline := false if c.Canvas.BoxRows() == c.BoxRows()+c.scrollback { chopline = true } var dummy [][]gowid.Cell n := 0 if !chopline { dummy = make([][]gowid.Cell, c.Canvas.BoxRows()+1) n += copy(dummy[n:], c.ViewPortCanvas.Canvas.Lines) c.Offset += 1 } else { dummy = make([][]gowid.Cell, c.Canvas.BoxRows()) n += copy(dummy[n:], c.ViewPortCanvas.Canvas.Lines[1:]) } copy(dummy[n:], sliceWithOneEmptyLine(c.BoxColumns())) c.ViewPortCanvas.Canvas.Lines = dummy } } else { // e.g. pgup, cursor up if c.IsScrollRegionSet() { start := c.scrollRegionStart + c.Offset end := c.scrollRegionEnd + c.Offset dummy := make([][]gowid.Cell, len(c.ViewPortCanvas.Canvas.Lines)) n := 0 n += copy(dummy[n:], c.ViewPortCanvas.Canvas.Lines[:start]) n += copy(dummy[n:], sliceWithOneEmptyLine(c.BoxColumns())) n += copy(dummy[n:], c.ViewPortCanvas.Canvas.Lines[start:end]) copy(dummy[n:], c.ViewPortCanvas.Canvas.Lines[end+1:]) c.ViewPortCanvas.Canvas.Lines = dummy } else { c.InsertLines(false, 1) } } } func sliceWithOneEmptyLine(n int) [][]gowid.Cell { return [][]gowid.Cell{emptyLine(n)} } func emptyLine(n int) []gowid.Cell { fillArr := make([]gowid.Cell, n) return fillArr } func (c *Canvas) LineFeed(reverse bool) { x, y := c.TermCursor() if reverse { if y <= 0 && 0 < c.scrollRegionStart { } else if y == c.scrollRegionStart { c.Scroll(ScrollUp) } else { y -= 1 } } else { if y >= c.BoxRows()-1 && y > c.scrollRegionEnd { } else if y == c.scrollRegionEnd { c.Scroll(ScrollDown) } else { y += 1 } } c.SetTermCursor(gwutil.SomeInt(x), gwutil.SomeInt(y)) } func (c *Canvas) SaveCursor(withAttrs bool) { myx, myy := c.TermCursor() c.savedx = gwutil.SomeInt(myx) c.savedy = gwutil.SomeInt(myy) c.savedstyles = make(map[string]bool) if withAttrs { c.savedfg = c.fg c.savedbg = c.bg for k, v := range c.styles { c.savedstyles[k] = v } } else { c.savedfg = gwutil.NoneInt() c.savedbg = gwutil.NoneInt() } } func (c *Canvas) RestoreCursor(withAttrs bool) { if !(c.savedx == gwutil.NoneInt() || c.savedy == gwutil.NoneInt()) { c.SetTermCursor(c.savedx, c.savedy) if withAttrs { c.fg = c.savedfg c.bg = c.savedbg c.styles = make(map[string]bool) for k, v := range c.savedstyles { c.styles[k] = v } } } } func (c *Canvas) NewLine() { c.CarriageReturn() c.LineFeed(false) } func (c *Canvas) MoveCursor(x, y int, relative bool, relativeX bool, relativeY bool) { if relative { relativeX = true relativeY = true } ctx, cty := c.TermCursor() if relativeX { x = ctx + x } if relativeY { y = cty + y } else if c.terminal.Modes().ConstrainScrolling { y += c.scrollRegionStart } c.SetTermCursor(gwutil.SomeInt(x), gwutil.SomeInt(y)) c.isRottenCursor = false } func (c *Canvas) Clear(newcx, newcy gwutil.IntOption) { for y := 0; y < c.BoxRows(); y++ { empty := emptyLine(c.BoxColumns()) c.SetLineAt(y, empty) } if !newcx.IsNone() && !newcy.IsNone() { c.SetTermCursor(newcx, newcy) } else { c.SetTermCursor(gwutil.SomeInt(0), gwutil.SomeInt(0)) } } func (c *Canvas) DECAln() { for i := 0; i < c.BoxRows(); i++ { for j := 0; j < c.BoxColumns(); j++ { c.SetCellAt(j, i, gowid.MakeCell('E', gowid.MakeTCellColorExt(tcell.ColorDefault), gowid.MakeTCellColorExt(tcell.ColorDefault), gowid.StyleNone)) } } } func (c *Canvas) UseAlternateScreen() { if !c.alternateActive { tmp := c.ViewPortCanvas c.ViewPortCanvas = c.alternate c.alternate = tmp c.alternateActive = true } } func (c *Canvas) UseOriginalScreen() { if c.alternateActive { tmp := c.ViewPortCanvas c.ViewPortCanvas = c.alternate c.alternate = tmp c.alternateActive = false } } func (c *Canvas) CSIClearTabstop(mode int) { switch mode { case 0: c.SetTabstop(gwutil.NoneInt(), true, false) case 3: c.SetTabstop(gwutil.NoneInt(), false, true) } } func (c *Canvas) CSISetKeyboardLEDs(mode int) { if mode >= 0 && mode <= 3 { c.RunCallbacks(LEDs{}, LEDSState(mode)) } } func (c *Canvas) CSIStatusReport(mode int) { switch mode { case 5: d2 := "\033[0n" _, err := c.terminal.Write([]byte(d2)) if err != nil { log.Warnf("Could not write all of %d bytes to terminal pty", len(d2)) } case 6: x, y := c.TermCursor() d2 := fmt.Sprintf("\033[%d;%dR", y+1, x+1) _, err := c.terminal.Write([]byte(d2)) if err != nil { log.Warnf("Could not write all of %d bytes to terminal pty", len(d2)) } } } // Report as vt102, like vterm.py func (c *Canvas) CSIGetDeviceAttributes(qmark bool) { if !qmark { d2 := "\033[?6c" _, err := c.terminal.Write([]byte(d2)) if err != nil { log.Warnf("Could not write all of %d bytes to terminal pty", len(d2)) } } } // CSISetScroll sets the scrolling region in the current terminal. top is the line // number of the first line, bottom the bottom line. If both are 0, the whole screen // is used. func (c *Canvas) CSISetScroll(top, bottom int) { if top == 0 { top = 1 } if bottom == 0 { bottom = c.BoxRows() } if top < bottom && bottom <= c.BoxRows() { _, y1 := c.ConstrainCoords(0, top-1, true) c.scrollRegionStart = y1 _, y2 := c.ConstrainCoords(0, bottom-1, true) c.scrollRegionEnd = y2 c.SetTermCursor(gwutil.SomeInt(0), gwutil.SomeInt(0)) } } func (c *Canvas) CSISetModes(args []int, qmark bool, reset bool) { flag := !reset for _, mode := range args { c.SetMode(mode, flag, qmark, reset) } } func (c *Canvas) SetMode(mode int, flag bool, qmark bool, reset bool) { if qmark { switch mode { case 1: c.terminal.Modes().KeysAutoWrap = true case 3: c.Clear(gwutil.NoneInt(), gwutil.NoneInt()) case 5: if c.terminal.Modes().ReverseVideo != flag { c.ReverseVideo(!flag) } c.terminal.Modes().ReverseVideo = flag case 6: c.terminal.Modes().ConstrainScrolling = flag c.SetTermCursor(gwutil.SomeInt(0), gwutil.SomeInt(0)) case 7: c.terminal.Modes().DontAutoWrap = !flag case 25: c.terminal.Modes().InvisibleCursor = !flag c.SetTermCursor(gwutil.NoneInt(), gwutil.NoneInt()) case 1000: c.terminal.Modes().VT200Mouse = flag case 1002: c.terminal.Modes().ReportButton = flag if flag { c.terminal.Modes().VT200Mouse = true } case 1003: c.terminal.Modes().ReportAny = flag if flag { c.terminal.Modes().VT200Mouse = true } case 1006: c.terminal.Modes().SgrModeMouse = flag case 1049: if flag { c.UseAlternateScreen() } else { c.UseOriginalScreen() } } } else { switch mode { case 3: c.terminal.Modes().DisplayCtrl = flag case 4: c.terminal.Modes().Insert = flag case 20: c.terminal.Modes().LfNl = flag } } } // TODO urwid uses undo - implement it func (c *Canvas) ReverseVideo(undo bool) { for i := 0; i < c.BoxRows(); i++ { for j := 0; j < c.BoxColumns(); j++ { cell := c.CellAt(j, i) fg := cell.ForegroundColor() bg := cell.BackgroundColor() c.SetCellAt(j, i, cell.WithBackgroundColor(fg).WithForegroundColor(bg)) } } } func (c *Canvas) InsertChars(startx, starty gwutil.IntOption, chars int, charo gwutil.RuneOption) { if startx.IsNone() || starty.IsNone() { myx, myy := c.TermCursor() startx = gwutil.SomeInt(myx) starty = gwutil.SomeInt(myy) } if chars == 0 { chars = 1 } var cell gowid.Cell if charo.IsNone() { cell = gowid.Cell{} } else { cell = c.MakeCellFrom(charo.Val()) } for chars > 0 { line := c.Line(starty.Val(), gowid.LineCopy{}).Line if startx.Val() >= len(line) { c.SetLineAt(starty.Val(), append(line, cell)) } else { dummy := make([]gowid.Cell, len(c.Line(starty.Val(), gowid.LineCopy{}).Line)) n := 0 n += copy(dummy[n:], line[0:startx.Val()]) n += copy(dummy[n:], []gowid.Cell{cell}) n += copy(dummy[n:], line[startx.Val():]) c.SetLineAt(starty.Val(), dummy) } chars-- } } func (c *Canvas) RemoveChars(startx, starty gwutil.IntOption, chars int) { if startx.IsNone() || starty.IsNone() { myx, myy := c.TermCursor() startx = gwutil.SomeInt(myx) starty = gwutil.SomeInt(myy) } if chars == 0 { chars = 1 } for chars > 0 { line := c.Line(starty.Val(), gowid.LineCopy{}).Line if startx.Val() >= len(line) { line = line[0:startx.Val()] } else { line = append(line[0:startx.Val()], line[startx.Val()+1:]...) } line = append(line, gowid.Cell{}) c.SetLineAt(starty.Val(), line) chars-- } } // InsertLines processes "CSI n L" e.g. "\033[5L". Lines are pushed down // and blank lines inserted. Note that the 5 is only processed if a scroll // region is defined - otherwise one line is inserted. func (c *Canvas) InsertLines(atCursor bool, lines int) { var starty gwutil.IntOption if atCursor { _, myy := c.TermCursor() starty = gwutil.SomeInt(myy) } else { starty = gwutil.SomeInt(c.scrollRegionStart) } if !c.IsScrollRegionSet() { lines = 1 } else if lines == 0 { lines = 1 } region := c.scrollRegionEnd + 1 - starty.Val() if lines < region { for i := 0; i < region-lines; i++ { c.SetLineAt(c.scrollRegionEnd-i, c.Line(c.scrollRegionEnd-(i+lines), gowid.LineCopy{}).Line) } } for i := 0; i < gwutil.Min(lines, region); i++ { line := emptyLine(c.BoxColumns()) c.SetLineAt(starty.Val()+i, line) } } func (c *Canvas) RemoveLines(atCursor bool, lines int) { var starty gwutil.IntOption if atCursor { _, myy := c.TermCursor() starty = gwutil.SomeInt(myy) } else { starty = gwutil.SomeInt(c.scrollRegionStart) } if !c.IsScrollRegionSet() { lines = 1 } else if lines == 0 { lines = 1 } region := c.scrollRegionEnd + 1 - starty.Val() if lines < region { for i := 0; i < region-lines; i++ { c.SetLineAt(starty.Val()+i, c.Line(starty.Val()+i+lines, gowid.LineCopy{}).Line) } } for i := 0; i < gwutil.Min(lines, region); i++ { line := emptyLine(c.BoxColumns()) c.SetLineAt(c.scrollRegionEnd-i, line) } } func (c *Canvas) Erase(startx, starty, endx, endy int) { sx, sy := c.ConstrainCoords(startx, starty, false) ex, ey := c.ConstrainCoords(endx, endy, false) if sy == ey { for i := sx; i < ex+1; i++ { c.SetCellAt(i, sy, gowid.Cell{}) } } else { y := sy for y <= ey { if y == sy { for i := sx; i < c.BoxColumns(); i++ { c.SetCellAt(i, y, gowid.Cell{}) } } else if y == ey { for i := 0; i < ex+1; i++ { c.SetCellAt(i, y, gowid.Cell{}) } } else { for i := 0; i < c.BoxColumns(); i++ { c.SetCellAt(i, y, gowid.Cell{}) } } y++ } } } func (c *Canvas) CSIEraseLine(mode int) { myx, myy := c.TermCursor() switch mode { case 0: c.Erase(myx, myy, c.BoxColumns()-1, myy) case 1: c.Erase(0, myy, myx, myy) case 2: for i := 0; i < c.BoxColumns(); i++ { c.SetCellAt(i, myy, gowid.Cell{}) } } } func (c *Canvas) CSIEraseDisplay(mode int) { myx, myy := c.TermCursor() switch mode { case 0: c.Erase(myx, myy, c.BoxColumns()-1, c.BoxRows()-1) case 1: c.Erase(0, 0, myx, myy) case 2: c.Clear(gwutil.SomeInt(myx), gwutil.SomeInt(myy)) } } func (c *Canvas) CSISetAttr(args []int) { if args[len(args)-1] == 0 { c.fg = gwutil.NoneInt() c.bg = gwutil.NoneInt() c.styles = make(map[string]bool) } c.fg, c.bg, c.styles = c.SGIToAttribs(args, c.fg, c.bg, c.styles) } func (c *Canvas) SGIToAttribs(args []int, fg, bg gwutil.IntOption, styles map[string]bool) (gwutil.IntOption, gwutil.IntOption, map[string]bool) { for i := 0; i < len(args); i++ { attr := args[i] switch { case 30 <= attr && attr <= 37: fg = gwutil.SomeInt(attr + 1 - 30) case 90 <= attr && attr <= 97: fg = gwutil.SomeInt(attr - 90 + 9) // 8 basic colors; 90 => black, 91 => red case 40 <= attr && attr <= 47: bg = gwutil.SomeInt(attr + 1 - 40) case 100 <= attr && attr <= 107: bg = gwutil.SomeInt(attr - 100 + 9) // 8 basic colors -> right index into tcell array case attr == 23: // TODO vim sends this case attr == 38: if i+2 < len(args) && args[i+1] == 5 && args[i+2] >= 0 && args[i+2] <= 255 { fg = gwutil.SomeInt(args[i+2] + 1) i += 2 } else if i+4 < len(args) && args[i+1] == 2 && args[i+2] >= 0 && args[i+2] <= 255 && args[i+3] >= 0 && args[i+3] <= 255 && args[i+4] >= 0 && args[i+4] <= 255 { fg = gwutil.SomeInt(gowid.CubeStart + (((args[i+2] * gowid.CubeSize256) + args[i+3]) * gowid.CubeSize256) + args[i+4] + 1) i += 4 } case attr == 39: delete(styles, "underline") fg = gwutil.NoneInt() case attr == 48: if i+2 < len(args) && args[i+1] == 5 && args[i+2] >= 0 && args[i+2] <= 255 { bg = gwutil.SomeInt(args[i+2] + 1) i += 2 } else if i+4 < len(args) && args[i+1] == 2 && args[i+2] >= 0 && args[i+2] <= 255 && args[i+3] >= 0 && args[i+3] <= 255 && args[i+4] >= 0 && args[i+4] <= 255 { bg = gwutil.SomeInt(gowid.CubeStart + (((args[i+2] * gowid.CubeSize256) + args[i+3]) * gowid.CubeSize256) + args[i+4] + 1) i += 4 } case attr == 49: bg = gwutil.NoneInt() case attr == 10: c.charset.ResetSgrIbmpc() c.terminal.Modes().DisplayCtrl = false case attr == 11 || attr == 12: c.charset.SetSgrIbmpc() c.terminal.Modes().DisplayCtrl = true case attr == 1: styles["bold"] = true case attr == 4: styles["underline"] = true case attr == 7: styles["reverse"] = true case attr == 5: styles["blink"] = true case attr == 22: delete(styles, "bold") case attr == 24: delete(styles, "underline") case attr == 25: delete(styles, "blink") case attr == 27: delete(styles, "reverse") case attr == 0: fg = gwutil.NoneInt() bg = gwutil.NoneInt() styles = make(map[string]bool) case attr == 3: case attr == 6: } } return fg, bg, styles } func (c *Canvas) Resize(width, height int) { x, y := c.TermCursor() if width > c.BoxColumns() { c.ExtendRight(gowid.EmptyLine(width - c.BoxColumns())) } else if width < c.BoxColumns() { c.TrimRight(width) } // Move upwards - so reduce the offset from the top by the amount the new height // is greater than the old height. c.Offset -= height - c.Height c.Height = height if c.Height > c.Canvas.BoxRows() { c.Canvas.AppendBelow(gowid.NewCanvasOfSize(width, c.Height-c.Canvas.BoxRows()), false, false) } else if c.Height < 1 { c.Height = 1 } if c.Offset < 0 { c.Offset = 0 } else if c.Offset > (c.Canvas.BoxRows() - c.Height) { c.Offset = c.Canvas.BoxRows() - c.Height } c.ResetScroll() x, y = c.ConstrainCoords(x, y, false) c.SetTermCursor(gwutil.SomeInt(x), gwutil.SomeInt(y)) c.InitTabstops(true) } func (c *Canvas) PushCursor(r rune) { x, y := c.TermCursor() wid := runewidth.RuneWidth(r) if !c.terminal.Modes().DontAutoWrap { if x+wid == c.BoxColumns() && !c.isRottenCursor { c.isRottenCursor = true c.PushRune(r, x, y) } else { x += wid if x >= c.BoxColumns() { if y >= c.scrollRegionEnd { c.Scroll(false) } else { y += 1 } x = wid c.SetTermCursor(gwutil.SomeInt(0), gwutil.SomeInt(y)) } c.PushRune(r, x, y) c.isRottenCursor = false } } else { if x+wid < c.BoxColumns() { x += wid } c.isRottenCursor = false c.PushRune(r, x, y) } } func (c *Canvas) PushRune(r rune, x, y int) { r2 := c.charset.ApplyMapping(r) if c.terminal.Modes().Insert { c.InsertChars(gwutil.NoneInt(), gwutil.NoneInt(), 1, gwutil.SomeRune(r2)) } else { c.SetRune(r2) } c.SetTermCursor(gwutil.SomeInt(x), gwutil.SomeInt(y)) } func (c *Canvas) SetRune(r rune) { x, y := c.ConstrainCoords(c.tcx, c.tcy, false) c.SetRuneAt(x, y, r) } func (c *Canvas) MakeCellFrom(r rune) gowid.Cell { var cell gowid.Cell = gowid.MakeCell(r, gowid.MakeTCellColorExt(tcell.ColorDefault), gowid.MakeTCellColorExt(tcell.ColorDefault), gowid.StyleNone) if !c.fg.IsNone() { cell = cell.WithForegroundColor(gowid.MakeTCellColorExt(tcell.Color(c.fg.Val() - 1) + tcell.ColorValid)) } if !c.bg.IsNone() { cell = cell.WithBackgroundColor(gowid.MakeTCellColorExt(tcell.Color(c.bg.Val() - 1) + tcell.ColorValid)) } if len(c.styles) > 0 { for k, _ := range c.styles { switch k { case "underline": cell = cell.WithStyle(gowid.StyleUnderline) case "bold": cell = cell.WithStyle(gowid.StyleBold) case "reverse": cell = cell.WithStyle(gowid.StyleReverse) case "blink": cell = cell.WithStyle(gowid.StyleBlink) } } } return cell } func (c *Canvas) SetRuneAt(x, y int, r rune) { c.SetCellAt(x, y, c.MakeCellFrom(r)) } func (c *Canvas) leaveEscapeOnly() { c.withinEscape = false c.escbuf = make([]byte, 0) } func (c *Canvas) LeaveEscapeResetState() { c.leaveEscapeOnly() c.parsestate = defaultState } // TODO am I always guaranteed to have something in escbuf? func (c *Canvas) ParseEscape(r byte) { leaveEscape := true switch { case c.parsestate == csiState: if _, ok := csiMap[r]; ok { c.ParseCSI(r) c.parsestate = defaultState } else if ((r == '-') || (r == '0') || (r == '1') || (r == '2') || (r == '3') || (r == '4') || (r == '5') || (r == '6') || (r == '7') || (r == '8') || (r == '9') || (r == ';')) || (len(c.escbuf) == 0 && r == '?') { c.escbuf = append(c.escbuf, r) leaveEscape = false } case c.parsestate == defaultState && r == ']': c.escbuf = make([]byte, 0) c.parsestate = oscState leaveEscape = false case c.parsestate == oscState && r == '\x07': c.ParseOSC(gwutil.LStripByte(c.escbuf, '0')) case c.parsestate == oscState && len(c.escbuf) > 0 && c.escbuf[len(c.escbuf)-1] == EscByte && r == '\\': c.ParseOSC(gwutil.LStripByte(c.escbuf[0:len(c.escbuf)-1], '0')) case c.parsestate == oscState && len(c.escbuf) > 0 && c.escbuf[0] == 'P' && len(c.escbuf) == 8: // TODO Palette (ESC]Pnrrggbb) case c.parsestate == oscState && len(c.escbuf) == 0 && r == 'R': // TODO Reset Palette case c.parsestate == oscState: c.escbuf = append(c.escbuf, r) leaveEscape = false case c.parsestate == defaultState && r == '[': c.escbuf = make([]byte, 0) c.parsestate = csiState leaveEscape = false case c.parsestate == defaultState && ((r == '%') || (r == '#') || (r == '(') || (r == ')')): c.escbuf = make([]byte, 1) c.escbuf[0] = r c.parsestate = nonCsiState leaveEscape = false case c.parsestate == defaultState && (r == '^' || r == 'P'): c.parsestate = ignoreState leaveEscape = false c.leaveEscapeOnly() case c.parsestate == nonCsiState: c.ParseNonCSI(r, c.escbuf[0]) case ((r == 'c') || (r == 'D') || (r == 'E') || (r == 'H') || (r == 'M') || (r == 'Z') || (r == '7') || (r == '8') || (r == '>') || (r == '=')): c.ParseNonCSI(r, 0) } if leaveEscape { c.LeaveEscapeResetState() } } func (c *Canvas) ParseOSC(osc []byte) { switch { case len(osc) > 0 && osc[0] == ';': c.RunCallbacks(Title{}, string(osc[1:])) case len(osc) > 1 && osc[0] == '3' && osc[1] == ';': c.RunCallbacks(Title{}, string(osc[2:])) } } func (c *Canvas) SetG01(r byte, mod byte) { if c.terminal.Modes().Charset == CharsetDefault { g := 1 if mod == '(' { g = 0 } var cset string switch r { case '0': cset = "vt100" case 'U': cset = "ibmpc" case 'K': cset = "user" default: cset = "default" } c.charset.Define(g, cset) } } func (c *Canvas) ParseNonCSI(r byte, mod byte) { switch { case r == '8' && mod == '#': c.DECAln() case mod == '%': if r == '@' { c.terminal.Modes().Charset = CharsetDefault } else if r == '8' || r == 'G' { c.terminal.Modes().Charset = CharsetUTF8 } case mod == '(' || mod == ')': c.SetG01(r, mod) case r == 'M': c.LineFeed(true) case r == 'D': c.LineFeed(false) case r == 'c': c.Reset() case r == 'E': c.NewLine() case r == 'H': c.SetTabstop(gwutil.NoneInt(), false, false) case r == 'Z': c.CSIGetDeviceAttributes(true) case r == '7': c.SaveCursor(true) case r == '8': c.RestoreCursor(true) } } func (c *Canvas) ParseCSI(r byte) { numbuf := make([]int, 0) qmark := false for i, u := range bytes.Split(c.escbuf, []byte{';'}) { if (i == 0) && (len(u) > 0) && (u[0] == '?') { qmark = true u = u[1:] } num, err := strconv.Atoi(string(u)) if err == nil { numbuf = append(numbuf, num) } } if cmd, ok := csiMap[r]; ok { for cmd.IsAlias() { cmd = csiMap[cmd.Alias()] } for len(numbuf) < cmd.MinArgs() { numbuf = append(numbuf, cmd.FallbackArg()) } for i, _ := range numbuf { if numbuf[i] == 0 { // TODO fishy... numbuf[i] = cmd.FallbackArg() } } cmd.Call(c, numbuf, qmark) } } func (c *Canvas) ProcessByte(b byte) { var r rune if c.terminal.Modes().Charset == CharsetUTF8 { c.utf8Buffer = append(c.utf8Buffer, b) r, _ = utf8.DecodeRune(c.utf8Buffer) if r == utf8.RuneError { return } c.utf8Buffer = c.utf8Buffer[:0] } else { r = rune(b) } c.ProcessByteOrCommand(r) } func (c *Canvas) ProcessByteOrCommand(r rune) { x, y := c.TermCursor() dc := c.terminal.Modes().DisplayCtrl switch { case r == '\x1b' && c.parsestate != oscState: c.withinEscape = true case r == '\\' && c.parsestate == ignoreState && c.withinEscape: c.LeaveEscapeResetState() case c.parsestate == ignoreState: // discard case r == '\x0d' && !dc: c.CarriageReturn() case r == '\x0f' && !dc: c.charset.Activate(0) case r == '\x0e' && !dc: c.charset.Activate(1) case ((r == '\x0a') || (r == '\x0b') || (r == '\x0c')) && !dc: c.LineFeed(false) if c.terminal.Modes().LfNl { c.CarriageReturn() } case r == '\x09' && !dc: c.Tab(8) case r == '\x08' && !dc: if x > 0 { c.SetTermCursor(gwutil.SomeInt(x-1), gwutil.SomeInt(y)) } case r == '\x07' && c.parsestate != oscState && !dc: c.RunCallbacks(Bell{}) case ((r == '\x18') || (r == '\x1a')) && !dc: c.LeaveEscapeResetState() case ((r == '\x00') || (r == '\x7f')) && !dc: // Ignored case c.withinEscape: c.ParseEscape(byte(r)) case r == '\x9b' && !dc: c.withinEscape = true c.escbuf = make([]byte, 0) c.parsestate = csiState default: c.PushCursor(r) } } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: gowid-1.4.0/widgets/terminal/terminal.go000066400000000000000000000507361426234454000202670ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. // Package terminal provides a widget that functions as a unix terminal. Like urwid, it emulates // a vt220 (roughly). Mouse support is provided. See the terminal demo for more. package terminal import ( "fmt" "io" "os" "os/exec" "strings" "sync" "syscall" "time" "unsafe" "github.com/creack/pty" "github.com/gcla/gowid" "github.com/gcla/gowid/gwutil" "github.com/gcla/gowid/widgets/columns" "github.com/gcla/gowid/widgets/holder" "github.com/gcla/gowid/widgets/null" "github.com/gcla/gowid/widgets/vscroll" tcell "github.com/gdamore/tcell/v2" "github.com/gdamore/tcell/v2/terminfo" "github.com/gdamore/tcell/v2/terminfo/dynamic" log "github.com/sirupsen/logrus" ) //====================================================================== // ITerminal is the interface required by terminal.Canvas. For example, when // the pty sends a byte sequence, the canvas needs to pass it on to the terminal // implementation - hence io.Writer. type ITerminal interface { io.Writer Width() int Height() int Modes() *Modes Terminfo() *terminfo.Terminfo } // IWidget encapsulates the requirements of a gowid widget that can represent // and interact with a terminal. type IWidget interface { // All the usual widget requirements gowid.IWidget // Support terminal interfaces needed by terminal.Canvas ITerminal // IHotKeyProvider specifies the keypress that will "unfocus" the widget, that is that will // for a period of time ensure that the widget does not accept keypresses. This allows // the containing gowid application to change focus to another widget e.g. by hitting // the cursor key inside a pile or column widget. IHotKeyProvider // IHotKeyPersistence determines how long a press of the hotkey will be in effect before // keyboard user input is sent back to the underlying terminal. IHotKeyPersistence // IPaste tracks whether the paste start sequence has been seen wthout a matching // paste end sequence IPaste // HotKeyActive returns true if the hotkey is currently in effect. HotKeyActive() bool // SetHotKeyActive sets the state of HotKeyActive. SetHotKeyActive(app gowid.IApp, down bool) // HotKeyDownTime returns the time at which the hotkey was pressed. HotKeyDownTime() time.Time // Scroll the terminal's buffer. Scroll(dir ScrollDir, page bool, lines int) // Reset the the terminal's buffer scroll; display what was current. ResetScroll() // Currently scrolled away from normal view Scrolling() bool } type IHotKeyFunctions interface { // Customized handling of hotkey sequences HotKeyFunctions() []HotKeyInputFn } type IScrollbar interface { ScrollbarEnabled() bool EnableScrollbar(app gowid.IApp) DisableScrollbar(app gowid.IApp) } type IHotKeyPersistence interface { HotKeyDuration() time.Duration } type IHotKeyProvider interface { HotKey() tcell.Key } type IPaste interface { PasteState(...bool) bool } type HotKeyInputFn func(ev *tcell.EventKey, w IWidget, app gowid.IApp) bool type HotKeyDuration struct { D time.Duration } func (t HotKeyDuration) HotKeyDuration() time.Duration { return t.D } type HotKey struct { K tcell.Key } func (t HotKey) HotKey() tcell.Key { return t.K } // For callback registration type Bell struct{} type LEDs struct{} type Title struct{} type ProcessExited struct{} type HotKeyCB struct{} type bell struct{} type leds struct{} type title struct{} type hotkey struct{} type Options struct { Command []string Env []string HotKey IHotKeyProvider HotKeyPersistence IHotKeyPersistence // the period of time a hotKey sticks after the first post-hotKey keypress Scrollback int Scrollbar bool // disabled regardless of setting if there is no scrollback HotKeyFns []HotKeyInputFn // allow custom behavior after pressing the hotkey EnableBracketedPaste bool KeyPressToEndScrollMode bool // set to true to enable legacy behavior - when the user has scrolled // back to the prompt, still require a keypress (q or Q) to end scroll-mode. } // Widget is a widget that hosts a terminal-based application. The user provides the // command to run, an optional environment in which to run it, and an optional hotKey. The hotKey is // used to "escape" from the terminal (if using only the keyboard), and serves a similar role to the // default ctrl-b in tmux. For example, to move focus to a widget to the right, the user could hit // ctrl-b . See examples/gowid-editor for a demo. type Widget struct { IHotKeyProvider IHotKeyPersistence params Options Cmd *exec.Cmd master *os.File canvas *Canvas modes Modes curWidth, curHeight int terminfo *terminfo.Terminfo title string leds LEDSState hotKeyDown bool hotKeyDownTime time.Time hotKeyTimer *time.Timer isScrolling bool paste bool hold *holder.Widget // used if scrollbar is enabled cols *columns.Widget // used if scrollbar is enabled sbar *vscroll.Widget // used if scrollbar is enabled scrollbarTmpOff bool // a simple hack to help with UserInput and Render Callbacks *gowid.Callbacks gowid.IsSelectable } func New(command []string) (*Widget, error) { return NewExt(Options{ Command: command, Env: os.Environ(), }) } func NewExt(opts Options) (*Widget, error) { var err error var ti *terminfo.Terminfo var term string for _, s := range opts.Env { if strings.HasPrefix(s, "TERM=") { term = s[len("TERM="):] break } } useDefault := true if term != "" { ti, err = findTerminfo(term) if err == nil { useDefault = false } } if useDefault { ti, err = findTerminfo("xterm") } if err != nil { return nil, err } if opts.HotKey == nil { opts.HotKey = HotKey{tcell.KeyCtrlB} } if opts.Scrollback <= 0 { opts.Scrollbar = false } var persistence IHotKeyPersistence if opts.HotKeyPersistence != nil { persistence = opts.HotKeyPersistence } else { persistence = &HotKeyDuration{ D: 2 * time.Second, } } // Always allocate so the scrollbar can be turned on later sbar := vscroll.NewExt(vscroll.VerticalScrollbarUnicodeRunes) hold := holder.New(null.New()) cols := columns.New([]gowid.IContainerWidget{ &gowid.ContainerWidget{hold, gowid.RenderWithWeight{W: 1}}, &gowid.ContainerWidget{sbar, gowid.RenderWithUnits{U: 1}}, }) res := &Widget{ params: opts, IHotKeyProvider: opts.HotKey, IHotKeyPersistence: persistence, terminfo: ti, sbar: sbar, cols: cols, hold: hold, Callbacks: gowid.NewCallbacks(), } res.hold.SetSubWidget(res, nil) res.cols.SetFocus(nil, 0) sbar.OnClickAbove(gowid.WidgetCallback{"cb", res.clickUp}) sbar.OnClickBelow(gowid.WidgetCallback{"cb", res.clickDown}) sbar.OnClickUpArrow(gowid.WidgetCallback{"cb", res.clickUpArrow}) sbar.OnClickDownArrow(gowid.WidgetCallback{"cb", res.clickDownArrow}) var _ gowid.IWidget = res var _ ITerminal = res var _ IWidget = res var _ IHotKeyFunctions = res var _ IScrollbar = res var _ io.Writer = res return res, nil } func (w *Widget) String() string { return fmt.Sprintf("terminal") } func (w *Widget) Scrolling() bool { return w.isScrolling } func (w *Widget) Modes() *Modes { return &w.modes } func (w *Widget) Terminfo() *terminfo.Terminfo { return w.terminfo } func (w *Widget) ScrollbarEnabled() bool { return w.params.Scrollbar } func (w *Widget) EnableScrollbar(app gowid.IApp) { w.params.Scrollbar = true } func (w *Widget) DisableScrollbar(app gowid.IApp) { w.params.Scrollbar = false } func (w *Widget) HotKeyFunctions() []HotKeyInputFn { return w.params.HotKeyFns } func (w *Widget) Bell(app gowid.IApp) { gowid.RunWidgetCallbacks(w.Callbacks, Bell{}, app, w) } func (w *Widget) SetLEDs(app gowid.IApp, mode LEDSState) { w.leds = mode gowid.RunWidgetCallbacks(w.Callbacks, LEDs{}, app, w) } func (w *Widget) GetLEDs() LEDSState { return w.leds } func (w *Widget) SetTitle(title string, app gowid.IApp) { w.title = title gowid.RunWidgetCallbacks(w.Callbacks, Title{}, app, w) } func (w *Widget) GetTitle() string { return w.title } func (w *Widget) OnProcessExited(f gowid.IWidgetChangedCallback) { gowid.AddWidgetCallback(w.Callbacks, ProcessExited{}, f) } func (w *Widget) RemoveOnProcessExited(f gowid.IIdentity) { gowid.RemoveWidgetCallback(w.Callbacks, ProcessExited{}, f) } func (w *Widget) OnSetTitle(f gowid.IWidgetChangedCallback) { gowid.AddWidgetCallback(w.Callbacks, Title{}, f) } func (w *Widget) RemoveOnSetTitle(f gowid.IIdentity) { gowid.RemoveWidgetCallback(w.Callbacks, Title{}, f) } func (w *Widget) OnBell(f gowid.IWidgetChangedCallback) { gowid.AddWidgetCallback(w.Callbacks, Bell{}, f) } func (w *Widget) RemoveOnBell(f gowid.IIdentity) { gowid.RemoveWidgetCallback(w.Callbacks, Bell{}, f) } func (w *Widget) OnHotKey(f gowid.IWidgetChangedCallback) { gowid.AddWidgetCallback(w.Callbacks, HotKeyCB{}, f) } func (w *Widget) RemoveOnHotKey(f gowid.IIdentity) { gowid.RemoveWidgetCallback(w.Callbacks, HotKeyCB{}, f) } func (w *Widget) PasteState(b ...bool) bool { if len(b) > 0 { w.paste = b[0] } return w.paste } func (w *Widget) HotKeyActive() bool { return w.hotKeyDown } func (w *Widget) SetHotKeyActive(app gowid.IApp, down bool) { w.hotKeyDown = down if w.hotKeyTimer != nil { w.hotKeyTimer.Stop() } gowid.RunWidgetCallbacks(w.Callbacks, HotKeyCB{}, app, w) if down { w.hotKeyDownTime = time.Now() w.hotKeyTimer = time.AfterFunc(w.HotKeyDuration(), func() { app.Run(gowid.RunFunction(func(app gowid.IApp) { w.SetHotKeyActive(app, false) gowid.RunWidgetCallbacks(w.Callbacks, HotKeyCB{}, app, w) })) }) } } func (w *Widget) HotKeyDownTime() time.Time { return w.hotKeyDownTime } func (w *Widget) Scroll(dir ScrollDir, page bool, lines int) { if page { lines = w.canvas.ScrollBuffer(dir, false, gwutil.NoneInt()) } else { lines = w.canvas.ScrollBuffer(dir, false, gwutil.SomeInt(lines)) } wasScrolling := w.isScrolling if lines != 0 { w.isScrolling = true } else if !w.params.KeyPressToEndScrollMode && dir == ScrollDown { // Disable scroll if we are at the bottom and we tried to scroll down // Thanks @Peter2121 ! w.isScrolling = false } if wasScrolling && !w.isScrolling { w.ResetScroll() } } func (w *Widget) ResetScroll() { w.isScrolling = false w.canvas.ScrollBuffer(false, true, gwutil.NoneInt()) } func (w *Widget) Width() int { return w.curWidth } func (w *Widget) Height() int { return w.curHeight } func (w *Widget) Connected() bool { return w.master != nil } func (w *Widget) Canvas() *Canvas { return w.canvas } func (w *Widget) SetCanvas(app gowid.IApp, c *Canvas) { w.canvas = c if app.GetScreen().CharacterSet() == "UTF-8" { w.canvas.terminal.Modes().Charset = CharsetUTF8 } } func (w *Widget) Write(p []byte) (n int, err error) { n, err = w.master.Write(p) return } func (w *Widget) UserInput(ev interface{}, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) bool { if !w.scrollbarTmpOff && w.params.Scrollbar { w.scrollbarTmpOff = true res := w.cols.UserInput(ev, size, focus, app) w.scrollbarTmpOff = false w.cols.SetFocus(app, 0) return res } return UserInput(w, ev, size, focus, app) } func (w *Widget) Render(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.ICanvas { box, ok := size.(gowid.IRenderBox) if !ok { panic(gowid.WidgetSizeError{Widget: w, Size: size, Required: "gowid.IRenderBox"}) } if !w.scrollbarTmpOff && w.params.Scrollbar { w.scrollbarTmpOff = true c := w.cols.Render(size, focus, app) w.scrollbarTmpOff = false return c } w.TouchTerminal(box.BoxColumns(), box.BoxRows(), app) w.sbar.Top = w.canvas.Offset w.sbar.Middle = w.canvas.scrollRegionEnd w.sbar.Bottom = gwutil.Max(0, w.canvas.ViewPortCanvas.Canvas.BoxRows()-(box.BoxRows()+w.canvas.Offset)) return w.canvas } func (w *Widget) RenderSize(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.IRenderBox { box, ok := size.(gowid.IRenderBox) if !ok { panic(gowid.WidgetSizeError{Widget: w, Size: size, Required: "gowid.IRenderBox"}) } return gowid.RenderBox{C: box.BoxColumns(), R: box.BoxRows()} } type terminalSizeSpec struct { Row uint16 Col uint16 Xpixel uint16 Ypixel uint16 } func (w *Widget) SetTerminalSize(width, height int) error { spec := &terminalSizeSpec{ Row: uint16(height), Col: uint16(width), } _, _, errno := syscall.Syscall(syscall.SYS_IOCTL, w.master.Fd(), syscall.TIOCSWINSZ, uintptr(unsafe.Pointer(spec)), ) var err error if errno != 0 { err = errno } return err } type StartCommandError struct { Command []string Err error } var _ error = StartCommandError{} func (e StartCommandError) Error() string { return fmt.Sprintf("Error running command %v: %v", e.Command, e.Err) } func (e StartCommandError) Cause() error { return e.Err } func (e StartCommandError) Unwrap() error { return e.Err } func (w *Widget) TouchTerminal(width, height int, app gowid.IApp) { setTermSize := false if w.Canvas() == nil { w.SetCanvas(app, NewCanvasOfSize(width, height, w.params.Scrollback, w)) } if !w.Connected() { err := w.StartCommand(app, width, height) // TODO check for errors if err != nil { panic(StartCommandError{Command: w.params.Command, Err: err}) } setTermSize = true } if !(w.Width() == width && w.Height() == height) { if !setTermSize { err := w.SetTerminalSize(width, height) if err != nil { log.WithFields(log.Fields{ "width": width, "height": height, "error": err, }).Warn("Could not set terminal size") } } w.Canvas().Resize(width, height) w.curWidth = width w.curHeight = height } } func (w *Widget) Signal(sig syscall.Signal) error { var err error if w.Cmd != nil { err = w.Cmd.Process.Signal(sig) } return err } func (w *Widget) RequestTerminate() error { return w.Signal(syscall.SIGTERM) } func (w *Widget) StartCommand(app gowid.IApp, width, height int) error { w.Cmd = exec.Command(w.params.Command[0], w.params.Command[1:]...) var err error var tty *os.File w.master, tty, err = PtyStart1(w.Cmd) if err != nil { return err } defer tty.Close() err = w.SetTerminalSize(width, height) if err != nil { log.WithFields(log.Fields{ "width": width, "height": height, "error": err, }).Warn("Could not set terminal size") } err = w.Cmd.Start() if err != nil { w.master.Close() return err } master := w.master canvas := w.canvas canvas.AddCallback(Title{}, gowid.Callback{title{}, func(args ...interface{}) { title := args[0].(string) app.Run(gowid.RunFunction(func(app gowid.IApp) { w.SetTitle(title, app) })) }}) canvas.AddCallback(Bell{}, gowid.Callback{bell{}, func(args ...interface{}) { app.Run(gowid.RunFunction(func(app gowid.IApp) { w.Bell(app) })) }}) canvas.AddCallback(LEDs{}, gowid.Callback{leds{}, func(args ...interface{}) { mode := args[0].(LEDSState) app.Run(gowid.RunFunction(func(app gowid.IApp) { w.SetLEDs(app, mode) })) }}) if w.params.EnableBracketedPaste { app.Run(gowid.RunFunction(func(app gowid.IApp) { for _, b := range enablePaste(w.terminfo) { canvas.ProcessByte(b) } })) } go func() { for { data := make([]byte, 4096) n, err := master.Read(data) if n == 0 && err == io.EOF { w.Cmd.Wait() app.Run(gowid.RunFunction(func(app gowid.IApp) { gowid.RunWidgetCallbacks(w.Callbacks, ProcessExited{}, app, w) })) break } else if err != nil { w.Cmd.Wait() app.Run(gowid.RunFunction(func(app gowid.IApp) { gowid.RunWidgetCallbacks(w.Callbacks, ProcessExited{}, app, w) })) break } app.Run(gowid.RunFunction(func(app gowid.IApp) { for _, b := range data[0:n] { canvas.ProcessByte(b) } })) } }() return nil } func (w *Widget) StopCommand() { if w.master != nil { w.master.Close() } } func (w *Widget) clickUp(app gowid.IApp, w2 gowid.IWidget) { w.Scroll(ScrollUp, true, 1) } func (w *Widget) clickDown(app gowid.IApp, w2 gowid.IWidget) { w.Scroll(ScrollDown, true, 1) } func (w *Widget) clickUpArrow(app gowid.IApp, w2 gowid.IWidget) { w.Scroll(ScrollUp, false, 1) } func (w *Widget) clickDownArrow(app gowid.IApp, w2 gowid.IWidget) { w.Scroll(ScrollDown, false, 1) } //'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' func UserInput(w IWidget, ev interface{}, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) bool { // Set true if this function has claimed the input res := false // True if input should be sent to tty passToTerminal := true if evk, ok := ev.(*tcell.EventKey); ok { if w.Scrolling() { // If we're currently scrolling, then this user input should // never be sent to the terminal. It's for controlling or exiting // scrolling. passToTerminal = false res = true switch evk.Key() { case tcell.KeyPgUp: w.Scroll(ScrollUp, true, 0) case tcell.KeyPgDn: w.Scroll(ScrollDown, true, 0) case tcell.KeyUp: w.Scroll(ScrollUp, false, 1) case tcell.KeyDown: w.Scroll(ScrollDown, false, 1) case tcell.KeyRune: switch evk.Rune() { case 'q', 'Q': w.ResetScroll() } default: res = false } } else if w.HotKeyActive() { // If we're not scrolling but the hotkey is still active (recently // pressed) then the input will not go to the terminal - it's hotkey // function processing. passToTerminal = false res = false deactivate := false if whk, ok := w.(IHotKeyFunctions); ok { for _, fn := range whk.HotKeyFunctions() { res = fn(evk, w, app) if res { deactivate = true break } } } if !res { res = true switch evk.Key() { case w.HotKey(): deactivate = true case tcell.KeyPgUp: w.Scroll(ScrollUp, true, 0) deactivate = true case tcell.KeyPgDn: w.Scroll(ScrollDown, true, 0) deactivate = true case tcell.KeyUp: w.Scroll(ScrollUp, false, 1) deactivate = true case tcell.KeyDown: w.Scroll(ScrollDown, false, 1) deactivate = true default: res = false } } if deactivate { w.SetHotKeyActive(app, false) } } else if evk.Key() == w.HotKey() { passToTerminal = false w.SetHotKeyActive(app, true) res = true // handled } } // If nothing has claimed the user input yet, then if the input is // mouse input, disqualify it if it's out of bounds of the terminal. if !res { if ev2, ok := ev.(*tcell.EventMouse); ok { mx, my := ev2.Position() if !((mx < w.Width()) && (my < w.Height())) { passToTerminal = false } } } if passToTerminal { seq, parsed := TCellEventToBytes(ev, w.Modes(), app.GetLastMouseState(), w, w.Terminfo()) if parsed { _, err := w.Write(seq) if err != nil { log.WithField("error", err).Warn("Could not send all input to terminal") } res = true } } return res } // PtyStart1 connects the supplied Cmd's stdin/stdout/stderr to a new tty // object. The function returns the pty and tty, and also an error which is // nil if the operation was successful. func PtyStart1(c *exec.Cmd) (pty2, tty *os.File, err error) { pty2, tty, err = pty.Open() if err != nil { return nil, nil, err } c.Stdout = tty c.Stdin = tty c.Stderr = tty if c.SysProcAttr == nil { c.SysProcAttr = &syscall.SysProcAttr{} } c.SysProcAttr.Setctty = true c.SysProcAttr.Setsid = true return pty2, tty, err } //====================================================================== var cachedTerminfo map[string]*terminfo.Terminfo var cachedTerminfoMutex sync.Mutex func init() { cachedTerminfo = make(map[string]*terminfo.Terminfo) } // findTerminfo returns a terminfo struct via tcell's dynamic method first, // then using the built-in databases. The aim is to use the terminfo database // most likely to be correct. Maybe even better would be parsing the terminfo // file directly using something like https://github.com/beevik/terminfo/, to // avoid the extra process. func findTerminfo(name string) (*terminfo.Terminfo, error) { cachedTerminfoMutex.Lock() if ti, ok := cachedTerminfo[name]; ok { cachedTerminfoMutex.Unlock() return ti, nil } ti, _, e := dynamic.LoadTerminfo(name) if e == nil { cachedTerminfo[name] = ti cachedTerminfoMutex.Unlock() return ti, nil } ti, e = terminfo.LookupTerminfo(name) return ti, e } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: gowid-1.4.0/widgets/terminal/terminal_test.go000066400000000000000000001213541426234454000213210ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. package terminal import ( "errors" "io" "strings" "testing" "github.com/gcla/gowid" "github.com/gcla/gowid/gwutil" tcell "github.com/gdamore/tcell/v2" "github.com/gdamore/tcell/v2/terminfo" "github.com/stretchr/testify/assert" ) //====================================================================== type FakeTerminal struct { modes *Modes } func (f *FakeTerminal) Modes() *Modes { return f.modes } func (f *FakeTerminal) Terminfo() *terminfo.Terminfo { panic(errors.New("Must not call!")) } func (f *FakeTerminal) Width() int { panic(errors.New("Must not call!")) } func (f *FakeTerminal) Height() int { panic(errors.New("Must not call!")) } func (f *FakeTerminal) Connected() bool { panic(errors.New("Must not call!")) } func (f *FakeTerminal) Write([]byte) (int, error) { panic(errors.New("Must not call!")) } func (f *FakeTerminal) Bell(gowid.IApp) { panic(errors.New("Must not call!")) } func (f *FakeTerminal) SetTitle(string, gowid.IApp) { panic(errors.New("Must not call!")) } func (f *FakeTerminal) GetTitle() string { panic(errors.New("Must not call!")) } func (f *FakeTerminal) SetLEDs(app gowid.IApp, mode LEDSState) { panic(errors.New("Must not call!")) } func (f *FakeTerminal) GetLEDs() LEDSState { panic(errors.New("Must not call!")) } func TestCanvas30(t *testing.T) { f := FakeTerminal{modes: &Modes{}} c := NewCanvasOfSize(10, 1, 100, &f) _, err := io.Copy(c, strings.NewReader("hello")) assert.NoError(t, err) res := strings.Join([]string{"hello "}, "\n") assert.Equal(t, res, c.String()) } func TestCanvas31(t *testing.T) { f := FakeTerminal{modes: &Modes{}} c := NewCanvasOfSize(4, 2, 100, &f) _, err := io.Copy(c, strings.NewReader("\033#8")) assert.NoError(t, err) res := strings.Join([]string{"EEEE", "EEEE"}, "\n") assert.Equal(t, res, c.String(), "Failed") _, err = io.Copy(c, strings.NewReader("\033[2J")) assert.NoError(t, err) res = strings.Join([]string{" ", " "}, "\n") assert.Equal(t, res, c.String(), "Failed") _, err = io.Copy(c, strings.NewReader("123")) assert.NoError(t, err) res = strings.Join([]string{"123 ", " "}, "\n") assert.Equal(t, res, c.String(), "Failed") _, err = io.Copy(c, strings.NewReader("\033[2Jab")) assert.NoError(t, err) res = strings.Join([]string{" a", "b "}, "\n") assert.Equal(t, res, c.String(), "Failed") _, err = io.Copy(c, strings.NewReader("\033[1;1Hxy")) assert.NoError(t, err) res = strings.Join([]string{"xy a", "b "}, "\n") assert.Equal(t, res, c.String(), "Failed") // going beyond bounds _, err = io.Copy(c, strings.NewReader("\033[10;10Hk")) assert.NoError(t, err) res = strings.Join([]string{"xy a", "b k"}, "\n") assert.Equal(t, res, c.String(), "Failed") _, err = io.Copy(c, strings.NewReader("\033[?3lv")) assert.NoError(t, err) res = strings.Join([]string{"v ", " "}, "\n") assert.Equal(t, res, c.String(), "Failed") // DecALN - fill canvas with E - "\033#8" // Move cursor to (0,0) - "\033[1;1H" // Erase line - _, err = io.Copy(c, strings.NewReader("\033#8\033[1;1H\033[2K")) assert.NoError(t, err) res = strings.Join([]string{" ", "EEEE"}, "\n") assert.Equal(t, res, c.String(), "Failed") _, err = io.Copy(c, strings.NewReader("\033#8\033[1;1H\033Da")) assert.NoError(t, err) res = strings.Join([]string{"EEEE", "aEEE"}, "\n") assert.Equal(t, res, c.String(), "Failed") // cursor at 2,2 (terminal coords) _, err = io.Copy(c, strings.NewReader("\033[2K")) assert.NoError(t, err) res = strings.Join([]string{"EEEE", " "}, "\n") assert.Equal(t, res, c.String(), "Failed") } func SetupCanvas1(c *Canvas, t *testing.T) { _, err := io.Copy(c, strings.NewReader("\033#8")) assert.NoError(t, err) res := strings.Join([]string{"EEEE", "EEEE", "EEEE", "EEEE"}, "\n") assert.Equal(t, res, c.String(), "Failed") // Set coords=1,1 _, err = io.Copy(c, strings.NewReader("\033[1;1H")) assert.NoError(t, err) res = strings.Join([]string{"EEEE", "EEEE", "EEEE", "EEEE"}, "\n") assert.Equal(t, res, c.String(), "Failed") x, y := c.TermCursor() assert.Equal(t, x, 0, "Failed") assert.Equal(t, y, 0, "Failed") _, err = io.Copy(c, strings.NewReader("\033[1;1Ha\033[2;1Hb\033[3;1Hc\033[4;1Hd")) assert.NoError(t, err) res = strings.Join([]string{"aEEE", "bEEE", "cEEE", "dEEE"}, "\n") assert.Equal(t, res, c.String(), "Failed") } func DoScroll(c *Canvas, t *testing.T) { // Set scroll region _, err := io.Copy(c, strings.NewReader("\033[1;3r")) assert.NoError(t, err) // Constrain scrolling, coords=1,1 _, err = io.Copy(c, strings.NewReader("\033[?6h")) assert.NoError(t, err) } func TestCanvas32(t *testing.T) { f := FakeTerminal{modes: &Modes{}} c := NewCanvasOfSize(4, 4, 100, &f) SetupCanvas1(c, t) // "aEEE", "bEEE", "cEEE", "dEEE" DoScroll(c, t) // set scroll region to [0,2] (both inclusive) res := strings.Join([]string{"aEEE", "bEEE", "cEEE", "dEEE"}, "\n") assert.Equal(t, res, c.String(), "Failed") // Move cursor to (0, 1) - "\033[2;1H" // Insert one line - "\033[1L" _, err := io.Copy(c, strings.NewReader("\033[2;1H\033[1L")) assert.NoError(t, err) res = strings.Join([]string{"aEEE", " ", "bEEE", "dEEE"}, "\n") assert.Equal(t, res, c.String(), "Failed") // Erase line _, err = io.Copy(c, strings.NewReader("\033[1M")) assert.NoError(t, err) res = strings.Join([]string{"aEEE", "bEEE", " ", "dEEE"}, "\n") assert.Equal(t, res, c.String(), "Failed") } func TestCanvas33(t *testing.T) { f := FakeTerminal{modes: &Modes{}} c := NewCanvasOfSize(4, 4, 100, &f) SetupCanvas1(c, t) DoScroll(c, t) // Insert line _, err := io.Copy(c, strings.NewReader("\033[2;1H\033[2L")) assert.NoError(t, err) res := strings.Join([]string{"aEEE", " ", " ", "dEEE"}, "\n") assert.Equal(t, res, c.String(), "Failed") // Erase line _, err = io.Copy(c, strings.NewReader("\033[2M")) assert.NoError(t, err) res = strings.Join([]string{"aEEE", " ", " ", "dEEE"}, "\n") assert.Equal(t, res, c.String(), "Failed") } func TestCanvas34(t *testing.T) { f := FakeTerminal{modes: &Modes{}} c := NewCanvasOfSize(4, 4, 100, &f) SetupCanvas1(c, t) res := strings.Join([]string{"aEEE", "bEEE", "cEEE", "dEEE"}, "\n") assert.Equal(t, res, c.String(), "Failed") // Set cursor to y=1 x=0 - "\033[2;1H" // Insert 1 line!! Scroll region not set - "\033[2L" // http://www.inwap.com/pdp10/ansicode.txt //runtime.Breakpoint() _, err := io.Copy(c, strings.NewReader("\033[2;1H\033[2L")) assert.NoError(t, err) res = strings.Join([]string{"aEEE", " ", "bEEE", "cEEE"}, "\n") assert.Equal(t, res, c.String(), "Failed") // Erase 1 line!! Scroll region not set - "\033[2M" // http://www.inwap.com/pdp10/ansicode.txt _, err = io.Copy(c, strings.NewReader("\033[2M")) assert.NoError(t, err) res = strings.Join([]string{"aEEE", "bEEE", "cEEE", " "}, "\n") assert.Equal(t, res, c.String(), "Failed") } func TestCanvas35(t *testing.T) { f := FakeTerminal{modes: &Modes{}} c := NewCanvasOfSize(4, 2, 100, &f) _, err := io.Copy(c, strings.NewReader("\033[1;1HAAAA\033[1;2HB")) assert.NoError(t, err) res := strings.Join([]string{"ABAA", " "}, "\n") assert.Equal(t, res, c.String(), "Failed") _, err = io.Copy(c, strings.NewReader("\033[1D")) assert.NoError(t, err) x, y := c.TermCursor() assert.Equal(t, x, 1, "Failed") assert.Equal(t, y, 0, "Failed") // set terminal insert mode - "\033[4h" // insert "**" // set terminal overwrite mode - "\033[4l" _, err = io.Copy(c, strings.NewReader("\033[4h**\033[4l")) assert.NoError(t, err) res = strings.Join([]string{"A**B", " "}, "\n") assert.Equal(t, res, c.String(), "Failed") } func TestCanvas36(t *testing.T) { f := FakeTerminal{modes: &Modes{}} c := NewCanvasOfSize(4, 2, 100, &f) _, err := io.Copy(c, strings.NewReader("\033[1;1HA B ")) assert.NoError(t, err) res := strings.Join([]string{"A B ", " "}, "\n") assert.Equal(t, res, c.String(), "Failed") _, err = io.Copy(c, strings.NewReader("\033[2;1HB\x08\033[2@A\x08")) assert.NoError(t, err) res = strings.Join([]string{"A B ", "A B "}, "\n") assert.Equal(t, res, c.String(), "Failed") } func TestCanvas37(t *testing.T) { f := FakeTerminal{modes: &Modes{}} c := NewCanvasOfSize(4, 6, 100, &f) // Set scroll region to top=2, bottom=6 - "\033[2;6r" // Constrain scrolling to this region - "\033[?6h" // Move cursor to row=4, col=0 - "\033[5;1H" _, err := io.Copy(c, strings.NewReader("\033[2;6r\033[?6h\033[5;1H")) assert.NoError(t, err) res := strings.Join([]string{" ", " ", " ", " ", " ", " "}, "\n") assert.Equal(t, res, c.String(), "Failed") _, err = io.Copy(c, strings.NewReader("A")) assert.NoError(t, err) res = strings.Join([]string{" ", " ", " ", " ", " ", "A "}, "\n") assert.Equal(t, res, c.String(), "Failed") // Move cursor to row=4, col=3 - "\033[5;4H" // Insert a - "a" _, err = io.Copy(c, strings.NewReader("\033[5;4Ha")) assert.NoError(t, err) res = strings.Join([]string{" ", " ", " ", " ", " ", "A a"}, "\n") assert.Equal(t, res, c.String(), "Failed") // Insert a CR/LF //runtime.Breakpoint() _, err = io.Copy(c, strings.NewReader("\x0d\x0a")) assert.NoError(t, err) res = strings.Join([]string{" ", " ", " ", " ", "A a", " "}, "\n") assert.Equal(t, res, c.String(), "Failed") _, err = io.Copy(c, strings.NewReader("a")) assert.NoError(t, err) res = strings.Join([]string{" ", " ", " ", " ", "A a", " "}, "\n") assert.Equal(t, res, c.String(), "Failed") _, err = io.Copy(c, strings.NewReader("B")) assert.NoError(t, err) res = strings.Join([]string{" ", " ", " ", " ", "A a", "B "}, "\n") assert.Equal(t, res, c.String(), "Failed") _, err = io.Copy(c, strings.NewReader("\033[5;4HB")) assert.NoError(t, err) res = strings.Join([]string{" ", " ", " ", " ", "A a", "B B"}, "\n") assert.Equal(t, res, c.String(), "Failed") _, err = io.Copy(c, strings.NewReader("\x08 b")) assert.NoError(t, err) res = strings.Join([]string{" ", " ", " ", " ", "A a", "B b"}, "\n") assert.Equal(t, res, c.String(), "Failed") _, err = io.Copy(c, strings.NewReader("\x0d\x0a")) assert.NoError(t, err) res = strings.Join([]string{" ", " ", " ", "A a", "B b", " "}, "\n") assert.Equal(t, res, c.String(), "Failed") } func AssertTermPositionIs(x2, y2 int, c *Canvas, t *testing.T) { x, y := c.TermCursor() assert.Equal(t, x2, x, "Failed") assert.Equal(t, y2, y, "Failed") } func TestCanvas38(t *testing.T) { f := FakeTerminal{modes: &Modes{}} c := NewCanvasOfSize(4, 2, 100, &f) AssertTermPositionIs(0, 0, c, t) _, err := io.Copy(c, strings.NewReader("\033[A")) assert.NoError(t, err) AssertTermPositionIs(0, 0, c, t) _, err = io.Copy(c, strings.NewReader("\033[B")) assert.NoError(t, err) AssertTermPositionIs(0, 1, c, t) _, err = io.Copy(c, strings.NewReader("\033[B")) assert.NoError(t, err) AssertTermPositionIs(0, 1, c, t) _, err = io.Copy(c, strings.NewReader("\033[C")) assert.NoError(t, err) AssertTermPositionIs(1, 1, c, t) _, err = io.Copy(c, strings.NewReader("\033[C\033[C\033[C")) assert.NoError(t, err) AssertTermPositionIs(3, 1, c, t) _, err = io.Copy(c, strings.NewReader("\033[D")) assert.NoError(t, err) AssertTermPositionIs(2, 1, c, t) _, err = io.Copy(c, strings.NewReader("\033[D\033[D\033[D\033[D\033[D")) assert.NoError(t, err) AssertTermPositionIs(0, 1, c, t) c.SetTermCursor(gwutil.SomeInt(0), gwutil.SomeInt(0)) AssertTermPositionIs(0, 0, c, t) _, err = io.Copy(c, strings.NewReader("\033#8")) assert.NoError(t, err) res := strings.Join([]string{"EEEE", "EEEE"}, "\n") assert.Equal(t, res, c.String(), "Failed") c.SetTermCursor(gwutil.SomeInt(2), gwutil.SomeInt(0)) _, err = io.Copy(c, strings.NewReader("\033[J")) assert.NoError(t, err) res = strings.Join([]string{"EE ", " "}, "\n") assert.Equal(t, res, c.String(), "Failed") c.SetTermCursor(gwutil.SomeInt(3), gwutil.SomeInt(0)) _, err = io.Copy(c, strings.NewReader("X")) assert.NoError(t, err) res = strings.Join([]string{"EE X", " "}, "\n") assert.Equal(t, res, c.String(), "Failed") c.SetTermCursor(gwutil.SomeInt(1), gwutil.SomeInt(0)) _, err = io.Copy(c, strings.NewReader("\033[K")) assert.NoError(t, err) res = strings.Join([]string{"E ", " "}, "\n") assert.Equal(t, res, c.String(), "Failed") c.Clear(gwutil.SomeInt(0), gwutil.SomeInt(0)) _, err = io.Copy(c, strings.NewReader("\033[7mx")) assert.NoError(t, err) res = strings.Join([]string{"x ", " "}, "\n") assert.Equal(t, res, c.String(), "Failed") assert.Equal(t, tcell.AttrReverse, c.CellAt(0, 0).Style().OnOff, "Failed") } func TestCanvas39(t *testing.T) { f := FakeTerminal{modes: &Modes{}} c := NewCanvasOfSize(4, 2, 100, &f) c.SetTermCursor(gwutil.SomeInt(0), gwutil.SomeInt(0)) // save reverse _, err := io.Copy(c, strings.NewReader("\033[7mx\0337\033[0my")) assert.NoError(t, err) res := strings.Join([]string{"xy ", " "}, "\n") assert.Equal(t, res, c.String(), "Failed") assert.Equal(t, c.CellAt(0, 0).Style().OnOff, tcell.AttrReverse, "Failed") assert.Equal(t, c.CellAt(0, 1).Style().OnOff, tcell.AttrNone, "Failed") AssertTermPositionIs(2, 0, c, t) _, err = io.Copy(c, strings.NewReader("\033[D\033[D")) assert.NoError(t, err) AssertTermPositionIs(0, 0, c, t) //io.Copy(c, strings.NewReader("\0337")) //AssertTermPositionIs(0, 0, c, t) _, err = io.Copy(c, strings.NewReader("z")) assert.NoError(t, err) AssertTermPositionIs(1, 0, c, t) res = strings.Join([]string{"zy ", " "}, "\n") assert.Equal(t, res, c.String(), "Failed") assert.Equal(t, c.Lines[0][0].Style().OnOff, tcell.AttrNone, "Failed") _, err = io.Copy(c, strings.NewReader("\0338")) assert.NoError(t, err) AssertTermPositionIs(1, 0, c, t) _, err = io.Copy(c, strings.NewReader("\033[D")) assert.NoError(t, err) _, err = io.Copy(c, strings.NewReader("k")) assert.NoError(t, err) AssertTermPositionIs(1, 0, c, t) strings.Join([]string{"ky ", " "}, "\n") assert.Equal(t, c.CellAt(0, 0).Style().OnOff, tcell.AttrReverse, "Failed") } func TestCanvas40(t *testing.T) { f := FakeTerminal{modes: &Modes{}} c := NewCanvasOfSize(3, 5, 100, &f) c.SetTermCursor(gwutil.SomeInt(0), gwutil.SomeInt(0)) res := strings.Join([]string{" ", " ", " ", " ", " "}, "\n") assert.Equal(t, res, c.String(), "Failed") AssertTermPositionIs(0, 0, c, t) _, err := io.Copy(c, strings.NewReader("a\x0d\x0ab\x0d\x0ac\x0d\x0ad\x0d\x0ae")) assert.NoError(t, err) res = strings.Join([]string{"a ", "b ", "c ", "d ", "e "}, "\n") assert.Equal(t, res, c.String(), "Failed") AssertTermPositionIs(1, 4, c, t) // scroll region _, err = io.Copy(c, strings.NewReader("\033[2;4r")) assert.NoError(t, err) res = strings.Join([]string{"a ", "b ", "c ", "d ", "e "}, "\n") assert.Equal(t, res, c.String(), "Failed") AssertTermPositionIs(0, 0, c, t) _, err = io.Copy(c, strings.NewReader("\033[4;1H")) assert.NoError(t, err) res = strings.Join([]string{"a ", "b ", "c ", "d ", "e "}, "\n") assert.Equal(t, res, c.String(), "Failed") AssertTermPositionIs(0, 3, c, t) _, err = io.Copy(c, strings.NewReader("\x0a")) assert.NoError(t, err) res = strings.Join([]string{"a ", "c ", "d ", " ", "e "}, "\n") assert.Equal(t, res, c.String(), "Failed") AssertTermPositionIs(0, 3, c, t) _, err = io.Copy(c, strings.NewReader("\033[2;1H")) assert.NoError(t, err) res = strings.Join([]string{"a ", "c ", "d ", " ", "e "}, "\n") assert.Equal(t, res, c.String(), "Failed") AssertTermPositionIs(0, 1, c, t) // _, err = io.Copy(c, strings.NewReader("\033M")) assert.NoError(t, err) res = strings.Join([]string{"a ", " ", "c ", "d ", "e "}, "\n") assert.Equal(t, res, c.String(), "Failed") AssertTermPositionIs(0, 1, c, t) } func TestEncoded1(t *testing.T) { f := FakeTerminal{modes: &Modes{}} c := NewCanvasOfSize(8, 2, 100, &f) f.Modes().Charset = CharsetUTF8 c.SetTermCursor(gwutil.SomeInt(0), gwutil.SomeInt(0)) res := strings.Join([]string{" ", " "}, "\n") assert.Equal(t, res, c.String(), "Failed") AssertTermPositionIs(0, 0, c, t) _, err := io.Copy(c, strings.NewReader("\033[1;0Habc你xyz")) assert.NoError(t, err) res = strings.Join([]string{"abc你xyz", " "}, "\n") assert.Equal(t, res, c.String(), "Failed") AssertTermPositionIs(7, 0, c, t) c.CarriageReturn() AssertTermPositionIs(0, 0, c, t) c.LineFeed(false) AssertTermPositionIs(0, 1, c, t) c.PushCursor('p') c.PushCursor('q') res = strings.Join([]string{"abc你xyz", "pq "}, "\n") assert.Equal(t, res, c.String(), "Failed") AssertTermPositionIs(2, 1, c, t) } func TestEncoded2(t *testing.T) { f := FakeTerminal{modes: &Modes{}} c := NewCanvasOfSize(3, 2, 100, &f) f.Modes().Charset = CharsetUTF8 c.SetTermCursor(gwutil.SomeInt(0), gwutil.SomeInt(0)) res := strings.Join([]string{" ", " "}, "\n") assert.Equal(t, res, c.String(), "Failed") AssertTermPositionIs(0, 0, c, t) _, err := io.Copy(c, strings.NewReader("\033[1;0Hab你x")) assert.NoError(t, err) res = strings.Join([]string{"ab ", "你x"}, "\n") assert.Equal(t, res, c.String(), "Failed") AssertTermPositionIs(2, 1, c, t) } func TestPrivacy1(t *testing.T) { f := FakeTerminal{modes: &Modes{}} c := NewCanvasOfSize(8, 2, 100, &f) f.Modes().Charset = CharsetUTF8 c.SetTermCursor(gwutil.SomeInt(0), gwutil.SomeInt(0)) res := strings.Join([]string{" ", " "}, "\n") assert.Equal(t, res, c.String(), "Failed") AssertTermPositionIs(0, 0, c, t) _, err := io.Copy(c, strings.NewReader("ab\033^foobar\033\\c")) assert.NoError(t, err) res = strings.Join([]string{"abc ", " "}, "\n") assert.Equal(t, res, c.String(), "Failed") AssertTermPositionIs(3, 0, c, t) } func TestCanvasVttest1(t *testing.T) { f := FakeTerminal{modes: &Modes{}} c := NewCanvasOfSize(80, 24, 100, &f) c.SetTermCursor(gwutil.SomeInt(0), gwutil.SomeInt(0)) res := strings.Join([]string{ " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", }, "\n") assert.Equal(t, res, c.String(), "Failed") AssertTermPositionIs(0, 0, c, t) _, err := io.Copy(c, strings.NewReader("\033[2J\033[?3l\033[2J\033[1;1H\033[1;1HAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\033[2;1HBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB\033[3;1HCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC\033[4;1HDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD\033[5;1HEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE\033[6;1HFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF\033[7;1HGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG\033[8;1HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH\033[9;1HIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII\033[10;1HJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJ\033[11;1HKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKK\033[12;1HLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLL\033[13;1HMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\033[14;1HNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNN\033[15;1HOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO\033[16;1HPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPP\033[17;1HQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQ\033[18;1HRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR\033[19;1HSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSS\033[20;1HTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTT\033[21;1HUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU\033[22;1HVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV\033[23;1HWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW\033[24;1HXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX\033[4;1HScreen accordion test (Insert & Delete Line). Push ")) assert.NoError(t, err) res = strings.Join([]string{ "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB", "CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC", "Screen accordion test (Insert & Delete Line). Push DDDDDDDDDDDDDDDDDDDDD", "EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE", "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF", "GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG", "HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH", "IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII", "JJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJ", "KKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKK", "LLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLL", "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM", "NNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNN", "OOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO", "PPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPP", "QQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQ", "RRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR", "SSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSS", "TTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTT", "UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU", "VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV", "WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW", "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", }, "\n") assert.Equal(t, res, c.String(), "Failed") AssertTermPositionIs(59, 3, c, t) _, err = io.Copy(c, strings.NewReader("\x0a\033M\033[2K")) assert.NoError(t, err) res = strings.Join([]string{ "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB", "CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC", " ", "EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE", "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF", "GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG", "HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH", "IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII", "JJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJ", "KKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKK", "LLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLL", "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM", "NNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNN", "OOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO", "PPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPP", "QQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQ", "RRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR", "SSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSS", "TTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTT", "UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU", "VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV", "WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW", "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", }, "\n") assert.Equal(t, res, c.String(), "Failed") _, err = io.Copy(c, strings.NewReader("\033[2;23r\033[?6h\033[1;1H\033[1L\033[1M")) assert.NoError(t, err) res = strings.Join([]string{ "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB", "CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC", " ", "EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE", "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF", "GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG", "HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH", "IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII", "JJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJ", "KKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKKK", "LLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLL", "MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM", "NNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNN", "OOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO", "PPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPP", "QQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQ", "RRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR", "SSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSS", "TTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTT", "UUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUUU", "VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV", " ", "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", }, "\n") assert.Equal(t, res, c.String(), "Failed") AssertTermPositionIs(0, 1, c, t) _, err = io.Copy(c, strings.NewReader("\033[2L\033[2M\033[3L\033[3M\033[4L\033[4M\033[5L\033[5M\033[6L\033[6M\033[7L\033[7M\033[8L\033[8M\033[9L\033[9M\033[10L\033[10M\033[11L\033[11M\033[12L\033[12M\033[13L\033[13M\033[14L\033[14M\033[15L\033[15M\033[16L\033[16M\033[17L\033[17M\033[18L\033[18M\033[19L\033[19M\033[20L\033[20M\033[21L\033[21M\033[22L\033[22M\033[23L\033[23M\033[24L\033[24M")) assert.NoError(t, err) res = strings.Join([]string{ "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", }, "\n") assert.Equal(t, res, c.String(), "Failed") AssertTermPositionIs(0, 1, c, t) _, err = io.Copy(c, strings.NewReader("\033[?6l\033[r\033[2;1HTop line: A's, bottom line: X's, this line, nothing more. Push ")) assert.NoError(t, err) res = strings.Join([]string{ "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", "Top line: A's, bottom line: X's, this line, nothing more. Push ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", }, "\n") assert.Equal(t, res, c.String(), "Failed") _, err = io.Copy(c, strings.NewReader("\033[2;1H\033[0J\033[1;2HB\033[1D")) //_, err := io.Copy(c, strings.NewReader("\033[2;1H\033[0J\033[1;2HB\033[1D\033[4h******************************************************************************\033[4l\033[4;1HTest of 'Insert Mode'. The top line should be 'A*** ... ***B'. Push ") assert.NoError(t, err) res = strings.Join([]string{ "ABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", }, "\n") assert.Equal(t, res, c.String(), "Failed") AssertTermPositionIs(1, 0, c, t) _, err = io.Copy(c, strings.NewReader("\033[4h******************************************************************************")) assert.NoError(t, err) res = strings.Join([]string{ "A******************************************************************************B", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", }, "\n") assert.Equal(t, res, c.String(), "Failed") AssertTermPositionIs(79, 0, c, t) _, err = io.Copy(c, strings.NewReader("\033[4l\033[4;1HTest of 'Insert Mode'. The top line should be 'A*** ... ***B'. Push ")) assert.NoError(t, err) res = strings.Join([]string{ "A******************************************************************************B", " ", " ", "Test of 'Insert Mode'. The top line should be 'A*** ... ***B'. Push ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", " ", }, "\n") assert.Equal(t, res, c.String(), "Failed") AssertTermPositionIs(76, 3, c, t) } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: gowid-1.4.0/widgets/terminal/utils.go000066400000000000000000000227121426234454000176050ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source code is governed by the MIT license // that can be found in the LICENSE file. // Based heavily on vterm.py from urwid package terminal import ( "fmt" "github.com/gcla/gowid" tcell "github.com/gdamore/tcell/v2" "github.com/gdamore/tcell/v2/terminfo" log "github.com/sirupsen/logrus" ) //====================================================================== type EventNotSupported struct { Event interface{} } var _ error = EventNotSupported{} func (e EventNotSupported) Error() string { return fmt.Sprintf("Terminal input event %v of type %T not supported yet", e.Event, e.Event) } func pasteStart(ti *terminfo.Terminfo) []byte { if ti.PasteStart != "" { return []byte(ti.PasteStart) } else { return []byte("\x1b[200~") } } func pasteEnd(ti *terminfo.Terminfo) []byte { if ti.PasteEnd != "" { return []byte(ti.PasteEnd) } else { return []byte("\x1b[201~") } } func enablePaste(ti *terminfo.Terminfo) []byte { if ti.EnablePaste != "" { return []byte(ti.EnablePaste) } else { return []byte("\x1b[?2004h") } } func disablePaste(ti *terminfo.Terminfo) []byte { if ti.DisablePaste != "" { return []byte(ti.DisablePaste) } else { return []byte("\x1b[?2004l") } } // TCellEventToBytes converts TCell's representation of a terminal event to // the string of bytes that would be the equivalent event according to the // supplied Terminfo object. It returns a tuple of the byte slice // representing the terminal event (if successful), and a bool (denoting // success or failure). This function is used by the TerminalWidget. Its // subprocess is connected to a tty controlled by gowid. Events from the // user are parsed by gowid via TCell - they are then translated by this // function before being written to the TerminalWidget subprocess's tty. func TCellEventToBytes(ev interface{}, mouse IMouseSupport, last gowid.MouseState, paster IPaste, ti *terminfo.Terminfo) ([]byte, bool) { res := make([]byte, 0) res2 := false switch ev := ev.(type) { case *tcell.EventPaste: res2 = true if paster.PasteState() { // Already saw start res = append(res, pasteEnd(ti)...) paster.PasteState(false) } else { res = append(res, pasteStart(ti)...) paster.PasteState(true) } case *tcell.EventKey: if ev.Key() < ' ' { str := []rune{rune(ev.Key())} res = append(res, string(str)...) res2 = true } else { res2 = true switch ev.Key() { case tcell.KeyRune: str := []rune{ev.Rune()} res = append(res, string(str)...) case tcell.KeyCR: str := []rune{rune(tcell.KeyCR)} res = append(res, string(str)...) case tcell.KeyF1: res = append(res, ti.KeyF1...) case tcell.KeyF2: res = append(res, ti.KeyF2...) case tcell.KeyF3: res = append(res, ti.KeyF3...) case tcell.KeyF4: res = append(res, ti.KeyF4...) case tcell.KeyF5: res = append(res, ti.KeyF5...) case tcell.KeyF6: res = append(res, ti.KeyF6...) case tcell.KeyF7: res = append(res, ti.KeyF7...) case tcell.KeyF8: res = append(res, ti.KeyF8...) case tcell.KeyF9: res = append(res, ti.KeyF9...) case tcell.KeyF10: res = append(res, ti.KeyF10...) case tcell.KeyF11: res = append(res, ti.KeyF11...) case tcell.KeyF12: res = append(res, ti.KeyF12...) case tcell.KeyF13: res = append(res, ti.KeyF13...) case tcell.KeyF14: res = append(res, ti.KeyF14...) case tcell.KeyF15: res = append(res, ti.KeyF15...) case tcell.KeyF16: res = append(res, ti.KeyF16...) case tcell.KeyF17: res = append(res, ti.KeyF17...) case tcell.KeyF18: res = append(res, ti.KeyF18...) case tcell.KeyF19: res = append(res, ti.KeyF19...) case tcell.KeyF20: res = append(res, ti.KeyF20...) case tcell.KeyF21: res = append(res, ti.KeyF21...) case tcell.KeyF22: res = append(res, ti.KeyF22...) case tcell.KeyF23: res = append(res, ti.KeyF23...) case tcell.KeyF24: res = append(res, ti.KeyF24...) case tcell.KeyF25: res = append(res, ti.KeyF25...) case tcell.KeyF26: res = append(res, ti.KeyF26...) case tcell.KeyF27: res = append(res, ti.KeyF27...) case tcell.KeyF28: res = append(res, ti.KeyF28...) case tcell.KeyF29: res = append(res, ti.KeyF29...) case tcell.KeyF30: res = append(res, ti.KeyF30...) case tcell.KeyF31: res = append(res, ti.KeyF31...) case tcell.KeyF32: res = append(res, ti.KeyF32...) case tcell.KeyF33: res = append(res, ti.KeyF33...) case tcell.KeyF34: res = append(res, ti.KeyF34...) case tcell.KeyF35: res = append(res, ti.KeyF35...) case tcell.KeyF36: res = append(res, ti.KeyF36...) case tcell.KeyF37: res = append(res, ti.KeyF37...) case tcell.KeyF38: res = append(res, ti.KeyF38...) case tcell.KeyF39: res = append(res, ti.KeyF39...) case tcell.KeyF40: res = append(res, ti.KeyF40...) case tcell.KeyF41: res = append(res, ti.KeyF41...) case tcell.KeyF42: res = append(res, ti.KeyF42...) case tcell.KeyF43: res = append(res, ti.KeyF43...) case tcell.KeyF44: res = append(res, ti.KeyF44...) case tcell.KeyF45: res = append(res, ti.KeyF45...) case tcell.KeyF46: res = append(res, ti.KeyF46...) case tcell.KeyF47: res = append(res, ti.KeyF47...) case tcell.KeyF48: res = append(res, ti.KeyF48...) case tcell.KeyF49: res = append(res, ti.KeyF49...) case tcell.KeyF50: res = append(res, ti.KeyF50...) case tcell.KeyF51: res = append(res, ti.KeyF51...) case tcell.KeyF52: res = append(res, ti.KeyF52...) case tcell.KeyF53: res = append(res, ti.KeyF53...) case tcell.KeyF54: res = append(res, ti.KeyF54...) case tcell.KeyF55: res = append(res, ti.KeyF55...) case tcell.KeyF56: res = append(res, ti.KeyF56...) case tcell.KeyF57: res = append(res, ti.KeyF57...) case tcell.KeyF58: res = append(res, ti.KeyF58...) case tcell.KeyF59: res = append(res, ti.KeyF59...) case tcell.KeyF60: res = append(res, ti.KeyF60...) case tcell.KeyF61: res = append(res, ti.KeyF61...) case tcell.KeyF62: res = append(res, ti.KeyF62...) case tcell.KeyF63: res = append(res, ti.KeyF63...) case tcell.KeyF64: res = append(res, ti.KeyF64...) case tcell.KeyInsert: res = append(res, ti.KeyInsert...) case tcell.KeyDelete: res = append(res, ti.KeyDelete...) case tcell.KeyHome: res = append(res, ti.KeyHome...) case tcell.KeyEnd: res = append(res, ti.KeyEnd...) case tcell.KeyHelp: res = append(res, ti.KeyHelp...) case tcell.KeyPgUp: res = append(res, ti.KeyPgUp...) case tcell.KeyPgDn: res = append(res, ti.KeyPgDn...) case tcell.KeyUp: res = append(res, ti.KeyUp...) case tcell.KeyDown: res = append(res, ti.KeyDown...) case tcell.KeyLeft: res = append(res, ti.KeyLeft...) case tcell.KeyRight: res = append(res, ti.KeyRight...) case tcell.KeyBacktab: res = append(res, ti.KeyBacktab...) case tcell.KeyExit: res = append(res, ti.KeyExit...) case tcell.KeyClear: res = append(res, ti.KeyClear...) case tcell.KeyPrint: res = append(res, ti.KeyPrint...) case tcell.KeyCancel: res = append(res, ti.KeyCancel...) case tcell.KeyDEL: res = append(res, ti.KeyBackspace...) case tcell.KeyBackspace: res = append(res, ti.KeyBackspace...) default: res2 = false panic(EventNotSupported{Event: ev}) } } case *tcell.EventMouse: if mouse.MouseEnabled() { var data string btnind := 0 switch ev.Buttons() { case tcell.Button1: btnind = 0 case tcell.Button2: btnind = 1 case tcell.Button3: btnind = 2 case tcell.WheelUp: btnind = 64 case tcell.WheelDown: btnind = 65 } lastind := 0 if last.LeftIsClicked() { lastind = 0 } else if last.MiddleIsClicked() { lastind = 1 } else if last.RightIsClicked() { lastind = 2 } switch ev.Buttons() { case tcell.Button1, tcell.Button2, tcell.Button3, tcell.WheelUp, tcell.WheelDown: mx, my := ev.Position() btn := btnind if (last.LeftIsClicked() && (ev.Buttons() == tcell.Button1)) || (last.MiddleIsClicked() && (ev.Buttons() == tcell.Button2)) || (last.RightIsClicked() && (ev.Buttons() == tcell.Button3)) { // assume the mouse pointer has been moved with button down, a "drag" btn += 32 } if mouse.MouseIsSgr() { data = fmt.Sprintf("\033[<%d;%d;%dM", btn, mx+1, my+1) } else { data = fmt.Sprintf("\033[M%c%c%c", btn+32, mx+33, my+33) } res = append(res, data...) res2 = true case tcell.ButtonNone: // TODO - how to report no press? mx, my := ev.Position() if last.LeftIsClicked() || last.MiddleIsClicked() || last.RightIsClicked() { // 0 means left mouse button, m means released if mouse.MouseIsSgr() { data = fmt.Sprintf("\033[<%d;%d;%dm", lastind, mx+1, my+1) } else if mouse.MouseReportAny() { data = fmt.Sprintf("\033[M%c%c%c", 35, mx+33, my+33) } } else if mouse.MouseReportAny() { if mouse.MouseIsSgr() { // +32 for motion, +3 for no button data = fmt.Sprintf("\033[<35;%d;%dm", mx+1, my+1) } else { data = fmt.Sprintf("\033[M%c%c%c", 35+32, mx+33, my+33) } } res = append(res, data...) res2 = true } } default: log.WithField("event", ev).Info("Event not implemented") } return res, res2 } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: gowid-1.4.0/widgets/text/000077500000000000000000000000001426234454000152635ustar00rootroot00000000000000gowid-1.4.0/widgets/text/testwidget.go000066400000000000000000000031301426234454000177720ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. // Package text provides a text field widget. package text import ( "fmt" "github.com/gcla/gowid" tcell "github.com/gdamore/tcell/v2" ) //====================================================================== type Widget1 struct { I int } func (w *Widget1) RenderSize(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.IRenderBox { if focus.Focus { return New(fmt.Sprintf("%df", w.I)).RenderSize(size, focus, app) } else { return New(fmt.Sprintf("%d ", w.I)).RenderSize(size, focus, app) } } func (w *Widget1) Render(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.ICanvas { if focus.Focus { return New(fmt.Sprintf("%df", w.I)).Render(size, focus, app) } else { return New(fmt.Sprintf("%d ", w.I)).Render(size, focus, app) } } func (w *Widget1) UserInput(ev interface{}, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) bool { switch ev := ev.(type) { case *tcell.EventKey: if focus.Focus { return New(fmt.Sprintf("%df", w.I)).UserInput(ev, size, focus, app) } else { return New(fmt.Sprintf("%d ", w.I)).UserInput(ev, size, focus, app) } case *tcell.EventMouse: // Take all mouse input so I can test clicking in different columns changing the focus return true } return false } func (w *Widget1) Selectable() bool { return true } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: gowid-1.4.0/widgets/text/text.go000066400000000000000000000602661426234454000166100ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. // Package text provides a text field widget. package text import ( "fmt" "io" "unicode" "unicode/utf8" "github.com/gcla/gowid" "github.com/gcla/gowid/gwutil" "github.com/mattn/go-runewidth" ) //====================================================================== type ICursor interface { CursorEnabled() bool SetCursorDisabled() CursorPos() int SetCursorPos(pos int, app gowid.IApp) } type SimpleCursor struct { Pos int } func (c *SimpleCursor) CursorEnabled() bool { return c.Pos != -1 } func (c *SimpleCursor) SetCursorDisabled() { c.Pos = -1 } func (c *SimpleCursor) CursorPos() int { return c.Pos } func (c *SimpleCursor) SetCursorPos(pos int, app gowid.IApp) { c.Pos = pos } //====================================================================== // For callback registration type ContentCB struct{} //====================================================================== // IContent represents a styled range of text. Different sections of the text can // have different styles. Behind the scenes, this is just implemented as an array of // (rune, ICellStyler) pairs - maybe nothing more complicated would ever be needed // in practise. See also TextContent. type IContent interface { Length() int Width() int ChrAt(idx int) rune RangeOver(start, end int, attrs gowid.IRenderContext, proc gowid.ICellProcessor) AddAt(idx int, content ContentSegment) DeleteAt(idx, length int) fmt.Stringer } type ICloneContent interface { Clone() IContent } // ContentSegment represents some text each character of which is styled the same // way. type ContentSegment struct { Style gowid.ICellStyler Text string } // StringContent makes a ContentSegment from a simple string. func StringContent(s string) ContentSegment { return ContentSegment{nil, s} } // StyledContent makes a ContentSegment from a string and an ICellStyler. func StyledContent(text string, style gowid.ICellStyler) ContentSegment { return ContentSegment{style, text} } // StyledRune is a styled rune. type StyledRune struct { Chr rune Attr gowid.ICellStyler } // Content is an array of AttributedRune and implements IContent. type Content []StyledRune var _ IContent = (*Content)(nil) var _ ICloneContent = (*Content)(nil) // NewContent constructs Content suitable for initializing a text Widget. func NewContent(content []ContentSegment) *Content { var length int for _, m := range content { length += len(m.Text) // might be underestimate } res := Content(make([]StyledRune, 0, length)) for _, m := range content { res = append(res, MakeAttributedRunes(m)...) } return &res } // MakeAttributedRunes converts a ContentSegment into an array of AttributeRune, // which is used to build a Content implementing IContent. func MakeAttributedRunes(m ContentSegment) []StyledRune { res := make([]StyledRune, 0, len(m.Text)) // might be underestimate s := m.Text for len(s) > 0 { c, size := utf8.DecodeRuneInString(s) res = append(res, StyledRune{c, m.Style}) s = s[size:] } return res } func (h *Content) Clone() IContent { runes := make([]StyledRune, len(*h)) copy(runes, *h) res := Content(runes) return &res } // AddAt will insert the supplied ContentSegment at index idx. func (h *Content) AddAt(idx int, content ContentSegment) { piece := MakeAttributedRunes(content) res := Content(make([]StyledRune, 0, len(piece)+len(*h))) res = append(res, (*h)[0:idx]...) res = append(res, piece...) res = append(res, (*h)[idx:]...) *h = res } // DeleteAt will remove a segment of content of the provided length starting at index idx. func (h *Content) DeleteAt(idx int, length int) { *h = append((*h)[0:idx], (*h)[idx+length:]...) } // RangeOver will call the supplied ICellProcessor for each element of the content between start and // end, having first transformed that content element into an AttributedRune by using the // accompanying ICellStyler and the IRenderContext. You can use this to build up an array // of Cells, for example, in the process of converting a text widget to something that // can be rendered in a canvas. func (h Content) RangeOver(start, end int, attrs gowid.IRenderContext, proc gowid.ICellProcessor) { var curStyler gowid.ICellStyler var f gowid.IColor var g gowid.IColor var s gowid.StyleAttrs var f2 gowid.TCellColor var g2 gowid.TCellColor for idx, j := start, 0; idx < end; idx, j = idx+1, j+1 { if h[idx].Attr != nil { if h[idx].Attr != curStyler { f, g, s = h[idx].Attr.GetStyle(attrs) f2 = gowid.IColorToTCell(f, gowid.ColorNone, attrs.GetColorMode()) g2 = gowid.IColorToTCell(g, gowid.ColorNone, attrs.GetColorMode()) curStyler = h[idx].Attr } proc.ProcessCell(gowid.MakeCell(h[idx].Chr, f2, g2, s)) } else { proc.ProcessCell(gowid.MakeCell(h[idx].Chr, gowid.ColorNone, gowid.ColorNone, gowid.StyleNone)) } } } // ChrAt will return the unstyled rune at index idx. func (h Content) ChrAt(idx int) rune { return h[idx].Chr } // Length will return the length of the content i.e. the number of runes it comprises. func (h Content) Length() int { return len(h) } // Width returns the number of screen cells the content takes. Different from Length if >1-width runes are used. func (h Content) Width() int { res := 0 for _, r := range h { res += runewidth.RuneWidth(r.Chr) } return res } // String implements fmt.Stringer. func (h Content) String() string { chars := make([]rune, h.Length()) for i := 0; i < h.Length(); i++ { chars[i] = h.ChrAt(i) } return string(chars) } //====================================================================== // Determines how a text widget's text is wrapped - clip means anything beyond the // specified column is clipped to the next newline type WrapType int const ( WrapAny WrapType = iota WrapClip ) // Widget can be used to display text on the screen, with optional styling for // specified regions of the text. type Widget struct { text IContent wrap WrapType align gowid.IHAlignment opts Options linesFromTop int Callbacks *gowid.Callbacks gowid.RejectUserInput gowid.NotSelectable } var _ gowid.IWidget = (*Widget)(nil) var _ io.Reader = (*Widget)(nil) var _ fmt.Stringer = (*Widget)(nil) type CopyableWidget struct { *Widget gowid.IIdentity gowid.IClipboardSelected } var _ gowid.IIdentityWidget = (*CopyableWidget)(nil) var _ gowid.IClipboard = (*CopyableWidget)(nil) var _ gowid.IClipboardSelected = (*CopyableWidget)(nil) // ISimple is a gowid Widget that supports getting and setting plain unstyled text. // It is used by edit.Widget, for example. This package's Widget type implements it. type ISimple interface { gowid.IWidget Text() string SetText(text string, app gowid.IApp) } // IWidget is a gowid IWidget with the following extra APIs. type IWidget interface { gowid.IWidget // Content returns an interface that provides access to the text and styling used. Content() IContent // Wrap determines whether the text is clipped, if too long, or flows onto the next line. Wrap() WrapType // Align can be used to keep each line of text left, right or center aligned. Align() gowid.IHAlignment // LinesFromTop is used to track how many widget lines are not in view off the top // given the current render. The widget tries to keep this the same when the widget // is re-rendered at a different size (e.g. the terminal is resized). LinesFromTop() int ClipIndicator() string } // Options is used to provide arguments to the various New initialization functions. type Options struct { Wrap WrapType ClipIndicator string Align gowid.IHAlignment } // New initializes a text widget with a string and some extra arguments e.g. to align // the text within each line, and to determine whether or not it's clipped. func New(text string, opts ...Options) *Widget { var opt Options if len(opts) > 0 { opt = opts[0] } content := &ContentSegment{nil, text} holder := NewContent([]ContentSegment{*content}) return NewFromContentExt(holder, opt) } func NewCopyable(text string, id gowid.IIdentity, cs gowid.IClipboardSelected, opts ...Options) *CopyableWidget { return &CopyableWidget{ Widget: New(text, opts...), IIdentity: id, IClipboardSelected: cs, } } // NewFromContent initializes a text widget with IContent, which can be built from a set // of content segments. This is a way of making a text widget with styling. func NewFromContent(content IContent) *Widget { res := &Widget{ text: content, linesFromTop: 0, Callbacks: gowid.NewCallbacks(), } return res } // NewFromContentExt initialized a text widget with IContent and some extra options such // as wrapping, alignment, etc. func NewFromContentExt(content IContent, opts Options) *Widget { if opts.Align == nil { opts.Align = gowid.HAlignLeft{} } res := &Widget{ text: content, wrap: opts.Wrap, align: opts.Align, opts: opts, Callbacks: gowid.NewCallbacks(), } return res } func (w *Widget) String() string { return fmt.Sprintf("text") } // Writer is a wrapper around a text Widget which, by including the app, can be used // to implement io.Writer. type Writer struct { *Widget gowid.IApp } // Write implements io.Writer. The app is required because the content setting API // requires the app because callbacks might be invoked which themselves require the // app. func (w *Writer) Write(p []byte) (n int, err error) { content := &ContentSegment{nil, string(p)} w.SetContent(w.IApp, NewContent([]ContentSegment{*content})) return len(p), nil } // Read makes Widget implement io.Reader. func (w *Widget) Read(p []byte) (n int, err error) { runes := make([]rune, 0) for i := 0; i < w.Content().Length(); i++ { runes = append(runes, w.Content().ChrAt(i)) } runesString := string(runes) num := copy(p, runesString) if num < len(p) { return num, io.EOF } else { return num, nil } } func (w *Widget) ClipIndicator() string { return w.opts.ClipIndicator } func (w *Widget) Content() IContent { return w.text } func (w *Widget) SetContent(app gowid.IApp, content IContent) { w.text = content gowid.RunWidgetCallbacks(w.Callbacks, ContentCB{}, app, w) } func (w *Widget) SetText(text string, app gowid.IApp) { content := &ContentSegment{nil, text} w.SetContent(app, NewContent([]ContentSegment{*content})) } func (w *Widget) Wrap() WrapType { return w.wrap } func (w *Widget) SetWrap(wrap WrapType, app gowid.IApp) { w.wrap = wrap } func (w *Widget) Align() gowid.IHAlignment { return w.align } func (w *Widget) SetAlign(align gowid.IHAlignment, app gowid.IApp) { w.align = align } func (w *Widget) LinesFromTop() int { return w.linesFromTop } func (w *Widget) SetLinesFromTop(l int, app gowid.IApp) { w.linesFromTop = l } func (w *Widget) RenderSize(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.IRenderBox { return gowid.CalculateRenderSizeFallback(w, size, focus, app) } func (w *Widget) Render(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.ICanvas { return Render(w, size, focus, app) } func (w *Widget) OnContentSet(cb gowid.IWidgetChangedCallback) { gowid.AddWidgetCallback(w.Callbacks, ContentCB{}, cb) } func (w *Widget) RemoveOnContentSet(cb gowid.IIdentity) { gowid.RemoveWidgetCallback(w.Callbacks, ContentCB{}, cb) } func IsBreakableSpace(chr rune) bool { return unicode.IsSpace(chr) && chr != '\u00A0' } // CalculateTopMiddleBottom will, for a given size, calculate three indices: // - the index of the line of text that should be at the top of the rendered area // - the number of lines to display // - the number of lines occluded from the bottom func CalculateTopMiddleBottom(w IWidget, size gowid.IRenderSize) (int, int, int) { cursor := false crow := -1 var cursorPos int w2, ok := w.(ICursor) if ok { cursor = w2.CursorEnabled() if cursor { cursorPos = w2.CursorPos() } } var maxCol int var maxRow int box, isBox := size.(gowid.IRenderBox) _, isFixed := size.(gowid.IRenderFixed) flow, isFlow := size.(gowid.IRenderFlowWith) haveMaxRow := isBox || isFixed content := w.Content() if haveMaxRow { if isFixed { maxRow = 1 maxCol = w.Content().Width() } else { maxRow = box.BoxRows() maxCol = box.BoxColumns() } } else { if !isFlow { // TODO - compute content twice maxCol = content.Width() } else { maxCol = flow.FlowColumns() } } layout := MakeTextLayout(content, maxCol, w.Wrap(), w.Align()) if cursor { _, crow = GetCoordsFromCursorPos(cursorPos, maxCol, layout, w.Content()) } if haveMaxRow && len(layout.Lines) > maxRow { idxAbove := w.LinesFromTop() idxBelow := maxRow + idxAbove if cursor { if crow >= idxBelow { shift := (crow + 1) - idxBelow idxBelow += shift idxAbove += shift } } return idxAbove, maxRow, gwutil.Max(len(layout.Lines)-idxBelow, 0) } else { // If the client is a scrollbar, if there's no maxrow (i.e. RenderFlow), the scroll should just // take up all the space. Or, if the rendered lines fill up all the space available. return 0, 1, 0 } } // ContentToCellArray is a helper type; it can be used to construct a Cell array by passing // it to a RangeOver() function. type ContentToCellArray struct { Cells []gowid.Cell Cur int } var _ gowid.ICellProcessor = (*ContentToCellArray)(nil) func (m *ContentToCellArray) ProcessCell(cell gowid.Cell) gowid.Cell { m.Cells[m.Cur] = cell m.Cur += runewidth.RuneWidth(cell.Rune()) return cell } // If rendered Fixed, then rows==1 and cols==len(text) func Render(w IWidget, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.ICanvas { cursor := false ccol := -1 crow := -1 var cursorPos int w2, ok := w.(ICursor) if ok { cursor = w2.CursorEnabled() if cursor { cursorPos = w2.CursorPos() } } var maxCol int var maxRow int box, isBox := size.(gowid.IRenderBox) _, isFixed := size.(gowid.IRenderFixed) flow, isFlow := size.(gowid.IRenderFlowWith) content := w.Content() haveMaxRow := isBox || isFixed if haveMaxRow { if isFixed { curcol := 0 maxRow = 1 var last rune // This is lame - find a better way for i := 0; i < w.Content().Length(); i++ { last = w.Content().ChrAt(i) if last == '\n' { maxRow++ if curcol > maxCol { maxCol = curcol } curcol = 0 } else { curcol += runewidth.RuneWidth(last) } } if curcol > maxCol { maxCol = curcol } // if last == '\n' { // maxRow-- // } } else { maxRow = box.BoxRows() maxCol = box.BoxColumns() } } else { if !isFlow { maxCol = content.Width() } else { maxCol = flow.FlowColumns() } } layout := MakeTextLayout(content, maxCol, w.Wrap(), w.Align()) lines := make([][]gowid.Cell, len(layout.Lines)) // Construct an array of lines from the layout, to be used as data // to construct the canvas. Walk through each segment returned by // the layout object count := 0 for x, segment := range layout.Lines { // Make enough cells to be able to render double-width runes. The second cell will be left // empty. lines[x] = make([]gowid.Cell, segment.EndWidth-segment.StartWidth) w.Content().RangeOver(segment.StartLength, segment.EndLength, app, &ContentToCellArray{Cells: lines[x]}) if segment.Clipped { //for i := len(w.ClipIndicator())-1; i >=0; i-- { ind := w.ClipIndicator() j := len(ind) - 1 for i := len(lines[x]) - 1; i >= 0; i-- { if j < 0 { break } lines[x][i] = lines[x][i].WithRune(rune(ind[j])) j -= 1 } } if len(lines[x]) < maxCol { switch w.Align().(type) { case gowid.HAlignRight: length := maxCol - len(lines[x]) lines[x] = append(gowid.CellsFromString(gwutil.StringOfLength(' ', length)), lines[x]...) case gowid.HAlignMiddle: length := (maxCol - len(lines[x])) / 2 lines[x] = append(gowid.CellsFromString(gwutil.StringOfLength(' ', length)), lines[x]...) default: } } count++ } if cursor { ccol, crow = GetCoordsFromCursorPos(cursorPos, maxCol, layout, w.Content()) } res := gowid.NewCanvasWithLines(lines) res.SetCursorCoords(ccol, crow) if haveMaxRow { if res.BoxRows() > maxRow { idxAbove := w.LinesFromTop() idxBelow := maxRow + idxAbove if cursor { if crow >= idxBelow { shift := (crow + 1) - idxBelow idxBelow += shift idxAbove += shift } } // If we would cut below the bottom of the render box, then shift the // cut up to the bottom of the render box if idxBelow > res.BoxRows() { idxAbove -= (idxBelow - res.BoxRows()) idxBelow = res.BoxRows() } res.Truncate(idxAbove, gwutil.Max(res.BoxRows()-idxBelow, 0)) } else { hor := gowid.CellFromRune(' ') horArr := make([]gowid.Cell, res.BoxColumns()) for i := 0; i < res.BoxColumns(); i++ { horArr[i] = hor } nl := res.BoxRows() for i := 0; i < maxRow-nl; i++ { res.AppendLine(horArr, false) } } } res.AlignRight() res.ExtendRight(gowid.EmptyLine(maxCol - res.BoxColumns())) return res } type IChrAt interface { ChrAt(i int) rune } // zero-based func GetCoordsFromCursorPos(cursorPos int, maxCol int, layout *TextLayout, at IChrAt) (x int, y int) { var crow, ccol int for lineNumber, segment := range layout.Lines { if segment.StartLength <= cursorPos && cursorPos <= segment.EndLength { crow = lineNumber ccol = 0 for i := segment.StartLength; i < gwutil.Min(segment.EndLength, cursorPos); i++ { ccol += runewidth.RuneWidth(at.ChrAt(i)) } } } return ccol, crow } // GetCursorPosFromCoords translates a (col, row) coord to a cursor position. It looks up the layout structure // at the right line, then adds the col to the segment start offset. func GetCursorPosFromCoords(ccol int, crow int, layout *TextLayout, at IChrAt) int { if len(layout.Lines) == 0 { return 0 } else if crow >= len(layout.Lines) { return layout.Lines[len(layout.Lines)-1].EndWidth } else { start := layout.Lines[crow].StartLength startw := layout.Lines[crow].StartWidth endw := layout.Lines[crow].EndWidth col := 0 for i := 0; i < gwutil.Min(endw-startw, ccol); { i += runewidth.RuneWidth(at.ChrAt(col + start)) col += 1 } return start + col } } //====================================================================== type LineLayout struct { StartLength int StartWidth int EndWidth int EndLength int Clipped bool } type TextLayout struct { Lines []LineLayout } // MakeTextLayout builds an array of line layouts from an IContent object. It applies the provided // text wrapping and alignment options. The line layouts can then be used to index the IContent // in order to build a canvas for rendering. func MakeTextLayout(content IContent, width int, wrap WrapType, align gowid.IHAlignment) *TextLayout { lines := make([]LineLayout, 0, 16) if width > 0 { switch wrap { case WrapClip: indexInLineWidth := 0 // current line index based on screen cells indexInLineLength := 0 // current line index based on runes skippingToEndOfLine := false // true if we had to cut off the text and are looking for a newline startOfCurrentLineLength := 0 startOfCurrentLineWidth := 0 for startOfCurrentLineLength+indexInLineLength < content.Length() { c := content.ChrAt(startOfCurrentLineLength + indexInLineLength) wid := runewidth.RuneWidth(c) if !skippingToEndOfLine && indexInLineWidth+wid > width { // end of space and no newline found lines = append(lines, LineLayout{ StartLength: startOfCurrentLineLength, StartWidth: startOfCurrentLineWidth, EndLength: startOfCurrentLineLength + indexInLineLength, EndWidth: startOfCurrentLineWidth + indexInLineWidth, Clipped: true, }) skippingToEndOfLine = true indexInLineWidth += wid indexInLineLength++ } else if c == '\n' { if !skippingToEndOfLine { lines = append(lines, LineLayout{ StartLength: startOfCurrentLineLength, StartWidth: startOfCurrentLineLength, EndLength: startOfCurrentLineLength + indexInLineLength, EndWidth: startOfCurrentLineWidth + indexInLineWidth, Clipped: false, }) } skippingToEndOfLine = false startOfCurrentLineLength += (indexInLineLength + 1) startOfCurrentLineWidth += (indexInLineWidth + 1) indexInLineLength = 0 indexInLineWidth = 0 } else { indexInLineWidth += wid indexInLineLength += 1 } } if !skippingToEndOfLine { lines = append(lines, LineLayout{ StartLength: startOfCurrentLineLength, StartWidth: startOfCurrentLineWidth, EndLength: startOfCurrentLineLength + indexInLineLength, EndWidth: startOfCurrentLineWidth + indexInLineWidth, Clipped: false, }) } case WrapAny: indexInSegmentLength := 0 // current line index indexInSegmentWidth := 0 // current line index startOfCurrentSegmentLength := 0 startOfCurrentSegmentWidth := 0 for startOfCurrentSegmentLength+indexInSegmentLength < content.Length() { c := content.ChrAt(startOfCurrentSegmentLength + indexInSegmentLength) if indexInSegmentWidth+runewidth.RuneWidth(c) > width { // end of space and no newline found lines = append(lines, LineLayout{ StartLength: startOfCurrentSegmentLength, StartWidth: startOfCurrentSegmentWidth, EndLength: startOfCurrentSegmentLength + indexInSegmentLength, EndWidth: startOfCurrentSegmentWidth + indexInSegmentWidth, Clipped: false, }) startOfCurrentSegmentLength += indexInSegmentLength startOfCurrentSegmentWidth += indexInSegmentWidth indexInSegmentLength = 0 indexInSegmentWidth = 0 } else if c == '\n' { lines = append(lines, LineLayout{ StartLength: startOfCurrentSegmentLength, StartWidth: startOfCurrentSegmentWidth, EndLength: startOfCurrentSegmentLength + indexInSegmentLength, EndWidth: startOfCurrentSegmentWidth + indexInSegmentWidth, Clipped: false, }) startOfCurrentSegmentLength += (indexInSegmentLength + 1) startOfCurrentSegmentWidth += (indexInSegmentWidth + 1) indexInSegmentLength = 0 indexInSegmentWidth = 0 } else { indexInSegmentWidth += runewidth.RuneWidth(c) indexInSegmentLength += 1 } } lines = append(lines, LineLayout{ StartLength: startOfCurrentSegmentLength, StartWidth: startOfCurrentSegmentWidth, EndLength: startOfCurrentSegmentLength + indexInSegmentLength, EndWidth: startOfCurrentSegmentWidth + indexInSegmentWidth, Clipped: false, }) default: panic(fmt.Errorf("Wrap %v not supported yet", wrap)) } } return &TextLayout{lines} } //====================================================================== // This meets both IText and ICursor, and allows me to make a canvas from a text widget // and a separately specified cursor position type WidgetWithCursor struct { *Widget *SimpleCursor } func (w *WidgetWithCursor) Render(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.ICanvas { return Render(w, size, focus, app) } func (w *WidgetWithCursor) CalculateTopMiddleBottom(size gowid.IRenderSize) (int, int, int) { return CalculateTopMiddleBottom(w, size) } //====================================================================== func (w *CopyableWidget) Clips(app gowid.IApp) []gowid.ICopyResult { return []gowid.ICopyResult{ gowid.CopyResult{ Name: "Displayed text", Val: w.Content().String(), }, } } func (w *CopyableWidget) UserInput(ev interface{}, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) bool { claimed := false if _, ok := ev.(gowid.CopyModeEvent); ok { // zero means deepest should try to claim - leaf knows it's a leaf. if app.InCopyMode() && app.CopyLevel() <= app.CopyModeClaimedAt() { app.CopyModeClaimedAt(app.CopyLevel()) app.CopyModeClaimedBy(w) claimed = true } } else if evc, ok := ev.(gowid.CopyModeClipsEvent); ok { evc.Action.Collect(w.Clips(app)) claimed = true } return claimed } func (w *CopyableWidget) Render(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.ICanvas { if app.InCopyMode() && app.CopyModeClaimedBy().ID() == w.ID() && focus.Focus { w2 := w.AlterWidget(w.Widget, app) return w2.Render(size, focus, app) } else { return w.Widget.Render(size, focus, app) } } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: gowid-1.4.0/widgets/text/text_test.go000066400000000000000000000350771426234454000176510ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source code is governed by the MIT license // that can be found in the LICENSE file. package text import ( "io" "strings" "testing" "github.com/gcla/gowid" "github.com/gcla/gowid/gwtest" log "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" ) //====================================================================== var testl gowid.PaletteRef var testl2 gowid.PaletteRef func init() { testl = gowid.MakePaletteRef("test") testl2 = gowid.MakePaletteRef("test2") } func TestAdd1(t *testing.T) { m1 := StyledContent("hello world", testl) m2 := StyledContent("foobar", testl2) t1 := NewContent([]ContentSegment{m1}) t1.AddAt(5, m2) assert.Equal(t, "hellofoobar world", t1.String()) t1.DeleteAt(3, 7) assert.Equal(t, "helr world", t1.String()) } func TestLayout1(t *testing.T) { tm1 := []ContentSegment{StyledContent("hello world", testl)} t1 := NewContent(tm1) l1 := MakeTextLayout(t1, 5, WrapClip, gowid.HAlignLeft{}) assert.Equal(t, 1, len(l1.Lines)) assert.Equal(t, LineLayout{StartWidth: 0, StartLength: 0, EndLength: 5, EndWidth: 5, Clipped: true}, l1.Lines[0]) t2 := NewContent(tm1) l2 := MakeTextLayout(t2, 5, WrapAny, gowid.HAlignLeft{}) assert.Equal(t, 3, len(l2.Lines)) assert.Equal(t, LineLayout{StartWidth: 0, StartLength: 0, EndLength: 5, EndWidth: 5}, l2.Lines[0]) assert.Equal(t, LineLayout{StartWidth: 5, StartLength: 5, EndLength: 10, EndWidth: 10}, l2.Lines[1]) assert.Equal(t, LineLayout{StartWidth: 10, StartLength: 10, EndLength: 11, EndWidth: 11}, l2.Lines[2]) } func TestLayoutW1(t *testing.T) { tm1 := []ContentSegment{StyledContent("hell现o world", testl)} t1 := NewContent(tm1) l1 := MakeTextLayout(t1, 5, WrapClip, gowid.HAlignLeft{}) assert.Equal(t, 1, len(l1.Lines)) assert.Equal(t, LineLayout{StartWidth: 0, StartLength: 0, EndLength: 4, EndWidth: 4, Clipped: true}, l1.Lines[0]) t2 := NewContent(tm1) l2 := MakeTextLayout(t2, 5, WrapAny, gowid.HAlignLeft{}) assert.Equal(t, 3, len(l2.Lines)) assert.Equal(t, LineLayout{StartWidth: 0, StartLength: 0, EndLength: 4, EndWidth: 4}, l2.Lines[0]) assert.Equal(t, LineLayout{StartWidth: 4, StartLength: 4, EndLength: 8, EndWidth: 9}, l2.Lines[1]) assert.Equal(t, LineLayout{StartWidth: 9, StartLength: 8, EndLength: 12, EndWidth: 13}, l2.Lines[2]) } func TestLayout2(t *testing.T) { tm1 := []ContentSegment{StyledContent("hello world", testl)} t1 := NewContent(tm1) l1 := MakeTextLayout(t1, 20, WrapClip, gowid.HAlignLeft{}) assert.Equal(t, len(l1.Lines), 1) assert.Equal(t, l1.Lines[0], LineLayout{StartWidth: 0, StartLength: 0, EndLength: 11, EndWidth: 11}) } func TestLayout3(t *testing.T) { tm1 := []ContentSegment{StyledContent("hello world\nsome more", testl)} t1 := NewContent(tm1) l1 := MakeTextLayout(t1, 7, WrapClip, gowid.HAlignLeft{}) assert.Equal(t, len(l1.Lines), 2) assert.Equal(t, l1.Lines[0], LineLayout{StartWidth: 0, StartLength: 0, EndLength: 7, Clipped: true, EndWidth: 7}) assert.Equal(t, l1.Lines[1], LineLayout{StartWidth: 12, StartLength: 12, EndLength: 19, Clipped: true, EndWidth: 19}) t2 := NewContent(tm1) l2 := MakeTextLayout(t2, 7, WrapAny, gowid.HAlignLeft{}) assert.Equal(t, len(l2.Lines), 4) assert.Equal(t, l2.Lines[0], LineLayout{StartWidth: 0, StartLength: 0, EndLength: 7, EndWidth: 7}) assert.Equal(t, l2.Lines[1], LineLayout{StartWidth: 7, StartLength: 7, EndLength: 11, EndWidth: 11}) assert.Equal(t, l2.Lines[2], LineLayout{StartWidth: 12, StartLength: 12, EndLength: 19, EndWidth: 19}) assert.Equal(t, l2.Lines[3], LineLayout{StartWidth: 19, StartLength: 19, EndLength: 21, EndWidth: 21}) } func TestLayout31(t *testing.T) { tm1 := []ContentSegment{StyledContent("he\nsome more", testl)} t1 := NewContent(tm1) l1 := MakeTextLayout(t1, 7, WrapClip, gowid.HAlignLeft{}) assert.Equal(t, len(l1.Lines), 2) assert.Equal(t, l1.Lines[0], LineLayout{StartWidth: 0, StartLength: 0, EndLength: 2, EndWidth: 2}) assert.Equal(t, l1.Lines[1], LineLayout{StartWidth: 3, StartLength: 3, EndLength: 10, Clipped: true, EndWidth: 10}) } func TestLayout4(t *testing.T) { tm1 := []ContentSegment{StyledContent("hello world\nsome", testl)} t1 := NewContent(tm1) l1 := MakeTextLayout(t1, 7, WrapClip, gowid.HAlignLeft{}) assert.Equal(t, len(l1.Lines), 2) assert.Equal(t, l1.Lines[0], LineLayout{StartWidth: 0, StartLength: 0, EndLength: 7, Clipped: true, EndWidth: 7}) assert.Equal(t, l1.Lines[1], LineLayout{StartWidth: 12, StartLength: 12, EndLength: 16, EndWidth: 16}) } func TestLayout5(t *testing.T) { tm1 := []ContentSegment{StyledContent("hello world\n\nsome", testl)} t1 := NewContent(tm1) l1 := MakeTextLayout(t1, 7, WrapClip, gowid.HAlignLeft{}) assert.Equal(t, len(l1.Lines), 3) assert.Equal(t, l1.Lines[0], LineLayout{StartWidth: 0, StartLength: 0, EndLength: 7, Clipped: true, EndWidth: 7}) assert.Equal(t, l1.Lines[1], LineLayout{StartWidth: 12, StartLength: 12, EndLength: 12, EndWidth: 12}) assert.Equal(t, l1.Lines[2], LineLayout{StartWidth: 13, StartLength: 13, EndLength: 17, EndWidth: 17}) t2 := NewContent(tm1) l2 := MakeTextLayout(t2, 7, WrapAny, gowid.HAlignLeft{}) assert.Equal(t, len(l2.Lines), 4) assert.Equal(t, l2.Lines[0], LineLayout{StartWidth: 0, StartLength: 0, EndLength: 7, EndWidth: 7}) assert.Equal(t, l2.Lines[1], LineLayout{StartWidth: 7, StartLength: 7, EndLength: 11, EndWidth: 11}) assert.Equal(t, l2.Lines[2], LineLayout{StartWidth: 12, StartLength: 12, EndLength: 12, EndWidth: 12}) assert.Equal(t, l2.Lines[3], LineLayout{StartWidth: 13, StartLength: 13, EndLength: 17, EndWidth: 17}) } func TestLayout6(t *testing.T) { tm1 := []ContentSegment{StyledContent("hello world\n", testl)} t1 := NewContent(tm1) l1 := MakeTextLayout(t1, 7, WrapClip, gowid.HAlignLeft{}) assert.Equal(t, 2, len(l1.Lines)) assert.Equal(t, LineLayout{StartWidth: 0, StartLength: 0, EndLength: 7, Clipped: true, EndWidth: 7}, l1.Lines[0]) assert.Equal(t, LineLayout{StartWidth: 12, StartLength: 12, EndLength: 12, Clipped: false, EndWidth: 12}, l1.Lines[1]) } func TestMultiline1(t *testing.T) { msg := `hello world this is cool` widget1 := New(msg) canvas1 := widget1.Render(gowid.RenderBox{C: 16, R: 6}, gowid.NotSelected, gwtest.D) log.Infof("Widget1 is %v", widget1) log.Infof("Canvas1 is %s", canvas1.String()) res := strings.Join([]string{ "hello ", "world ", "this ", "is ", "cool ", " ", }, "\n") assert.Equal(t, res, canvas1.String()) } func TestCanvas1(t *testing.T) { widget1 := New("hello world") canvas1 := widget1.Render(gowid.RenderBox{C: 20, R: 1}, gowid.NotSelected, gwtest.D) log.Infof("Widget1 is %v", widget1) log.Infof("Canvas1 is %s", canvas1.String()) res := strings.Join([]string{"hello world "}, "\n") assert.Equal(t, res, canvas1.String()) } func TestCanvas1b(t *testing.T) { widget1 := New("hello world this is a test") canvas1 := widget1.Render(gowid.RenderBox{C: 7, R: 3}, gowid.NotSelected, gwtest.D) log.Infof("Widget1 is %v", widget1) log.Infof("Canvas1 is %s", canvas1.String()) res := strings.Join([]string{"hello w", "orld th", "is is a"}, "\n") assert.Equal(t, res, canvas1.String()) } func TestCanvas2(t *testing.T) { widget1 := New("hello world") canvas1 := widget1.Render(gowid.RenderFlowWith{C: 7}, gowid.NotSelected, gwtest.D) log.Infof("Widget2 is %v", widget1) log.Infof("Canvas2 is %s", canvas1.String()) res := strings.Join([]string{"hello w", "orld "}, "\n") assert.Equal(t, res, canvas1.String()) } func TestCanvas3(t *testing.T) { widget1 := New("hello world") canvas1 := widget1.Render(gowid.RenderFlowWith{C: 7}, gowid.NotSelected, gwtest.D) log.Infof("Widget3 is %v", widget1) log.Infof("Canvas3 is %s", canvas1.String()) res := strings.Join([]string{"hello w", "orld "}, "\n") assert.Equal(t, res, canvas1.String()) } func TestCanvas4(t *testing.T) { widget1 := New("hello world every day") canvas1 := widget1.Render(gowid.RenderFlowWith{C: 7}, gowid.NotSelected, gwtest.D) log.Infof("Widget4 is %v", widget1) log.Infof("Canvas4 is %s", canvas1.String()) res := strings.Join([]string{"hello w", "orld ev", "ery day"}, "\n") assert.Equal(t, res, canvas1.String()) } func TestCanvas5(t *testing.T) { widget1 := New("hello world every day") canvas1 := widget1.Render(gowid.RenderFlowWith{C: 3}, gowid.NotSelected, gwtest.D) log.Infof("Widget5 is %v", widget1) log.Infof("Canvas5 is %s", canvas1.String()) res := strings.Join([]string{"hel", "lo ", "wor", "ld ", "eve", "ry ", "day"}, "\n") assert.Equal(t, res, canvas1.String()) } func TestCanvas6(t *testing.T) { widget1 := New("hello transubstantiation") canvas1 := widget1.Render(gowid.RenderFlowWith{C: 8}, gowid.NotSelected, gwtest.D) log.Infof("Widget6 is %v", widget1) log.Infof("Canvas6 is %s", canvas1.String()) res := strings.Join([]string{"hello tr", "ansubsta", "ntiation"}, "\n") assert.Equal(t, res, canvas1.String()) } func TestCanvas6b(t *testing.T) { widget1 := New("hello transubstantiation good") canvas1 := widget1.Render(gowid.RenderFlowWith{C: 8}, gowid.NotSelected, gwtest.D) log.Infof("Widget6 is %v", widget1) log.Infof("Canvas6 is %s", canvas1.String()) res := strings.Join([]string{"hello tr", "ansubsta", "ntiation", " good "}, "\n") assert.Equal(t, res, canvas1.String()) } func TestCanvas7(t *testing.T) { widget1 := New("hello transubstantiation good") canvas1 := widget1.Render(gowid.RenderFlowWith{C: 11}, gowid.NotSelected, gwtest.D) log.Infof("Widget7 is %v", widget1) log.Infof("Canvas7 is %s", canvas1.String()) res := strings.Join([]string{"hello trans", "ubstantiati", "on good "}, "\n") assert.Equal(t, res, canvas1.String()) } func TestCanvas8(t *testing.T) { widget1 := New("hello transubstantiation boy") canvas1 := widget1.Render(gowid.RenderFlowWith{C: 11}, gowid.NotSelected, gwtest.D) log.Infof("Widget8 is %v", widget1) log.Infof("Canvas8 is %s", canvas1.String()) res := strings.Join([]string{"hello trans", "ubstantiati", "on boy "}, "\n") assert.Equal(t, res, canvas1.String()) } func TestCanvas11(t *testing.T) { widget1 := New("hello transubstantiation good") //fwidget1 := NewFramed(widget1) canvas1 := widget1.Render(gowid.RenderBox{C: 5, R: 3}, gowid.NotSelected, gwtest.D) log.Infof("Widget11 is %v", widget1) log.Infof("Canvas11 is %s", canvas1.String()) res := strings.Join([]string{"hello", " tran", "subst"}, "\n") assert.Equal(t, res, canvas1.String()) } func TestCanvas16(t *testing.T) { widget1 := New("line 1 line 2 line 3") canvas1 := widget1.Render(gowid.RenderFlowWith{C: 8}, gowid.NotSelected, gwtest.D) log.Infof("Widget16 is %v", widget1) log.Infof("Canvas16 is %s", canvas1.String()) res := strings.Join([]string{"line 1 l", "ine 2 li", "ne 3 "}, "\n") assert.Equal(t, res, canvas1.String()) } func TestCanvas24(t *testing.T) { widget1 := New("line 1line 2line 3") canvas1 := widget1.Render(gowid.RenderFlowWith{C: 6}, gowid.NotSelected, gwtest.D) canvas1.Truncate(1, 0) log.Infof("Widget18 is %v", widget1) log.Infof("Canvas18 is %v", canvas1) log.Infof("Canvas18 is %s", canvas1.String()) res := strings.Join([]string{"line 2", "line 3"}, "\n") assert.Equal(t, res, canvas1.String()) } func TestCanvas25(t *testing.T) { widget1 := New("line 1line 2line 3") canvas1 := widget1.Render(gowid.RenderFlowWith{C: 6}, gowid.NotSelected, gwtest.D) canvas1.Truncate(0, 2) log.Infof("Widget18 is %v", widget1) log.Infof("Canvas18 is %s", canvas1.String()) res := strings.Join([]string{"line 1"}, "\n") assert.Equal(t, res, canvas1.String()) } func TestCanvas27(t *testing.T) { widget1 := New("") canvas1 := widget1.Render(gowid.RenderFlowWith{C: 0}, gowid.NotSelected, gwtest.D) log.Infof("Widget is %v", widget1) log.Infof("Canvas is '%s'", canvas1.String()) res := strings.Join([]string{""}, "\n") assert.Equal(t, res, canvas1.String()) } func TestCanvas28(t *testing.T) { widget1 := New("the needs of the many outweigh\nthe needs of the few.\nOr the one.", Options{ Wrap: WrapClip, }) canvas1 := widget1.Render(gowid.RenderFlowWith{C: 10}, gowid.NotSelected, gwtest.D) log.Infof("Widget is %v", widget1) log.Infof("Canvas is '%s'", canvas1.String()) res := strings.Join([]string{"the needs ", "the needs ", "Or the one"}, "\n") assert.Equal(t, res, canvas1.String()) } func TestTextAlign1(t *testing.T) { widget1 := New("hel你o\nworld\nf你o\nba") canvas1 := widget1.Render(gowid.RenderFlowWith{C: 7}, gowid.NotSelected, gwtest.D) res := strings.Join([]string{"hel你o ", "world ", "f你o ", "ba "}, "\n") assert.Equal(t, res, canvas1.String()) } func TestTextAlign2(t *testing.T) { widget1 := New("hello\nworld\nfoo\nba", Options{ Align: gowid.HAlignRight{}, }) canvas1 := widget1.Render(gowid.RenderFlowWith{C: 7}, gowid.NotSelected, gwtest.D) res := strings.Join([]string{" hello", " world", " foo", " ba"}, "\n") assert.Equal(t, res, canvas1.String()) } func TestTextAlign3(t *testing.T) { widget1 := New("hello\nworld\nfoo\nba", Options{ Align: gowid.HAlignMiddle{}, }) canvas1 := widget1.Render(gowid.RenderFlowWith{C: 7}, gowid.NotSelected, gwtest.D) res := strings.Join([]string{" hello ", " world ", " foo ", " ba "}, "\n") assert.Equal(t, res, canvas1.String()) } func TestText1(t *testing.T) { w := New("hello world") c1 := w.Render(gowid.RenderFlowWith{C: 20}, gowid.Focused, gwtest.D) assert.Equal(t, c1.String(), "hello world ") tset := false w.OnContentSet(gowid.WidgetCallback{"cb", func(app gowid.IApp, w gowid.IWidget) { tset = true }}) _, err := io.Copy(&Writer{w, gwtest.D}, strings.NewReader("goodbye everyone")) assert.NoError(t, err) assert.Equal(t, tset, true) tset = false c2 := w.Render(gowid.RenderFlowWith{C: 20}, gowid.Focused, gwtest.D) assert.Equal(t, c2.String(), "goodbye everyone ") _, err = io.Copy(&Writer{w, gwtest.D}, strings.NewReader("multi\nline\ntest")) assert.NoError(t, err) assert.Equal(t, tset, true) tset = false c3 := w.Render(gowid.RenderFlowWith{C: 10}, gowid.Focused, gwtest.D) assert.Equal(t, c3.String(), "multi \nline \ntest ") } func TestText2(t *testing.T) { w := New("hello world") c1 := w.Render(gowid.RenderFixed{}, gowid.Focused, gwtest.D) assert.Equal(t, c1.String(), "hello world") } func TestText3(t *testing.T) { w := New("hello yo") // TODO - make 2nd last arg 0 gwtest.RenderBoxManyTimes(t, w, 0, 10, 0, 10) gwtest.RenderFlowManyTimes(t, w, 0, 10) } func TestChinese1(t *testing.T) { w := New("|你|好|,|世|界|") c1 := w.Render(gowid.RenderFixed{}, gowid.Focused, gwtest.D) // Each width-2 rune takes up 2 screen cells assert.Equal(t, "|你|好|,|世|界|", c1.String()) } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: gowid-1.4.0/widgets/tree/000077500000000000000000000000001426234454000152365ustar00rootroot00000000000000gowid-1.4.0/widgets/tree/tree.go000066400000000000000000000461641426234454000165370ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. // Package tree is a widget that displays a collection of widgets organized in a tree structure. package tree import ( "fmt" "strings" "github.com/gcla/gowid" "github.com/gcla/gowid/gwutil" "github.com/gcla/gowid/widgets/list" lru "github.com/hashicorp/golang-lru" ) //====================================================================== type IModel interface { Leaf() string Children() IIterator fmt.Stringer } type IIterator interface { Value() IModel Next() bool } type IExpandedCallback interface { Expanded(app gowid.IApp) } type ExpandedFunction func(app gowid.IApp) func (f ExpandedFunction) Expanded(app gowid.IApp) { f(app) } type ICollapsedCallback interface { Collapsed(app gowid.IApp) } type CollapsedFunction func(app gowid.IApp) func (f CollapsedFunction) Collapsed(app gowid.IApp) { f(app) } type ICollapsible interface { IModel IsCollapsed() bool SetCollapsed(gowid.IApp, bool) } //====================================================================== // For callback registration type Collapsed struct{} type Expanded struct{} //====================================================================== type iterator struct { current int tree *Tree } func (i *iterator) Value() IModel { return i.tree.theChildren[i.current] } func (i *iterator) Next() bool { i.current++ return i.current < len(i.tree.theChildren) } //====================================================================== type Tree struct { theLeaf string theChildren []IModel } func NewTree(leaf string, children []IModel) *Tree { return &Tree{leaf, children} } func (t *Tree) Leaf() string { return t.theLeaf } func (t *Tree) SetLeaf(s string) { t.theLeaf = s } func (t *Tree) Children() IIterator { return &iterator{current: -1, tree: t} } func (t *Tree) GetChildren() []IModel { return t.theChildren } func (t *Tree) SetChildren(c []IModel) { newC := make([]IModel, len(c)) copy(newC, c) t.theChildren = newC } func (t *Tree) String() string { res := t.theLeaf var idx int var i IIterator for idx, i = 0, t.Children(); i.Next(); idx++ { } if idx > 0 { res = res + "+[" st := make([]string, idx) for idx, i = 0, t.Children(); i.Next(); idx++ { st[idx] = i.Value().String() } res = res + strings.Join(st, ",") + "]" } return res } //====================================================================== type collapsibleIterator struct { sub IIterator tree *Collapsible } func (i *collapsibleIterator) Value() IModel { return i.sub.Value() } func (i *collapsibleIterator) Next() bool { return !i.tree.IsCollapsed() && i.sub.Next() } //====================================================================== type Collapsible struct { *Tree collapsed bool Callbacks *gowid.Callbacks } func NewCollapsible(leaf string, children []IModel) *Collapsible { t := NewTree(leaf, children) return &Collapsible{ Tree: t, Callbacks: gowid.NewCallbacks(), } } func (t *Collapsible) String() string { var res string if t.IsCollapsed() { res = "" } else { res = "< >" } res = fmt.Sprintf("%s%s", res, t.Tree.String()) return res } func (t *Collapsible) IsCollapsed() bool { return t.collapsed } func (t *Collapsible) SetCollapsed(app gowid.IApp, collapsed bool) { t.collapsed = collapsed if t.IsCollapsed() { t.Callbacks.RunCallbacks(Collapsed{}, app) } else { t.Callbacks.RunCallbacks(Expanded{}, app) } } func (t *Collapsible) AddOnCollapsed(name interface{}, cb ICollapsedCallback) { t.Callbacks.AddCallback(Collapsed{}, gowid.Callback{name, gowid.CallbackFunction( func(args ...interface{}) { app := args[0].(gowid.IApp) cb.Collapsed(app) }, ), }) } func (t *Collapsible) RemoveOnCollapsed(name interface{}) { t.Callbacks.RemoveCallback(Collapsed{}, gowid.CallbackID{Name: name}) } func (t *Collapsible) AddOnExpanded(name interface{}, cb IExpandedCallback) { t.Callbacks.AddCallback(Expanded{}, gowid.Callback{name, gowid.CallbackFunction( func(args ...interface{}) { app := args[0].(gowid.IApp) cb.Expanded(app) }, ), }) } func (t *Collapsible) RemoveOnExpanded(name interface{}) { t.Callbacks.RemoveCallback(Expanded{}, gowid.CallbackID{Name: name}) } func (t *Collapsible) Children() IIterator { return &collapsibleIterator{sub: t.Tree.Children(), tree: t} } //====================================================================== // IPos is the interface of a type that represents the position of a // sub-tree or leaf in a tree. // // nil means invalid // [] means the root of the tree // [0] means 0th child of root // [3] means 3rd child of root // [1,2] means 2nd child of 1st child of root // type IPos interface { GetSubStructure(IModel) IModel Copy() IPos Indices() []int SetIndices([]int) Equal(list.IWalkerPosition) bool GreaterThan(list.IWalkerPosition) bool fmt.Stringer } func IsSubPosition(outer IPos, inner IPos) bool { oidc := outer.Indices() iidc := inner.Indices() if len(iidc) < len(oidc) { return false } for i := 0; i < len(oidc); i++ { if oidc[i] != iidc[i] { return false } } return true } //====================================================================== // TreePos is a simple implementation of IPos. type TreePos struct { Pos []int } func NewPos() *TreePos { res := TreePos{} res.Pos = make([]int, 0) return &res } func NewPosExt(pos []int) *TreePos { res := TreePos{ Pos: pos, } return &res } func (tp *TreePos) Equal(other list.IWalkerPosition) bool { switch o := other.(type) { case *TreePos: if (tp.Pos == nil) || (o.Pos == nil) { panic(gowid.InvalidTypeToCompare{LHS: tp.Pos, RHS: o.Pos}) } if len(tp.Pos) != len(o.Pos) { return false } for i := range tp.Pos { if tp.Pos[i] != o.Pos[i] { return false } } return true default: panic(gowid.InvalidTypeToCompare{LHS: tp, RHS: other}) } } func (tp *TreePos) GreaterThan(other list.IWalkerPosition) bool { switch o := other.(type) { case *TreePos: if (tp.Pos == nil) || (o.Pos == nil) { panic(gowid.InvalidTypeToCompare{LHS: tp.Pos, RHS: o.Pos}) } for i := 0; i < gwutil.Min(len(tp.Pos), len(o.Pos)); i++ { // e.g. [3,4] > [3] if tp.Pos[i] > o.Pos[i] { return true } else if tp.Pos[i] < o.Pos[i] { return false } } if len(tp.Pos) > len(o.Pos) { return true } return false default: panic(gowid.InvalidTypeToCompare{LHS: tp, RHS: other}) } } func (tp *TreePos) Copy() IPos { tpCopy := *tp tpCopy.Pos = make([]int, len(tp.Pos)) copy(tpCopy.Pos, tp.Pos) return &tpCopy } func (tp *TreePos) Indices() []int { return tp.Pos } func (tp *TreePos) SetIndices(indices []int) { tp.Pos = make([]int, len(indices)) copy(tp.Pos, indices) } func (tp *TreePos) String() string { return fmt.Sprintf("%v", tp.Pos) } // // Returns nil if the treepos is invalid, or a tree pointer if valid // // GetSubStructure returns the (sub-)Tree at this position from the tree argument provided. func (tp *TreePos) GetSubStructure(tree IModel) IModel { var res IModel indices := tp.Indices() // we won't modify any of these if len(indices) == 0 { res = tree } else { var idx int var it IIterator // Walk through immediate children of tree until we hit the sub-tree at the correct index for idx, it = -1, tree.Children(); idx < indices[0] && it.Next(); idx++ { } if idx == indices[0] { // tp with the current tree-level stripped off i.e. one deeper res = NewPosExt(indices[1:]).GetSubStructure(it.Value()) } } return res } // ConfirmPosition returns true if there is a tree at position tp of argument tree. func ConfirmPosition(tp IPos, tree IModel) bool { res := false if tp.GetSubStructure(tree) != nil { res = true } return res } // FirstChildPosition returns the IPos corresponding to the first child // of tp, or nil if there are no children. func FirstChildPosition(tp IPos, tree IModel) IPos { tpCopy := tp.Copy() tpCopy.SetIndices(append(tpCopy.Indices(), 0)) if ConfirmPosition(tpCopy, tree) { return tpCopy } else { return nil } } // LastChildPosition returns the IPos corresponding to the last child // of tp, or nil if there are no children. func LastChildPosition(tp IPos, tree IModel) IPos { subTree := tp.GetSubStructure(tree) if subTree != nil { var idx int var i IIterator for idx, i = 0, subTree.Children(); i.Next(); idx++ { } if idx > 0 { tp2 := tp.Copy() tp2.SetIndices(append(tp2.Indices(), idx-1)) return tp2 } } return nil } // LastInDirection navigates the tree starting at tp and moving in the // direction determined by the function f until the end is reached. The // function returns the last position that was passed through. func LastInDirection(tp IPos, tree IModel, f func(IPos, IModel) IPos) IPos { var cur IPos var nextPos IPos for nextPos = tp; nextPos != nil; nextPos = f(nextPos, tree) { cur = nextPos } return cur } // LastDescendant moves to the last child of the current level, and // then to the last child of that node, and onwards until the end // is reached. func LastDescendant(tp IPos, tree IModel) IPos { var f = func(a IPos, t IModel) IPos { return LastChildPosition(a, t) } return LastInDirection(tp, tree, f) } // NextSiblingPosition returns the position of the next sibling relative to // the position tp. func NextSiblingPosition(tp IPos, tree IModel) IPos { var res IPos tpCopy := tp.Copy() indices := tpCopy.Indices() if len(indices) > 0 { indices = append(indices[:len(indices)-1], indices[len(indices)-1]+1) tpCopy.SetIndices(indices) if ConfirmPosition(tpCopy, tree) { res = tpCopy } } return res } // PreviousSiblingPosition returns the position of the previous sibling // relative to the position tp. func PreviousSiblingPosition(tp IPos, tree IModel) IPos { var res IPos indices := tp.Indices() if len(indices) > 0 && indices[len(indices)-1] > 0 { tpCopy := tp.Copy() indices := tpCopy.Indices() indices[len(indices)-1]-- tpCopy.SetIndices(indices) res = tpCopy } return res } // ParentPosition returns the position of the parent of position tp or // nil if tp is the root node. func ParentPosition(tp IPos) IPos { indices := tp.Indices() if len(indices) > 1 { tpCopy := tp.Copy() indices := tpCopy.Indices() indices = indices[:len(indices)-1] tpCopy.SetIndices(indices) return tpCopy } else if len(indices) == 1 { return NewPos() } else { // [] means root of tree, and there's only one root return nil } } // NextOfKin returns the position of the sibling of the parent of tp if // that position exists, and if not, the sibling of the parent's parent, // and on upwards. If the next of kin is not found, nil is returned. func NextOfKin(tp IPos, tree IModel) IPos { var res IPos parent := ParentPosition(tp) if parent != nil { res = NextSiblingPosition(parent, tree) if res == nil { res = NextOfKin(parent, tree) } } return res } // NextPosition is used to navigate the tree in a depth first // manner. Starting at the current position, the first child is // selected. If there isn't one, then the current node's sibling is // selected. If there isn't one, then the "next of kin" is selected. func NextPosition(tp IPos, tree IModel) IPos { var res IPos = FirstChildPosition(tp, tree) if res == nil { res = NextSiblingPosition(tp, tree) if res == nil { res = NextOfKin(tp, tree) } } return res } // PreviousPosition is used to navigate the tree backwards in a depth first // manner. func PreviousPosition(tp IPos, tree IModel) IPos { var res IPos prevSib := PreviousSiblingPosition(tp, tree) if prevSib != nil { res = LastDescendant(prevSib, tree) } else { res = ParentPosition(tp) } return res } //====================================================================== type ISearchPred interface { CheckNode(IModel, IPos) bool } type SearchPred func(IModel, IPos) bool func (s SearchPred) CheckNode(tree IModel, pos IPos) bool { return s(tree, pos) } func DepthFirstSearch(tree IModel, fn ISearchPred) IPos { pos := NewPos() return depthFirstSearchImpl(tree, pos, fn) } func depthFirstSearchImpl(tree IModel, pos *TreePos, fn ISearchPred) IPos { if tree == nil { return nil } if fn.CheckNode(tree, pos) { return pos } cs := tree.Children() tpos := pos.Copy().(*TreePos) tpos.Pos = append(tpos.Pos, 0) i := 0 for cs.Next() { tpos.Pos[len(tpos.Pos)-1] = i rpos := depthFirstSearchImpl(cs.Value(), tpos, fn) if rpos != nil { return rpos } i += 1 } return nil } //====================================================================== type IWidgetMaker interface { MakeWidget(pos IPos, tree IModel) gowid.IWidget } type WidgetMakerFunction func(pos IPos, tree IModel) gowid.IWidget func (f WidgetMakerFunction) MakeWidget(pos IPos, tree IModel) gowid.IWidget { return f(pos, tree) } type IDecorator interface { MakeDecoration(pos IPos, tree IModel, wmaker IWidgetMaker) gowid.IWidget } type DecoratorFunction func(pos IPos, tree IModel, wmaker IWidgetMaker) gowid.IWidget func (f DecoratorFunction) MakeDecoration(pos IPos, tree IModel, wmaker IWidgetMaker) gowid.IWidget { return f(pos, tree, wmaker) } type ITreeWalker interface { Tree() IModel Maker() IWidgetMaker Decorator() IDecorator Focus() list.IWalkerPosition } type TreeWalker struct { tree IModel pos IPos maker IWidgetMaker decorator IDecorator *gowid.Callbacks gowid.FocusCallbacks } var _ ITreeWalker = (*TreeWalker)(nil) var _ list.IWalker = (*TreeWalker)(nil) func NewWalker(tree IModel, pos IPos, maker IWidgetMaker, dec IDecorator) *TreeWalker { cb := gowid.NewCallbacks() res := &TreeWalker{ tree: tree, pos: pos, maker: maker, decorator: dec, Callbacks: cb, } res.FocusCallbacks = gowid.FocusCallbacks{CB: &res.Callbacks} return res } func (f *TreeWalker) Tree() IModel { return f.tree } func (f *TreeWalker) Maker() IWidgetMaker { return f.maker } func (f *TreeWalker) Decorator() IDecorator { return f.decorator } // list.IWalker func (f *TreeWalker) At(pos list.IWalkerPosition) gowid.IWidget { if pos == nil { return nil } return WidgetAt(f, pos.(IPos)) } func WidgetAt(walker ITreeWalker, pos IPos) gowid.IWidget { stree := pos.GetSubStructure(walker.Tree()) return walker.Decorator().MakeDecoration(pos, stree, walker.Maker()) } // list.IWalker func (f *TreeWalker) Focus() list.IWalkerPosition { return f.pos } func (f *TreeWalker) SetFocus(pos list.IWalkerPosition, app gowid.IApp) { old := f.pos f.pos = pos.(IPos) if !old.Equal(f.pos) { // the new widget in focus gowid.RunWidgetCallbacks(f.Callbacks, gowid.FocusCB{}, app, f) } } type IWalkerCallback interface { gowid.IIdentity Changed(app gowid.IApp, tree ITreeWalker, data ...interface{}) } type walkerCallbackProxy struct { IWalkerCallback } func (p walkerCallbackProxy) Call(args ...interface{}) { t := args[0].(gowid.IApp) w := args[1].(ITreeWalker) p.IWalkerCallback.Changed(t, w, args[2:]...) } type WalkerFunction func(app gowid.IApp, tree ITreeWalker) func (f WalkerFunction) Changed(app gowid.IApp, tree ITreeWalker, data ...interface{}) { f(app, tree) } // WidgetCallback is a simple struct with a name field for IIdentity and // that embeds a WidgetChangedFunction to be issued as a callback when a widget // property changes. type WalkerCallback struct { Name interface{} WalkerFunction } func MakeCallback(name interface{}, fn WalkerFunction) WalkerCallback { return WalkerCallback{ Name: name, WalkerFunction: fn, } } func (f WalkerCallback) ID() interface{} { return f.Name } func RunWalkerCallbacks(c gowid.ICallbacks, name interface{}, app gowid.IApp, data ...interface{}) { data2 := append([]interface{}{app}, data...) c.RunCallbacks(name, data2...) } func AddWalkerCallback(c gowid.ICallbacks, name interface{}, cb IWalkerCallback) { c.AddCallback(name, walkerCallbackProxy{cb}) } func RemoveWalkerCallback(c gowid.ICallbacks, name interface{}, id gowid.IIdentity) { c.RemoveCallback(name, id) } func (t *TreeWalker) OnFocusChanged(f IWalkerCallback) { AddWalkerCallback(t, gowid.FocusCB{}, f) } func (t *TreeWalker) RemoveOnFocusChanged(f gowid.IIdentity) { RemoveWalkerCallback(t, gowid.FocusCB{}, f) } // list.IWalker func (f *TreeWalker) Next(pos list.IWalkerPosition) list.IWalkerPosition { return WalkerNext(f, pos) } // list.IWalker func (f *TreeWalker) Previous(pos list.IWalkerPosition) list.IWalkerPosition { return WalkerPrevious(f, pos) } //====================================================================== func WalkerNext(f ITreeWalker, pos list.IWalkerPosition) list.IWalkerPosition { fc := pos.(IPos) np := NextPosition(fc, f.Tree()) if np != nil { return np } return nil } func WalkerPrevious(f ITreeWalker, pos list.IWalkerPosition) list.IWalkerPosition { fc := pos.(IPos) np := PreviousPosition(fc, f.Tree()) if np != nil { return np } return nil } //====================================================================== // Could consider something more sophisticated e.g. https://godoc.org/github.com/hashicorp/golang-lru // // CacheKey tracks content for a given tree and its expansion state. Note tree position isn't tracked // in case objects are inserted into the tree in such a way that the position moves. type CacheKey struct { Tree IModel Exp bool } type CachingMaker struct { IWidgetMaker cache *lru.Cache } type CachingMakerOptions struct { CacheSize int } func NewCachingMaker(dec IWidgetMaker, opts ...CachingMakerOptions) *CachingMaker { var opt CachingMakerOptions if len(opts) > 0 { opt = opts[0] } else { opt = CachingMakerOptions{4096} } cache, err := lru.New(opt.CacheSize) if err != nil { panic(err) } return &CachingMaker{dec, cache} } func (d *CachingMaker) MakeWidget(pos IPos, tree IModel) gowid.IWidget { exp := true if ct, ok := tree.(ICollapsible); ok { exp = !ct.IsCollapsed() } key := CacheKey{tree, exp} if w, ok := d.cache.Get(key); ok { return w.(gowid.IWidget) } else { w = d.IWidgetMaker.MakeWidget(pos, tree) if w != nil { d.cache.Add(key, w) } return w.(gowid.IWidget) } } //====================================================================== type CachingDecorator struct { IDecorator cache *lru.Cache } type CachingDecoratorOptions struct { CacheSize int } func NewCachingDecorator(dec IDecorator, opts ...CachingDecoratorOptions) *CachingDecorator { var opt CachingDecoratorOptions if len(opts) > 0 { opt = opts[0] } else { opt = CachingDecoratorOptions{4096} } cache, err := lru.New(opt.CacheSize) if err != nil { panic(err) } return &CachingDecorator{dec, cache} } func (d *CachingDecorator) MakeDecoration(pos IPos, tree IModel, wmaker IWidgetMaker) gowid.IWidget { exp := true if ct, ok := tree.(ICollapsible); ok { exp = !ct.IsCollapsed() } key := CacheKey{tree, exp} var w gowid.IWidget if wc, ok := d.cache.Get(key); ok { return wc.(gowid.IWidget) } else { w = d.IDecorator.MakeDecoration(pos, tree, wmaker) if w != nil { d.cache.Add(key, w) } return w } } //====================================================================== func New(walker list.IWalker) *list.Widget { res := list.New(walker) var _ gowid.IWidget = res return res } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: gowid-1.4.0/widgets/tree/tree_test.go000066400000000000000000000044451426234454000175720ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source code is governed by the MIT license // that can be found in the LICENSE file. package tree import ( "testing" log "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" ) func TestTree0(t *testing.T) { var _ IPos = (*TreePos)(nil) } func TestTreePos1(t *testing.T) { tp1 := TreePos{ Pos: []int{3, 4, 5}, } tp2 := TreePos{ Pos: []int{3, 4, 3}, } assert.Equal(t, true, tp1.GreaterThan(&tp2)) assert.Equal(t, false, tp2.GreaterThan(&tp1)) } func TestTreePos2(t *testing.T) { tp1 := TreePos{ Pos: []int{3, 4, 5, 6}, } tp2 := TreePos{ Pos: []int{3, 4, 3}, } assert.Equal(t, true, tp1.GreaterThan(&tp2)) assert.Equal(t, false, tp2.GreaterThan(&tp1)) } func TestTree1(t *testing.T) { // t <-> 0 // l1,s1,l2,l3 <-> t // s1,l2,l3 <-> t,l1 // l4,l5,l2,l3 <-> t,l1,s2 leaf1 := &Tree{"leaf1", []IModel{}} leaf2 := &Tree{"leaf2", []IModel{}} leaf3 := &Tree{"leaf3", []IModel{}} leaf4 := &Tree{"leaf4", []IModel{}} leaf5 := &Tree{"leaf5", []IModel{}} stree1 := &Tree{"stree1", []IModel{leaf4, leaf5}} parent1 := &Tree{"parent1", []IModel{leaf1, stree1, leaf2, leaf3}} log.Infof("Tree is %s", parent1.String()) var lpos, spos IPos for spos = NewPos(); spos != nil; spos = NextPosition(spos, parent1) { log.Infof("Cur pos is %v and tree is %v", spos.String(), spos.GetSubStructure(parent1).String()) lpos = spos.Copy() log.Infof("Last pos loop was %v", lpos.String()) } log.Infof("Last pos was %v", lpos.String()) for spos = lpos; spos != nil; spos = PreviousPosition(spos, parent1) { log.Infof("Backwards Cur pos is %v and tree is %v", spos.String(), spos.GetSubStructure(parent1).String()) } tp := NewPosExt([]int{0}) tt := tp.GetSubStructure(parent1) assert.Equal(t, leaf1, tt) count := 0 var pos IPos dfs := DepthFirstSearch(parent1, SearchPred(func(t IModel, p IPos) bool { count += 1 t2 := t.(*Tree) pos = p return t2.theLeaf == "leaf2" })) pos2 := pos.(*TreePos) assert.NotNil(t, dfs) assert.Equal(t, 6, count) assert.Equal(t, []int{2}, pos2.Indices()) st := pos2.GetSubStructure(parent1).(*Tree) assert.Equal(t, "leaf2", st.theLeaf) } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: gowid-1.4.0/widgets/vpadding/000077500000000000000000000000001426234454000160735ustar00rootroot00000000000000gowid-1.4.0/widgets/vpadding/vpadding.go000066400000000000000000000223041426234454000202170ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. // Package vpadding provides a widget that pads an inner widget on the top and bottom. package vpadding import ( "errors" "fmt" "github.com/gcla/gowid" "github.com/gcla/gowid/widgets/fill" tcell "github.com/gdamore/tcell/v2" ) //====================================================================== type IVerticalPadding interface { Align() gowid.IVAlignment Height() gowid.IWidgetDimension } type IWidget interface { gowid.ICompositeWidget IVerticalPadding } // Widget wraps a widget and aligns it vertically according to the supplied arguments. The wrapped // widget can be aligned to the top, bottom or middle, and can be provided with a specific height in #lines. // type Widget struct { gowid.IWidget alignment gowid.IVAlignment height gowid.IWidgetDimension *gowid.Callbacks gowid.FocusCallbacks gowid.SubWidgetCallbacks } func New(inner gowid.IWidget, alignment gowid.IVAlignment, height gowid.IWidgetDimension) *Widget { res := &Widget{ IWidget: inner, alignment: alignment, height: height, } res.FocusCallbacks = gowid.FocusCallbacks{CB: &res.Callbacks} res.SubWidgetCallbacks = gowid.SubWidgetCallbacks{CB: &res.Callbacks} var _ gowid.IWidget = res return res } func NewBox(inner gowid.IWidget, rows int) *Widget { return New(inner, gowid.VAlignTop{}, gowid.RenderWithUnits{U: rows}) } func (w *Widget) String() string { return fmt.Sprintf("vpad[%v]", w.SubWidget()) } func (w *Widget) SubWidget() gowid.IWidget { return w.IWidget } func (w *Widget) SetSubWidget(wi gowid.IWidget, app gowid.IApp) { w.IWidget = wi gowid.RunWidgetCallbacks(w.Callbacks, gowid.SubWidgetCB{}, app, w) } func (w *Widget) OnSetAlign(f gowid.IWidgetChangedCallback) { gowid.AddWidgetCallback(w.Callbacks, gowid.VAlignCB{}, f) } func (w *Widget) RemoveOnSetAlign(f gowid.IIdentity) { gowid.RemoveWidgetCallback(w.Callbacks, gowid.VAlignCB{}, f) } func (w *Widget) Align() gowid.IVAlignment { return w.alignment } func (w *Widget) SetAlign(i gowid.IVAlignment, app gowid.IApp) { w.alignment = i gowid.RunWidgetCallbacks(w.Callbacks, gowid.VAlignCB{}, app, w) } func (w *Widget) OnSetHeight(f gowid.IWidgetChangedCallback) { gowid.AddWidgetCallback(w.Callbacks, gowid.HeightCB{}, f) } func (w *Widget) RemoveOnSetHeight(f gowid.IIdentity) { gowid.RemoveWidgetCallback(w.Callbacks, gowid.HeightCB{}, f) } func (w *Widget) Height() gowid.IWidgetDimension { return w.height } func (w *Widget) SetHeight(i gowid.IWidgetDimension, app gowid.IApp) { w.height = i gowid.RunWidgetCallbacks(w.Callbacks, gowid.HeightCB{}, app, w) } // SubWidgetSize returns the size that will be passed down to the // subwidget's Render(), based on the size passed to the current widget. // If this widget is rendered in a Flow context and the vertical height // specified is in Units, then the subwidget is rendered in a Box content // with Units-number-of-rows. This gives the subwidget an opportunity to // render to fill the space given to it, rather than risking truncation. If // the subwidget cannot render in Box mode, then wrap it in a // FlowToBoxWidget first. // func (w *Widget) SubWidgetSize(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.IRenderSize { return SubWidgetSize(w, size, focus, app) } func (w *Widget) RenderSize(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.IRenderBox { return gowid.CalculateRenderSizeFallback(w, size, focus, app) } func (w *Widget) UserInput(ev interface{}, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) bool { return UserInput(w, ev, size, focus, app) } func (w *Widget) Render(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.ICanvas { return Render(w, size, focus, app) } //'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' func SubWidgetSize(w IVerticalPadding, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.IRenderSize { size2 := size // If there is a vertical offset specified, the relative features should reduce the size of the // supplied size i.e. it should be relative to the reduced screen size switch al := w.Align().(type) { case gowid.VAlignTop: switch s := size.(type) { case gowid.IRenderBox: size2 = gowid.RenderBox{C: s.BoxColumns(), R: s.BoxRows() - al.Margin} } } // rows := -1 // switch ss := w.Height().(type) { // case gowid.IRenderWithUnits: // rows = ss.Units() // } return gowid.ComputeVerticalSubSizeUnsafe(size2, w.Height(), -1, -1) } func Render(w IWidget, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.ICanvas { var subWidgetCanvas gowid.ICanvas var rowsToUseInResult int subSize := w.SubWidgetSize(size, focus, app) subWidgetCanvas = w.SubWidget().Render(subSize, focus, app) subWidgetRows := subWidgetCanvas.BoxRows() // Compute number of rows to use in final canvas switch sz := size.(type) { case gowid.IRenderBox: rowsToUseInResult = sz.BoxRows() case gowid.IRenderFlowWith: switch w.Height().(type) { case gowid.IRenderFlow, gowid.IRenderFixed, gowid.IRenderWithUnits: rowsToUseInResult = subWidgetRows default: panic(fmt.Errorf("Height spec %v cannot be used in flow mode for %T", w.Height(), w)) } case gowid.IRenderFixed: switch w.Height().(type) { case gowid.IRenderFlow, gowid.IRenderFixed: rowsToUseInResult = subWidgetRows switch al := w.Align().(type) { case gowid.VAlignTop: rowsToUseInResult += al.Margin } case gowid.IRenderWithUnits: rowsToUseInResult = w.Height().(gowid.IRenderWithUnits).Units() default: panic(fmt.Errorf("This spec %v of type %T cannot be used in flow mode for %T", w.Height(), w.Height(), w)) } default: panic(gowid.WidgetSizeError{Widget: w, Size: size}) } maxCol := subWidgetCanvas.BoxColumns() fill := fill.NewEmpty() switch al := w.Align().(type) { case gowid.VAlignBottom: if rowsToUseInResult > subWidgetRows { fc := fill.Render(gowid.RenderBox{C: maxCol, R: rowsToUseInResult - subWidgetRows}, gowid.NotSelected, app) fc.AppendBelow(subWidgetCanvas, true, false) subWidgetCanvas = fc } else { subWidgetCanvas.Truncate(rowsToUseInResult-subWidgetRows, 0) } case gowid.VAlignMiddle: if rowsToUseInResult > subWidgetRows { topl := (rowsToUseInResult - subWidgetRows) / 2 bottoml := rowsToUseInResult - (topl + subWidgetRows) fc1 := fill.Render(gowid.RenderBox{C: maxCol, R: topl}, gowid.NotSelected, app) fc2 := fill.Render(gowid.RenderBox{C: maxCol, R: bottoml}, gowid.NotSelected, app) fc1.AppendBelow(subWidgetCanvas, true, false) subWidgetCanvas = fc1 subWidgetCanvas.AppendBelow(fc2, false, false) } else { topl := (subWidgetRows - rowsToUseInResult) / 2 bottoml := subWidgetRows - (rowsToUseInResult + topl) subWidgetCanvas.Truncate(topl, bottoml) } case gowid.VAlignTop: if rowsToUseInResult > subWidgetRows+al.Margin { topl := al.Margin bottoml := rowsToUseInResult - (topl + subWidgetRows) fc1 := fill.Render(gowid.RenderBox{C: maxCol, R: topl}, gowid.NotSelected, app) fc2 := fill.Render(gowid.RenderBox{C: maxCol, R: bottoml}, gowid.NotSelected, app) fc1.AppendBelow(subWidgetCanvas, true, false) subWidgetCanvas = fc1 subWidgetCanvas.AppendBelow(fc2, false, false) } else if rowsToUseInResult > al.Margin { topl := al.Margin bottoml := subWidgetRows - (rowsToUseInResult - al.Margin) subWidgetCanvas.Truncate(0, bottoml) fc1 := fill.Render(gowid.RenderBox{C: maxCol, R: topl}, gowid.NotSelected, app) fc1.AppendBelow(subWidgetCanvas, true, false) subWidgetCanvas = fc1 } else { topl := rowsToUseInResult subWidgetCanvas = fill.Render(gowid.RenderBox{C: maxCol, R: topl}, gowid.NotSelected, app) } default: panic(errors.New("Invalid vertical alignment setting")) } // The embedded widget might have rendered with a different width gowid.MakeCanvasRightSize(subWidgetCanvas, size) return subWidgetCanvas } func UserInput(w IWidget, ev interface{}, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) bool { subSize := w.SubWidgetSize(size, focus, app) subWidgetBox := w.SubWidget().RenderSize(subSize, focus, app) subWidgetRows := subWidgetBox.BoxRows() myBox := w.RenderSize(size, focus, app) rowsToUseInResult := myBox.BoxRows() var yd int switch al := w.Align().(type) { case gowid.VAlignBottom: yd = subWidgetRows - rowsToUseInResult case gowid.VAlignMiddle: yd = (subWidgetRows - rowsToUseInResult) / 2 case gowid.VAlignTop: if rowsToUseInResult > subWidgetRows+al.Margin { yd = -al.Margin } else if rowsToUseInResult > al.Margin { yd = -al.Margin } else { yd = -(rowsToUseInResult - 1) } } // Note that yd will be less than zero, so this translates upwards transEv := gowid.TranslatedMouseEvent(ev, 0, yd) if evm, ok := transEv.(*tcell.EventMouse); ok { _, transY := evm.Position() if transY < subWidgetRows && transY >= 0 { return gowid.UserInputIfSelectable(w.SubWidget(), transEv, subSize, focus, app) } } else { return gowid.UserInputIfSelectable(w.SubWidget(), transEv, subSize, focus, app) } return false } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: gowid-1.4.0/widgets/vpadding/vpadding_test.go000066400000000000000000000161721426234454000212640ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source code is governed by the MIT license // that can be found in the LICENSE file. package vpadding import ( "strings" "testing" "github.com/gcla/gowid" "github.com/gcla/gowid/gwtest" "github.com/gcla/gowid/widgets/checkbox" "github.com/gcla/gowid/widgets/fill" "github.com/gcla/gowid/widgets/pile" "github.com/gcla/gowid/widgets/text" tcell "github.com/gdamore/tcell/v2" log "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" ) func TestVerticalPadding1(t *testing.T) { w1 := New(fill.New('x'), gowid.VAlignMiddle{}, gowid.RenderWithUnits{U: 2}) c1 := w1.Render(gowid.RenderBox{C: 3, R: 4}, gowid.Focused, gwtest.D) assert.Equal(t, c1.String(), " \nxxx\nxxx\n ") w2 := New(fill.New('x'), gowid.VAlignMiddle{}, gowid.RenderWithRatio{0.5}) c2 := w2.Render(gowid.RenderBox{C: 3, R: 4}, gowid.Focused, gwtest.D) assert.Equal(t, c2.String(), " \nxxx\nxxx\n ") w3 := New(fill.New('x'), gowid.VAlignMiddle{}, gowid.RenderWithRatio{0.75}) c3 := w3.Render(gowid.RenderBox{C: 3, R: 4}, gowid.Focused, gwtest.D) assert.Equal(t, c3.String(), "xxx\nxxx\nxxx\n ") w4 := New(fill.New('x'), gowid.VAlignTop{}, gowid.RenderWithRatio{0.75}) c4 := w4.Render(gowid.RenderBox{C: 3, R: 4}, gowid.Focused, gwtest.D) assert.Equal(t, c4.String(), "xxx\nxxx\nxxx\n ") w5 := New(fill.New('x'), gowid.VAlignMiddle{}, gowid.RenderWithRatio{0.8}) c5 := w5.Render(gowid.RenderBox{C: 3, R: 4}, gowid.Focused, gwtest.D) assert.Equal(t, c5.String(), "xxx\nxxx\nxxx\n ") w6 := New(fill.New('x'), gowid.VAlignMiddle{}, gowid.RenderWithRatio{0.88}) c6 := w6.Render(gowid.RenderBox{C: 3, R: 4}, gowid.Focused, gwtest.D) assert.Equal(t, c6.String(), "xxx\nxxx\nxxx\nxxx") w7 := New(fill.New('x'), gowid.VAlignTop{1}, gowid.RenderWithUnits{U: 3}) c7 := w7.Render(gowid.RenderBox{C: 3, R: 4}, gowid.Focused, gwtest.D) assert.Equal(t, c7.String(), " \nxxx\nxxx\nxxx") w8 := New(fill.New('x'), gowid.VAlignTop{2}, gowid.RenderWithUnits{U: 3}) c8 := w8.Render(gowid.RenderBox{C: 3, R: 4}, gowid.Focused, gwtest.D) assert.Equal(t, c8.String(), " \n \nxxx\nxxx") w9 := New(fill.New('x'), gowid.VAlignTop{2}, gowid.RenderWithUnits{U: 3}) c9 := w9.Render(gowid.RenderBox{C: 3, R: 3}, gowid.Focused, gwtest.D) assert.Equal(t, c9.String(), " \n \nxxx") w10 := New(fill.New('x'), gowid.VAlignTop{2}, gowid.RenderWithUnits{U: 4}) c10 := w10.Render(gowid.RenderBox{C: 3, R: 8}, gowid.Focused, gwtest.D) assert.Equal(t, c10.String(), " \n \nxxx\nxxx\nxxx\nxxx\n \n ") w11 := New(fill.New('x'), gowid.VAlignBottom{}, gowid.RenderWithUnits{U: 3}) c11 := w11.Render(gowid.RenderBox{C: 3, R: 4}, gowid.Focused, gwtest.D) assert.Equal(t, c11.String(), " \nxxx\nxxx\nxxx") p1 := pile.New([]gowid.IContainerWidget{ &gowid.ContainerWidget{ IWidget: text.New("111"), D: gowid.RenderFixed{}, }, &gowid.ContainerWidget{ IWidget: text.New("222"), D: gowid.RenderFixed{}, }, &gowid.ContainerWidget{ IWidget: text.New("333"), D: gowid.RenderFixed{}, }, &gowid.ContainerWidget{ IWidget: text.New("444"), D: gowid.RenderFixed{}, }, }) w12 := New(p1, gowid.VAlignBottom{}, gowid.RenderWithUnits{U: 3}) c12 := w12.Render(gowid.RenderBox{C: 3, R: 4}, gowid.Focused, gwtest.D) assert.Equal(t, c12.String(), " \n111\n222\n333") w13 := New(p1, gowid.VAlignBottom{}, gowid.RenderWithUnits{U: 3}) c13 := w13.Render(gowid.RenderBox{C: 3, R: 2}, gowid.Focused, gwtest.D) assert.Equal(t, c13.String(), "111\n222") for _, w := range []gowid.IWidget{w1, w2, w3, w4, w5, w6, w7, w8, w9, w10} { gwtest.RenderBoxManyTimes(t, w, 0, 10, 0, 10) } for _, w := range []gowid.IWidget{w1, w7, w8, w9, w10} { gwtest.RenderFlowManyTimes(t, w, 0, 10) } } func TestCanvas17(t *testing.T) { widget1i := text.New("line 1line 2line 3") widget1 := NewBox(widget1i, 2) canvas1 := widget1.Render(gowid.RenderFlowWith{C: 6}, gowid.NotSelected, gwtest.D) log.Infof("Widget17 is %v", widget1) log.Infof("Canvas17 is %s", canvas1.String()) res := strings.Join([]string{"line 1", "line 2"}, "\n") if res != canvas1.String() { t.Errorf("Failed") } } func TestCanvas18(t *testing.T) { widget1i := text.New("line 1 line 2 line 3") widget1 := NewBox(widget1i, 5) canvas1 := widget1.Render(gowid.RenderFlowWith{C: 6}, gowid.NotSelected, gwtest.D) log.Infof("Widget18 is %v", widget1) log.Infof("Canvas18 is %s", canvas1.String()) res := strings.Join([]string{"line 1", " line ", "2 line", " 3 ", " "}, "\n") if res != canvas1.String() { t.Errorf("Failed") } gwtest.RenderBoxManyTimes(t, widget1, 0, 10, 0, 10) gwtest.RenderFlowManyTimes(t, widget1, 0, 10) gwtest.RenderFixedDoesNotPanic(t, widget1) } func TestCheckbox3(t *testing.T) { ct := &gwtest.CheckBoxTester{Gotit: false} assert.Equal(t, ct.Gotit, false) w := checkbox.New(false) w.OnClick(ct) ev := tcell.NewEventKey(tcell.KeyRune, ' ', tcell.ModNone) w.UserInput(ev, gowid.RenderFixed{}, gowid.Focused, gwtest.D) assert.Equal(t, ct.Gotit, true) ct.Gotit = false assert.Equal(t, ct.Gotit, false) evlmx1y0 := tcell.NewEventMouse(1, 0, tcell.Button1, 0) evnonex1y0 := tcell.NewEventMouse(1, 0, tcell.ButtonNone, 0) sz := gowid.RenderBox{C: 5, R: 3} w2 := New(w, gowid.VAlignTop{}, gowid.RenderFixed{}) ct.Gotit = false assert.Equal(t, ct.Gotit, false) w2.UserInput(evlmx1y0, sz, gowid.Focused, gwtest.D) gwtest.D.SetLastMouseState(gowid.MouseState{true, false, false}) w2.UserInput(evnonex1y0, sz, gowid.Focused, gwtest.D) gwtest.D.SetLastMouseState(gowid.MouseState{}) assert.Equal(t, ct.Gotit, true) w3 := New(w, gowid.VAlignBottom{}, gowid.RenderFixed{}) ct.Gotit = false assert.Equal(t, ct.Gotit, false) w3.UserInput(evlmx1y0, sz, gowid.Focused, gwtest.D) gwtest.D.SetLastMouseState(gowid.MouseState{true, false, false}) w3.UserInput(evnonex1y0, sz, gowid.Focused, gwtest.D) gwtest.D.SetLastMouseState(gowid.MouseState{}) assert.Equal(t, ct.Gotit, false) evlmx1y2 := tcell.NewEventMouse(1, 2, tcell.ButtonNone, 0) w3.UserInput(evlmx1y2, gowid.RenderBox{C: 10, R: 1}, gowid.Focused, gwtest.D) gwtest.D.SetLastMouseState(gowid.MouseState{true, false, false}) w3.UserInput(evnonex1y0, gowid.RenderBox{C: 10, R: 1}, gowid.Focused, gwtest.D) gwtest.D.SetLastMouseState(gowid.MouseState{}) assert.Equal(t, ct.Gotit, true) w4 := New(w, gowid.VAlignTop{1}, gowid.RenderFixed{}) ct.Gotit = false assert.Equal(t, ct.Gotit, false) w4.UserInput(evlmx1y0, sz, gowid.Focused, gwtest.D) gwtest.D.SetLastMouseState(gowid.MouseState{true, false, false}) w4.UserInput(evnonex1y0, sz, gowid.Focused, gwtest.D) gwtest.D.SetLastMouseState(gowid.MouseState{}) assert.Equal(t, ct.Gotit, false) evlmx1y1 := tcell.NewEventMouse(1, 1, tcell.Button1, 0) evnonex1y1 := tcell.NewEventMouse(1, 1, tcell.ButtonNone, 0) w4.UserInput(evlmx1y1, sz, gowid.Focused, gwtest.D) gwtest.D.SetLastMouseState(gowid.MouseState{true, false, false}) w4.UserInput(evnonex1y1, sz, gowid.Focused, gwtest.D) gwtest.D.SetLastMouseState(gowid.MouseState{}) assert.Equal(t, ct.Gotit, true) } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: gowid-1.4.0/widgets/vscroll/000077500000000000000000000000001426234454000157635ustar00rootroot00000000000000gowid-1.4.0/widgets/vscroll/vscroll.go000066400000000000000000000230521426234454000200000ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source // code is governed by the MIT license that can be found in the LICENSE // file. // Package vscroll provides a vertical scrollbar widget with mouse support. See the editor // demo for more. package vscroll import ( "fmt" "github.com/gcla/gowid" "github.com/gcla/gowid/gwutil" tcell "github.com/gdamore/tcell/v2" ) //====================================================================== type VerticalScrollbarRunes struct { Up, Down, Space, Handle rune } var ( VerticalScrollbarAsciiRunes = VerticalScrollbarRunes{'^', 'v', ' ', '#'} VerticalScrollbarUnicodeRunes = VerticalScrollbarRunes{'▲', '▼', ' ', '█'} ) //====================================================================== type ClickUp struct{} type ClickDown struct{} type ClickAbove struct{} type ClickBelow struct{} type RightClick struct{} //====================================================================== type IVerticalScrollbar interface { GetTop() int GetMiddle() int GetBottom() int ClickUp(app gowid.IApp) ClickDown(app gowid.IApp) ClickAbove(app gowid.IApp) ClickBelow(app gowid.IApp) GetRunes() VerticalScrollbarRunes } type IRightMouseClick interface { RightClick(frac float32, app gowid.IApp) } type IWidget interface { gowid.IWidget IVerticalScrollbar } type Widget struct { Top int Middle int Bottom int Runes VerticalScrollbarRunes Callbacks *gowid.Callbacks gowid.IsSelectable } func New() *Widget { return NewWithChars(VerticalScrollbarAsciiRunes) } func NewUnicode() *Widget { return NewWithChars(VerticalScrollbarUnicodeRunes) } func NewWithChars(runes VerticalScrollbarRunes) *Widget { return NewExt(runes) } func NewExt(runes VerticalScrollbarRunes) *Widget { return &Widget{ Top: -1, Middle: -1, Bottom: -1, Runes: runes, Callbacks: gowid.NewCallbacks(), } } func (w *Widget) String() string { return fmt.Sprintf("vscroll[t=%d,m=%d,b=%d]", w.GetTop(), w.GetMiddle(), w.GetBottom()) } func (w *Widget) GetTop() int { return w.Top } func (w *Widget) GetMiddle() int { return w.Middle } func (w *Widget) GetBottom() int { return w.Bottom } func (w *Widget) OnClickBelow(f gowid.IWidgetChangedCallback) { gowid.AddWidgetCallback(w.Callbacks, ClickBelow{}, f) } func (w *Widget) RemoveOnClickBelow(f gowid.IIdentity) { gowid.RemoveWidgetCallback(w.Callbacks, ClickBelow{}, f) } func (w *Widget) OnClickAbove(f gowid.IWidgetChangedCallback) { gowid.AddWidgetCallback(w.Callbacks, ClickAbove{}, f) } func (w *Widget) RemoveOnClickAbove(f gowid.IIdentity) { gowid.RemoveWidgetCallback(w.Callbacks, ClickAbove{}, f) } func (w *Widget) OnRightClick(f gowid.IWidgetChangedCallback) { gowid.AddWidgetCallback(w.Callbacks, RightClick{}, f) } func (w *Widget) RemoveOnRightClick(f gowid.IIdentity) { gowid.RemoveWidgetCallback(w.Callbacks, RightClick{}, f) } func (w *Widget) OnClickDownArrow(f gowid.IWidgetChangedCallback) { gowid.AddWidgetCallback(w.Callbacks, ClickDown{}, f) } func (w *Widget) RemoveOnClickDownArrow(f gowid.IIdentity) { gowid.RemoveWidgetCallback(w.Callbacks, ClickDown{}, f) } func (w *Widget) OnClickUpArrow(f gowid.IWidgetChangedCallback) { gowid.AddWidgetCallback(w.Callbacks, ClickUp{}, f) } func (w *Widget) RemoveOnClickUpArrow(f gowid.IIdentity) { gowid.RemoveWidgetCallback(w.Callbacks, ClickUp{}, f) } func (w *Widget) RenderSize(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.IRenderBox { return RenderSize(w, size, focus, app) } func (w *Widget) UserInput(ev interface{}, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) bool { return UserInput(w, ev, size, focus, app) } func (w *Widget) Render(size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.ICanvas { return Render(w, size, focus, app) } func (w *Widget) GetRunes() VerticalScrollbarRunes { return w.Runes } func (w *Widget) ClickUp(app gowid.IApp) { gowid.RunWidgetCallbacks(w.Callbacks, ClickUp{}, app, w) } func (w *Widget) ClickDown(app gowid.IApp) { gowid.RunWidgetCallbacks(w.Callbacks, ClickDown{}, app, w) } func (w *Widget) ClickAbove(app gowid.IApp) { gowid.RunWidgetCallbacks(w.Callbacks, ClickAbove{}, app, w) } func (w *Widget) ClickBelow(app gowid.IApp) { gowid.RunWidgetCallbacks(w.Callbacks, ClickBelow{}, app, w) } func (w *Widget) RightClick(frac float32, app gowid.IApp) { gowid.RunWidgetCallbacks(w.Callbacks, RightClick{}, app, w, frac) } //'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' func RenderSize(w interface{}, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.IRenderBox { cols, haveCols := size.(gowid.IColumns) rows, haveRows := size.(gowid.IRows) switch { case haveCols && haveRows: return gowid.RenderBox{C: cols.Columns(), R: rows.Rows()} case haveCols: return gowid.RenderBox{C: cols.Columns(), R: 1} default: panic(gowid.WidgetSizeError{Widget: w, Size: size}) } } func UserInput(w IVerticalScrollbar, ev interface{}, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) bool { if ev2, ok := ev.(*tcell.EventMouse); ok { switch ev2.Buttons() { case tcell.Button1, tcell.Button3: b3 := (ev2.Buttons() == tcell.Button3) t, m, b := w.GetTop(), w.GetMiddle(), w.GetBottom() rows := 1 if box, ok := size.(gowid.IRenderBox); ok { rows = box.BoxRows() } //t, m, b = granularizeSplits(t, m, b, gwutil.Max(0, rows-2)) splits := gwutil.HamiltonAllocation([]int{t, m, b}, gwutil.Max(0, rows-2)) // Make sure that the "handle" in the middle is always at least 1 row tall if splits[1] == 0 { fixSplit(1, 0, 2, &splits) } // Make sure that unless we're at the top, there is a space to click to go closer to the top // and vice-versa for the bottom if t != 0 && splits[0] == 0 { fixSplit(0, 1, 2, &splits) } if b != 0 && splits[2] == 0 { fixSplit(2, 0, 1, &splits) } _, y := ev2.Position() res := false switch b3 { case true: if w, ok := w.(IRightMouseClick); ok { var frac float32 switch { case y == 0: frac = 0.0 case y <= splits[2]+splits[1]+splits[0]: if rows > 2 { frac = float32(y-1) / float32(rows-2) } default: frac = 1.0 } w.RightClick(frac, app) res = true } case false: switch { case y == 0: w.ClickUp(app) res = true case y <= splits[0]: w.ClickAbove(app) res = true case y <= splits[1]+splits[0]: case y <= splits[2]+splits[1]+splits[0]: w.ClickBelow(app) res = true default: w.ClickDown(app) res = true } } return res default: return false } } else { return false } } func fixSplit(i int, o1, o2 int, splits *[]int) { if (*splits)[o1] > (*splits)[o2] { if (*splits)[o1] > 2 { (*splits)[i]++ (*splits)[o1]-- } } else { if (*splits)[o2] > 2 { (*splits)[i]++ (*splits)[o2]-- } } } func Render(w IWidget, size gowid.IRenderSize, focus gowid.Selector, app gowid.IApp) gowid.ICanvas { // If a col and row not provided, what can I choose?? cl, isCols := size.(gowid.IColumns) if !isCols { panic(gowid.WidgetSizeError{Widget: w, Size: size, Required: "gowid.IColumns"}) } cols := cl.Columns() rows := 1 if rs, isRows := size.(gowid.IRows); isRows { rows = rs.Rows() } t, m, b := w.GetTop(), w.GetMiddle(), w.GetBottom() splits := gwutil.HamiltonAllocation([]int{t, m, b}, gwutil.Max(0, rows-2)) // Make sure that the "handle" in the middle is always at least 1 row tall if splits[1] == 0 { fixSplit(1, 0, 2, &splits) } // Make sure that unless we're at the top, there is a space to click to go closer to the top // and vice-versa for the bottom if t != 0 && splits[0] == 0 { fixSplit(0, 1, 2, &splits) } if b != 0 && splits[2] == 0 { fixSplit(2, 0, 1, &splits) } fill := gowid.CellFromRune(w.GetRunes().Handle) fillArr := make([]gowid.Cell, 0) blank := gowid.CellFromRune(w.GetRunes().Space) blankArr := make([]gowid.Cell, 0) for i := 0; i < cols; i++ { fillArr = append(fillArr, fill) blankArr = append(blankArr, blank) } resblankabove := gowid.NewCanvas() resblankbelow := gowid.NewCanvas() resfill := gowid.NewCanvas() // [2, 4, 7] dup1 := make([]gowid.Cell, cols) copy(dup1, blankArr) resblankabove.Lines = append(resblankabove.Lines, dup1) var dup []gowid.Cell for i := 0; i < rows-2; i++ { if i < splits[0] { dup = make([]gowid.Cell, cols) copy(dup, blankArr) resblankabove.Lines = append(resblankabove.Lines, dup) } else if i < splits[1]+splits[0] { dup = make([]gowid.Cell, cols) copy(dup, fillArr) resfill.Lines = append(resfill.Lines, dup) } else { dup = make([]gowid.Cell, cols) copy(dup, blankArr) resblankbelow.Lines = append(resblankbelow.Lines, dup) } } dup2 := make([]gowid.Cell, cols) copy(dup2, blankArr) resblankbelow.Lines = append(resblankbelow.Lines, dup2) resblankabove.AlignRight() resfill.AlignRightWith(gowid.MakeCell( '#', gowid.MakeTCellColorExt(tcell.ColorDefault), gowid.MakeTCellColorExt(tcell.ColorDefault), gowid.StyleNone)) resblankbelow.AlignRight() res := gowid.NewCanvas() res.AppendBelow(resblankabove, false, false) res.AppendBelow(resfill, false, false) res.AppendBelow(resblankbelow, false, false) l := len(res.Lines) if l > 0 { for i := 0; i < len(res.Lines[0]); i++ { res.Lines[0][i] = res.Lines[0][i].WithRune(w.GetRunes().Up) } for i := 0; i < len(res.Lines[0]); i++ { res.Lines[l-1][i] = res.Lines[l-1][i].WithRune(w.GetRunes().Down) } } return res } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: gowid-1.4.0/widgets/vscroll/vscroll_test.go000066400000000000000000000047261426234454000210460ustar00rootroot00000000000000// Copyright 2019-2022 Graham Clark. All rights reserved. Use of this source code is governed by the MIT license // that can be found in the LICENSE file. package vscroll import ( "strings" "testing" "github.com/gcla/gowid" "github.com/gcla/gowid/gwtest" "github.com/gcla/gowid/gwutil" "github.com/stretchr/testify/assert" ) //====================================================================== func TestVerticalSplits(t *testing.T) { x, y, z := 1, 1, 1 splits := gwutil.HamiltonAllocation([]int{x, y, z}, 3) assert.Equal(t, []int{1, 1, 1}, splits) x, y, z = 5, 5, 5 splits = gwutil.HamiltonAllocation([]int{x, y, z}, 3) assert.Equal(t, []int{1, 1, 1}, splits) x, y, z = 5, 5, 5 splits = gwutil.HamiltonAllocation([]int{x, y, z}, 6) assert.Equal(t, []int{2, 2, 2}, splits) x, y, z = 2, 4, 6 splits = gwutil.HamiltonAllocation([]int{x, y, z}, 6) assert.Equal(t, []int{1, 2, 3}, splits) x, y, z = 0, 3, 6 splits = gwutil.HamiltonAllocation([]int{x, y, z}, 6) assert.Equal(t, []int{0, 2, 4}, splits) x, y, z = 1, 3, 8 splits = gwutil.HamiltonAllocation([]int{x, y, z}, 6) assert.Equal(t, []int{1, 1, 4}, splits) x, y, z = 1, 3, 16 splits = gwutil.HamiltonAllocation([]int{x, y, z}, 6) assert.Equal(t, []int{0, 1, 5}, splits) x, y, z = 1, 3, 16 splits = gwutil.HamiltonAllocation([]int{x, y, z}, 0) assert.Equal(t, []int{0, 0, 0}, splits) x, y, z = 18, 14, 643 splits = gwutil.HamiltonAllocation([]int{x, y, z}, 12) assert.Equal(t, []int{0, 0, 12}, splits) } func TestVerticalScrollbar(t *testing.T) { w := New() c1 := w.Render(gowid.RenderBox{C: 2, R: 5}, gowid.Focused, gwtest.D) assert.Equal(t, c1.String(), "^^\n \n##\n \nvv") assert.Panics(t, func() { w.Render(gowid.RenderFixed{}, gowid.Focused, gwtest.D) }) } func TestVerticalScrollbar2(t *testing.T) { w := New() c1 := w.Render(gowid.RenderBox{C: 1, R: 5}, gowid.Focused, gwtest.D) assert.Equal(t, c1.String(), strings.Join([]string{"^", " ", "#", " ", "v"}, "\n")) w.Top = 0 w.Middle = 1 w.Bottom = 1 c1 = w.Render(gowid.RenderBox{C: 1, R: 5}, gowid.Focused, gwtest.D) assert.Equal(t, c1.String(), strings.Join([]string{"^", "#", "#", " ", "v"}, "\n")) // w.Top = 1 // w.Middle = 1 // w.Bottom = 97 // c1 = w.Render(gowid.RenderBox{C: 1, R: 5}, true, gwtest.D) // assert.Equal(t, c1.String(), strings.Join([]string{"^", "#", "#", " ", "v"}, "\n")) } //====================================================================== // Local Variables: // mode: Go // fill-column: 110 // End: