aboutsummaryrefslogtreecommitdiff
path: root/vendor/maunium.net/go/tview
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/maunium.net/go/tview')
-rw-r--r--vendor/maunium.net/go/tview/CODE_OF_CONDUCT.md73
-rw-r--r--vendor/maunium.net/go/tview/CONTRIBUTING.md31
-rw-r--r--vendor/maunium.net/go/tview/LICENSE.txt21
-rw-r--r--vendor/maunium.net/go/tview/README.md94
-rw-r--r--vendor/maunium.net/go/tview/application.go408
-rw-r--r--vendor/maunium.net/go/tview/box.go378
-rw-r--r--vendor/maunium.net/go/tview/button.go137
-rw-r--r--vendor/maunium.net/go/tview/checkbox.go175
-rw-r--r--vendor/maunium.net/go/tview/doc.go111
-rw-r--r--vendor/maunium.net/go/tview/dropdown.go380
-rw-r--r--vendor/maunium.net/go/tview/flex.go173
-rw-r--r--vendor/maunium.net/go/tview/focusable.go8
-rw-r--r--vendor/maunium.net/go/tview/form.go499
-rw-r--r--vendor/maunium.net/go/tview/frame.go157
-rw-r--r--vendor/maunium.net/go/tview/grid.go624
-rw-r--r--vendor/maunium.net/go/tview/inputfield.go328
-rw-r--r--vendor/maunium.net/go/tview/list.go327
-rw-r--r--vendor/maunium.net/go/tview/modal.go131
-rw-r--r--vendor/maunium.net/go/tview/pages.go257
-rw-r--r--vendor/maunium.net/go/tview/primitive.go48
-rw-r--r--vendor/maunium.net/go/tview/styles.go34
-rw-r--r--vendor/maunium.net/go/tview/table.go1025
-rw-r--r--vendor/maunium.net/go/tview/textview.go899
-rw-r--r--vendor/maunium.net/go/tview/tview.gifbin0 -> 2226085 bytes
-rw-r--r--vendor/maunium.net/go/tview/util.go456
25 files changed, 6774 insertions, 0 deletions
diff --git a/vendor/maunium.net/go/tview/CODE_OF_CONDUCT.md b/vendor/maunium.net/go/tview/CODE_OF_CONDUCT.md
new file mode 100644
index 0000000..601e63b
--- /dev/null
+++ b/vendor/maunium.net/go/tview/CODE_OF_CONDUCT.md
@@ -0,0 +1,73 @@
+# Contributor Covenant Code of Conduct
+
+## Our Pledge
+
+In the interest of fostering an open and welcoming environment, we as
+contributors and maintainers pledge to making participation in our project and
+our community a harassment-free experience for everyone, regardless of age, body
+size, disability, ethnicity, gender identity and expression, level of experience,
+education, socio-economic status, nationality, personal appearance, race,
+religion, or sexual identity and orientation.
+
+## Our Standards
+
+Examples of behavior that contributes to creating a positive environment
+include:
+
+* Using welcoming and inclusive language
+* Being respectful of differing viewpoints and experiences
+* Gracefully accepting constructive criticism
+* Focusing on what is best for the community
+* Showing empathy towards other community members
+
+Examples of unacceptable behavior by participants include:
+
+* The use of sexualized language or imagery and unwelcome sexual attention or
+ advances
+* Trolling, insulting/derogatory comments, and personal or political attacks
+* Public or private harassment
+* Publishing others' private information, such as a physical or electronic
+ address, without explicit permission
+* Other conduct which could reasonably be considered inappropriate in a
+ professional setting
+
+## Our Responsibilities
+
+Project maintainers are responsible for clarifying the standards of acceptable
+behavior and are expected to take appropriate and fair corrective action in
+response to any instances of unacceptable behavior.
+
+Project maintainers have the right and responsibility to remove, edit, or
+reject comments, commits, code, wiki edits, issues, and other contributions
+that are not aligned to this Code of Conduct, or to ban temporarily or
+permanently any contributor for other behaviors that they deem inappropriate,
+threatening, offensive, or harmful.
+
+## Scope
+
+This Code of Conduct applies both within project spaces and in public spaces
+when an individual is representing the project or its community. Examples of
+representing a project or community include using an official project e-mail
+address, posting via an official social media account, or acting as an appointed
+representative at an online or offline event. Representation of a project may be
+further defined and clarified by project maintainers.
+
+## Enforcement
+
+Instances of abusive, harassing, or otherwise unacceptable behavior may be
+reported by contacting the project team at https://rentafounder.com/page/about-me/. All
+complaints will be reviewed and investigated and will result in a response that
+is deemed necessary and appropriate to the circumstances. The project team is
+obligated to maintain confidentiality with regard to the reporter of an incident.
+Further details of specific enforcement policies may be posted separately.
+
+Project maintainers who do not follow or enforce the Code of Conduct in good
+faith may face temporary or permanent repercussions as determined by other
+members of the project's leadership.
+
+## Attribution
+
+This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
+available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
+
+[homepage]: https://www.contributor-covenant.org
diff --git a/vendor/maunium.net/go/tview/CONTRIBUTING.md b/vendor/maunium.net/go/tview/CONTRIBUTING.md
new file mode 100644
index 0000000..0d96292
--- /dev/null
+++ b/vendor/maunium.net/go/tview/CONTRIBUTING.md
@@ -0,0 +1,31 @@
+# Contributing to tview
+
+First of all, thank you for taking the time to contribute.
+
+The following provides you with some guidance on how to contribute to this project. Mainly, it is meant to save us all some time so please read it, it's not long.
+
+Please note that this document is work in progress so I might add to it in the future.
+
+## Issues
+
+- Please include enough information so everybody understands your request.
+- Screenshots or code that illustrates your point always helps.
+- It's fine to ask for help. But you should have checked out the [documentation](https://godoc.org/github.com/rivo/tview) first in any case.
+- If you request a new feature, state your motivation and share a use case that you faced where you needed that new feature. It should be something that others will also need.
+
+## Pull Requests
+
+If you have a feature request, open an issue first before sending me a pull request. It may save you from writing code that will get rejected. If your case is strong, there is a good chance that I will add the feature for you.
+
+I'm very picky about the code that goes into this repo. So if you violate any of the following guidelines, there is a good chance I won't merge your pull request.
+
+- There must be a strong case for your additions/changes, such as:
+ - Bug fixes
+ - Features that are needed (see "Issues" above; state your motivation)
+ - Improvements in stability or performance (if readability does not suffer)
+- Your code must follow the structure of the existing code. Don't just patch something on. Try to understand how `tview` is currently designed and follow that design. Your code needs to be consistent with existing code.
+- If you're adding code that increases the work required to maintain the project, you must be willing to take responsibility for that extra work. I will ask you to maintain your part of the code in the long run.
+- Function/type/variable/constant names must be as descriptive as they are right now. Follow the conventions of the package.
+- All functions/types/variables/constants, even private ones, must have comments in good English. These comments must be elaborate enough so that new users of the package understand them and can follow them. Provide examples if you have to.
+- Your changes must not decrease the project's [Go Report](https://goreportcard.com/report/github.com/rivo/tview) rating.
+- No breaking changes unless there is absolutely no other way.
diff --git a/vendor/maunium.net/go/tview/LICENSE.txt b/vendor/maunium.net/go/tview/LICENSE.txt
new file mode 100644
index 0000000..8aa2645
--- /dev/null
+++ b/vendor/maunium.net/go/tview/LICENSE.txt
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) [year] [fullname]
+
+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.
diff --git a/vendor/maunium.net/go/tview/README.md b/vendor/maunium.net/go/tview/README.md
new file mode 100644
index 0000000..fb99073
--- /dev/null
+++ b/vendor/maunium.net/go/tview/README.md
@@ -0,0 +1,94 @@
+# Rich Interactive Widgets for Terminal UIs
+
+[![Godoc Reference](https://img.shields.io/badge/godoc-reference-blue.svg)](https://godoc.org/github.com/rivo/tview)
+[![Go Report](https://img.shields.io/badge/go%20report-A%2B-brightgreen.svg)](https://goreportcard.com/report/github.com/rivo/tview)
+
+This Go package provides commonly needed components for terminal based user interfaces.
+
+![Screenshot](tview.gif)
+
+Among these components are:
+
+- __Input forms__ (include __input/password fields__, __drop-down selections__, __checkboxes__, and __buttons__)
+- Navigable multi-color __text views__
+- Sophisticated navigable __table views__
+- Selectable __lists__
+- __Grid__, __Flexbox__ and __page layouts__
+- Modal __message windows__
+- An __application__ wrapper
+
+They come with lots of customization options and can be easily extended to fit your needs.
+
+## Installation
+
+```bash
+go get github.com/rivo/tview
+```
+
+## Hello World
+
+This basic example creates a box titled "Hello, World!" and displays it in your terminal:
+
+```go
+package main
+
+import (
+ "github.com/rivo/tview"
+)
+
+func main() {
+ box := tview.NewBox().SetBorder(true).SetTitle("Hello, world!")
+ if err := tview.NewApplication().SetRoot(box, true).Run(); err != nil {
+ panic(err)
+ }
+}
+```
+
+Check out the [GitHub Wiki](https://github.com/rivo/tview/wiki) for more examples along with screenshots. Or try the examples in the "demos" subdirectory.
+
+For a presentation highlighting this package, compile and run the program found in the "demos/presentation" subdirectory.
+
+## Documentation
+
+Refer to https://godoc.org/github.com/rivo/tview for the package's documentation.
+
+## Dependencies
+
+This package is based on [maunium.net/go/tcell](https://maunium.net/go/tcell) (and its dependencies).
+
+## Your Feedback
+
+Add your issue here on GitHub. Feel free to get in touch if you have any questions.
+
+## Version History
+
+(There are no corresponding tags in the project. I only keep such a history in this README.)
+
+- v0.12 (2018-03-13)
+ - Added "suspended mode" to `Application`.
+- v0.11 (2018-03-02)
+ - Added a `RemoveItem()` function to `Grid` and `Flex`.
+- v0.10 (2018-02-22)
+ - Direct access to the `screen` object through callback in `Box` (i.e. for all primitives).
+- v0.9 (2018-02-20)
+ - Introduced `Grid` layout.
+ - Direct access to the `screen` object through callbacks in `Application`.
+- v0.8 (2018-01-17)
+ - Color tags can now be used almost everywhere.
+- v0.7 (2018-01-16)
+ - Forms can now also have a horizontal layout.
+- v0.6 (2018-01-14)
+ - All primitives can now intercept all key events when they have focus.
+ - Key events can also be intercepted globally (changed to a more general, consistent handling)
+- v0.5 (2018-01-13)
+ - `TextView` now has word wrapping and text alignment
+- v0.4 (2018-01-12)
+ - `TextView` now accepts color tags with any W3C color (including RGB hex values).
+ - Support for wide unicode characters.
+- v0.3 (2018-01-11)
+ - Added masking to `InputField` and password entry to `Form`.
+- v0.2 (2018-01-10)
+ - Added `Styles` variable with default colors for primitives.
+ - Completed some missing InputField functions.
+- v0.1 (2018-01-06)
+ - First Release.
diff --git a/vendor/maunium.net/go/tview/application.go b/vendor/maunium.net/go/tview/application.go
new file mode 100644
index 0000000..f3d6328
--- /dev/null
+++ b/vendor/maunium.net/go/tview/application.go
@@ -0,0 +1,408 @@
+package tview
+
+import (
+ "fmt"
+ "os"
+ "sync"
+
+ "maunium.net/go/tcell"
+)
+
+// Application represents the top node of an application.
+//
+// It is not strictly required to use this class as none of the other classes
+// depend on it. However, it provides useful tools to set up an application and
+// plays nicely with all widgets.
+type Application struct {
+ sync.RWMutex
+
+ // The application's screen.
+ screen tcell.Screen
+
+ // The primitive which currently has the keyboard focus.
+ focus Primitive
+
+ // The root primitive to be seen on the screen.
+ root Primitive
+
+ // Whether or not the application resizes the root primitive.
+ rootFullscreen bool
+
+ // An optional capture function which receives a key event and returns the
+ // event to be forwarded to the default input handler (nil if nothing should
+ // be forwarded).
+ inputCapture func(event *tcell.EventKey) *tcell.EventKey
+
+ // An optional capture function which receives a mouse event and returns the
+ // event to be forwarded to the default input handler (nil if nothing should
+ // be forwarded).
+ mouseCapture func(event *tcell.EventMouse) *tcell.EventMouse
+
+ // An optional callback function which is invoked just before the root
+ // primitive is drawn.
+ beforeDraw func(screen tcell.Screen) bool
+
+ // An optional callback function which is invoked after the root primitive
+ // was drawn.
+ afterDraw func(screen tcell.Screen)
+
+ // If this value is true, the application has entered suspended mode.
+ suspended bool
+}
+
+// NewApplication creates and returns a new application.
+func NewApplication() *Application {
+ return &Application{}
+}
+
+// SetInputCapture sets a function which captures all key events before they are
+// forwarded to the key event handler of the primitive which currently has
+// focus. This function can then choose to forward that key event (or a
+// different one) by returning it or stop the key event processing by returning
+// nil.
+//
+// Note that this also affects the default event handling of the application
+// itself: Such a handler can intercept the Ctrl-C event which closes the
+// applicatoon.
+func (a *Application) SetInputCapture(capture func(event *tcell.EventKey) *tcell.EventKey) *Application {
+ a.inputCapture = capture
+ return a
+}
+
+// GetInputCapture returns the function installed with SetInputCapture() or nil
+// if no such function has been installed.
+func (a *Application) GetInputCapture() func(event *tcell.EventKey) *tcell.EventKey {
+ return a.inputCapture
+}
+
+// SetMouseCapture sets a function which captures all mouse events before they are
+// forwarded to the mouse event handler of the primitive which currently has
+// focus. This function can then choose to forward that mouse event (or a
+// different one) by returning it or stop the key event processing by returning
+// nil.
+func (a *Application) SetMouseCapture(capture func(event *tcell.EventMouse) *tcell.EventMouse) *Application {
+ a.mouseCapture = capture
+ return a
+}
+
+// GetMouseCapture returns the function installed with SetMouseCapture() or nil
+// if no such function has been installed.
+func (a *Application) GetMouseCapture() func(event *tcell.EventMouse) *tcell.EventMouse {
+ return a.mouseCapture
+}
+
+func (a *Application) GetScreen() tcell.Screen {
+ return a.screen
+}
+
+// Run starts the application and thus the event loop. This function returns
+// when Stop() was called.
+func (a *Application) Run() error {
+ var err error
+ a.Lock()
+
+ // Make a screen.
+ a.screen, err = tcell.NewScreen()
+ if err != nil {
+ a.Unlock()
+ return err
+ }
+ if err = a.screen.Init(); err != nil {
+ a.Unlock()
+ return err
+ }
+ a.screen.EnableMouse()
+
+ // Draw the screen for the first time.
+ a.Unlock()
+ a.Draw()
+
+ // Start event loop.
+ for {
+ a.Lock()
+ screen := a.screen
+ if a.suspended {
+ a.suspended = false // Clear previous suspended flag.
+ }
+ a.Unlock()
+ if screen == nil {
+ break
+ }
+
+ // Wait for next event.
+ event := a.screen.PollEvent()
+ if event == nil {
+ a.Lock()
+ if a.suspended {
+ // This screen was renewed due to suspended mode.
+ a.suspended = false
+ a.Unlock()
+ continue // Resume.
+ }
+ a.Unlock()
+
+ // The screen was finalized. Exit the loop.
+ break
+ }
+
+ switch event := event.(type) {
+ case *tcell.EventKey:
+ a.RLock()
+ p := a.focus
+ a.RUnlock()
+
+ // Intercept keys.
+ if a.inputCapture != nil {
+ event = a.inputCapture(event)
+ if event == nil {
+ break // Don't forward event.
+ }
+ }
+
+ // Pass other key events to the currently focused primitive.
+ if p != nil {
+ if handler := p.InputHandler(); handler != nil {
+ handler(event, func(p Primitive) {
+ a.SetFocus(p)
+ })
+ a.Draw()
+ }
+ }
+ case *tcell.EventMouse:
+ a.RLock()
+ p := a.focus
+ a.RUnlock()
+
+ // Intercept keys.
+ if a.mouseCapture != nil {
+ event = a.mouseCapture(event)
+ if event == nil {
+ break // Don't forward event.
+ }
+ }
+
+ // Pass other key events to the currently focused primitive.
+ if p != nil {
+ if handler := p.MouseHandler(); handler != nil {
+ handler(event, func(p Primitive) {
+ a.SetFocus(p)
+ })
+ //a.Draw()
+ }
+ }
+ case *tcell.EventResize:
+ a.Lock()
+ screen := a.screen
+ a.Unlock()
+ screen.Clear()
+ a.Draw()
+ }
+ }
+
+ return nil
+}
+
+// Stop stops the application, causing Run() to return.
+func (a *Application) Stop() {
+ a.RLock()
+ defer a.RUnlock()
+ if a.screen == nil {
+ return
+ }
+ a.screen.Fini()
+ a.screen = nil
+}
+
+// Suspend temporarily suspends the application by exiting terminal UI mode and
+// invoking the provided function "f". When "f" returns, terminal UI mode is
+// entered again and the application resumes.
+//
+// A return value of true indicates that the application was suspended and "f"
+// was called. If false is returned, the application was already suspended,
+// terminal UI mode was not exited, and "f" was not called.
+func (a *Application) Suspend(f func()) bool {
+ a.Lock()
+
+ if a.suspended || a.screen == nil {
+ // Application is already suspended.
+ a.Unlock()
+ return false
+ }
+
+ // Enter suspended mode.
+ a.suspended = true
+ a.Unlock()
+ a.Stop()
+
+ // Deal with panics during suspended mode. Exit the program.
+ defer func() {
+ if p := recover(); p != nil {
+ fmt.Println(p)
+ os.Exit(1)
+ }
+ }()
+
+ // Wait for "f" to return.
+ f()
+
+ // Make a new screen and redraw.
+ a.Lock()
+ var err error
+ a.screen, err = tcell.NewScreen()
+ if err != nil {
+ a.Unlock()
+ panic(err)
+ }
+ if err = a.screen.Init(); err != nil {
+ a.Unlock()
+ panic(err)
+ }
+ a.Unlock()
+ a.Draw()
+
+ // Continue application loop.
+ return true
+}
+
+// Draw refreshes the screen. It calls the Draw() function of the application's
+// root primitive and then syncs the screen buffer.
+func (a *Application) Draw() *Application {
+ a.RLock()
+ screen := a.screen
+ root := a.root
+ fullscreen := a.rootFullscreen
+ before := a.beforeDraw
+ after := a.afterDraw
+ a.RUnlock()
+
+ // Maybe we're not ready yet or not anymore.
+ if screen == nil || root == nil {
+ return a
+ }
+
+ // Resize if requested.
+ if fullscreen && root != nil {
+ width, height := screen.Size()
+ root.SetRect(0, 0, width, height)
+ }
+
+ // Call before handler if there is one.
+ if before != nil {
+ if before(screen) {
+ screen.Show()
+ return a
+ }
+ }
+
+ // Draw all primitives.
+ root.Draw(screen)
+
+ // Call after handler if there is one.
+ if after != nil {
+ after(screen)
+ }
+
+ // Sync screen.
+ screen.Show()
+
+ return a
+}
+
+// SetBeforeDrawFunc installs a callback function which is invoked just before
+// the root primitive is drawn during screen updates. If the function returns
+// true, drawing will not continue, i.e. the root primitive will not be drawn
+// (and an after-draw-handler will not be called).
+//
+// Note that the screen is not cleared by the application. To clear the screen,
+// you may call screen.Clear().
+//
+// Provide nil to uninstall the callback function.
+func (a *Application) SetBeforeDrawFunc(handler func(screen tcell.Screen) bool) *Application {
+ a.beforeDraw = handler
+ return a
+}
+
+// GetBeforeDrawFunc returns the callback function installed with
+// SetBeforeDrawFunc() or nil if none has been installed.
+func (a *Application) GetBeforeDrawFunc() func(screen tcell.Screen) bool {
+ return a.beforeDraw
+}
+
+// SetAfterDrawFunc installs a callback function which is invoked after the root
+// primitive was drawn during screen updates.
+//
+// Provide nil to uninstall the callback function.
+func (a *Application) SetAfterDrawFunc(handler func(screen tcell.Screen)) *Application {
+ a.afterDraw = handler
+ return a
+}
+
+// GetAfterDrawFunc returns the callback function installed with
+// SetAfterDrawFunc() or nil if none has been installed.
+func (a *Application) GetAfterDrawFunc() func(screen tcell.Screen) {
+ return a.afterDraw
+}
+
+// SetRoot sets the root primitive for this application. If "fullscreen" is set
+// to true, the root primitive's position will be changed to fill the screen.
+//
+// This function must be called at least once or nothing will be displayed when
+// the application starts.
+//
+// It also calls SetFocus() on the primitive.
+func (a *Application) SetRoot(root Primitive, fullscreen bool) *Application {
+ a.Lock()
+ a.root = root
+ a.rootFullscreen = fullscreen
+ if a.screen != nil {
+ a.screen.Clear()
+ }
+ a.Unlock()
+
+ a.SetFocus(root)
+
+ return a
+}
+
+// ResizeToFullScreen resizes the given primitive such that it fills the entire
+// screen.
+func (a *Application) ResizeToFullScreen(p Primitive) *Application {
+ a.RLock()
+ width, height := a.screen.Size()
+ a.RUnlock()
+ p.SetRect(0, 0, width, height)
+ return a
+}
+
+// SetFocus sets the focus on a new primitive. All key events will be redirected
+// to that primitive. Callers must ensure that the primitive will handle key
+// events.
+//
+// Blur() will be called on the previously focused primitive. Focus() will be
+// called on the new primitive.
+func (a *Application) SetFocus(p Primitive) *Application {
+ a.Lock()
+ if a.focus != nil {
+ a.focus.Blur()
+ }
+ a.focus = p
+ if a.screen != nil {
+ a.screen.HideCursor()
+ }
+ a.Unlock()
+ if p != nil {
+ p.Focus(func(p Primitive) {
+ a.SetFocus(p)
+ })
+ }
+
+ return a
+}
+
+// GetFocus returns the primitive which has the current focus. If none has it,
+// nil is returned.
+func (a *Application) GetFocus() Primitive {
+ a.RLock()
+ defer a.RUnlock()
+ return a.focus
+}
diff --git a/vendor/maunium.net/go/tview/box.go b/vendor/maunium.net/go/tview/box.go
new file mode 100644
index 0000000..1bcbff0
--- /dev/null
+++ b/vendor/maunium.net/go/tview/box.go
@@ -0,0 +1,378 @@
+package tview
+
+import (
+ "maunium.net/go/tcell"
+)
+
+// Box implements Primitive with a background and optional elements such as a
+// border and a title. Most subclasses keep their content contained in the box
+// but don't necessarily have to.
+//
+// Note that all classes which subclass from Box will also have access to its
+// functions.
+//
+// See https://github.com/rivo/tview/wiki/Box for an example.
+type Box struct {
+ // The position of the rect.
+ x, y, width, height int
+
+ // The inner rect reserved for the box's content.
+ innerX, innerY, innerWidth, innerHeight int
+
+ // Border padding.
+ paddingTop, paddingBottom, paddingLeft, paddingRight int
+
+ // The box's background color.
+ backgroundColor tcell.Color
+
+ // Whether or not a border is drawn, reducing the box's space for content by
+ // two in width and height.
+ border bool
+
+ // The color of the border.
+ borderColor tcell.Color
+
+ // The title. Only visible if there is a border, too.
+ title string
+
+ // The color of the title.
+ titleColor tcell.Color
+
+ // The alignment of the title.
+ titleAlign int
+
+ // Provides a way to find out if this box has focus. We always go through
+ // this interface because it may be overridden by implementing classes.
+ focus Focusable
+
+ // Whether or not this box has focus.
+ hasFocus bool
+
+ // If set to true, the inner rect of this box will be within the screen at the
+ // last time the box was drawn.
+ clampToScreen bool
+
+ // An optional capture function which receives a key event and returns the
+ // event to be forwarded to the primitive's default input handler (nil if
+ // nothing should be forwarded).
+ inputCapture func(event *tcell.EventKey) *tcell.EventKey
+
+ // An optional capture function which receives a mouse event and returns the
+ // event to be forwarded to the primitive's default mouse handler (nil if
+ // nothing should be forwarded).
+ mouseCapture func(event *tcell.EventMouse) *tcell.EventMouse
+
+ // An optional function which is called before the box is drawn.
+ draw func(screen tcell.Screen, x, y, width, height int) (int, int, int, int)
+}
+
+// NewBox returns a Box without a border.
+func NewBox() *Box {
+ b := &Box{
+ width: 15,
+ height: 10,
+ innerX: -1, // Mark as uninitialized.
+ backgroundColor: Styles.PrimitiveBackgroundColor,
+ borderColor: Styles.BorderColor,
+ titleColor: Styles.TitleColor,
+ titleAlign: AlignCenter,
+ clampToScreen: true,
+ }
+ b.focus = b
+ return b
+}
+
+// SetBorderPadding sets the size of the borders around the box content.
+func (b *Box) SetBorderPadding(top, bottom, left, right int) *Box {
+ b.paddingTop, b.paddingBottom, b.paddingLeft, b.paddingRight = top, bottom, left, right
+ return b
+}
+
+// GetRect returns the current position of the rectangle, x, y, width, and
+// height.
+func (b *Box) GetRect() (int, int, int, int) {
+ return b.x, b.y, b.width, b.height
+}
+
+// GetInnerRect returns the position of the inner rectangle (x, y, width,
+// height), without the border and without any padding.
+func (b *Box) GetInnerRect() (int, int, int, int) {
+ if b.innerX >= 0 {
+ return b.innerX, b.innerY, b.innerWidth, b.innerHeight
+ }
+ x, y, width, height := b.GetRect()
+ if b.border {
+ x++
+ y++
+ width -= 2
+ height -= 2
+ }
+ return x + b.paddingLeft,
+ y + b.paddingTop,
+ width - b.paddingLeft - b.paddingRight,
+ height - b.paddingTop - b.paddingBottom
+}
+
+// SetRect sets a new position of the primitive.
+func (b *Box) SetRect(x, y, width, height int) {
+ b.x = x
+ b.y = y
+ b.width = width
+ b.height = height
+}
+
+// SetDrawFunc sets a callback function which is invoked after the box primitive
+// has been drawn. This allows you to add a more individual style to the box
+// (and all primitives which extend it).
+//
+// The function is provided with the box's dimensions (set via SetRect()). It
+// must return the box's inner dimensions (x, y, width, height) which will be
+// returned by GetInnerRect(), used by descendent primitives to draw their own
+// content.
+func (b *Box) SetDrawFunc(handler func(screen tcell.Screen, x, y, width, height int) (int, int, int, int)) *Box {
+ b.draw = handler
+ return b
+}
+
+// GetDrawFunc returns the callback function which was installed with
+// SetDrawFunc() or nil if no such function has been installed.
+func (b *Box) GetDrawFunc() func(screen tcell.Screen, x, y, width, height int) (int, int, int, int) {
+ return b.draw
+}
+
+// WrapInputHandler wraps an input handler (see InputHandler()) with the
+// functionality to capture input (see SetInputCapture()) before passing it
+// on to the provided (default) input handler.
+//
+// This is only meant to be used by subclassing primitives.
+func (b *Box) WrapInputHandler(inputHandler func(*tcell.EventKey, func(p Primitive))) func(*tcell.EventKey, func(p Primitive)) {
+ return func(event *tcell.EventKey, setFocus func(p Primitive)) {
+ if b.inputCapture != nil {
+ event = b.inputCapture(event)
+ }
+ if event != nil && inputHandler != nil {
+ inputHandler(event, setFocus)
+ }
+ }
+}
+
+// InputHandler returns nil.
+func (b *Box) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) {
+ return b.WrapInputHandler(nil)
+}
+
+// SetInputCapture installs a function which captures key events before they are
+// forwarded to the primitive's default key event handler. This function can
+// then choose to forward that key event (or a different one) to the default
+// handler by returning it. If nil is returned, the default handler will not
+// be called.
+//
+// Providing a nil handler will remove a previously existing handler.
+func (b *Box) SetInputCapture(capture func(event *tcell.EventKey) *tcell.EventKey) *Box {
+ b.inputCapture = capture
+ return b
+}
+
+// GetInputCapture returns the function installed with SetInputCapture() or nil
+// if no such function has been installed.
+func (b *Box) GetInputCapture() func(event *tcell.EventKey) *tcell.EventKey {
+ return b.inputCapture
+}
+
+// WrapMouseHandler wraps a mouse handler (see MouseHandler()) with the
+// functionality to capture mouse events (see SetMouseCapture()) before passing it
+// on to the provided (default) mouse handler.
+//
+// This is only meant to be used by subclassing primitives.
+func (b *Box) WrapMouseHandler(mouseHandler func(*tcell.EventMouse, func(p Primitive))) func(*tcell.EventMouse, func(p Primitive)) {
+ return func(event *tcell.EventMouse, setFocus func(p Primitive)) {
+ if b.mouseCapture != nil {
+ event = b.mouseCapture(event)
+ }
+ if event != nil && mouseHandler != nil {
+ mouseHandler(event, setFocus)
+ }
+ }
+}
+
+// MouseHandler returns nil.
+func (b *Box) MouseHandler() func(event *tcell.EventMouse, setFocus func(p Primitive)) {
+ return b.WrapMouseHandler(nil)
+}
+
+// SetMouseCapture installs a function which captures mouse events before they are
+// forwarded to the primitive's default mouse event handler. This function can
+// then choose to forward that mouse event (or a different one) to the default
+// handler by returning it. If nil is returned, the default handler will not
+// be called.
+//
+// Providing a nil handler will remove a previously existing handler.
+func (b *Box) SetMouseCapture(capture func(event *tcell.EventMouse) *tcell.EventMouse) *Box {
+ b.mouseCapture = capture
+ return b
+}
+
+// GetMouseCapture returns the function installed with SetMouseCapture() or nil
+// if no such function has been installed.
+func (b *Box) GetMouseCapture() func(event *tcell.EventMouse) *tcell.EventMouse {
+ return b.mouseCapture
+}
+
+// SetBackgroundColor sets the box's background color.
+func (b *Box) SetBackgroundColor(color tcell.Color) *Box {
+ b.backgroundColor = color
+ return b
+}
+
+// SetBorder sets the flag indicating whether or not the box should have a
+// border.
+func (b *Box) SetBorder(show bool) *Box {
+ b.border = show
+ return b
+}
+
+func (b *Box) HasBorder() bool {
+ return b.border
+}
+
+// SetBorderColor sets the box's border color.
+func (b *Box) SetBorderColor(color tcell.Color) *Box {
+ b.borderColor = color
+ return b
+}
+
+// SetTitle sets the box's title.
+func (b *Box) SetTitle(title string) *Box {
+ b.title = title
+ return b
+}
+
+// SetTitleColor sets the box's title color.
+func (b *Box) SetTitleColor(color tcell.Color) *Box {
+ b.titleColor = color
+ return b
+}
+
+// SetTitleAlign sets the alignment of the title, one of AlignLeft, AlignCenter,
+// or AlignRight.
+func (b *Box) SetTitleAlign(align int) *Box {
+ b.titleAlign = align
+ return b
+}
+
+func (b *Box) GetBackgroundColor() tcell.Color {
+ return b.backgroundColor
+}
+
+func (b *Box) GetBorderColor() tcell.Color {
+ return b.borderColor
+}
+
+// Draw draws this primitive onto the screen.
+func (b *Box) Draw(screen tcell.Screen) {
+ // Don't draw anything if there is no space.
+ if b.width <= 0 || b.height <= 0 {
+ return
+ }
+
+ def := tcell.StyleDefault
+
+ // Fill background.
+ background := def.Background(b.backgroundColor)
+ for y := b.y; y < b.y+b.height; y++ {
+ for x := b.x; x < b.x+b.width; x++ {
+ screen.SetContent(x, y, ' ', nil, background)
+ }
+ }
+
+ // Draw border.
+ if b.border && b.width >= 2 && b.height >= 2 {
+ border := background.Foreground(b.borderColor)
+ var vertical, horizontal, topLeft, topRight, bottomLeft, bottomRight rune
+ if b.focus.HasFocus() {
+ vertical = GraphicsDbVertBar
+ horizontal = GraphicsDbHorBar
+ topLeft = GraphicsDbTopLeftCorner
+ topRight = GraphicsDbTopRightCorner
+ bottomLeft = GraphicsDbBottomLeftCorner
+ bottomRight = GraphicsDbBottomRightCorner
+ } else {
+ vertical = GraphicsHoriBar
+ horizontal = GraphicsVertBar
+ topLeft = GraphicsTopLeftCorner
+ topRight = GraphicsTopRightCorner
+ bottomLeft = GraphicsBottomLeftCorner
+ bottomRight = GraphicsBottomRightCorner
+ }
+ for x := b.x + 1; x < b.x+b.width-1; x++ {
+ screen.SetContent(x, b.y, vertical, nil, border)
+ screen.SetContent(x, b.y+b.height-1, vertical, nil, border)
+ }
+ for y := b.y + 1; y < b.y+b.height-1; y++ {
+ screen.SetContent(b.x, y, horizontal, nil, border)
+ screen.SetContent(b.x+b.width-1, y, horizontal, nil, border)
+ }
+ screen.SetContent(b.x, b.y, topLeft, nil, border)
+ screen.SetContent(b.x+b.width-1, b.y, topRight, nil, border)
+ screen.SetContent(b.x, b.y+b.height-1, bottomLeft, nil, border)
+ screen.SetContent(b.x+b.width-1, b.y+b.height-1, bottomRight, nil, border)
+
+ // Draw title.
+ if b.title != "" && b.width >= 4 {
+ _, printed := Print(screen, b.title, b.x+1, b.y, b.width-2, b.titleAlign, b.titleColor)
+ if StringWidth(b.title)-printed > 0 && printed > 0 {
+ _, _, style, _ := screen.GetContent(b.x+b.width-2, b.y)
+ fg, _, _ := style.Decompose()
+ Print(screen, string(GraphicsEllipsis), b.x+b.width-2, b.y, 1, AlignLeft, fg)
+ }
+ }
+ }
+
+ // Call custom draw function.
+ if b.draw != nil {
+ b.innerX, b.innerY, b.innerWidth, b.innerHeight = b.draw(screen, b.x, b.y, b.width, b.height)
+ } else {
+ // Remember the inner rect.
+ b.innerX = -1
+ b.innerX, b.innerY, b.innerWidth, b.innerHeight = b.GetInnerRect()
+ }
+
+ // Clamp inner rect to screen.
+ if b.clampToScreen {
+ width, height := screen.Size()
+ if b.innerX < 0 {
+ b.innerWidth += b.innerX
+ b.innerX = 0
+ }
+ if b.innerX+b.innerWidth >= width {
+ b.innerWidth = width - b.innerX
+ }
+ if b.innerY+b.innerHeight >= height {
+ b.innerHeight = height - b.innerY
+ }
+ if b.innerY < 0 {
+ b.innerHeight += b.innerY
+ b.innerY = 0
+ }
+ }
+}
+
+// Focus is called when this primitive receives focus.
+func (b *Box) Focus(delegate func(p Primitive)) {
+ b.hasFocus = true
+}
+
+// Blur is called when this primitive loses focus.
+func (b *Box) Blur() {
+ b.hasFocus = false
+}
+
+// HasFocus returns whether or not this primitive has focus.
+func (b *Box) HasFocus() bool {
+ return b.hasFocus
+}
+
+// GetFocusable returns the item's Focusable.
+func (b *Box) GetFocusable() Focusable {
+ return b.focus
+}
diff --git a/vendor/maunium.net/go/tview/button.go b/vendor/maunium.net/go/tview/button.go
new file mode 100644
index 0000000..cb47eae
--- /dev/null
+++ b/vendor/maunium.net/go/tview/button.go
@@ -0,0 +1,137 @@
+package tview
+
+import (
+ "maunium.net/go/tcell"
+)
+
+// Button is labeled box that triggers an action when selected.
+//
+// See https://github.com/rivo/tview/wiki/Button for an example.
+type Button struct {
+ *Box
+
+ // The text to be displayed before the input area.
+ label string
+
+ // The label color.
+ labelColor tcell.Color
+
+ // The label color when the button is in focus.
+ labelColorActivated tcell.Color
+
+ // The background color when the button is in focus.
+ backgroundColorActivated tcell.Color
+
+ // An optional function which is called when the button was selected.
+ selected func()
+
+ // An optional function which is called when the user leaves the button. A
+ // key is provided indicating which key was pressed to leave (tab or backtab).
+ blur func(tcell.Key)
+}
+
+// NewButton returns a new input field.
+func NewButton(label string) *Button {
+ box := NewBox().SetBackgroundColor(Styles.ContrastBackgroundColor)
+ box.SetRect(0, 0, StringWidth(label)+4, 1)
+ return &Button{
+ Box: box,
+ label: label,
+ labelColor: Styles.PrimaryTextColor,
+ labelColorActivated: Styles.InverseTextColor,
+ backgroundColorActivated: Styles.PrimaryTextColor,
+ }
+}
+
+// SetLabel sets the button text.
+func (b *Button) SetLabel(label string) *Button {
+ b.label = label
+ return b
+}
+
+// GetLabel returns the button text.
+func (b *Button) GetLabel() string {
+ return b.label
+}
+
+// SetLabelColor sets the color of the button text.
+func (b *Button) SetLabelColor(color tcell.Color) *Button {
+ b.labelColor = color
+ return b
+}
+
+// SetLabelColorActivated sets the color of the button text when the button is
+// in focus.
+func (b *Button) SetLabelColorActivated(color tcell.Color) *Button {
+ b.labelColorActivated = color
+ return b
+}
+
+// SetBackgroundColorActivated sets the background color of the button text when
+// the button is in focus.
+func (b *Button) SetBackgroundColorActivated(color tcell.Color) *Button {
+ b.backgroundColorActivated = color
+ return b
+}
+
+// SetSelectedFunc sets a handler which is called when the button was selected.
+func (b *Button) SetSelectedFunc(handler func()) *Button {
+ b.selected = handler
+ return b
+}
+
+// SetBlurFunc sets a handler which is called when the user leaves the button.
+// The callback function is provided with the key that was pressed, which is one
+// of the following:
+//
+// - KeyEscape: Leaving the button with no specific direction.
+// - KeyTab: Move to the next field.
+// - KeyBacktab: Move to the previous field.
+func (b *Button) SetBlurFunc(handler func(key tcell.Key)) *Button {
+ b.blur = handler
+ return b
+}
+
+// Draw draws this primitive onto the screen.
+func (b *Button) Draw(screen tcell.Screen) {
+ // Draw the box.
+ borderColor := b.borderColor
+ backgroundColor := b.backgroundColor
+ if b.focus.HasFocus() {
+ b.backgroundColor = b.backgroundColorActivated
+ b.borderColor = b.labelColorActivated
+ defer func() {
+ b.borderColor = borderColor
+ }()
+ }
+ b.Box.Draw(screen)
+ b.backgroundColor = backgroundColor
+
+ // Draw label.
+ x, y, width, height := b.GetInnerRect()
+ if width > 0 && height > 0 {
+ y = y + height/2
+ labelColor := b.labelColor
+ if b.focus.HasFocus() {
+ labelColor = b.labelColorActivated
+ }
+ Print(screen, b.label, x, y, width, AlignCenter, labelColor)
+ }
+}
+
+// InputHandler returns the handler for this primitive.
+func (b *Button) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) {
+ return b.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p Primitive)) {
+ // Process key event.
+ switch key := event.Key(); key {
+ case tcell.KeyEnter: // Selected.
+ if b.selected != nil {
+ b.selected()
+ }
+ case tcell.KeyBacktab, tcell.KeyTab, tcell.KeyEscape: // Leave. No action.
+ if b.blur != nil {
+ b.blur(key)
+ }
+ }
+ })
+}
diff --git a/vendor/maunium.net/go/tview/checkbox.go b/vendor/maunium.net/go/tview/checkbox.go
new file mode 100644
index 0000000..83404b8
--- /dev/null
+++ b/vendor/maunium.net/go/tview/checkbox.go
@@ -0,0 +1,175 @@
+package tview
+
+import (
+ "maunium.net/go/tcell"
+)
+
+// Checkbox implements a simple box for boolean values which can be checked and
+// unchecked.
+//
+// See https://github.com/rivo/tview/wiki/Checkbox for an example.
+type Checkbox struct {
+ *Box
+
+ // Whether or not this box is checked.
+ checked bool
+
+ // The text to be displayed before the input area.
+ label string
+
+ // The label color.
+ labelColor tcell.Color
+
+ // The background color of the input area.
+ fieldBackgroundColor tcell.Color
+
+ // The text color of the input area.
+ fieldTextColor tcell.Color
+
+ // An optional function which is called when the user changes the checked
+ // state of this checkbox.
+ changed func(checked bool)
+
+ // An optional function which is called when the user indicated that they
+ // are done entering text. The key which was pressed is provided (tab,
+ // shift-tab, or escape).
+ done func(tcell.Key)
+}
+
+// NewCheckbox returns a new input field.
+func NewCheckbox() *Checkbox {
+ return &Checkbox{
+ Box: NewBox(),
+ labelColor: Styles.SecondaryTextColor,
+ fieldBackgroundColor: Styles.ContrastBackgroundColor,
+ fieldTextColor: Styles.PrimaryTextColor,
+ }
+}
+
+// SetChecked sets the state of the checkbox.
+func (c *Checkbox) SetChecked(checked bool) *Checkbox {
+ c.checked = checked
+ return c
+}
+
+// IsChecked returns whether or not the box is checked.
+func (c *Checkbox) IsChecked() bool {
+ return c.checked
+}
+
+// SetLabel sets the text to be displayed before the input area.
+func (c *Checkbox) SetLabel(label string) *Checkbox {
+ c.label = label
+ return c
+}
+
+// GetLabel returns the text to be displayed before the input area.
+func (c *Checkbox) GetLabel() string {
+ return c.label
+}
+
+// SetLabelColor sets the color of the label.
+func (c *Checkbox) SetLabelColor(color tcell.Color) *Checkbox {
+ c.labelColor = color
+ return c
+}
+
+// SetFieldBackgroundColor sets the background color of the input area.
+func (c *Checkbox) SetFieldBackgroundColor(color tcell.Color) *Checkbox {
+ c.fieldBackgroundColor = color
+ return c
+}
+
+// SetFieldTextColor sets the text color of the input area.
+func (c *Checkbox) SetFieldTextColor(color tcell.Color) *Checkbox {
+ c.fieldTextColor = color
+ return c
+}
+
+// SetFormAttributes sets attributes shared by all form items.
+func (c *Checkbox) SetFormAttributes(label string, labelColor, bgColor, fieldTextColor, fieldBgColor tcell.Color) FormItem {
+ c.label = label
+ c.labelColor = labelColor
+ c.backgroundColor = bgColor
+ c.fieldTextColor = fieldTextColor
+ c.fieldBackgroundColor = fieldBgColor
+ return c
+}
+
+// GetFieldWidth returns this primitive's field width.
+func (c *Checkbox) GetFieldWidth() int {
+ return 1
+}
+
+// SetChangedFunc sets a handler which is called when the checked state of this
+// checkbox was changed by the user. The handler function receives the new
+// state.
+func (c *Checkbox) SetChangedFunc(handler func(checked bool)) *Checkbox {
+ c.changed = handler
+ return c
+}
+
+// SetDoneFunc sets a handler which is called when the user is done entering
+// text. The callback function is provided with the key that was pressed, which
+// is one of the following:
+//
+// - KeyEscape: Abort text input.
+// - KeyTab: Move to the next field.
+// - KeyBacktab: Move to the previous field.
+func (c *Checkbox) SetDoneFunc(handler func(key tcell.Key)) *Checkbox {
+ c.done = handler
+ return c
+}
+
+// SetFinishedFunc calls SetDoneFunc().
+func (c *Checkbox) SetFinishedFunc(handler func(key tcell.Key)) FormItem {
+ return c.SetDoneFunc(handler)
+}
+
+// Draw draws this primitive onto the screen.
+func (c *Checkbox) Draw(screen tcell.Screen) {
+ c.Box.Draw(screen)
+
+ // Prepare
+ x, y, width, height := c.GetInnerRect()
+ rightLimit := x + width
+ if height < 1 || rightLimit <= x {
+ return
+ }
+
+ // Draw label.
+ _, drawnWidth := Print(screen, c.label, x, y, rightLimit-x, AlignLeft, c.labelColor)
+ x += drawnWidth
+
+ // Draw checkbox.
+ fieldStyle := tcell.StyleDefault.Background(c.fieldBackgroundColor).Foreground(c.fieldTextColor)
+ if c.focus.HasFocus() {
+ fieldStyle = fieldStyle.Background(c.fieldTextColor).Foreground(c.fieldBackgroundColor)
+ }
+ checkedRune := 'X'
+ if !c.checked {
+ checkedRune = ' '
+ }
+ screen.SetContent(x, y, checkedRune, nil, fieldStyle)
+}
+
+// InputHandler returns the handler for this primitive.
+func (c *Checkbox) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) {
+ return c.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p Primitive)) {
+ // Process key event.
+ switch key := event.Key(); key {
+ case tcell.KeyRune, tcell.KeyEnter: // Check.
+ if key == tcell.KeyRune && event.Rune() != ' ' {
+ break
+ }
+ c.checked = !c.checked
+ if c.changed != nil {
+ c.changed(c.checked)
+ }
+ case tcell.KeyTab, tcell.KeyBacktab, tcell.KeyEscape: // We're done.
+ if c.done != nil {
+ c.done(key)
+ }
+ }
+ })
+}
diff --git a/vendor/maunium.net/go/tview/doc.go b/vendor/maunium.net/go/tview/doc.go
new file mode 100644
index 0000000..101dcd5
--- /dev/null
+++ b/vendor/maunium.net/go/tview/doc.go
@@ -0,0 +1,111 @@
+/*
+Package tview implements rich widgets for terminal based user interfaces. The
+widgets provided with this package are useful for data exploration and data
+entry.
+
+Widgets
+
+The package implements the following widgets:
+
+ - TextView: Scrollable windows that display multi-colored text. Text may also
+ be highlighted.
+ - Table: Scrollable display of tabular data. Table cells, rows, or columns may
+ also be highlighted.
+ - List: A navigable text list with optional keyboard shortcuts.
+ - InputField: One-line input fields to enter text.
+ - DropDown: Drop-down selection fields.
+ - Checkbox: Selectable checkbox for boolean values.
+ - Button: Buttons which get activated when the user selects them.
+ - Form: Forms composed of input fields, drop down selections, checkboxes, and
+ buttons.
+ - Modal: A centered window with a text message and one or more buttons.
+ - Flex: A Flexbox based layout manager.
+ - Pages: A page based layout manager.
+
+The package also provides Application which is used to poll the event queue and
+draw widgets on screen.
+
+Hello World
+
+The following is a very basic example showing a box with the title "Hello,
+world!":
+
+ package main
+
+ import (
+ "github.com/rivo/tview"
+ )
+
+ func main() {
+ box := tview.NewBox().SetBorder(true).SetTitle("Hello, world!")
+ if err := tview.NewApplication().SetRoot(box, true).Run(); err != nil {
+ panic(err)
+ }
+ }
+
+First, we create a box primitive with a border and a title. Then we create an
+application, set the box as its root primitive, and run the event loop. The
+application exits when the application's Stop() function is called or when
+Ctrl-C is pressed.
+
+If we have a primitive which consumes key presses, we call the application's
+SetFocus() function to redirect all key presses to that primitive. Most
+primitives then offer ways to install handlers that allow you to react to any
+actions performed on them.
+
+More Demos
+
+You will find more demos in the "demos" subdirectory. It also contains a
+presentation (written using tview) which gives an overview of the different
+widgets and how they can be used.
+
+Colors
+
+Throughout this package, colors are specified using the tcell.Color type.
+Functions such as tcell.GetColor(), tcell.NewHexColor(), and tcell.NewRGBColor()
+can be used to create colors from W3C color names or RGB values.
+
+Almost all strings which are displayed can contain color tags. Color tags are
+W3C color names or six hexadecimal digits following a hash tag, wrapped in
+square brackets. Examples:
+
+ This is a [red]warning[white]!
+ The sky is [#8080ff]blue[#ffffff].
+
+A color tag changes the color of the characters following that color tag. This
+applies to almost everything from box titles, list text, form item labels, to
+table cells. In a TextView, this functionality has to be switched on explicitly.
+See the TextView documentation for more information.
+
+In the rare event that you want to display a string such as "[red]" or
+"[#00ff1a]" without applying its effect, you need to put an opening square
+bracket before the closing square bracket. Examples:
+
+ [red[] will be output as [red]
+ ["123"[] will be output as ["123"]
+ [#6aff00[[] will be output as [#6aff00[]
+
+Styles
+
+When primitives are instantiated, they are initialized with colors taken from
+the global Styles variable. You may change this variable to adapt the look and
+feel of the primitives to your preferred style.
+
+Unicode Support
+
+This package supports unicode characters including wide characters.
+
+Type Hierarchy
+
+All widgets listed above contain the Box type. All of Box's functions are
+therefore available for all widgets, too.
+
+All widgets also implement the Primitive interface. There is also the Focusable
+interface which is used to override functions in subclassing types.
+
+The tview package is based on https://maunium.net/go/tcell. It uses types
+and constants from that package (e.g. colors and keyboard values).
+
+This package does not process mouse input (yet).
+*/
+package tview
diff --git a/vendor/maunium.net/go/tview/dropdown.go b/vendor/maunium.net/go/tview/dropdown.go
new file mode 100644
index 0000000..981d1bd
--- /dev/null
+++ b/vendor/maunium.net/go/tview/dropdown.go
@@ -0,0 +1,380 @@
+package tview
+
+import (
+ "strings"
+
+ "maunium.net/go/tcell"
+ runewidth "github.com/mattn/go-runewidth"
+)
+
+// dropDownOption is one option that can be selected in a drop-down primitive.
+type dropDownOption struct {
+ Text string // The text to be displayed in the drop-down.
+ Selected func() // The (optional) callback for when this option was selected.
+}
+
+// DropDown implements a selection widget whose options become visible in a
+// drop-down list when activated.
+//
+// See https://github.com/rivo/tview/wiki/DropDown for an example.
+type DropDown struct {
+ *Box
+
+ // The options from which the user can choose.
+ options []*dropDownOption
+
+ // The index of the currently selected option. Negative if no option is
+ // currently selected.
+ currentOption int
+
+ // Set to true if the options are visible and selectable.
+ open bool
+
+ // The runes typed so far to directly access one of the list items.
+ prefix string
+
+ // The list element for the options.
+ list *List
+
+ // The text to be displayed before the input area.
+ label string
+
+ // The label color.
+ labelColor tcell.Color
+
+ // The background color of the input area.
+ fieldBackgroundColor tcell.Color
+
+ // The text color of the input area.
+ fieldTextColor tcell.Color
+
+ // The color for prefixes.
+ prefixTextColor tcell.Color
+
+ // The screen width of the input area. A value of 0 means extend as much as
+ // possible.
+ fieldWidth int
+
+ // An optional function which is called when the user indicated that they
+ // are done selecting options. The key which was pressed is provided (tab,
+ // shift-tab, or escape).
+ done func(tcell.Key)
+}
+
+// NewDropDown returns a new drop-down.
+func NewDropDown() *DropDown {
+ list := NewList().ShowSecondaryText(false)
+ list.SetMainTextColor(Styles.PrimitiveBackgroundColor).
+ SetSelectedTextColor(Styles.PrimitiveBackgroundColor).
+ SetSelectedBackgroundColor(Styles.PrimaryTextColor).
+ SetBackgroundColor(Styles.MoreContrastBackgroundColor)
+
+ d := &DropDown{
+ Box: NewBox(),
+ currentOption: -1,
+ list: list,
+ labelColor: Styles.SecondaryTextColor,
+ fieldBackgroundColor: Styles.ContrastBackgroundColor,
+ fieldTextColor: Styles.PrimaryTextColor,
+ prefixTextColor: Styles.ContrastSecondaryTextColor,
+ }
+
+ d.focus = d
+
+ return d
+}
+
+// SetCurrentOption sets the index of the currently selected option. This may
+// be a negative value to indicate that no option is currently selected.
+func (d *DropDown) SetCurrentOption(index int) *DropDown {
+ d.currentOption = index
+ d.list.SetCurrentItem(index)
+ return d
+}
+
+// GetCurrentOption returns the index of the currently selected option as well
+// as its text. If no option was selected, -1 and an empty string is returned.
+func (d *DropDown) GetCurrentOption() (int, string) {
+ var text string
+ if d.currentOption >= 0 && d.currentOption < len(d.options) {
+ text = d.options[d.currentOption].Text
+ }
+ return d.currentOption, text
+}
+
+// SetLabel sets the text to be displayed before the input area.
+func (d *DropDown) SetLabel(label string) *DropDown {
+ d.label = label
+ return d
+}
+
+// GetLabel returns the text to be displayed before the input area.
+func (d *DropDown) GetLabel() string {
+ return d.label
+}
+
+// SetLabelColor sets the color of the label.
+func (d *DropDown) SetLabelColor(color tcell.Color) *DropDown {
+ d.labelColor = color
+ return d
+}
+
+// SetFieldBackgroundColor sets the background color of the options area.
+func (d *DropDown) SetFieldBackgroundColor(color tcell.Color) *DropDown {
+ d.fieldBackgroundColor = color
+ return d
+}
+
+// SetFieldTextColor sets the text color of the options area.
+func (d *DropDown) SetFieldTextColor(color tcell.Color) *DropDown {
+ d.fieldTextColor = color
+ return d
+}
+
+// SetPrefixTextColor sets the color of the prefix string. The prefix string is
+// shown when the user starts typing text, which directly selects the first
+// option that starts with the typed string.
+func (d *DropDown) SetPrefixTextColor(color tcell.Color) *DropDown {
+ d.prefixTextColor = color
+ return d
+}
+
+// SetFormAttributes sets attributes shared by all form items.
+func (d *DropDown) SetFormAttributes(label string, labelColor, bgColor, fieldTextColor, fieldBgColor tcell.Color) FormItem {
+ d.label = label
+ d.labelColor = labelColor
+ d.backgroundColor = bgColor
+ d.fieldTextColor = fieldTextColor
+ d.fieldBackgroundColor = fieldBgColor
+ return d
+}
+
+// SetFieldWidth sets the screen width of the options area. A value of 0 means
+// extend to as long as the longest option text.
+func (d *DropDown) SetFieldWidth(width int) *DropDown {
+ d.fieldWidth = width
+ return d
+}
+
+// GetFieldWidth returns this primitive's field screen width.
+func (d *DropDown) GetFieldWidth() int {
+ if d.fieldWidth > 0 {
+ return d.fieldWidth
+ }
+ fieldWidth := 0
+ for _, option := range d.options {
+ width := StringWidth(option.Text)
+ if width > fieldWidth {
+ fieldWidth = width
+ }
+ }
+ return fieldWidth
+}
+
+// AddOption adds a new selectable option to this drop-down. The "selected"
+// callback is called when this option was selected. It may be nil.
+func (d *DropDown) AddOption(text string, selected func()) *DropDown {
+ d.options = append(d.options, &dropDownOption{Text: text, Selected: selected})
+ d.list.AddItem(text, "", 0, selected)
+ return d
+}
+
+// SetOptions replaces all current options with the ones provided and installs
+// one callback function which is called when one of the options is selected.
+// It will be called with the option's text and its index into the options
+// slice. The "selected" parameter may be nil.
+func (d *DropDown) SetOptions(texts []string, selected func(text string, index int)) *DropDown {
+ d.list.Clear()
+ d.options = nil
+ for index, text := range texts {
+ func(t string, i int) {
+ d.AddOption(text, func() {
+ if selected != nil {
+ selected(t, i)
+ }
+ })
+ }(text, index)
+ }
+ return d
+}
+
+// SetDoneFunc sets a handler which is called when the user is done selecting
+// options. The callback function is provided with the key that was pressed,
+// which is one of the following:
+//
+// - KeyEscape: Abort selection.
+// - KeyTab: Move to the next field.
+// - KeyBacktab: Move to the previous field.
+func (d *DropDown) SetDoneFunc(handler func(key tcell.Key)) *DropDown {
+ d.done = handler
+ return d
+}
+
+// SetFinishedFunc calls SetDoneFunc().
+func (d *DropDown) SetFinishedFunc(handler func(key tcell.Key)) FormItem {
+ return d.SetDoneFunc(handler)
+}
+
+// Draw draws this primitive onto the screen.
+func (d *DropDown) Draw(screen tcell.Screen) {
+ d.Box.Draw(screen)
+
+ // Prepare.
+ x, y, width, height := d.GetInnerRect()
+ rightLimit := x + width
+ if height < 1 || rightLimit <= x {
+ return
+ }
+
+ // Draw label.
+ _, drawnWidth := Print(screen, d.label, x, y, rightLimit-x, AlignLeft, d.labelColor)
+ x += drawnWidth
+
+ // What's the longest option text?
+ maxWidth := 0
+ for _, option := range d.options {
+ strWidth := StringWidth(option.Text)
+ if strWidth > maxWidth {
+ maxWidth = strWidth
+ }
+ }
+
+ // Draw selection area.
+ fieldWidth := d.fieldWidth
+ if fieldWidth == 0 {
+ fieldWidth = maxWidth
+ }
+ if rightLimit-x < fieldWidth {
+ fieldWidth = rightLimit - x
+ }
+ fieldStyle := tcell.StyleDefault.Background(d.fieldBackgroundColor)
+ if d.GetFocusable().HasFocus() && !d.open {
+ fieldStyle = fieldStyle.Background(d.fieldTextColor)
+ }
+ for index := 0; index < fieldWidth; index++ {
+ screen.SetContent(x+index, y, ' ', nil, fieldStyle)
+ }
+
+ // Draw selected text.
+ if d.open && len(d.prefix) > 0 {
+ // Show the prefix.
+ Print(screen, d.prefix, x, y, fieldWidth, AlignLeft, d.prefixTextColor)
+ prefixWidth := runewidth.StringWidth(d.prefix)
+ listItemText := d.options[d.list.GetCurrentItem()].Text
+ if prefixWidth < fieldWidth && len(d.prefix) < len(listItemText) {
+ Print(screen, listItemText[len(d.prefix):], x+prefixWidth, y, fieldWidth-prefixWidth, AlignLeft, d.fieldTextColor)
+ }
+ } else {
+ if d.currentOption >= 0 && d.currentOption < len(d.options) {
+ color := d.fieldTextColor
+ // Just show the current selection.
+ if d.GetFocusable().HasFocus() && !d.open {
+ color = d.fieldBackgroundColor
+ }
+ Print(screen, d.options[d.currentOption].Text, x, y, fieldWidth, AlignLeft, color)
+ }
+ }
+
+ // Draw options list.
+ if d.HasFocus() && d.open {
+ // We prefer to drop down but if there is no space, maybe drop up?
+ lx := x
+ ly := y + 1
+ lwidth := maxWidth
+ lheight := len(d.options)
+ _, sheight := screen.Size()
+ if ly+lheight >= sheight && ly-2 > lheight-ly {
+ ly = y - lheight
+ if ly < 0 {
+ ly = 0
+ }
+ }
+ if ly+lheight >= sheight {
+ lheight = sheight - ly
+ }
+ d.list.SetRect(lx, ly, lwidth, lheight)
+ d.list.Draw(screen)
+ }
+}
+
+// InputHandler returns the handler for this primitive.
+func (d *DropDown) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) {
+ return d.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p Primitive)) {
+ // A helper function which selects an item in the drop-down list based on
+ // the current prefix.
+ evalPrefix := func() {
+ if len(d.prefix) > 0 {
+ for index, option := range d.options {
+ if strings.HasPrefix(strings.ToLower(option.Text), d.prefix) {
+ d.list.SetCurrentItem(index)
+ return
+ }
+ }
+ // Prefix does not match any item. Remove last rune.
+ r := []rune(d.prefix)
+ d.prefix = string(r[:len(r)-1])
+ }
+ }
+
+ // Process key event.
+ switch key := event.Key(); key {
+ case tcell.KeyEnter, tcell.KeyRune, tcell.KeyDown:
+ d.prefix = ""
+
+ // If the first key was a letter already, it becomes part of the prefix.
+ if r := event.Rune(); key == tcell.KeyRune && r != ' ' {
+ d.prefix += string(r)
+ evalPrefix()
+ }
+
+ // Hand control over to the list.
+ d.open = true
+ d.list.SetSelectedFunc(func(index int, mainText, secondaryText string, shortcut rune) {
+ // An option was selected. Close the list again.
+ d.open = false
+ setFocus(d)
+ d.currentOption = index
+
+ // Trigger "selected" event.
+ if d.options[d.currentOption].Selected != nil {
+ d.options[d.currentOption].Selected()
+ }
+ }).SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
+ if event.Key() == tcell.KeyRune {
+ d.prefix += string(event.Rune())
+ evalPrefix()
+ } else if event.Key() == tcell.KeyBackspace || event.Key() == tcell.KeyBackspace2 {
+ if len(d.prefix) > 0 {
+ r := []rune(d.prefix)
+ d.prefix = string(r[:len(r)-1])
+ }
+ evalPrefix()
+ } else {
+ d.prefix = ""
+ }
+ return event
+ })
+ setFocus(d.list)
+ case tcell.KeyEscape, tcell.KeyTab, tcell.KeyBacktab:
+ if d.done != nil {
+ d.done(key)
+ }
+ }
+ })
+}
+
+// Focus is called by the application when the primitive receives focus.
+func (d *DropDown) Focus(delegate func(p Primitive)) {
+ d.Box.Focus(delegate)
+ if d.open {
+ delegate(d.list)
+ }
+}
+
+// HasFocus returns whether or not this primitive has focus.
+func (d *DropDown) HasFocus() bool {
+ if d.open {
+ return d.list.HasFocus()
+ }
+ return d.hasFocus
+}
diff --git a/vendor/maunium.net/go/tview/flex.go b/vendor/maunium.net/go/tview/flex.go
new file mode 100644
index 0000000..a698442
--- /dev/null
+++ b/vendor/maunium.net/go/tview/flex.go
@@ -0,0 +1,173 @@
+package tview
+
+import (
+ "maunium.net/go/tcell"
+)
+
+// Configuration values.
+const (
+ FlexRow = iota
+ FlexColumn
+)
+
+// flexItem holds layout options for one item.
+type flexItem struct {
+ Item Primitive // The item to be positioned. May be nil for an empty item.
+ FixedSize int // The item's fixed size which may not be changed, 0 if it has no fixed size.
+ Proportion int // The item's proportion.
+ Focus bool // Whether or not this item attracts the layout's focus.
+}
+
+// Flex is a basic implementation of the Flexbox layout. The contained
+// primitives are arranged horizontally or vertically. The way they are
+// distributed along that dimension depends on their layout settings, which is
+// either a fixed length or a proportional length. See AddItem() for details.
+//
+// See https://github.com/rivo/tview/wiki/Flex for an example.
+type Flex struct {
+ *Box
+
+ // The items to be positioned.
+ items []flexItem
+
+ // FlexRow or FlexColumn.
+ direction int
+
+ // If set to true, will use the entire screen as its available space instead
+ // its box dimensions.
+ fullScreen bool
+}
+
+// NewFlex returns a new flexbox layout container with no primitives and its
+// direction set to FlexColumn. To add primitives to this layout, see AddItem().
+// To change the direction, see SetDirection().
+func NewFlex() *Flex {
+ f := &Flex{
+ Box: NewBox(),
+ direction: FlexColumn,
+ }
+ f.focus = f
+ return f
+}
+
+// SetDirection sets the direction in which the contained primitives are
+// distributed. This can be either FlexColumn (default) or FlexRow.
+func (f *Flex) SetDirection(direction int) *Flex {
+ f.direction = direction
+ return f
+}
+
+// SetFullScreen sets the flag which, when true, causes the flex layout to use
+// the entire screen space instead of whatever size it is currently assigned to.
+func (f *Flex) SetFullScreen(fullScreen bool) *Flex {
+ f.fullScreen = fullScreen
+ return f
+}
+
+// AddItem adds a new item to the container. The "fixedSize" argument is a width
+// or height that may not be changed by the layout algorithm. A value of 0 means
+// that its size is flexible and may be changed. The "proportion" argument
+// defines the relative size of the item compared to other flexible-size items.
+// For example, items with a proportion of 2 will be twice as large as items
+// with a proportion of 1. Must be at least 1 if fixedSize > 0 (ignored
+// otherwise)
+//
+// If "focus" is set to true, the item will receive focus when the Flex
+// primitive receives focus. If multiple items have the "focus" flag set to
+// true, the first one will receive focus.
+//
+// You can provide a nil value for the primitive. This will still consume screen
+// space but nothing will be drawn.
+func (f *Flex) AddItem(item Primitive, fixedSize, proportion int, focus bool) *Flex {
+ f.items = append(f.items, flexItem{Item: item, FixedSize: fixedSize, Proportion: proportion, Focus: focus})
+ return f
+}
+
+// RemoveItem removes all items for the given primitive from the container,
+// keeping the order of the remaining items intact.
+func (f *Flex) RemoveItem(p Primitive) *Flex {
+ for index := len(f.items) - 1; index >= 0; index-- {
+ if f.items[index].Item == p {
+ f.items = append(f.items[:index], f.items[index+1:]...)
+ }
+ }
+ return f
+}
+
+// Draw draws this primitive onto the screen.
+func (f *Flex) Draw(screen tcell.Screen) {
+ f.Box.Draw(screen)
+
+ // Calculate size and position of the items.
+
+ // Do we use the entire screen?
+ if f.fullScreen {
+ width, height := screen.Size()
+ f.SetRect(0, 0, width, height)
+ }
+
+ // How much space can we distribute?
+ x, y, width, height := f.GetInnerRect()
+ var proportionSum int
+ distSize := width
+ if f.direction == FlexRow {
+ distSize = height
+ }
+ for _, item := range f.items {
+ if item.FixedSize > 0 {
+ distSize -= item.FixedSize
+ } else {
+ proportionSum += item.Proportion
+ }
+ }
+
+ // Calculate positions and draw items.
+ pos := x
+ if f.direction == FlexRow {
+ pos = y
+ }
+ for _, item := range f.items {
+ size := item.FixedSize
+ if size <= 0 {
+ size = distSize * item.Proportion / proportionSum
+ distSize -= size
+ proportionSum -= item.Proportion
+ }
+ if item.Item != nil {
+ if f.direction == FlexColumn {
+ item.Item.SetRect(pos, y, size, height)
+ } else {
+ item.Item.SetRect(x, pos, width, size)
+ }
+ }
+ pos += size
+
+ if item.Item != nil {
+ if item.Item.GetFocusable().HasFocus() {
+ defer item.Item.Draw(screen)
+ } else {
+ item.Item.Draw(screen)
+ }
+ }
+ }
+}
+
+// Focus is called when this primitive receives focus.
+func (f *Flex) Focus(delegate func(p Primitive)) {
+ for _, item := range f.items {
+ if item.Item != nil && item.Focus {
+ delegate(item.Item)
+ return
+ }
+ }
+}
+
+// HasFocus returns whether or not this primitive has focus.
+func (f *Flex) HasFocus() bool {
+ for _, item := range f.items {
+ if item.Item != nil && item.Item.GetFocusable().HasFocus() {
+ return true
+ }
+ }
+ return false
+}
diff --git a/vendor/maunium.net/go/tview/focusable.go b/vendor/maunium.net/go/tview/focusable.go
new file mode 100644
index 0000000..99fdaaf
--- /dev/null
+++ b/vendor/maunium.net/go/tview/focusable.go
@@ -0,0 +1,8 @@
+package tview
+
+// Focusable provides a method which determines if a primitive has focus.
+// Composed primitives may be focused based on the focused state of their
+// contained primitives.
+type Focusable interface {
+ HasFocus() bool
+}
diff --git a/vendor/maunium.net/go/tview/form.go b/vendor/maunium.net/go/tview/form.go
new file mode 100644
index 0000000..b1e8f5e
--- /dev/null
+++ b/vendor/maunium.net/go/tview/form.go
@@ -0,0 +1,499 @@
+package tview
+
+import (
+ "strings"
+
+ "maunium.net/go/tcell"
+)
+
+// DefaultFormFieldWidth is the default field screen width of form elements
+// whose field width is flexible (0). This is used in the Form class for
+// horizontal layouts.
+var DefaultFormFieldWidth = 10
+
+// FormItem is the interface all form items must implement to be able to be
+// included in a form.
+type FormItem interface {
+ Primitive
+
+ // GetLabel returns the item's label text.
+ GetLabel() string
+
+ // SetFormAttributes sets a number of item attributes at once.
+ SetFormAttributes(label string, labelColor, bgColor, fieldTextColor, fieldBgColor tcell.Color) FormItem
+
+ // GetFieldWidth returns the width of the form item's field (the area which
+ // is manipulated by the user) in number of screen cells. A value of 0
+ // indicates the the field width is flexible and may use as much space as
+ // required.
+ GetFieldWidth() int
+
+ // SetEnteredFunc sets the handler function for when the user finished
+ // entering data into the item. The handler may receive events for the
+ // Enter key (we're done), the Escape key (cancel input), the Tab key (move to
+ // next field), and the Backtab key (move to previous field).
+ SetFinishedFunc(handler func(key tcell.Key)) FormItem
+}
+
+// Form allows you to combine multiple one-line form elements into a vertical
+// or horizontal layout. Form elements include types such as InputField or
+// Checkbox. These elements can be optionally followed by one or more buttons
+// for which you can define form-wide actions (e.g. Save, Clear, Cancel).
+//
+// See https://github.com/rivo/tview/wiki/Form for an example.
+type Form struct {
+ *Box
+
+ // The items of the form (one row per item).
+ items []FormItem
+
+ // The buttons of the form.
+ buttons []*Button
+
+ // If set to true, instead of position items and buttons from top to bottom,
+ // they are positioned from left to right.
+ horizontal bool
+
+ // The alignment of the buttons.
+ buttonsAlign int
+
+ // The number of empty rows between items.
+ itemPadding int
+
+ // The index of the item or button which has focus. (Items are counted first,
+ // buttons are counted last.)
+ focusedElement int
+
+ // The label color.
+ labelColor tcell.Color
+
+ // The background color of the input area.
+ fieldBackgroundColor tcell.Color
+
+ // The text color of the input area.
+ fieldTextColor tcell.Color
+
+ // The background color of the buttons.
+ buttonBackgroundColor tcell.Color
+
+ // The color of the button text.
+ buttonTextColor tcell.Color
+
+ // An optional function which is called when the user hits Escape.
+ cancel func()
+}
+
+// NewForm returns a new form.
+func NewForm() *Form {
+ box := NewBox().SetBorderPadding(1, 1, 1, 1)
+
+ f := &Form{
+ Box: box,
+ itemPadding: 1,
+ labelColor: Styles.SecondaryTextColor,
+ fieldBackgroundColor: Styles.ContrastBackgroundColor,
+ fieldTextColor: Styles.PrimaryTextColor,
+ buttonBackgroundColor: Styles.ContrastBackgroundColor,
+ buttonTextColor: Styles.PrimaryTextColor,
+ }
+
+ f.focus = f
+
+ return f
+}
+
+// SetItemPadding sets the number of empty rows between form items for vertical
+// layouts and the number of empty cells between form items for horizontal
+// layouts.
+func (f *Form) SetItemPadding(padding int) *Form {
+ f.itemPadding = padding
+ return f
+}
+
+// SetHorizontal sets the direction the form elements are laid out. If set to
+// true, instead of positioning them from top to bottom (the default), they are
+// positioned from left to right, moving into the next row if there is not
+// enough space.
+func (f *Form) SetHorizontal(horizontal bool) *Form {
+ f.horizontal = horizontal
+ return f
+}
+
+// SetLabelColor sets the color of the labels.
+func (f *Form) SetLabelColor(color tcell.Color) *Form {
+ f.labelColor = color
+ return f
+}
+
+// SetFieldBackgroundColor sets the background color of the input areas.
+func (f *Form) SetFieldBackgroundColor(color tcell.Color) *Form {
+ f.fieldBackgroundColor = color
+ return f
+}
+
+// SetFieldTextColor sets the text color of the input areas.
+func (f *Form) SetFieldTextColor(color tcell.Color) *Form {
+ f.fieldTextColor = color
+ return f
+}
+
+// SetButtonsAlign sets how the buttons align horizontally, one of AlignLeft
+// (the default), AlignCenter, and AlignRight. This is only
+func (f *Form) SetButtonsAlign(align int) *Form {
+ f.buttonsAlign = align
+ return f
+}
+
+// SetButtonBackgroundColor sets the background color of the buttons.
+func (f *Form) SetButtonBackgroundColor(color tcell.Color) *Form {
+ f.buttonBackgroundColor = color
+ return f
+}
+
+// SetButtonTextColor sets the color of the button texts.
+func (f *Form) SetButtonTextColor(color tcell.Color) *Form {
+ f.buttonTextColor = color
+ return f
+}
+
+// AddInputField adds an input field to the form. It has a label, an optional
+// initial value, a field width (a value of 0 extends it as far as possible),
+// an optional accept function to validate the item's value (set to nil to
+// accept any text), and an (optional) callback function which is invoked when
+// the input field's text has changed.
+func (f *Form) AddInputField(label, value string, fieldWidth int, accept func(textToCheck string, lastChar rune) bool, changed func(text string)) *Form {
+ f.items = append(f.items, NewInputField().
+ SetLabel(label).
+ SetText(value).
+ SetFieldWidth(fieldWidth).
+ SetAcceptanceFunc(accept).
+ SetChangedFunc(changed))
+ return f
+}
+
+// AddPasswordField adds a password field to the form. This is similar to an
+// input field except that the user's input not shown. Instead, a "mask"
+// character is displayed. The password field has a label, an optional initial
+// value, a field width (a value of 0 extends it as far as possible), and an
+// (optional) callback function which is invoked when the input field's text has
+// changed.
+func (f *Form) AddPasswordField(label, value string, fieldWidth int, mask rune, changed func(text string)) *Form {
+ if mask == 0 {
+ mask = '*'
+ }
+ f.items = append(f.items, NewInputField().
+ SetLabel(label).
+ SetText(value).
+ SetFieldWidth(fieldWidth).
+ SetMaskCharacter(mask).
+ SetChangedFunc(changed))
+ return f
+}
+
+// AddDropDown adds a drop-down element to the form. It has a label, options,
+// and an (optional) callback function which is invoked when an option was
+// selected. The initial option may be a negative value to indicate that no
+// option is currently selected.
+func (f *Form) AddDropDown(label string, options []string, initialOption int, selected func(option string, optionIndex int)) *Form {
+ f.items = append(f.items, NewDropDown().
+ SetLabel(label).
+ SetCurrentOption(initialOption).
+ SetOptions(options, selected))
+ return f
+}
+
+// AddCheckbox adds a checkbox to the form. It has a label, an initial state,
+// and an (optional) callback function which is invoked when the state of the
+// checkbox was changed by the user.
+func (f *Form) AddCheckbox(label string, checked bool, changed func(checked bool)) *Form {
+ f.items = append(f.items, NewCheckbox().
+ SetLabel(label).
+ SetChecked(checked).
+ SetChangedFunc(changed))
+ return f
+}
+
+// AddButton adds a new button to the form. The "selected" function is called
+// when the user selects this button. It may be nil.
+func (f *Form) AddButton(label string, selected func()) *Form {
+ f.buttons = append(f.buttons, NewButton(label).SetSelectedFunc(selected))
+ return f
+}
+
+// Clear removes all input elements from the form, including the buttons if
+// specified.
+func (f *Form) Clear(includeButtons bool) *Form {
+ f.items = nil
+ if includeButtons {
+ f.buttons = nil
+ }
+ f.focusedElement = 0
+ return f
+}
+
+// AddFormItem adds a new item to the form. This can be used to add your own
+// objects to the form. Note, however, that the Form class will override some
+// of its attributes to make it work in the form context.
+func (f *Form) AddFormItem(item FormItem) *Form {
+ f.items = append(f.items, item)
+ return f
+}
+
+// GetFormItem returns the form element at the given position, starting with
+// index 0. Elements are referenced in the order they were added. Buttons are
+// not included.
+func (f *Form) GetFormItem(index int) FormItem {
+ return f.items[index]
+}
+
+// SetCancelFunc sets a handler which is called when the user hits the Escape
+// key.
+func (f *Form) SetCancelFunc(callback func()) *Form {
+ f.cancel = callback
+ return f
+}
+
+// Draw draws this primitive onto the screen.
+func (f *Form) Draw(screen tcell.Screen) {
+ f.Box.Draw(screen)
+
+ // Determine the dimensions.
+ x, y, width, height := f.GetInnerRect()
+ topLimit := y
+ bottomLimit := y + height
+ rightLimit := x + width
+ startX := x
+
+ // Find the longest label.
+ var maxLabelWidth int
+ for _, item := range f.items {
+ label := strings.TrimSpace(item.GetLabel())
+ labelWidth := StringWidth(label)
+ if labelWidth > maxLabelWidth {
+ maxLabelWidth = labelWidth
+ }
+ }
+ maxLabelWidth++ // Add one space.
+
+ // Calculate positions of form items.
+ positions := make([]struct{ x, y, width, height int }, len(f.items)+len(f.buttons))
+ var focusedPosition struct{ x, y, width, height int }
+ for index, item := range f.items {
+ // Calculate the space needed.
+ label := strings.TrimSpace(item.GetLabel())
+ labelWidth := StringWidth(label)
+ var itemWidth int
+ if f.horizontal {
+ fieldWidth := item.GetFieldWidth()
+ if fieldWidth == 0 {
+ fieldWidth = DefaultFormFieldWidth
+ }
+ label += " "
+ labelWidth++
+ itemWidth = labelWidth + fieldWidth
+ } else {
+ // We want all fields to align vertically.
+ label += strings.Repeat(" ", maxLabelWidth-labelWidth)
+ itemWidth = width
+ }
+
+ // Advance to next line if there is no space.
+ if f.horizontal && x+labelWidth+1 >= rightLimit {
+ x = startX
+ y += 2
+ }
+
+ // Adjust the item's attributes.
+ if x+itemWidth >= rightLimit {
+ itemWidth = rightLimit - x
+ }
+ item.SetFormAttributes(
+ label,
+ f.labelColor,
+ f.backgroundColor,
+ f.fieldTextColor,
+ f.fieldBackgroundColor,
+ )
+
+ // Save position.
+ positions[index].x = x
+ positions[index].y = y
+ positions[index].width = itemWidth
+ positions[index].height = 1
+ if item.GetFocusable().HasFocus() {
+ focusedPosition = positions[index]
+ }
+
+ // Advance to next item.
+ if f.horizontal {
+ x += itemWidth + f.itemPadding
+ } else {
+ y += 1 + f.itemPadding
+ }
+ }
+
+ // How wide are the buttons?
+ buttonWidths := make([]int, len(f.buttons))
+ buttonsWidth := 0
+ for index, button := range f.buttons {
+ w := StringWidth(button.GetLabel()) + 4
+ buttonWidths[index] = w
+ buttonsWidth += w + 1
+ }
+ buttonsWidth--
+
+ // Where do we place them?
+ if !f.horizontal && x+buttonsWidth < rightLimit {
+ if f.buttonsAlign == AlignRight {
+ x = rightLimit - buttonsWidth
+ } else if f.buttonsAlign == AlignCenter {
+ x = (x + rightLimit - buttonsWidth) / 2
+ }
+
+ // In vertical layouts, buttons always appear after an empty line.
+ if f.itemPadding == 0 {
+ y++
+ }
+ }
+
+ // Calculate positions of buttons.
+ for index, button := range f.buttons {
+ space := rightLimit - x
+ buttonWidth := buttonWidths[index]
+ if f.horizontal {
+ if space < buttonWidth-4 {
+ x = startX
+ y += 2
+ space = width
+ }
+ } else {
+ if space < 1 {
+ break // No space for this button anymore.
+ }
+ }
+ if buttonWidth > space {
+ buttonWidth = space
+ }
+ button.SetLabelColor(f.buttonTextColor).
+ SetLabelColorActivated(f.buttonBackgroundColor).
+ SetBackgroundColorActivated(f.buttonTextColor).
+ SetBackgroundColor(f.buttonBackgroundColor)
+
+ buttonIndex := index + len(f.items)
+ positions[buttonIndex].x = x
+ positions[buttonIndex].y = y
+ positions[buttonIndex].width = buttonWidth
+ positions[buttonIndex].height = 1
+
+ if button.HasFocus() {
+ focusedPosition = positions[buttonIndex]
+ }
+
+ x += buttonWidth + 1
+ }
+
+ // Determine vertical offset based on the position of the focused item.
+ var offset int
+ if focusedPosition.y+focusedPosition.height > bottomLimit {
+ offset = focusedPosition.y + focusedPosition.height - bottomLimit
+ if focusedPosition.y-offset < topLimit {
+ offset = focusedPosition.y - topLimit
+ }
+ }
+
+ // Draw items.
+ for index, item := range f.items {
+ // Set position.
+ y := positions[index].y - offset
+ height := positions[index].height
+ item.SetRect(positions[index].x, y, positions[index].width, height)
+
+ // Is this item visible?
+ if y+height <= topLimit || y >= bottomLimit {
+ continue
+ }
+
+ // Draw items with focus last (in case of overlaps).
+ if item.GetFocusable().HasFocus() {
+ defer item.Draw(screen)
+ } else {
+ item.Draw(screen)
+ }
+ }
+
+ // Draw buttons.
+ for index, button := range f.buttons {
+ // Set position.
+ buttonIndex := index + len(f.items)
+ y := positions[buttonIndex].y - offset
+ height := positions[buttonIndex].height
+ button.SetRect(positions[buttonIndex].x, y, positions[buttonIndex].width, height)
+
+ // Is this button visible?
+ if y+height <= topLimit || y >= bottomLimit {
+ continue
+ }
+
+ // Draw button.
+ button.Draw(screen)
+ }
+}
+
+// Focus is called by the application when the primitive receives focus.
+func (f *Form) Focus(delegate func(p Primitive)) {
+ if len(f.items)+len(f.buttons) == 0 {
+ return
+ }
+
+ // Hand on the focus to one of our child elements.
+ if f.focusedElement < 0 || f.focusedElement >= len(f.items)+len(f.buttons) {
+ f.focusedElement = 0
+ }
+ handler := func(key tcell.Key) {
+ switch key {
+ case tcell.KeyTab, tcell.KeyEnter:
+ f.focusedElement++
+ f.Focus(delegate)
+ case tcell.KeyBacktab:
+ f.focusedElement--
+ if f.focusedElement < 0 {
+ f.focusedElement = len(f.items) + len(f.buttons) - 1
+ }
+ f.Focus(delegate)
+ case tcell.KeyEscape:
+ if f.cancel != nil {
+ f.cancel()
+ } else {
+ f.focusedElement = 0
+ f.Focus(delegate)
+ }
+ }
+ }
+
+ if f.focusedElement < len(f.items) {
+ // We're selecting an item.
+ item := f.items[f.focusedElement]
+ item.SetFinishedFunc(handler)
+ delegate(item)
+ } else {
+ // We're selecting a button.
+ button := f.buttons[f.focusedElement-len(f.items)]
+ button.SetBlurFunc(handler)
+ delegate(button)
+ }
+}
+
+// HasFocus returns whether or not this primitive has focus.
+func (f *Form) HasFocus() bool {
+ for _, item := range f.items {
+ if item.GetFocusable().HasFocus() {
+ return true
+ }
+ }
+ for _, button := range f.buttons {
+ if button.focus.HasFocus() {
+ return true
+ }
+ }
+ return false
+}
diff --git a/vendor/maunium.net/go/tview/frame.go b/vendor/maunium.net/go/tview/frame.go
new file mode 100644
index 0000000..47455a2
--- /dev/null
+++ b/vendor/maunium.net/go/tview/frame.go
@@ -0,0 +1,157 @@
+package tview
+
+import (
+ "maunium.net/go/tcell"
+)
+
+// frameText holds information about a line of text shown in the frame.
+type frameText struct {
+ Text string // The text to be displayed.
+ Header bool // true = place in header, false = place in footer.
+ Align int // One of the Align constants.
+ Color tcell.Color // The text color.
+}
+
+// Frame is a wrapper which adds a border around another primitive. The top area
+// (header) and the bottom area (footer) may also contain text.
+//
+// See https://github.com/rivo/tview/wiki/Frame for an example.
+type Frame struct {
+ *Box
+
+ // The contained primitive.
+ primitive Primitive
+
+ // The lines of text to be displayed.
+ text []*frameText
+
+ // Border spacing.
+ top, bottom, header, footer, left, right int
+}
+
+// NewFrame returns a new frame around the given primitive. The primitive's
+// size will be changed to fit within this frame.
+func NewFrame(primitive Primitive) *Frame {
+ box := NewBox()
+
+ f := &Frame{
+ Box: box,
+ primitive: primitive,
+ top: 1,
+ bottom: 1,
+ header: 1,
+ footer: 1,
+ left: 1,
+ right: 1,
+ }
+
+ f.focus = f
+
+ return f
+}
+
+// AddText adds text to the frame. Set "header" to true if the text is to appear
+// in the header, above the contained primitive. Set it to false for it to
+// appear in the footer, below the contained primitive. "align" must be one of
+// the Align constants. Rows in the header are printed top to bottom, rows in
+// the footer are printed bottom to top. Note that long text can overlap as
+// different alignments will be placed on the same row.
+func (f *Frame) AddText(text string, header bool, align int, color tcell.Color) *Frame {
+ f.text = append(f.text, &frameText{
+ Text: text,
+ Header: header,
+ Align: align,
+ Color: color,
+ })
+ return f
+}
+
+// Clear removes all text from the frame.
+func (f *Frame) Clear() *Frame {
+ f.text = nil
+ return f
+}
+
+// SetBorders sets the width of the frame borders as well as "header" and
+// "footer", the vertical space between the header and footer text and the
+// contained primitive (does not apply if there is no text).
+func (f *Frame) SetBorders(top, bottom, header, footer, left, right int) *Frame {
+ f.top, f.bottom, f.header, f.footer, f.left, f.right = top, bottom, header, footer, left, right
+ return f
+}
+
+// Draw draws this primitive onto the screen.
+func (f *Frame) Draw(screen tcell.Screen) {
+ f.Box.Draw(screen)
+
+ // Calculate start positions.
+ x, top, width, height := f.GetInnerRect()
+ bottom := top + height - 1
+ x += f.left
+ top += f.top
+ bottom -= f.bottom
+ width -= f.left + f.right
+ if width <= 0 || top >= bottom {
+ return // No space left.
+ }
+
+ // Draw text.
+ var rows [6]int // top-left, top-center, top-right, bottom-left, bottom-center, bottom-right.
+ topMax := top
+ bottomMin := bottom
+ for _, text := range f.text {
+ // Where do we place this text?
+ var y int
+ if text.Header {
+ y = top + rows[text.Align]
+ rows[text.Align]++
+ if y >= bottomMin {
+ continue
+ }
+ if y+1 > topMax {
+ topMax = y + 1
+ }
+ } else {
+ y = bottom - rows[3+text.Align]
+ rows[3+text.Align]++
+ if y <= topMax {
+ continue
+ }
+ if y-1 < bottomMin {
+ bottomMin = y - 1
+ }
+ }
+
+ // Draw text.
+ Print(screen, text.Text, x, y, width, text.Align, text.Color)
+ }
+
+ // Set the size of the contained primitive.
+ if topMax > top {
+ top = topMax + f.header
+ }
+ if bottomMin < bottom {
+ bottom = bottomMin - f.footer
+ }
+ if top > bottom {
+ return // No space for the primitive.
+ }
+ f.primitive.SetRect(x, top, width, bottom+1-top)
+
+ // Finally, draw the contained primitive.
+ f.primitive.Draw(screen)
+}
+
+// Focus is called when this primitive receives focus.
+func (f *Frame) Focus(delegate func(p Primitive)) {
+ delegate(f.primitive)
+}
+
+// HasFocus returns whether or not this primitive has focus.
+func (f *Frame) HasFocus() bool {
+ focusable, ok := f.primitive.(Focusable)
+ if ok {
+ return focusable.HasFocus()
+ }
+ return false
+}
diff --git a/vendor/maunium.net/go/tview/grid.go b/vendor/maunium.net/go/tview/grid.go
new file mode 100644
index 0000000..719ac7a
--- /dev/null
+++ b/vendor/maunium.net/go/tview/grid.go
@@ -0,0 +1,624 @@
+package tview
+
+import (
+ "math"
+
+ "maunium.net/go/tcell"
+)
+
+// gridItem represents one primitive and its possible position on a grid.
+type gridItem struct {
+ Item Primitive // The item to be positioned. May be nil for an empty item.
+ Row, Column int // The top-left grid cell where the item is placed.
+ Width, Height int // The number of rows and columns the item occupies.
+ MinGridWidth, MinGridHeight int // The minimum grid width/height for which this item is visible.
+ Focus bool // Whether or not this item attracts the layout's focus.
+
+ visible bool // Whether or not this item was visible the last time the grid was drawn.
+ x, y, w, h int // The last position of the item relative to the top-left corner of the grid. Undefined if visible is false.
+}
+
+// Grid is an implementation of a grid-based layout. It works by defining the
+// size of the rows and columns, then placing primitives into the grid.
+//
+// Some settings can lead to the grid exceeding its available space. SetOffset()
+// can then be used to scroll in steps of rows and columns. These offset values
+// can also be controlled with the arrow keys (or the "g","G", "j", "k", "h",
+// and "l" keys) while the grid has focus and none of its contained primitives
+// do.
+//
+// See https://github.com/rivo/tview/wiki/Grid for an example.
+type Grid struct {
+ *Box
+
+ // The items to be positioned.
+ items []*gridItem
+
+ // The definition of the rows and columns of the grid. See
+ // SetRows()/SetColumns() for details.
+ rows, columns []int
+
+ // The minimum sizes for rows and columns.
+ minWidth, minHeight int
+
+ // The size of the gaps between neighboring primitives. This is automatically
+ // set to 1 if borders is true.
+ gapRows, gapColumns int
+
+ // The number of rows and columns skipped before drawing the top-left corner
+ // of the grid.
+ rowOffset, columnOffset int
+
+ // Whether or not borders are drawn around grid items. If this is set to true,
+ // a gap size of 1 is automatically assumed (which is filled with the border
+ // graphics).
+ borders bool
+
+ // The color of the borders around grid items.
+ bordersColor tcell.Color
+}
+
+// NewGrid returns a new grid-based layout container with no initial primitives.
+func NewGrid() *Grid {
+ g := &Grid{
+ Box: NewBox(),
+ bordersColor: Styles.GraphicsColor,
+ }
+ g.focus = g
+ return g
+}
+
+// SetRows defines how the rows of the grid are distributed. Each value defines
+// the size of one row, starting with the leftmost row. Values greater 0
+// represent absolute row widths (gaps not included). Values less or equal 0
+// represent proportional row widths or fractions of the remaining free space,
+// where 0 is treated the same as -1. That is, a row with a value of -3 will
+// have three times the width of a row with a value of -1 (or 0). The minimum
+// width set with SetMinSize() is always observed.
+//
+// Primitives may extend beyond the rows defined explicitly with this function.
+// A value of 0 is assumed for any undefined row. In fact, if you never call
+// this function, all rows occupied by primitives will have the same width.
+// On the other hand, unoccupied rows defined with this function will always
+// take their place.
+//
+// Assuming a total width of the grid of 100 cells and a minimum width of 0, the
+// following call will result in rows with widths of 30, 10, 15, 15, and 30
+// cells:
+//
+// grid.SetRows(30, 10, -1, -1, -2)
+//
+// If a primitive were then placed in the 6th and 7th row, the resulting widths
+// would be: 30, 10, 10, 10, 20, 10, and 10 cells.
+//
+// If you then called SetMinSize() as follows:
+//
+// grid.SetMinSize(15, 20)
+//
+// The resulting widths would be: 30, 15, 15, 15, 20, 15, and 15 cells, a total
+// of 125 cells, 25 cells wider than the available grid width.
+func (g *Grid) SetRows(rows ...int) *Grid {
+ g.rows = rows
+ return g
+}
+
+// SetColumns defines how the columns of the grid are distributed. These values
+// behave the same as the row values provided with SetRows(), see there for
+// a definition and examples.
+//
+// The provided values correspond to column heights, the first value defining
+// the height of the topmost column.
+func (g *Grid) SetColumns(columns ...int) *Grid {
+ g.columns = columns
+ return g
+}
+
+// SetSize is a shortcut for SetRows() and SetColumns() where all row and column
+// values are set to the given size values. See SetRows() for details on sizes.
+func (g *Grid) SetSize(numRows, numColumns, rowSize, columnSize int) *Grid {
+ g.rows = make([]int, numRows)
+ for index := range g.rows {
+ g.rows[index] = rowSize
+ }
+ g.columns = make([]int, numColumns)
+ for index := range g.columns {
+ g.columns[index] = columnSize
+ }
+ return g
+}
+
+// SetMinSize sets an absolute minimum width for rows and an absolute minimum
+// height for columns. Panics if negative values are provided.
+func (g *Grid) SetMinSize(row, column int) *Grid {
+ if row < 0 || column < 0 {
+ panic("Invalid minimum row/column size")
+ }
+ g.minHeight, g.minWidth = row, column
+ return g
+}
+
+// SetGap sets the size of the gaps between neighboring primitives on the grid.
+// If borders are drawn (see SetBorders()), these values are ignored and a gap
+// of 1 is assumed. Panics if negative values are provided.
+func (g *Grid) SetGap(row, column int) *Grid {
+ if row < 0 || column < 0 {
+ panic("Invalid gap size")
+ }
+ g.gapRows, g.gapColumns = row, column
+ return g
+}
+
+// SetBorders sets whether or not borders are drawn around grid items. Setting
+// this value to true will cause the gap values (see SetGap()) to be ignored and
+// automatically assumed to be 1 where the border graphics are drawn.
+func (g *Grid) SetBorders(borders bool) *Grid {
+ g.borders = borders
+ return g
+}
+
+// SetBordersColor sets the color of the item borders.
+func (g *Grid) SetBordersColor(color tcell.Color) *Grid {
+ g.bordersColor = color
+ return g
+}
+
+// AddItem adds a primitive and its position to the grid. The top-left corner
+// of the primitive will be located in the top-left corner of the grid cell at
+// the given row and column and will span "width" rows and "height" columns. For
+// example, for a primitive to occupy rows 2, 3, and 4 and columns 5 and 6:
+//
+// grid.AddItem(p, 2, 4, 3, 2, true)
+//
+// If width or height is 0, the primitive will not be drawn.
+//
+// You can add the same primitive multiple times with different grid positions.
+// The minGridWidth and minGridHeight values will then determine which of those
+// positions will be used. This is similar to CSS media queries. These minimum
+// values refer to the overall size of the grid. If multiple items for the same
+// primitive apply, the one that has at least one highest minimum value will be
+// used, or the primitive added last if those values are the same. Example:
+//
+// grid.AddItem(p, 0, 0, 0, 0, 0, 0, true). // Hide in small grids.
+// AddItem(p, 0, 0, 1, 2, 100, 0, true). // One-column layout for medium grids.
+// AddItem(p, 1, 1, 3, 2, 300, 0, true) // Multi-column layout for large grids.
+//
+// To use the same grid layout for all sizes, simply set minGridWidth and
+// minGridHeight to 0.
+//
+// If the item's focus is set to true, it will receive focus when the grid
+// receives focus. If there are multiple items with a true focus flag, the last
+// visible one that was added will receive focus.
+func (g *Grid) AddItem(p Primitive, row, column, height, width, minGridHeight, minGridWidth int, focus bool) *Grid {
+ g.items = append(g.items, &gridItem{
+ Item: p,
+ Row: row,
+ Column: column,
+ Height: height,
+ Width: width,
+ MinGridHeight: minGridHeight,
+ MinGridWidth: minGridWidth,
+ Focus: focus,
+ })
+ return g
+}
+
+// RemoveItem removes all items for the given primitive from the grid, keeping
+// the order of the remaining items intact.
+func (g *Grid) RemoveItem(p Primitive) *Grid {
+ for index := len(g.items) - 1; index >= 0; index-- {
+ if g.items[index].Item == p {
+ g.items = append(g.items[:index], g.items[index+1:]...)
+ }
+ }
+ return g
+}
+
+// Clear removes all items from the grid.
+func (g *Grid) Clear() *Grid {
+ g.items = nil
+ return g
+}
+
+// SetOffset sets the number of rows and columns which are skipped before
+// drawing the first grid cell in the top-left corner. As the grid will never
+// completely move off the screen, these values may be adjusted the next time
+// the grid is drawn. The actual position of the grid may also be adjusted such
+// that contained primitives that have focus are visible.
+func (g *Grid) SetOffset(rows, columns int) *Grid {
+ g.rowOffset, g.columnOffset = rows, columns
+ return g
+}
+
+// GetOffset returns the current row and column offset (see SetOffset() for
+// details).
+func (g *Grid) GetOffset() (rows, columns int) {
+ return g.rowOffset, g.columnOffset
+}
+
+// Focus is called when this primitive receives focus.
+func (g *Grid) Focus(delegate func(p Primitive)) {
+ for _, item := range g.items {
+ if item.Focus {
+ delegate(item.Item)
+ return
+ }
+ }
+ g.hasFocus = true
+}
+
+// Blur is called when this primitive loses focus.
+func (g *Grid) Blur() {
+ g.hasFocus = false
+}
+
+// HasFocus returns whether or not this primitive has focus.
+func (g *Grid) HasFocus() bool {
+ for _, item := range g.items {
+ if item.visible && item.Item.GetFocusable().HasFocus() {
+ return true
+ }
+ }
+ return false
+}
+
+// InputHandler returns the handler for this primitive.
+func (g *Grid) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) {
+ return g.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p Primitive)) {
+ switch event.Key() {
+ case tcell.KeyRune:
+ switch event.Rune() {
+ case 'g':
+ g.rowOffset, g.columnOffset = 0, 0
+ case 'G':
+ g.rowOffset = math.MaxInt32
+ case 'j':
+ g.rowOffset++
+ case 'k':
+ g.rowOffset--
+ case 'h':
+ g.columnOffset--
+ case 'l':
+ g.columnOffset++
+ }
+ case tcell.KeyHome:
+ g.rowOffset, g.columnOffset = 0, 0
+ case tcell.KeyEnd:
+ g.rowOffset = math.MaxInt32
+ case tcell.KeyUp:
+ g.rowOffset--
+ case tcell.KeyDown:
+ g.rowOffset++
+ case tcell.KeyLeft:
+ g.columnOffset--
+ case tcell.KeyRight:
+ g.columnOffset++
+ }
+ })
+}
+
+// Draw draws this primitive onto the screen.
+func (g *Grid) Draw(screen tcell.Screen) {
+ g.Box.Draw(screen)
+ x, y, width, height := g.GetInnerRect()
+
+ // Make a list of items which apply.
+ items := make(map[Primitive]*gridItem)
+ for _, item := range g.items {
+ item.visible = false
+ if item.Width <= 0 || item.Height <= 0 || width < item.MinGridWidth || height < item.MinGridHeight {
+ continue
+ }
+ previousItem, ok := items[item.Item]
+ if ok && item.Width < previousItem.Width && item.Height < previousItem.Height {
+ continue
+ }
+ items[item.Item] = item
+ }
+
+ // How many rows and columns do we have?
+ rows := len(g.rows)
+ columns := len(g.columns)
+ for _, item := range items {
+ rowEnd := item.Row + item.Height
+ if rowEnd > rows {
+ rows = rowEnd
+ }
+ columnEnd := item.Column + item.Width
+ if columnEnd > columns {
+ columns = columnEnd
+ }
+ }
+ if rows == 0 || columns == 0 {
+ return // No content.
+ }
+
+ // Where are they located?
+ rowPos := make([]int, rows)
+ rowHeight := make([]int, rows)
+ columnPos := make([]int, columns)
+ columnWidth := make([]int, columns)
+
+ // How much space do we distribute?
+ remainingWidth := width
+ remainingHeight := height
+ proportionalWidth := 0
+ proportionalHeight := 0
+ for index, row := range g.rows {
+ if row > 0 {
+ if row < g.minHeight {
+ row = g.minHeight
+ }
+ remainingHeight -= row
+ rowHeight[index] = row
+ } else if row == 0 {
+ proportionalHeight++
+ } else {
+ proportionalHeight += -row
+ }
+ }
+ for index, column := range g.columns {
+ if column > 0 {
+ if column < g.minWidth {
+ column = g.minWidth
+ }
+ remainingWidth -= column
+ columnWidth[index] = column
+ } else if column == 0 {
+ proportionalWidth++
+ } else {
+ proportionalWidth += -column
+ }
+ }
+ if g.borders {
+ remainingHeight -= rows + 1
+ remainingWidth -= columns + 1
+ } else {
+ remainingHeight -= (rows - 1) * g.gapRows
+ remainingWidth -= (columns - 1) * g.gapColumns
+ }
+ if rows > len(g.rows) {
+ proportionalHeight += rows - len(g.rows)
+ }
+ if columns > len(g.columns) {
+ proportionalWidth += columns - len(g.columns)
+ }
+
+ // Distribute proportional rows/columns.
+ gridWidth := 0
+ gridHeight := 0
+ for index := 0; index < rows; index++ {
+ row := 0
+ if index < len(g.rows) {
+ row = g.rows[index]
+ }
+ if row > 0 {
+ if row < g.minHeight {
+ row = g.minHeight
+ }
+ gridHeight += row
+ continue // Not proportional. We already know the width.
+ } else if row == 0 {
+ row = 1
+ } else {
+ row = -row
+ }
+ rowAbs := row * remainingHeight / proportionalHeight
+ remainingHeight -= rowAbs
+ proportionalHeight -= row
+ if rowAbs < g.minHeight {
+ rowAbs = g.minHeight
+ }
+ rowHeight[index] = rowAbs
+ gridHeight += rowAbs
+ }
+ for index := 0; index < columns; index++ {
+ column := 0
+ if index < len(g.columns) {
+ column = g.columns[index]
+ }
+ if column > 0 {
+ if column < g.minWidth {
+ column = g.minWidth
+ }
+ gridWidth += column
+ continue // Not proportional. We already know the height.
+ } else if column == 0 {
+ column = 1
+ } else {
+ column = -column
+ }
+ columnAbs := column * remainingWidth / proportionalWidth
+ remainingWidth -= columnAbs
+ proportionalWidth -= column
+ if columnAbs < g.minWidth {
+ columnAbs = g.minWidth
+ }
+ columnWidth[index] = columnAbs
+ gridWidth += columnAbs
+ }
+ if g.borders {
+ gridHeight += rows + 1
+ gridWidth += columns + 1
+ } else {
+ gridHeight += (rows - 1) * g.gapRows
+ gridWidth += (columns - 1) * g.gapColumns
+ }
+
+ // Calculate row/column positions.
+ columnX, rowY := x, y
+ if g.borders {
+ columnX++
+ rowY++
+ }
+ for index, row := range rowHeight {
+ rowPos[index] = rowY
+ gap := g.gapRows
+ if g.borders {
+ gap = 1
+ }
+ rowY += row + gap
+ }
+ for index, column := range columnWidth {
+ columnPos[index] = columnX
+ gap := g.gapColumns
+ if g.borders {
+ gap = 1
+ }
+ columnX += column + gap
+ }
+
+ // Calculate primitive positions.
+ var focus *gridItem // The item which has focus.
+ for primitive, item := range items {
+ px := columnPos[item.Column]
+ py := rowPos[item.Row]
+ var pw, ph int
+ for index := 0; index < item.Height; index++ {
+ ph += rowHeight[item.Row+index]
+ }
+ for index := 0; index < item.Width; index++ {
+ pw += columnWidth[item.Column+index]
+ }
+ if g.borders {
+ pw += item.Width - 1
+ ph += item.Height - 1
+ } else {
+ pw += (item.Width - 1) * g.gapColumns
+ ph += (item.Height - 1) * g.gapRows
+ }
+ item.x, item.y, item.w, item.h = px, py, pw, ph
+ item.visible = true
+ if primitive.GetFocusable().HasFocus() {
+ focus = item
+ }
+ }
+
+ // Calculate screen offsets.
+ var offsetX, offsetY, add int
+ if g.rowOffset < 0 {
+ g.rowOffset = 0
+ }
+ if g.columnOffset < 0 {
+ g.columnOffset = 0
+ }
+ if g.borders {
+ add = 1
+ }
+ for row := 0; row < rows-1; row++ {
+ remainingHeight := gridHeight - offsetY
+ if focus != nil && focus.y-add <= offsetY || // Don't let the focused item move out of screen.
+ row >= g.rowOffset && (focus == nil || focus != nil && focus.y-offsetY < height) || // We've reached the requested offset.
+ remainingHeight <= height { // We have enough space to show the rest.
+ if row > 0 {
+ if focus != nil && focus.y+focus.h+add-offsetY > height {
+ offsetY += focus.y + focus.h + add - offsetY - height
+ }
+ if remainingHeight < height {
+ offsetY = gridHeight - height
+ }
+ }
+ g.rowOffset = row
+ break
+ }
+ offsetY = rowPos[row+1] - add
+ }
+ for column := 0; column < columns-1; column++ {
+ remainingWidth := gridWidth - offsetX
+ if focus != nil && focus.x-add <= offsetX || // Don't let the focused item move out of screen.
+ column >= g.columnOffset && (focus == nil || focus != nil && focus.x-offsetX < width) || // We've reached the requested offset.
+ remainingWidth <= width { // We have enough space to show the rest.
+ if column > 0 {
+ if focus != nil && focus.x+focus.w+add-offsetX > width {
+ offsetX += focus.x + focus.w + add - offsetX - width
+ } else if remainingWidth < width {
+ offsetX = gridWidth - width
+ }
+ }
+ g.columnOffset = column
+ break
+ }
+ offsetX = columnPos[column+1] - add
+ }
+
+ // Draw primitives and borders.
+ for primitive, item := range items {
+ // Final primitive position.
+ if !item.visible {
+ continue
+ }
+ item.x -= offsetX
+ item.y -= offsetY
+ if item.x+item.w > width {
+ item.w = width - item.x
+ }
+ if item.y+item.h > height {
+ item.h = height - item.y
+ }
+ if item.x < 0 {
+ item.w += item.x
+ item.x = 0
+ }
+ if item.y < 0 {
+ item.h += item.y
+ item.y = 0
+ }
+ if item.w <= 0 || item.h <= 0 {
+ item.visible = false
+ continue
+ }
+ primitive.SetRect(x+item.x, y+item.y, item.w, item.h)
+
+ // Draw primitive.
+ if item == focus {
+ defer primitive.Draw(screen)
+ } else {
+ primitive.Draw(screen)
+ }
+
+ // Draw border around primitive.
+ if g.borders {
+ for bx := item.x; bx < item.x+item.w; bx++ { // Top/bottom lines.
+ if bx < 0 || bx >= width {
+ continue
+ }
+ by := item.y - 1
+ if by >= 0 && by < height {
+ PrintJoinedBorder(screen, x+bx, y+by, GraphicsHoriBar, g.bordersColor)
+ }
+ by = item.y + item.h
+ if by >= 0 && by < height {
+ PrintJoinedBorder(screen, x+bx, y+by, GraphicsHoriBar, g.bordersColor)
+ }
+ }
+ for by := item.y; by < item.y+item.h; by++ { // Left/right lines.
+ if by < 0 || by >= height {
+ continue
+ }
+ bx := item.x - 1
+ if bx >= 0 && bx < width {
+ PrintJoinedBorder(screen, x+bx, y+by, GraphicsVertBar, g.bordersColor)
+ }
+ bx = item.x + item.w
+ if bx >= 0 && bx < width {
+ PrintJoinedBorder(screen, x+bx, y+by, GraphicsVertBar, g.bordersColor)
+ }
+ }
+ bx, by := item.x-1, item.y-1 // Top-left corner.
+ if bx >= 0 && bx < width && by >= 0 && by < height {
+ PrintJoinedBorder(screen, x+bx, y+by, GraphicsTopLeftCorner, g.bordersColor)
+ }
+ bx, by = item.x+item.w, item.y-1 // Top-right corner.
+ if bx >= 0 && bx < width && by >= 0 && by < height {
+ PrintJoinedBorder(screen, x+bx, y+by, GraphicsTopRightCorner, g.bordersColor)
+ }
+ bx, by = item.x-1, item.y+item.h // Bottom-left corner.
+ if bx >= 0 && bx < width && by >= 0 && by < height {
+ PrintJoinedBorder(screen, x+bx, y+by, GraphicsBottomLeftCorner, g.bordersColor)
+ }
+ bx, by = item.x+item.w, item.y+item.h // Bottom-right corner.
+ if bx >= 0 && bx < width && by >= 0 && by < height {
+ PrintJoinedBorder(screen, x+bx, y+by, GraphicsBottomRightCorner, g.bordersColor)
+ }
+ }
+ }
+}
diff --git a/vendor/maunium.net/go/tview/inputfield.go b/vendor/maunium.net/go/tview/inputfield.go
new file mode 100644
index 0000000..c168106
--- /dev/null
+++ b/vendor/maunium.net/go/tview/inputfield.go
@@ -0,0 +1,328 @@
+package tview
+
+import (
+ "math"
+ "regexp"
+ "strings"
+ "unicode/utf8"
+
+ "maunium.net/go/tcell"
+ runewidth "github.com/mattn/go-runewidth"
+)
+
+// InputField is a one-line box (three lines if there is a title) where the
+// user can enter text.
+//
+// Use SetMaskCharacter() to hide input from onlookers (e.g. for password
+// input).
+//
+// See https://github.com/rivo/tview/wiki/InputField for an example.
+type InputField struct {
+ *Box
+
+ // The text that was entered.
+ text string
+
+ // The text to be displayed before the input area.
+ label string
+
+ // The text to be displayed in the input area when "text" is empty.
+ placeholder string
+
+ // The label color.
+ labelColor tcell.Color
+
+ // The background color of the input area.
+ fieldBackgroundColor tcell.Color
+
+ // The text color of the input area.
+ fieldTextColor tcell.Color
+
+ // The text color of the placeholder.
+ placeholderTextColor tcell.Color
+
+ // The screen width of the input area. A value of 0 means extend as much as
+ // possible.
+ fieldWidth int
+
+ // A character to mask entered text (useful for password fields). A value of 0
+ // disables masking.
+ maskCharacter rune
+
+ // An optional function which may reject the last character that was entered.
+ accept func(text string, ch rune) bool
+
+ // An optional function which is called when the input has changed.
+ changed func(text string)
+
+ // An optional function which is called when the user indicated that they
+ // are done entering text. The key which was pressed is provided (tab,
+ // shift-tab, enter, or escape).
+ done func(tcell.Key)
+}
+
+// NewInputField returns a new input field.
+func NewInputField() *InputField {
+ return &InputField{
+ Box: NewBox(),
+ labelColor: Styles.SecondaryTextColor,
+ fieldBackgroundColor: Styles.ContrastBackgroundColor,
+ fieldTextColor: Styles.PrimaryTextColor,
+ placeholderTextColor: Styles.ContrastSecondaryTextColor,
+ }
+}
+
+// SetText sets the current text of the input field.
+func (i *InputField) SetText(text string) *InputField {
+ i.text = text
+ if i.changed != nil {
+ i.changed(text)
+ }
+ return i
+}
+
+// GetText returns the current text of the input field.
+func (i *InputField) GetText() string {
+ return i.text
+}
+
+// SetLabel sets the text to be displayed before the input area.
+func (i *InputField) SetLabel(label string) *InputField {
+ i.label = label
+ return i
+}
+
+// GetLabel returns the text to be displayed before the input area.
+func (i *InputField) GetLabel() string {
+ return i.label
+}
+
+// SetPlaceholder sets the text to be displayed when the input text is empty.
+func (i *InputField) SetPlaceholder(text string) *InputField {
+ i.placeholder = text
+ return i
+}
+
+// SetLabelColor sets the color of the label.
+func (i *InputField) SetLabelColor(color tcell.Color) *InputField {
+ i.labelColor = color
+ return i
+}
+
+// SetFieldBackgroundColor sets the background color of the input area.
+func (i *InputField) SetFieldBackgroundColor(color tcell.Color) *InputField {
+ i.fieldBackgroundColor = color
+ return i
+}
+
+// SetFieldTextColor sets the text color of the input area.
+func (i *InputField) SetFieldTextColor(color tcell.Color) *InputField {
+ i.fieldTextColor = color
+ return i
+}
+
+// SetPlaceholderExtColor sets the text color of placeholder text.
+func (i *InputField) SetPlaceholderExtColor(color tcell.Color) *InputField {
+ i.placeholderTextColor = color
+ return i
+}
+
+// SetFormAttributes sets attributes shared by all form items.
+func (i *InputField) SetFormAttributes(label string, labelColor, bgColor, fieldTextColor, fieldBgColor tcell.Color) FormItem {
+ i.label = label
+ i.labelColor = labelColor
+ i.backgroundColor = bgColor
+ i.fieldTextColor = fieldTextColor
+ i.fieldBackgroundColor = fieldBgColor
+ return i
+}
+
+// SetFieldWidth sets the screen width of the input area. A value of 0 means
+// extend as much as possible.
+func (i *InputField) SetFieldWidth(width int) *InputField {
+ i.fieldWidth = width
+ return i
+}
+
+// GetFieldWidth returns this primitive's field width.
+func (i *InputField) GetFieldWidth() int {
+ return i.fieldWidth
+}
+
+// SetMaskCharacter sets a character that masks user input on a screen. A value
+// of 0 disables masking.
+func (i *InputField) SetMaskCharacter(mask rune) *InputField {
+ i.maskCharacter = mask
+ return i
+}
+
+// SetAcceptanceFunc sets a handler which may reject the last character that was
+// entered (by returning false).
+//
+// This package defines a number of variables Prefixed with InputField which may
+// be used for common input (e.g. numbers, maximum text length).
+func (i *InputField) SetAcceptanceFunc(handler func(textToCheck string, lastChar rune) bool) *InputField {
+ i.accept = handler
+ return i
+}
+
+// SetChangedFunc sets a handler which is called whenever the text of the input
+// field has changed. It receives the current text (after the change).
+func (i *InputField) SetChangedFunc(handler func(text string)) *InputField {
+ i.changed = handler
+ return i
+}
+
+// SetDoneFunc sets a handler which is called when the user is done entering
+// text. The callback function is provided with the key that was pressed, which
+// is one of the following:
+//
+// - KeyEnter: Done entering text.
+// - KeyEscape: Abort text input.
+// - KeyTab: Move to the next field.
+// - KeyBacktab: Move to the previous field.
+func (i *InputField) SetDoneFunc(handler func(key tcell.Key)) *InputField {
+ i.done = handler
+ return i
+}
+
+// SetFinishedFunc calls SetDoneFunc().
+func (i *InputField) SetFinishedFunc(handler func(key tcell.Key)) FormItem {
+ return i.SetDoneFunc(handler)
+}
+
+// Draw draws this primitive onto the screen.
+func (i *InputField) Draw(screen tcell.Screen) {
+ i.Box.Draw(screen)
+
+ // Prepare
+ x, y, width, height := i.GetInnerRect()
+ rightLimit := x + width
+ if height < 1 || rightLimit <= x {
+ return
+ }
+
+ // Draw label.
+ _, drawnWidth := Print(screen, i.label, x, y, rightLimit-x, AlignLeft, i.labelColor)
+ x += drawnWidth
+
+ // Draw input area.
+ fieldWidth := i.fieldWidth
+ if fieldWidth == 0 {
+ fieldWidth = math.MaxInt32
+ }
+ if rightLimit-x < fieldWidth {
+ fieldWidth = rightLimit - x
+ }
+ fieldStyle := tcell.StyleDefault.Background(i.fieldBackgroundColor)
+ for index := 0; index < fieldWidth; index++ {
+ screen.SetContent(x+index, y, ' ', nil, fieldStyle)
+ }
+
+ // Draw placeholder text.
+ text := i.text
+ if text == "" && i.placeholder != "" {
+ Print(screen, i.placeholder, x, y, fieldWidth, AlignLeft, i.placeholderTextColor)
+ }
+
+ // Draw entered text.
+ if i.maskCharacter > 0 {
+ text = strings.Repeat(string(i.maskCharacter), utf8.RuneCountInString(i.text))
+ }
+ fieldWidth-- // We need one cell for the cursor.
+ if fieldWidth < runewidth.StringWidth(text) {
+ runes := []rune(text)
+ for pos := len(runes) - 1; pos >= 0; pos-- {
+ ch := runes[pos]
+ w := runewidth.RuneWidth(ch)
+ if fieldWidth-w < 0 {
+ break
+ }
+ _, _, style, _ := screen.GetContent(x+fieldWidth-w, y)
+ style = style.Foreground(i.fieldTextColor)
+ for w > 0 {
+ fieldWidth--
+ screen.SetContent(x+fieldWidth, y, ch, nil, style)
+ w--
+ }
+ }
+ } else {
+ pos := 0
+ for _, ch := range text {
+ w := runewidth.RuneWidth(ch)
+ _, _, style, _ := screen.GetContent(x+pos, y)
+ style = style.Foreground(i.fieldTextColor)
+ for w > 0 {
+ screen.SetContent(x+pos, y, ch, nil, style)
+ pos++
+ w--
+ }
+ }
+ }
+
+ // Set cursor.
+ if i.focus.HasFocus() {
+ i.setCursor(screen)
+ }
+}
+
+// setCursor sets the cursor position.
+func (i *InputField) setCursor(screen tcell.Screen) {
+ x := i.x
+ y := i.y
+ rightLimit := x + i.width
+ if i.border {
+ x++
+ y++
+ rightLimit -= 2
+ }
+ fieldWidth := runewidth.StringWidth(i.text)
+ if i.fieldWidth > 0 && fieldWidth > i.fieldWidth-1 {
+ fieldWidth = i.fieldWidth - 1
+ }
+ x += StringWidth(i.label) + fieldWidth
+ if x >= rightLimit {
+ x = rightLimit - 1
+ }
+ screen.ShowCursor(x, y)
+}
+
+// InputHandler returns the handler for this primitive.
+func (i *InputField) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) {
+ return i.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p Primitive)) {
+ // Trigger changed events.
+ currentText := i.text
+ defer func() {
+ if i.text != currentText && i.changed != nil {
+ i.changed(i.text)
+ }
+ }()
+
+ // Process key event.
+ switch key := event.Key(); key {
+ case tcell.KeyRune: // Regular character.
+ newText := i.text + string(event.Rune())
+ if i.accept != nil {
+ if !i.accept(newText, event.Rune()) {
+ break
+ }
+ }
+ i.text = newText
+ case tcell.KeyCtrlU: // Delete all.
+ i.text = ""
+ case tcell.KeyCtrlW: // Delete last word.
+ lastWord := regexp.MustCompile(`\s*\S+\s*$`)
+ i.text = lastWord.ReplaceAllString(i.text, "")
+ case tcell.KeyBackspace, tcell.KeyBackspace2: // Delete last character.
+ if len(i.text) == 0 {
+ break
+ }
+ runes := []rune(i.text)
+ i.text = string(runes[:len(runes)-1])
+ case tcell.KeyEnter, tcell.KeyTab, tcell.KeyBacktab, tcell.KeyEscape: // We're done.
+ if i.done != nil {
+ i.done(key)
+ }
+ }
+ })
+}
diff --git a/vendor/maunium.net/go/tview/list.go b/vendor/maunium.net/go/tview/list.go
new file mode 100644
index 0000000..7395985
--- /dev/null
+++ b/vendor/maunium.net/go/tview/list.go
@@ -0,0 +1,327 @@
+package tview
+
+import (
+ "fmt"
+
+ "maunium.net/go/tcell"
+)
+
+// listItem represents one item in a List.
+type listItem struct {
+ MainText string // The main text of the list item.
+ SecondaryText string // A secondary text to be shown underneath the main text.
+ Shortcut rune // The key to select the list item directly, 0 if there is no shortcut.
+ Selected func() // The optional function which is called when the item is selected.
+}
+
+// List displays rows of items, each of which can be selected.
+//
+// See https://github.com/rivo/tview/wiki/List for an example.
+type List struct {
+ *Box
+
+ // The items of the list.
+ items []*listItem
+
+ // The index of the currently selected item.
+ currentItem int
+
+ // Whether or not to show the secondary item texts.
+ showSecondaryText bool
+
+ // The item main text color.
+ mainTextColor tcell.Color
+
+ // The item secondary text color.
+ secondaryTextColor tcell.Color
+
+ // The item shortcut text color.
+ shortcutColor tcell.Color
+
+ // The text color for selected items.
+ selectedTextColor tcell.Color
+
+ // The background color for selected items.
+ selectedBackgroundColor tcell.Color
+
+ // An optional function which is called when the user has navigated to a list
+ // item.
+ changed func(index int, mainText, secondaryText string, shortcut rune)
+
+ // An optional function which is called when a list item was selected. This
+ // function will be called even if the list item defines its own callback.
+ selected func(index int, mainText, secondaryText string, shortcut rune)
+
+ // An optional function which is called when the user presses the Escape key.
+ done func()
+}
+
+// NewList returns a new form.
+func NewList() *List {
+ return &List{
+ Box: NewBox(),
+ showSecondaryText: true,
+ mainTextColor: Styles.PrimaryTextColor,
+ secondaryTextColor: Styles.TertiaryTextColor,
+ shortcutColor: Styles.SecondaryTextColor,
+ selectedTextColor: Styles.PrimitiveBackgroundColor,
+ selectedBackgroundColor: Styles.PrimaryTextColor,
+ }
+}
+
+// SetCurrentItem sets the currently selected item by its index. This triggers
+// a "changed" event.
+func (l *List) SetCurrentItem(index int) *List {
+ l.currentItem = index
+ if l.currentItem < len(l.items) && l.changed != nil {
+ item := l.items[l.currentItem]
+ l.changed(l.currentItem, item.MainText, item.SecondaryText, item.Shortcut)
+ }
+ return l
+}
+
+// GetCurrentItem returns the index of the currently selected list item.
+func (l *List) GetCurrentItem() int {
+ return l.currentItem
+}
+
+// SetMainTextColor sets the color of the items' main text.
+func (l *List) SetMainTextColor(color tcell.Color) *List {
+ l.mainTextColor = color
+ return l
+}
+
+// SetSecondaryTextColor sets the color of the items' secondary text.
+func (l *List) SetSecondaryTextColor(color tcell.Color) *List {
+ l.secondaryTextColor = color
+ return l
+}
+
+// SetShortcutColor sets the color of the items' shortcut.
+func (l *List) SetShortcutColor(color tcell.Color) *List {
+ l.shortcutColor = color
+ return l
+}
+
+// SetSelectedTextColor sets the text color of selected items.
+func (l *List) SetSelectedTextColor(color tcell.Color) *List {
+ l.selectedTextColor = color
+ return l
+}
+
+// SetSelectedBackgroundColor sets the background color of selected items.
+func (l *List) SetSelectedBackgroundColor(color tcell.Color) *List {
+ l.selectedBackgroundColor = color
+ return l
+}
+
+// ShowSecondaryText determines whether or not to show secondary item texts.
+func (l *List) ShowSecondaryText(show bool) *List {
+ l.showSecondaryText = show
+ return l
+}
+
+// SetChangedFunc sets the function which is called when the user navigates to
+// a list item. The function receives the item's index in the list of items
+// (starting with 0), its main text, secondary text, and its shortcut rune.
+//
+// This function is also called when the first item is added or when
+// SetCurrentItem() is called.
+func (l *List) SetChangedFunc(handler func(int, string, string, rune)) *List {
+ l.changed = handler
+ return l
+}
+
+// SetSelectedFunc sets the function which is called when the user selects a
+// list item by pressing Enter on the current selection. The function receives
+// the item's index in the list of items (starting with 0), its main text,
+// secondary text, and its shortcut rune.
+func (l *List) SetSelectedFunc(handler func(int, string, string, rune)) *List {
+ l.selected = handler
+ return l
+}
+
+// SetDoneFunc sets a function which is called when the user presses the Escape
+// key.
+func (l *List) SetDoneFunc(handler func()) *List {
+ l.done = handler
+ return l
+}
+
+// AddItem adds a new item to the list. An item has a main text which will be
+// highlighted when selected. It also has a secondary text which is shown
+// underneath the main text (if it is set to visible) but which may remain
+// empty.
+//
+// The shortcut is a key binding. If the specified rune is entered, the item
+// is selected immediately. Set to 0 for no binding.
+//
+// The "selected" callback will be invoked when the user selects the item. You
+// may provide nil if no such item is needed or if all events are handled
+// through the selected callback set with SetSelectedFunc().
+func (l *List) AddItem(mainText, secondaryText string, shortcut rune, selected func()) *List {
+ l.items = append(l.items, &listItem{
+ MainText: mainText,
+ SecondaryText: secondaryText,
+ Shortcut: shortcut,
+ Selected: selected,
+ })
+ if len(l.items) == 1 && l.changed != nil {
+ item := l.items[0]
+ l.changed(0, item.MainText, item.SecondaryText, item.Shortcut)
+ }
+ return l
+}
+
+// Clear removes all items from the list.
+func (l *List) Clear() *List {
+ l.items = nil
+ l.currentItem = 0
+ return l
+}
+
+// Draw draws this primitive onto the screen.
+func (l *List) Draw(screen tcell.Screen) {
+ l.Box.Draw(screen)
+
+ // Determine the dimensions.
+ x, y, width, height := l.GetInnerRect()
+ bottomLimit := y + height
+
+ // Do we show any shortcuts?
+ var showShortcuts bool
+ for _, item := range l.items {
+ if item.Shortcut != 0 {
+ showShortcuts = true
+ x += 4
+ width -= 4
+ break
+ }
+ }
+
+ // We want to keep the current selection in view. What is our offset?
+ var offset int
+ if l.showSecondaryText {
+ if l.currentItem >= height/2 {
+ offset = l.currentItem + 1 - (height / 2)
+ }
+ } else {
+ if l.currentItem >= height {
+ offset = l.currentItem + 1 - height
+ }
+ }
+
+ // Draw the list items.
+ for index, item := range l.items {
+ if index < offset {
+ continue
+ }
+
+ if y >= bottomLimit {
+ break
+ }
+
+ // Shortcuts.
+ if showShortcuts && item.Shortcut != 0 {
+ Print(screen, fmt.Sprintf("(%s)", string(item.Shortcut)), x-5, y, 4, AlignRight, l.shortcutColor)
+ }
+
+ // Main text.
+ Print(screen, item.MainText, x, y, width, AlignLeft, l.mainTextColor)
+
+ // Background color of selected text.
+ if index == l.currentItem {
+ textWidth := StringWidth(item.MainText)
+ for bx := 0; bx < textWidth && bx < width; bx++ {
+ m, c, style, _ := screen.GetContent(x+bx, y)
+ fg, _, _ := style.Decompose()
+ if fg == l.mainTextColor {
+ fg = l.selectedTextColor
+ }
+ style = style.Background(l.selectedBackgroundColor).Foreground(fg)
+ screen.SetContent(x+bx, y, m, c, style)
+ }
+ }
+
+ y++
+
+ if y >= bottomLimit {
+ break
+ }
+
+ // Secondary text.
+ if l.showSecondaryText {
+ Print(screen, item.SecondaryText, x, y, width, AlignLeft, l.secondaryTextColor)
+ y++
+ }
+ }
+}
+
+// InputHandler returns the handler for this primitive.
+func (l *List) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) {
+ return l.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p Primitive)) {
+ previousItem := l.currentItem
+
+ switch key := event.Key(); key {
+ case tcell.KeyTab, tcell.KeyDown, tcell.KeyRight:
+ l.currentItem++
+ case tcell.KeyBacktab, tcell.KeyUp, tcell.KeyLeft:
+ l.currentItem--
+ case tcell.KeyHome:
+ l.currentItem = 0
+ case tcell.KeyEnd:
+ l.currentItem = len(l.items) - 1
+ case tcell.KeyPgDn:
+ l.currentItem += 5
+ case tcell.KeyPgUp:
+ l.currentItem -= 5
+ case tcell.KeyEnter:
+ item := l.items[l.currentItem]
+ if item.Selected != nil {
+ item.Selected()
+ }
+ if l.selected != nil {
+ l.selected(l.currentItem, item.MainText, item.SecondaryText, item.Shortcut)
+ }
+ case tcell.KeyEscape:
+ if l.done != nil {
+ l.done()
+ }
+ case tcell.KeyRune:
+ ch := event.Rune()
+ if ch != ' ' {
+ // It's not a space bar. Is it a shortcut?
+ var found bool
+ for index, item := range l.items {
+ if item.Shortcut == ch {
+ // We have a shortcut.
+ found = true
+ l.currentItem = index
+ break
+ }
+ }
+ if !found {
+ break
+ }
+ }
+ item := l.items[l.currentItem]
+ if item.Selected != nil {
+ item.Selected()
+ }
+ if l.selected != nil {
+ l.selected(l.currentItem, item.MainText, item.SecondaryText, item.Shortcut)
+ }
+ }
+
+ if l.currentItem < 0 {
+ l.currentItem = len(l.items) - 1
+ } else if l.currentItem >= len(l.items) {
+ l.currentItem = 0
+ }
+
+ if l.currentItem != previousItem && l.currentItem < len(l.items) && l.changed != nil {
+ item := l.items[l.currentItem]
+ l.changed(l.currentItem, item.MainText, item.SecondaryText, item.Shortcut)
+ }
+ })
+}
diff --git a/vendor/maunium.net/go/tview/modal.go b/vendor/maunium.net/go/tview/modal.go
new file mode 100644
index 0000000..f53a265
--- /dev/null
+++ b/vendor/maunium.net/go/tview/modal.go
@@ -0,0 +1,131 @@
+package tview
+
+import (
+ "maunium.net/go/tcell"
+)
+
+// Modal is a centered message window used to inform the user or prompt them
+// for an immediate decision. It needs to have at least one button (added via
+// AddButtons()) or it will never disappear.
+//
+// See https://github.com/rivo/tview/wiki/Modal for an example.
+type Modal struct {
+ *Box
+
+ // The framed embedded in the modal.
+ frame *Frame
+
+ // The form embedded in the modal's frame.
+ form *Form
+
+ // The message text (original, not word-wrapped).
+ text string
+
+ // The text color.
+ textColor tcell.Color
+
+ // The optional callback for when the user clicked one of the buttons. It
+ // receives the index of the clicked button and the button's label.
+ done func(buttonIndex int, buttonLabel string)
+}
+
+// NewModal returns a new modal message window.
+func NewModal() *Modal {
+ m := &Modal{
+ Box: NewBox(),
+ textColor: Styles.PrimaryTextColor,
+ }
+ m.form = NewForm().
+ SetButtonsAlign(AlignCenter).
+ SetButtonBackgroundColor(Styles.PrimitiveBackgroundColor).
+ SetButtonTextColor(Styles.PrimaryTextColor)
+ m.form.SetBackgroundColor(Styles.ContrastBackgroundColor).SetBorderPadding(0, 0, 0, 0)
+ m.frame = NewFrame(m.form).SetBorders(0, 0, 1, 0, 0, 0)
+ m.frame.SetBorder(true).
+ SetBackgroundColor(Styles.ContrastBackgroundColor).
+ SetBorderPadding(1, 1, 1, 1)
+ m.focus = m
+ return m
+}
+
+// SetTextColor sets the color of the message text.
+func (m *Modal) SetTextColor(color tcell.Color) *Modal {
+ m.textColor = color
+ return m
+}
+
+// SetDoneFunc sets a handler which is called when one of the buttons was
+// pressed. It receives the index of the button as well as its label text. The
+// handler is also called when the user presses the Escape key. The index will
+// then be negative and the label text an emptry string.
+func (m *Modal) SetDoneFunc(handler func(buttonIndex int, buttonLabel string)) *Modal {
+ m.done = handler
+ return m
+}
+
+// SetText sets the message text of the window. The text may contain line
+// breaks. Note that words are wrapped, too, based on the final size of the
+// window.
+func (m *Modal) SetText(text string) *Modal {
+ m.text = text
+ return m
+}
+
+// AddButtons adds buttons to the window. There must be at least one button and
+// a "done" handler so the window can be closed again.
+func (m *Modal) AddButtons(labels []string) *Modal {
+ for index, label := range labels {
+ func(i int, l string) {
+ m.form.AddButton(label, func() {
+ if m.done != nil {
+ m.done(i, l)
+ }
+ })
+ }(index, label)
+ }
+ return m
+}
+
+// Focus is called when this primitive receives focus.
+func (m *Modal) Focus(delegate func(p Primitive)) {
+ delegate(m.form)
+}
+
+// HasFocus returns whether or not this primitive has focus.
+func (m *Modal) HasFocus() bool {
+ return m.form.HasFocus()
+}
+
+// Draw draws this primitive onto the screen.
+func (m *Modal) Draw(screen tcell.Screen) {
+ // Calculate the width of this modal.
+ buttonsWidth := 0
+ for _, button := range m.form.buttons {
+ buttonsWidth += StringWidth(button.label) + 4 + 2
+ }
+ buttonsWidth -= 2
+ screenWidth, screenHeight := screen.Size()
+ width := screenWidth / 3
+ if width < buttonsWidth {
+ width = buttonsWidth
+ }
+ // width is now without the box border.
+
+ // Reset the text and find out how wide it is.
+ m.frame.Clear()
+ lines := WordWrap(m.text, width)
+ for _, line := range lines {
+ m.frame.AddText(line, true, AlignCenter, m.textColor)
+ }
+
+ // Set the modal's position and size.
+ height := len(lines) + 6
+ width += 4
+ x := (screenWidth - width) / 2
+ y := (screenHeight - height) / 2
+ m.SetRect(x, y, width, height)
+
+ // Draw the frame.
+ m.frame.SetRect(x, y, width, height)
+ m.frame.Draw(screen)
+}
diff --git a/vendor/maunium.net/go/tview/pages.go b/vendor/maunium.net/go/tview/pages.go
new file mode 100644
index 0000000..7442207
--- /dev/null
+++ b/vendor/maunium.net/go/tview/pages.go
@@ -0,0 +1,257 @@
+package tview
+
+import (
+ "maunium.net/go/tcell"
+)
+
+// page represents one page of a Pages object.
+type page struct {
+ Name string // The page's name.
+ Item Primitive // The page's primitive.
+ Resize bool // Whether or not to resize the page when it is drawn.
+ Visible bool // Whether or not this page is visible.
+}
+
+// Pages is a container for other primitives often used as the application's
+// root primitive. It allows to easily switch the visibility of the contained
+// primitives.
+//
+// See https://github.com/rivo/tview/wiki/Pages for an example.
+type Pages struct {
+ *Box
+
+ // The contained pages.
+ pages []*page
+
+ // We keep a reference to the function which allows us to set the focus to
+ // a newly visible page.
+ setFocus func(p Primitive)
+
+ // An optional handler which is called whenever the visibility or the order of
+ // pages changes.
+ changed func()
+}
+
+// NewPages returns a new Pages object.
+func NewPages() *Pages {
+ p := &Pages{
+ Box: NewBox(),
+ }
+ p.focus = p
+ return p
+}
+
+// SetChangedFunc sets a handler which is called whenever the visibility or the
+// order of any visible pages changes. This can be used to redraw the pages.
+func (p *Pages) SetChangedFunc(handler func()) *Pages {
+ p.changed = handler
+ return p
+}
+
+// AddPage adds a new page with the given name and primitive. If there was
+// previously a page with the same name, it is overwritten. Leaving the name
+// empty may cause conflicts in other functions.
+//
+// Visible pages will be drawn in the order they were added (unless that order
+// was changed in one of the other functions). If "resize" is set to true, the
+// primitive will be set to the size available to the Pages primitive whenever
+// the pages are drawn.
+func (p *Pages) AddPage(name string, item Primitive, resize, visible bool) *Pages {
+ for index, pg := range p.pages {
+ if pg.Name == name {
+ p.pages = append(p.pages[:index], p.pages[index+1:]...)
+ break
+ }
+ }
+ p.pages = append(p.pages, &page{Item: item, Name: name, Resize: resize, Visible: visible})
+ if p.changed != nil {
+ p.changed()
+ }
+ if p.HasFocus() {
+ p.Focus(p.setFocus)
+ }
+ return p
+}
+
+// AddAndSwitchToPage calls AddPage(), then SwitchToPage() on that newly added
+// page.
+func (p *Pages) AddAndSwitchToPage(name string, item Primitive, resize bool) *Pages {
+ p.AddPage(name, item, resize, true)
+ p.SwitchToPage(name)
+ return p
+}
+
+// RemovePage removes the page with the given name.
+func (p *Pages) RemovePage(name string) *Pages {
+ hasFocus := p.HasFocus()
+ for index, page := range p.pages {
+ if page.Name == name {
+ p.pages = append(p.pages[:index], p.pages[index+1:]...)
+ if page.Visible && p.changed != nil {
+ p.changed()
+ }
+ break
+ }
+ }
+ if hasFocus {
+ p.Focus(p.setFocus)
+ }
+ return p
+}
+
+// Clear removes all pages
+func (p *Pages) Clear() *Pages {
+ p.pages = []*page{}
+ if p.changed != nil {
+ p.changed()
+ }
+ return p
+}
+
+// HasPage returns true if a page with the given name exists in this object.
+func (p *Pages) HasPage(name string) bool {
+ for _, page := range p.pages {
+ if page.Name == name {
+ return true
+ }
+ }
+ return false
+}
+
+// ShowPage sets a page's visibility to "true" (in addition to any other pages
+// which are already visible).
+func (p *Pages) ShowPage(name string) *Pages {
+ for _, page := range p.pages {
+ if page.Name == name {
+ page.Visible = true
+ if p.changed != nil {
+ p.changed()
+ }
+ break
+ }
+ }
+ if p.HasFocus() {
+ p.Focus(p.setFocus)
+ }
+ return p
+}
+
+// HidePage sets a page's visibility to "false".
+func (p *Pages) HidePage(name string) *Pages {
+ for _, page := range p.pages {
+ if page.Name == name {
+ page.Visible = false
+ if p.changed != nil {
+ p.changed()
+ }
+ break
+ }
+ }
+ if p.HasFocus() {
+ p.Focus(p.setFocus)
+ }
+ return p
+}
+
+// SwitchToPage sets a page's visibility to "true" and all other pages'
+// visibility to "false".
+func (p *Pages) SwitchToPage(name string) *Pages {
+ for _, page := range p.pages {
+ if page.Name == name {
+ page.Visible = true
+ } else {
+ page.Visible = false
+ }
+ }
+ if p.changed != nil {
+ p.changed()
+ }
+ if p.HasFocus() {
+ p.Focus(p.setFocus)
+ }
+ return p
+}
+
+// SendToFront changes the order of the pages such that the page with the given
+// name comes last, causing it to be drawn last with the next update (if
+// visible).
+func (p *Pages) SendToFront(name string) *Pages {
+ for index, page := range p.pages {
+ if page.Name == name {
+ if index < len(p.pages)-1 {
+ p.pages = append(append(p.pages[:index], p.pages[index+1:]...), page)
+ }
+ if page.Visible && p.changed != nil {
+ p.changed()
+ }
+ break
+ }
+ }
+ if p.HasFocus() {
+ p.Focus(p.setFocus)
+ }
+ return p
+}
+
+// SendToBack changes the order of the pages such that the page with the given
+// name comes first, causing it to be drawn first with the next update (if
+// visible).
+func (p *Pages) SendToBack(name string) *Pages {
+ for index, pg := range p.pages {
+ if pg.Name == name {
+ if index > 0 {
+ p.pages = append(append([]*page{pg}, p.pages[:index]...), p.pages[index+1:]...)
+ }
+ if pg.Visible && p.changed != nil {
+ p.changed()
+ }
+ break
+ }
+ }
+ if p.HasFocus() {
+ p.Focus(p.setFocus)
+ }
+ return p
+}
+
+// HasFocus returns whether or not this primitive has focus.
+func (p *Pages) HasFocus() bool {
+ for _, page := range p.pages {
+ if page.Item.GetFocusable().HasFocus() {
+ return true
+ }
+ }
+ return false
+}
+
+// Focus is called by the application when the primitive receives focus.
+func (p *Pages) Focus(delegate func(p Primitive)) {
+ if delegate == nil {
+ return // We cannot delegate so we cannot focus.
+ }
+ p.setFocus = delegate
+ var topItem Primitive
+ for _, page := range p.pages {
+ if page.Visible {
+ topItem = page.Item
+ }
+ }
+ if topItem != nil {
+ delegate(topItem)
+ }
+}
+
+// Draw draws this primitive onto the screen.
+func (p *Pages) Draw(screen tcell.Screen) {
+ p.Box.Draw(screen)
+ for _, page := range p.pages {
+ if !page.Visible {
+ continue
+ }
+ if page.Resize {
+ x, y, width, height := p.GetInnerRect()
+ page.Item.SetRect(x, y, width, height)
+ }
+ page.Item.Draw(screen)
+ }
+}
diff --git a/vendor/maunium.net/go/tview/primitive.go b/vendor/maunium.net/go/tview/primitive.go
new file mode 100644
index 0000000..a59033f
--- /dev/null
+++ b/vendor/maunium.net/go/tview/primitive.go
@@ -0,0 +1,48 @@
+package tview
+
+import "maunium.net/go/tcell"
+
+// Primitive is the top-most interface for all graphical primitives.
+type Primitive interface {
+ // Draw draws this primitive onto the screen. Implementers can call the
+ // screen's ShowCursor() function but should only do so when they have focus.
+ // (They will need to keep track of this themselves.)
+ Draw(screen tcell.Screen)
+
+ // GetRect returns the current position of the primitive, x, y, width, and
+ // height.
+ GetRect() (int, int, int, int)
+
+ // SetRect sets a new position of the primitive.
+ SetRect(x, y, width, height int)
+
+ // InputHandler returns a handler which receives key events when it has focus.
+ // It is called by the Application class.
+ //
+ // A value of nil may also be returned, in which case this primitive cannot
+ // receive focus and will not process any key events.
+ //
+ // The handler will receive the key event and a function that allows it to
+ // set the focus to a different primitive, so that future key events are sent
+ // to that primitive.
+ //
+ // The Application's Draw() function will be called automatically after the
+ // handler returns.
+ //
+ // The Box class provides functionality to intercept keyboard input. If you
+ // subclass from Box, it is recommended that you wrap your handler using
+ // Box.WrapInputHandler() so you inherit that functionality.
+ InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive))
+
+ MouseHandler() func(event *tcell.EventMouse, setFocus func(p Primitive))
+
+ // Focus is called by the application when the primitive receives focus.
+ // Implementers may call delegate() to pass the focus on to another primitive.
+ Focus(delegate func(p Primitive))
+
+ // Blur is called by the application when the primitive loses focus.
+ Blur()
+
+ // GetFocusable returns the item's Focusable.
+ GetFocusable() Focusable
+}
diff --git a/vendor/maunium.net/go/tview/styles.go b/vendor/maunium.net/go/tview/styles.go
new file mode 100644
index 0000000..8c3e0d7
--- /dev/null
+++ b/vendor/maunium.net/go/tview/styles.go
@@ -0,0 +1,34 @@
+package tview
+
+import "maunium.net/go/tcell"
+
+// Styles defines various colors used when primitives are initialized. These
+// may be changed to accommodate a different look and feel.
+//
+// The default is for applications with a black background and basic colors:
+// black, white, yellow, green, and blue.
+var Styles = struct {
+ PrimitiveBackgroundColor tcell.Color // Main background color for primitives.
+ ContrastBackgroundColor tcell.Color // Background color for contrasting elements.
+ MoreContrastBackgroundColor tcell.Color // Background color for even more contrasting elements.
+ BorderColor tcell.Color // Box borders.
+ TitleColor tcell.Color // Box titles.
+ GraphicsColor tcell.Color // Graphics.
+ PrimaryTextColor tcell.Color // Primary text.
+ SecondaryTextColor tcell.Color // Secondary text (e.g. labels).
+ TertiaryTextColor tcell.Color // Tertiary text (e.g. subtitles, notes).
+ InverseTextColor tcell.Color // Text on primary-colored backgrounds.
+ ContrastSecondaryTextColor tcell.Color // Secondary text on ContrastBackgroundColor-colored backgrounds.
+}{
+ PrimitiveBackgroundColor: tcell.ColorBlack,
+ ContrastBackgroundColor: tcell.ColorBlue,
+ MoreContrastBackgroundColor: tcell.ColorGreen,
+ BorderColor: tcell.ColorWhite,
+ TitleColor: tcell.ColorWhite,
+ GraphicsColor: tcell.ColorWhite,
+ PrimaryTextColor: tcell.ColorWhite,
+ SecondaryTextColor: tcell.ColorYellow,
+ TertiaryTextColor: tcell.ColorGreen,
+ InverseTextColor: tcell.ColorBlue,
+ ContrastSecondaryTextColor: tcell.ColorDarkCyan,
+}
diff --git a/vendor/maunium.net/go/tview/table.go b/vendor/maunium.net/go/tview/table.go
new file mode 100644
index 0000000..4f2336f
--- /dev/null
+++ b/vendor/maunium.net/go/tview/table.go
@@ -0,0 +1,1025 @@
+package tview
+
+import (
+ "sort"
+
+ "maunium.net/go/tcell"
+ colorful "github.com/lucasb-eyer/go-colorful"
+)
+
+// TableCell represents one cell inside a Table. You can instantiate this type
+// directly but all colors (background and text) will be set to their default
+// which is black.
+type TableCell struct {
+ // The text to be displayed in the table cell.
+ Text string
+
+ // The alignment of the cell text. One of AlignLeft (default), AlignCenter,
+ // or AlignRight.
+ Align int
+
+ // The maximum width of the cell in screen space. This is used to give a
+ // column a maximum width. Any cell text whose screen width exceeds this width
+ // is cut off. Set to 0 if there is no maximum width.
+ MaxWidth int
+
+ // If the total table width is less than the available width, this value is
+ // used to add extra width to a column. See SetExpansion() for details.
+ Expansion int
+
+ // The color of the cell text.
+ Color tcell.Color
+
+ // The background color of the cell.
+ BackgroundColor tcell.Color
+
+ // If set to true, this cell cannot be selected.
+ NotSelectable bool
+
+ // The position and width of the cell the last time table was drawn.
+ x, y, width int
+}
+
+// NewTableCell returns a new table cell with sensible defaults. That is, left
+// aligned text with the primary text color (see Styles) and a transparent
+// background (using the background of the Table).
+func NewTableCell(text string) *TableCell {
+ return &TableCell{
+ Text: text,
+ Align: AlignLeft,
+ Color: Styles.PrimaryTextColor,
+ BackgroundColor: tcell.ColorDefault,
+ }
+}
+
+// SetText sets the cell's text.
+func (c *TableCell) SetText(text string) *TableCell {
+ c.Text = text
+ return c
+}
+
+// SetAlign sets the cell's text alignment, one of AlignLeft, AlignCenter, or
+// AlignRight.
+func (c *TableCell) SetAlign(align int) *TableCell {
+ c.Align = align
+ return c
+}
+
+// SetMaxWidth sets maximum width of the cell in screen space. This is used to
+// give a column a maximum width. Any cell text whose screen width exceeds this
+// width is cut off. Set to 0 if there is no maximum width.
+func (c *TableCell) SetMaxWidth(maxWidth int) *TableCell {
+ c.MaxWidth = maxWidth
+ return c
+}
+
+// SetExpansion sets the value by which the column of this cell expands if the
+// available width for the table is more than the table width (prior to applying
+// this expansion value). This is a proportional value. The amount of unused
+// horizontal space is divided into widths to be added to each column. How much
+// extra width a column receives depends on the expansion value: A value of 0
+// (the default) will not cause the column to increase in width. Other values
+// are proportional, e.g. a value of 2 will cause a column to grow by twice
+// the amount of a column with a value of 1.
+//
+// Since this value affects an entire column, the maximum over all visible cells
+// in that column is used.
+//
+// This function panics if a negative value is provided.
+func (c *TableCell) SetExpansion(expansion int) *TableCell {
+ if expansion < 0 {
+ panic("Table cell expansion values may not be negative")
+ }
+ c.Expansion = expansion
+ return c
+}
+
+// SetTextColor sets the cell's text color.
+func (c *TableCell) SetTextColor(color tcell.Color) *TableCell {
+ c.Color = color
+ return c
+}
+
+// SetBackgroundColor sets the cell's background color. Set to
+// tcell.ColorDefault to use the table's background color.
+func (c *TableCell) SetBackgroundColor(color tcell.Color) *TableCell {
+ c.BackgroundColor = color
+ return c
+}
+
+// SetSelectable sets whether or not this cell can be selected by the user.
+func (c *TableCell) SetSelectable(selectable bool) *TableCell {
+ c.NotSelectable = !selectable
+ return c
+}
+
+// GetLastPosition returns the position of the table cell the last time it was
+// drawn on screen. If the cell is not on screen, the return values are
+// undefined.
+//
+// Because the Table class will attempt to keep selected cells on screen, this
+// function is most useful in response to a "selected" event (see
+// SetSelectedFunc()) or a "selectionChanged" event (see
+// SetSelectionChangedFunc()).
+func (c *TableCell) GetLastPosition() (x, y, width int) {
+ return c.x, c.y, c.width
+}
+
+// Table visualizes two-dimensional data consisting of rows and columns. Each
+// Table cell is defined via SetCell() by the TableCell type. They can be added
+// dynamically to the table and changed any time.
+//
+// The most compact display of a table is without borders. Each row will then
+// occupy one row on screen and columns are separated by the rune defined via
+// SetSeparator() (a space character by default).
+//
+// When borders are turned on (via SetBorders()), each table cell is surrounded
+// by lines. Therefore one table row will require two rows on screen.
+//
+// Columns will use as much horizontal space as they need. You can constrain
+// their size with the MaxWidth parameter of the TableCell type.
+//
+// Fixed Columns
+//
+// You can define fixed rows and rolumns via SetFixed(). They will always stay
+// in their place, even when the table is scrolled. Fixed rows are always the
+// top rows. Fixed columns are always the leftmost columns.
+//
+// Selections
+//
+// You can call SetSelectable() to set columns and/or rows to "selectable". If
+// the flag is set only for columns, entire columns can be selected by the user.
+// If it is set only for rows, entire rows can be selected. If both flags are
+// set, individual cells can be selected. The "selected" handler set via
+// SetSelectedFunc() is invoked when the user presses Enter on a selection.
+//
+// Navigation
+//
+// If the table extends beyond the available space, it can be navigated with
+// key bindings similar to Vim:
+//
+// - h, left arrow: Move left by one column.
+// - l, right arrow: Move right by one column.
+// - j, down arrow: Move down by one row.
+// - k, up arrow: Move up by one row.
+// - g, home: Move to the top.
+// - G, end: Move to the bottom.
+// - Ctrl-F, page down: Move down by one page.
+// - Ctrl-B, page up: Move up by one page.
+//
+// When there is no selection, this affects the entire table (except for fixed
+// rows and columns). When there is a selection, the user moves the selection.
+// The class will attempt to keep the selection from moving out of the screen.
+//
+// Use SetInputCapture() to override or modify keyboard input.
+//
+// See https://github.com/rivo/tview/wiki/Table for an example.
+type Table struct {
+ *Box
+
+ // Whether or not this table has borders around each cell.
+ borders bool
+
+ // The color of the borders or the separator.
+ bordersColor tcell.Color
+
+ // If there are no borders, the column separator.
+ separator rune
+
+ // The cells of the table. Rows first, then columns.
+ cells [][]*TableCell
+
+ // The rightmost column in the data set.
+ lastColumn int
+
+ // The number of fixed rows / columns.
+ fixedRows, fixedColumns int
+
+ // Whether or not rows or columns can be selected. If both are set to true,
+ // cells can be selected.
+ rowsSelectable, columnsSelectable bool
+
+ // The currently selected row and column.
+ selectedRow, selectedColumn int
+
+ // The number of rows/columns by which the table is scrolled down/to the
+ // right.
+ rowOffset, columnOffset int
+
+ // If set to true, the table's last row will always be visible.
+ trackEnd bool
+
+ // The number of visible rows the last time the table was drawn.
+ visibleRows int
+
+ // An optional function which gets called when the user presses Enter on a
+ // selected cell. If entire rows selected, the column value is undefined.
+ // Likewise for entire columns.
+ selected func(row, column int)
+
+ // An optional function which gets called when the user changes the selection.
+ // If entire rows selected, the column value is undefined.
+ // Likewise for entire columns.
+ selectionChanged func(row, column int)
+
+ // An optional function which gets called when the user presses Escape, Tab,
+ // or Backtab. Also when the user presses Enter if nothing is selectable.
+ done func(key tcell.Key)
+}
+
+// NewTable returns a new table.
+func NewTable() *Table {
+ return &Table{
+ Box: NewBox(),
+ bordersColor: Styles.GraphicsColor,
+ separator: ' ',
+ lastColumn: -1,
+ }
+}
+
+// Clear removes all table data.
+func (t *Table) Clear() *Table {
+ t.cells = nil
+ t.lastColumn = -1
+ return t
+}
+
+// SetBorders sets whether or not each cell in the table is surrounded by a
+// border.
+func (t *Table) SetBorders(show bool) *Table {
+ t.borders = show
+ return t
+}
+
+// SetBordersColor sets the color of the cell borders.
+func (t *Table) SetBordersColor(color tcell.Color) *Table {
+ t.bordersColor = color
+ return t
+}
+
+// SetSeparator sets the character used to fill the space between two
+// neighboring cells. This is a space character ' ' per default but you may
+// want to set it to GraphicsVertBar (or any other rune) if the column
+// separation should be more visible. If cell borders are activated, this is
+// ignored.
+//
+// Separators have the same color as borders.
+func (t *Table) SetSeparator(separator rune) *Table {
+ t.separator = separator
+ return t
+}
+
+// SetFixed sets the number of fixed rows and columns which are always visible
+// even when the rest of the cells are scrolled out of view. Rows are always the
+// top-most ones. Columns are always the left-most ones.
+func (t *Table) SetFixed(rows, columns int) *Table {
+ t.fixedRows, t.fixedColumns = rows, columns
+ return t
+}
+
+// SetSelectable sets the flags which determine what can be selected in a table.
+// There are three selection modi:
+//
+// - rows = false, columns = false: Nothing can be selected.
+// - rows = true, columns = false: Rows can be selected.
+// - rows = false, columns = true: Columns can be selected.
+// - rows = true, columns = true: Individual cells can be selected.
+func (t *Table) SetSelectable(rows, columns bool) *Table {
+ t.rowsSelectable, t.columnsSelectable = rows, columns
+ return t
+}
+
+// GetSelectable returns what can be selected in a table. Refer to
+// SetSelectable() for details.
+func (t *Table) GetSelectable() (rows, columns bool) {
+ return t.rowsSelectable, t.columnsSelectable
+}
+
+// GetSelection returns the position of the current selection.
+// If entire rows are selected, the column index is undefined.
+// Likewise for entire columns.
+func (t *Table) GetSelection() (row, column int) {
+ return t.selectedRow, t.selectedColumn
+}
+
+// Select sets the selected cell. Depending on the selection settings
+// specified via SetSelectable(), this may be an entire row or column, or even
+// ignored completely.
+func (t *Table) Select(row, column int) *Table {
+ t.selectedRow, t.selectedColumn = row, column
+ return t
+}
+
+// SetOffset sets how many rows and columns should be skipped when drawing the
+// table. This is useful for large tables that do not fit on the screen.
+// Navigating a selection can change these values.
+//
+// Fixed rows and columns are never skipped.
+func (t *Table) SetOffset(row, column int) *Table {
+ t.rowOffset, t.columnOffset = row, column
+ return t
+}
+
+// GetOffset returns the current row and column offset. This indicates how many
+// rows and columns the table is scrolled down and to the right.
+func (t *Table) GetOffset() (row, column int) {
+ return t.rowOffset, t.columnOffset
+}
+
+// SetSelectedFunc sets a handler which is called whenever the user presses the
+// Enter key on a selected cell/row/column. The handler receives the position of
+// the selection and its cell contents. If entire rows are selected, the column
+// index is undefined. Likewise for entire columns.
+func (t *Table) SetSelectedFunc(handler func(row, column int)) *Table {
+ t.selected = handler
+ return t
+}
+
+// SetSelectionChangedFunc sets a handler which is called whenever the user
+// navigates to a new selection. The handler receives the position of the new
+// selection. If entire rows are selected, the column index is undefined.
+// Likewise for entire columns.
+func (t *Table) SetSelectionChangedFunc(handler func(row, column int)) *Table {
+ t.selectionChanged = handler
+ return t
+}
+
+// SetDoneFunc sets a handler which is called whenever the user presses the
+// Escape, Tab, or Backtab key. If nothing is selected, it is also called when
+// user presses the Enter key (because pressing Enter on a selection triggers
+// the "selected" handler set via SetSelectedFunc()).
+func (t *Table) SetDoneFunc(handler func(key tcell.Key)) *Table {
+ t.done = handler
+ return t
+}
+
+// SetCell sets the content of a cell the specified position. It is ok to
+// directly instantiate a TableCell object. If the cell has contain, at least
+// the Text and Color fields should be set.
+//
+// Note that setting cells in previously unknown rows and columns will
+// automatically extend the internal table representation, e.g. starting with
+// a row of 100,000 will immediately create 100,000 empty rows.
+//
+// To avoid unnecessary garbage collection, fill columns from left to right.
+func (t *Table) SetCell(row, column int, cell *TableCell) *Table {
+ if row >= len(t.cells) {
+ t.cells = append(t.cells, make([][]*TableCell, row-len(t.cells)+1)...)
+ }
+ rowLen := len(t.cells[row])
+ if column >= rowLen {
+ t.cells[row] = append(t.cells[row], make([]*TableCell, column-rowLen+1)...)
+ for c := rowLen; c < column; c++ {
+ t.cells[row][c] = &TableCell{}
+ }
+ }
+ t.cells[row][column] = cell
+ if column > t.lastColumn {
+ t.lastColumn = column
+ }
+ return t
+}
+
+// SetCellSimple calls SetCell() with the given text, left-aligned, in white.
+func (t *Table) SetCellSimple(row, column int, text string) *Table {
+ t.SetCell(row, column, NewTableCell(text))
+ return t
+}
+
+// GetCell returns the contents of the cell at the specified position. A valid
+// TableCell object is always returns but it will be uninitialized if the cell
+// was not previously set.
+func (t *Table) GetCell(row, column int) *TableCell {
+ if row >= len(t.cells) || column >= len(t.cells[row]) {
+ return &TableCell{}
+ }
+ return t.cells[row][column]
+}
+
+// GetRowCount returns the number of rows in the table.
+func (t *Table) GetRowCount() int {
+ return len(t.cells)
+}
+
+// GetColumnCount returns the (maximum) number of columns in the table.
+func (t *Table) GetColumnCount() int {
+ if len(t.cells) == 0 {
+ return 0
+ }
+ return t.lastColumn + 1
+}
+
+// ScrollToBeginning scrolls the table to the beginning to that the top left
+// corner of the table is shown. Note that this position may be corrected if
+// there is a selection.
+func (t *Table) ScrollToBeginning() *Table {
+ t.trackEnd = false
+ t.columnOffset = 0
+ t.rowOffset = 0
+ return t
+}
+
+// ScrollToEnd scrolls the table to the beginning to that the bottom left corner
+// of the table is shown. Adding more rows to the table will cause it to
+// automatically scroll with the new data. Note that this position may be
+// corrected if there is a selection.
+func (t *Table) ScrollToEnd() *Table {
+ t.trackEnd = true
+ t.columnOffset = 0
+ t.rowOffset = len(t.cells)
+ return t
+}
+
+// Draw draws this primitive onto the screen.
+func (t *Table) Draw(screen tcell.Screen) {
+ t.Box.Draw(screen)
+
+ // What's our available screen space?
+ x, y, width, height := t.GetInnerRect()
+ if t.borders {
+ t.visibleRows = height / 2
+ } else {
+ t.visibleRows = height
+ }
+
+ // Return the cell at the specified position (nil if it doesn't exist).
+ getCell := func(row, column int) *TableCell {
+ if row < 0 || column < 0 || row >= len(t.cells) || column >= len(t.cells[row]) {
+ return nil
+ }
+ return t.cells[row][column]
+ }
+
+ // If this cell is not selectable, find the next one.
+ if t.rowsSelectable || t.columnsSelectable {
+ if t.selectedColumn < 0 {
+ t.selectedColumn = 0
+ }
+ if t.selectedRow < 0 {
+ t.selectedRow = 0
+ }
+ for t.selectedRow < len(t.cells) {
+ cell := getCell(t.selectedRow, t.selectedColumn)
+ if cell == nil || !cell.NotSelectable {
+ break
+ }
+ t.selectedColumn++
+ if t.selectedColumn > t.lastColumn {
+ t.selectedColumn = 0
+ t.selectedRow++
+ }
+ }
+ }
+
+ // Clamp row offsets.
+ if t.rowsSelectable {
+ if t.selectedRow >= t.fixedRows && t.selectedRow < t.fixedRows+t.rowOffset {
+ t.rowOffset = t.selectedRow - t.fixedRows
+ t.trackEnd = false
+ }
+ if t.borders {
+ if 2*(t.selectedRow+1-t.rowOffset) >= height {
+ t.rowOffset = t.selectedRow + 1 - height/2
+ t.trackEnd = false
+ }
+ } else {
+ if t.selectedRow+1-t.rowOffset >= height {
+ t.rowOffset = t.selectedRow + 1 - height
+ t.trackEnd = false
+ }
+ }
+ }
+ if t.borders {
+ if 2*(len(t.cells)-t.rowOffset) < height {
+ t.trackEnd = true
+ }
+ } else {
+ if len(t.cells)-t.rowOffset < height {
+ t.trackEnd = true
+ }
+ }
+ if t.trackEnd {
+ if t.borders {
+ t.rowOffset = len(t.cells) - height/2
+ } else {
+ t.rowOffset = len(t.cells) - height
+ }
+ }
+ if t.rowOffset < 0 {
+ t.rowOffset = 0
+ }
+
+ // Clamp column offset. (Only left side here. The right side is more
+ // difficult and we'll do it below.)
+ if t.columnsSelectable && t.selectedColumn >= t.fixedColumns && t.selectedColumn < t.fixedColumns+t.columnOffset {
+ t.columnOffset = t.selectedColumn - t.fixedColumns
+ }
+ if t.columnOffset < 0 {
+ t.columnOffset = 0
+ }
+ if t.selectedColumn < 0 {
+ t.selectedColumn = 0
+ }
+
+ // Determine the indices and widths of the columns and rows which fit on the
+ // screen.
+ var (
+ columns, rows, widths []int
+ tableHeight, tableWidth int
+ )
+ rowStep := 1
+ if t.borders {
+ rowStep = 2 // With borders, every table row takes two screen rows.
+ tableWidth = 1 // We start at the second character because of the left table border.
+ }
+ indexRow := func(row int) bool { // Determine if this row is visible, store its index.
+ if tableHeight >= height {
+ return false
+ }
+ rows = append(rows, row)
+ tableHeight += rowStep
+ return true
+ }
+ for row := 0; row < t.fixedRows && row < len(t.cells); row++ { // Do the fixed rows first.
+ if !indexRow(row) {
+ break
+ }
+ }
+ for row := t.fixedRows + t.rowOffset; row < len(t.cells); row++ { // Then the remaining rows.
+ if !indexRow(row) {
+ break
+ }
+ }
+ var (
+ skipped, lastTableWidth, expansionTotal int
+ expansions []int
+ )
+ColumnLoop:
+ for column := 0; ; column++ {
+ // If we've moved beyond the right border, we stop or skip a column.
+ for tableWidth-1 >= width { // -1 because we include one extra column if the separator falls on the right end of the box.
+ // We've moved beyond the available space.
+ if column < t.fixedColumns {
+ break ColumnLoop // We're in the fixed area. We're done.
+ }
+ if !t.columnsSelectable && skipped >= t.columnOffset {
+ break ColumnLoop // There is no selection and we've already reached the offset.
+ }
+ if t.columnsSelectable && t.selectedColumn-skipped == t.fixedColumns {
+ break ColumnLoop // The selected column reached the leftmost point before disappearing.
+ }
+ if t.columnsSelectable && skipped >= t.columnOffset &&
+ (t.selectedColumn < column && lastTableWidth < width-1 && tableWidth < width-1 || t.selectedColumn < column-1) {
+ break ColumnLoop // We've skipped as many as requested and the selection is visible.
+ }
+ if len(columns) <= t.fixedColumns {
+ break // Nothing to skip.
+ }
+
+ // We need to skip a column.
+ skipped++
+ lastTableWidth -= widths[t.fixedColumns] + 1
+ tableWidth -= widths[t.fixedColumns] + 1
+ columns = append(columns[:t.fixedColumns], columns[t.fixedColumns+1:]...)
+ widths = append(widths[:t.fixedColumns], widths[t.fixedColumns+1:]...)
+ expansions = append(expansions[:t.fixedColumns], expansions[t.fixedColumns+1:]...)
+ }
+
+ // What's this column's width (without expansion)?
+ maxWidth := -1
+ expansion := 0
+ for _, row := range rows {
+ if cell := getCell(row, column); cell != nil {
+ cellWidth := StringWidth(cell.Text)
+ if cell.MaxWidth > 0 && cell.MaxWidth < cellWidth {
+ cellWidth = cell.MaxWidth
+ }
+ if cellWidth > maxWidth {
+ maxWidth = cellWidth
+ }
+ if cell.Expansion > expansion {
+ expansion = cell.Expansion
+ }
+ }
+ }
+ if maxWidth < 0 {
+ break // No more cells found in this column.
+ }
+
+ // Store new column info at the end.
+ columns = append(columns, column)
+ widths = append(widths, maxWidth)
+ lastTableWidth = tableWidth
+ tableWidth += maxWidth + 1
+ expansions = append(expansions, expansion)
+ expansionTotal += expansion
+ }
+ t.columnOffset = skipped
+
+ // If we have space left, distribute it.
+ if tableWidth < width {
+ toDistribute := width - tableWidth
+ for index, expansion := range expansions {
+ if expansionTotal <= 0 {
+ break
+ }
+ expWidth := toDistribute * expansion / expansionTotal
+ widths[index] += expWidth
+ tableWidth += expWidth
+ toDistribute -= expWidth
+ expansionTotal -= expansion
+ }
+ }
+
+ // Helper function which draws border runes.
+ borderStyle := tcell.StyleDefault.Background(t.backgroundColor).Foreground(t.bordersColor)
+ drawBorder := func(colX, rowY int, ch rune) {
+ screen.SetContent(x+colX, y+rowY, ch, nil, borderStyle)
+ }
+
+ // Draw the cells (and borders).
+ var columnX int
+ if !t.borders {
+ columnX--
+ }
+ for columnIndex, column := range columns {
+ columnWidth := widths[columnIndex]
+ for rowY, row := range rows {
+ if t.borders {
+ // Draw borders.
+ rowY *= 2
+ for pos := 0; pos < columnWidth && columnX+1+pos < width; pos++ {
+ drawBorder(columnX+pos+1, rowY, GraphicsHoriBar)
+ }
+ ch := GraphicsCross
+ if columnIndex == 0 {
+ if rowY == 0 {
+ ch = GraphicsTopLeftCorner
+ } else {
+ ch = GraphicsLeftT
+ }
+ } else if rowY == 0 {
+ ch = GraphicsTopT
+ }
+ drawBorder(columnX, rowY, ch)
+ rowY++
+ if rowY >= height {
+ break // No space for the text anymore.
+ }
+ drawBorder(columnX, rowY, GraphicsVertBar)
+ } else if columnIndex > 0 {
+ // Draw separator.
+ drawBorder(columnX, rowY, t.separator)
+ }
+
+ // Get the cell.
+ cell := getCell(row, column)
+ if cell == nil {
+ continue
+ }
+
+ // Draw text.
+ finalWidth := columnWidth
+ if columnX+1+columnWidth >= width {
+ finalWidth = width - columnX - 1
+ }
+ cell.x, cell.y, cell.width = x+columnX+1, y+rowY, finalWidth
+ _, printed := Print(screen, cell.Text, x+columnX+1, y+rowY, finalWidth, cell.Align, cell.Color)
+ if StringWidth(cell.Text)-printed > 0 && printed > 0 {
+ _, _, style, _ := screen.GetContent(x+columnX+1+finalWidth-1, y+rowY)
+ fg, _, _ := style.Decompose()
+ Print(screen, string(GraphicsEllipsis), x+columnX+1+finalWidth-1, y+rowY, 1, AlignLeft, fg)
+ }
+ }
+
+ // Draw bottom border.
+ if rowY := 2 * len(rows); t.borders && rowY < height {
+ for pos := 0; pos < columnWidth && columnX+1+pos < width; pos++ {
+ drawBorder(columnX+pos+1, rowY, GraphicsHoriBar)
+ }
+ ch := GraphicsBottomT
+ if columnIndex == 0 {
+ ch = GraphicsBottomLeftCorner
+ }
+ drawBorder(columnX, rowY, ch)
+ }
+
+ columnX += columnWidth + 1
+ }
+
+ // Draw right border.
+ if t.borders && len(t.cells) > 0 && columnX < width {
+ for rowY := range rows {
+ rowY *= 2
+ if rowY+1 < height {
+ drawBorder(columnX, rowY+1, GraphicsVertBar)
+ }
+ ch := GraphicsRightT
+ if rowY == 0 {
+ ch = GraphicsTopRightCorner
+ }
+ drawBorder(columnX, rowY, ch)
+ }
+ if rowY := 2 * len(rows); rowY < height {
+ drawBorder(columnX, rowY, GraphicsBottomRightCorner)
+ }
+ }
+
+ // Helper function which colors the background of a box.
+ colorBackground := func(fromX, fromY, w, h int, backgroundColor, textColor tcell.Color, selected bool) {
+ for by := 0; by < h && fromY+by < y+height; by++ {
+ for bx := 0; bx < w && fromX+bx < x+width; bx++ {
+ m, c, style, _ := screen.GetContent(fromX+bx, fromY+by)
+ if selected {
+ fg, _, _ := style.Decompose()
+ if fg == textColor || fg == t.bordersColor {
+ fg = backgroundColor
+ }
+ if fg == tcell.ColorDefault {
+ fg = t.backgroundColor
+ }
+ style = style.Background(textColor).Foreground(fg)
+ } else {
+ if backgroundColor == tcell.ColorDefault {
+ continue
+ }
+ style = style.Background(backgroundColor)
+ }
+ screen.SetContent(fromX+bx, fromY+by, m, c, style)
+ }
+ }
+ }
+
+ // Color the cell backgrounds. To avoid undesirable artefacts, we combine
+ // the drawing of a cell by background color, selected cells last.
+ cellsByBackgroundColor := make(map[tcell.Color][]*struct {
+ x, y, w, h int
+ text tcell.Color
+ selected bool
+ })
+ var backgroundColors []tcell.Color
+ for rowY, row := range rows {
+ columnX := 0
+ rowSelected := t.rowsSelectable && !t.columnsSelectable && row == t.selectedRow
+ for columnIndex, column := range columns {
+ columnWidth := widths[columnIndex]
+ cell := getCell(row, column)
+ if cell == nil {
+ continue
+ }
+ bx, by, bw, bh := x+columnX, y+rowY, columnWidth+1, 1
+ if t.borders {
+ by = y + rowY*2
+ bw++
+ bh = 3
+ }
+ columnSelected := t.columnsSelectable && !t.rowsSelectable && column == t.selectedColumn
+ cellSelected := !cell.NotSelectable && (columnSelected || rowSelected || t.rowsSelectable && t.columnsSelectable && column == t.selectedColumn && row == t.selectedRow)
+ entries, ok := cellsByBackgroundColor[cell.BackgroundColor]
+ cellsByBackgroundColor[cell.BackgroundColor] = append(entries, &struct {
+ x, y, w, h int
+ text tcell.Color
+ selected bool
+ }{
+ x: bx,
+ y: by,
+ w: bw,
+ h: bh,
+ text: cell.Color,
+ selected: cellSelected,
+ })
+ if !ok {
+ backgroundColors = append(backgroundColors, cell.BackgroundColor)
+ }
+ columnX += columnWidth + 1
+ }
+ }
+ sort.Slice(backgroundColors, func(i int, j int) bool {
+ // Draw brightest colors last (i.e. on top).
+ r, g, b := backgroundColors[i].RGB()
+ c := colorful.Color{R: float64(r) / 255, G: float64(g) / 255, B: float64(b) / 255}
+ _, _, li := c.Hcl()
+ r, g, b = backgroundColors[j].RGB()
+ c = colorful.Color{R: float64(r) / 255, G: float64(g) / 255, B: float64(b) / 255}
+ _, _, lj := c.Hcl()
+ return li < lj
+ })
+ for _, bgColor := range backgroundColors {
+ entries := cellsByBackgroundColor[bgColor]
+ for _, cell := range entries {
+ if cell.selected {
+ defer colorBackground(cell.x, cell.y, cell.w, cell.h, bgColor, cell.text, true)
+ } else {
+ colorBackground(cell.x, cell.y, cell.w, cell.h, bgColor, cell.text, false)
+ }
+ }
+ }
+}
+
+// InputHandler returns the handler for this primitive.
+func (t *Table) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) {
+ return t.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p Primitive)) {
+ key := event.Key()
+
+ if (!t.rowsSelectable && !t.columnsSelectable && key == tcell.KeyEnter) ||
+ key == tcell.KeyEscape ||
+ key == tcell.KeyTab ||
+ key == tcell.KeyBacktab {
+ if t.done != nil {
+ t.done(key)
+ }
+ return
+ }
+
+ // Movement functions.
+ previouslySelectedRow, previouslySelectedColumn := t.selectedRow, t.selectedColumn
+ var (
+ getCell = func(row, column int) *TableCell {
+ if row < 0 || column < 0 || row >= len(t.cells) || column >= len(t.cells[row]) {
+ return nil
+ }
+ return t.cells[row][column]
+ }
+
+ previous = func() {
+ for t.selectedRow >= 0 {
+ cell := getCell(t.selectedRow, t.selectedColumn)
+ if cell == nil || !cell.NotSelectable {
+ return
+ }
+ t.selectedColumn--
+ if t.selectedColumn < 0 {
+ t.selectedColumn = t.lastColumn
+ t.selectedRow--
+ }
+ }
+ }
+
+ next = func() {
+ if t.selectedColumn > t.lastColumn {
+ t.selectedColumn = 0
+ t.selectedRow++
+ if t.selectedRow >= len(t.cells) {
+ t.selectedRow = len(t.cells) - 1
+ }
+ }
+ for t.selectedRow < len(t.cells) {
+ cell := getCell(t.selectedRow, t.selectedColumn)
+ if cell == nil || !cell.NotSelectable {
+ return
+ }
+ t.selectedColumn++
+ if t.selectedColumn > t.lastColumn {
+ t.selectedColumn = 0
+ t.selectedRow++
+ }
+ }
+ t.selectedColumn = t.lastColumn
+ t.selectedRow = len(t.cells) - 1
+ previous()
+ }
+
+ home = func() {
+ if t.rowsSelectable {
+ t.selectedRow = 0
+ t.selectedColumn = 0
+ next()
+ } else {
+ t.trackEnd = false
+ t.rowOffset = 0
+ t.columnOffset = 0
+ }
+ }
+
+ end = func() {
+ if t.rowsSelectable {
+ t.selectedRow = len(t.cells) - 1
+ t.selectedColumn = t.lastColumn
+ previous()
+ } else {
+ t.trackEnd = true
+ t.columnOffset = 0
+ }
+ }
+
+ down = func() {
+ if t.rowsSelectable {
+ t.selectedRow++
+ if t.selectedRow >= len(t.cells) {
+ t.selectedRow = len(t.cells) - 1
+ }
+ next()
+ } else {
+ t.rowOffset++
+ }
+ }
+
+ up = func() {
+ if t.rowsSelectable {
+ t.selectedRow--
+ if t.selectedRow < 0 {
+ t.selectedRow = 0
+ }
+ previous()
+ } else {
+ t.trackEnd = false
+ t.rowOffset--
+ }
+ }
+
+ left = func() {
+ if t.columnsSelectable {
+ t.selectedColumn--
+ if t.selectedColumn < 0 {
+ t.selectedColumn = 0
+ }
+ previous()
+ } else {
+ t.columnOffset--
+ }
+ }
+
+ right = func() {
+ if t.columnsSelectable {
+ t.selectedColumn++
+ if t.selectedColumn > t.lastColumn {
+ t.selectedColumn = t.lastColumn
+ }
+ next()
+ } else {
+ t.columnOffset++
+ }
+ }
+
+ pageDown = func() {
+ if t.rowsSelectable {
+ t.selectedRow += t.visibleRows
+ if t.selectedRow >= len(t.cells) {
+ t.selectedRow = len(t.cells) - 1
+ }
+ next()
+ } else {
+ t.rowOffset += t.visibleRows
+ }
+ }
+
+ pageUp = func() {
+ if t.rowsSelectable {
+ t.selectedRow -= t.visibleRows
+ if t.selectedRow < 0 {
+ t.selectedRow = 0
+ }
+ previous()
+ } else {
+ t.trackEnd = false
+ t.rowOffset -= t.visibleRows
+ }
+ }
+ )
+
+ switch key {
+ case tcell.KeyRune:
+ switch event.Rune() {
+ case 'g':
+ home()
+ case 'G':
+ end()
+ case 'j':
+ down()
+ case 'k':
+ up()
+ case 'h':
+ left()
+ case 'l':
+ right()
+ }
+ case tcell.KeyHome:
+ home()
+ case tcell.KeyEnd:
+ end()
+ case tcell.KeyUp:
+ up()
+ case tcell.KeyDown:
+ down()
+ case tcell.KeyLeft:
+ left()
+ case tcell.KeyRight:
+ right()
+ case tcell.KeyPgDn, tcell.KeyCtrlF:
+ pageDown()
+ case tcell.KeyPgUp, tcell.KeyCtrlB:
+ pageUp()
+ case tcell.KeyEnter:
+ if (t.rowsSelectable || t.columnsSelectable) && t.selected != nil {
+ t.selected(t.selectedRow, t.selectedColumn)
+ }
+ }
+
+ // If the selection has changed, notify the handler.
+ if t.selectionChanged != nil &&
+ (t.rowsSelectable && previouslySelectedRow != t.selectedRow ||
+ t.columnsSelectable && previouslySelectedColumn != t.selectedColumn) {
+ t.selectionChanged(t.selectedRow, t.selectedColumn)
+ }
+ })
+}
diff --git a/vendor/maunium.net/go/tview/textview.go b/vendor/maunium.net/go/tview/textview.go
new file mode 100644
index 0000000..8de0121
--- /dev/null
+++ b/vendor/maunium.net/go/tview/textview.go
@@ -0,0 +1,899 @@
+package tview
+
+import (
+ "bytes"
+ "fmt"
+ "regexp"
+ "sync"
+ "unicode/utf8"
+
+ "maunium.net/go/tcell"
+ runewidth "github.com/mattn/go-runewidth"
+)
+
+// TabSize is the number of spaces with which a tab character will be replaced.
+var TabSize = 4
+
+// textViewIndex contains information about each line displayed in the text
+// view.
+type textViewIndex struct {
+ Line int // The index into the "buffer" variable.
+ Pos int // The index into the "buffer" string (byte position).
+ NextPos int // The (byte) index of the next character in this buffer line.
+ Width int // The screen width of this line.
+ Color tcell.Color // The starting color.
+ Region string // The starting region ID.
+}
+
+// TextView is a box which displays text. It implements the io.Writer interface
+// so you can stream text to it. This does not trigger a redraw automatically
+// but if a handler is installed via SetChangedFunc(), you can cause it to be
+// redrawn.
+//
+// Navigation
+//
+// If the text view is scrollable (the default), text is kept in a buffer which
+// may be larger than the screen and can be navigated similarly to Vim:
+//
+// - h, left arrow: Move left.
+// - l, right arrow: Move right.
+// - j, down arrow: Move down.
+// - k, up arrow: Move up.
+// - g, home: Move to the top.
+// - G, end: Move to the bottom.
+// - Ctrl-F, page down: Move down by one page.
+// - Ctrl-B, page up: Move up by one page.
+//
+// If the text is not scrollable, any text above the top visible line is
+// discarded.
+//
+// Use SetInputCapture() to override or modify keyboard input.
+//
+// Colors
+//
+// If dynamic colors are enabled via SetDynamicColors(), text color can be
+// changed dynamically by embedding color strings in square brackets. This works
+// the same way as anywhere else. Please see the package documentation for more
+// information.
+//
+// Regions and Highlights
+//
+// If regions are enabled via SetRegions(), you can define text regions within
+// the text and assign region IDs to them. Text regions start with region tags.
+// Region tags are square brackets that contain a region ID in double quotes,
+// for example:
+//
+// We define a ["rg"]region[""] here.
+//
+// A text region ends with the next region tag. Tags with no region ID ([""])
+// don't start new regions. They can therefore be used to mark the end of a
+// region. Region IDs must satisfy the following regular expression:
+//
+// [a-zA-Z0-9_,;: \-\.]+
+//
+// Regions can be highlighted by calling the Highlight() function with one or
+// more region IDs. This can be used to display search results, for example.
+//
+// The ScrollToHighlight() function can be used to jump to the currently
+// highlighted region once when the text view is drawn the next time.
+//
+// See https://github.com/rivo/tview/wiki/TextView for an example.
+type TextView struct {
+ sync.Mutex
+ *Box
+
+ // The text buffer.
+ buffer []string
+
+ // The last bytes that have been received but are not part of the buffer yet.
+ recentBytes []byte
+
+ // The processed line index. This is nil if the buffer has changed and needs
+ // to be re-indexed.
+ index []*textViewIndex
+
+ // The text alignment, one of AlignLeft, AlignCenter, or AlignRight.
+ align int
+
+ // Indices into the "index" slice which correspond to the first line of the
+ // first highlight and the last line of the last highlight. This is calculated
+ // during re-indexing. Set to -1 if there is no current highlight.
+ fromHighlight, toHighlight int
+
+ // A set of region IDs that are currently highlighted.
+ highlights map[string]struct{}
+
+ // The last width for which the current table is drawn.
+ lastWidth int
+
+ // The screen width of the longest line in the index (not the buffer).
+ longestLine int
+
+ // The index of the first line shown in the text view.
+ lineOffset int
+
+ // If set to true, the text view will always remain at the end of the content.
+ trackEnd bool
+
+ // The number of characters to be skipped on each line (not in wrap mode).
+ columnOffset int
+
+ // The height of the content the last time the text view was drawn.
+ pageSize int
+
+ // If set to true, the text view will keep a buffer of text which can be
+ // navigated when the text is longer than what fits into the box.
+ scrollable bool
+
+ // If set to true, lines that are longer than the available width are wrapped
+ // onto the next line. If set to false, any characters beyond the available
+ // width are discarded.
+ wrap bool
+
+ // If set to true and if wrap is also true, lines are split at spaces or
+ // after punctuation characters.
+ wordWrap bool
+
+ // The (starting) color of the text.
+ textColor tcell.Color
+
+ // If set to true, the text color can be changed dynamically by piping color
+ // strings in square brackets to the text view.
+ dynamicColors bool
+
+ // If set to true, region tags can be used to define regions.
+ regions bool
+
+ // A temporary flag which, when true, will automatically bring the current
+ // highlight(s) into the visible screen.
+ scrollToHighlights bool
+
+ // An optional function which is called when the content of the text view has
+ // changed.
+ changed func()
+
+ // An optional function which is called when the user presses one of the
+ // following keys: Escape, Enter, Tab, Backtab.
+ done func(tcell.Key)
+}
+
+// NewTextView returns a new text view.
+func NewTextView() *TextView {
+ return &TextView{
+ Box: NewBox(),
+ highlights: make(map[string]struct{}),
+ lineOffset: -1,
+ scrollable: true,
+ align: AlignLeft,
+ wrap: true,
+ textColor: Styles.PrimaryTextColor,
+ dynamicColors: false,
+ }
+}
+
+// SetScrollable sets the flag that decides whether or not the text view is
+// scrollable. If true, text is kept in a buffer and can be navigated.
+func (t *TextView) SetScrollable(scrollable bool) *TextView {
+ t.scrollable = scrollable
+ if !scrollable {
+ t.trackEnd = true
+ }
+ return t
+}
+
+// SetWrap sets the flag that, if true, leads to lines that are longer than the
+// available width being wrapped onto the next line. If false, any characters
+// beyond the available width are not displayed.
+func (t *TextView) SetWrap(wrap bool) *TextView {
+ if t.wrap != wrap {
+ t.index = nil
+ }
+ t.wrap = wrap
+ return t
+}
+
+// SetWordWrap sets the flag that, if true and if the "wrap" flag is also true
+// (see SetWrap()), wraps the line at spaces or after punctuation marks. Note
+// that trailing spaces will not be printed.
+//
+// This flag is ignored if the "wrap" flag is false.
+func (t *TextView) SetWordWrap(wrapOnWords bool) *TextView {
+ if t.wordWrap != wrapOnWords {
+ t.index = nil
+ }
+ t.wordWrap = wrapOnWords
+ return t
+}
+
+// SetTextAlign sets the text alignment within the text view. This must be
+// either AlignLeft, AlignCenter, or AlignRight.
+func (t *TextView) SetTextAlign(align int) *TextView {
+ if t.align != align {
+ t.index = nil
+ }
+ t.align = align
+ return t
+}
+
+// SetTextColor sets the initial color of the text (which can be changed
+// dynamically by sending color strings in square brackets to the text view if
+// dynamic colors are enabled).
+func (t *TextView) SetTextColor(color tcell.Color) *TextView {
+ t.textColor = color
+ return t
+}
+
+// SetText sets the text of this text view to the provided string. Previously
+// contained text will be removed.
+func (t *TextView) SetText(text string) *TextView {
+ t.Clear()
+ fmt.Fprint(t, text)
+ return t
+}
+
+// SetDynamicColors sets the flag that allows the text color to be changed
+// dynamically. See class description for details.
+func (t *TextView) SetDynamicColors(dynamic bool) *TextView {
+ if t.dynamicColors != dynamic {
+ t.index = nil
+ }
+ t.dynamicColors = dynamic
+ return t
+}
+
+// SetRegions sets the flag that allows to define regions in the text. See class
+// description for details.
+func (t *TextView) SetRegions(regions bool) *TextView {
+ if t.regions != regions {
+ t.index = nil
+ }
+ t.regions = regions
+ return t
+}
+
+// SetChangedFunc sets a handler function which is called when the text of the
+// text view has changed. This is typically used to cause the application to
+// redraw the screen.
+func (t *TextView) SetChangedFunc(handler func()) *TextView {
+ t.changed = handler
+ return t
+}
+
+// SetDoneFunc sets a handler which is called when the user presses on the
+// following keys: Escape, Enter, Tab, Backtab. The key is passed to the
+// handler.
+func (t *TextView) SetDoneFunc(handler func(key tcell.Key)) *TextView {
+ t.done = handler
+ return t
+}
+
+// ScrollToBeginning scrolls to the top left corner of the text if the text view
+// is scrollable.
+func (t *TextView) ScrollToBeginning() *TextView {
+ if !t.scrollable {
+ return t
+ }
+ t.trackEnd = false
+ t.lineOffset = 0
+ t.columnOffset = 0
+ return t
+}
+
+// ScrollToEnd scrolls to the bottom left corner of the text if the text view
+// is scrollable. Adding new rows to the end of the text view will cause it to
+// scroll with the new data.
+func (t *TextView) ScrollToEnd() *TextView {
+ if !t.scrollable {
+ return t
+ }
+ t.trackEnd = true
+ t.columnOffset = 0
+ return t
+}
+
+// Clear removes all text from the buffer.
+func (t *TextView) Clear() *TextView {
+ t.buffer = nil
+ t.recentBytes = nil
+ t.index = nil
+ return t
+}
+
+// Highlight specifies which regions should be highlighted. See class
+// description for details on regions. Empty region strings are ignored.
+//
+// Text in highlighted regions will be drawn inverted, i.e. with their
+// background and foreground colors swapped.
+//
+// Calling this function will remove any previous highlights. To remove all
+// highlights, call this function without any arguments.
+func (t *TextView) Highlight(regionIDs ...string) *TextView {
+ t.highlights = make(map[string]struct{})
+ for _, id := range regionIDs {
+ if id == "" {
+ continue
+ }
+ t.highlights[id] = struct{}{}
+ }
+ t.index = nil
+ return t
+}
+
+// GetHighlights returns the IDs of all currently highlighted regions.
+func (t *TextView) GetHighlights() (regionIDs []string) {
+ for id := range t.highlights {
+ regionIDs = append(regionIDs, id)
+ }
+ return
+}
+
+// ScrollToHighlight will cause the visible area to be scrolled so that the
+// highlighted regions appear in the visible area of the text view. This
+// repositioning happens the next time the text view is drawn. It happens only
+// once so you will need to call this function repeatedly to always keep
+// highlighted regions in view.
+//
+// Nothing happens if there are no highlighted regions or if the text view is
+// not scrollable.
+func (t *TextView) ScrollToHighlight() *TextView {
+ if len(t.highlights) == 0 || !t.scrollable || !t.regions {
+ return t
+ }
+ t.index = nil
+ t.scrollToHighlights = true
+ t.trackEnd = false
+ return t
+}
+
+// GetRegionText returns the text of the region with the given ID. If dynamic
+// colors are enabled, color tags are stripped from the text. Newlines are
+// always returned as '\n' runes.
+//
+// If the region does not exist or if regions are turned off, an empty string
+// is returned.
+func (t *TextView) GetRegionText(regionID string) string {
+ if !t.regions || regionID == "" {
+ return ""
+ }
+
+ var (
+ buffer bytes.Buffer
+ currentRegionID string
+ )
+
+ for _, str := range t.buffer {
+ // Find all color tags in this line.
+ var colorTagIndices [][]int
+ if t.dynamicColors {
+ colorTagIndices = colorPattern.FindAllStringIndex(str, -1)
+ }
+
+ // Find all regions in this line.
+ var (
+ regionIndices [][]int
+ regions [][]string
+ )
+ if t.regions {
+ regionIndices = regionPattern.FindAllStringIndex(str, -1)
+ regions = regionPattern.FindAllStringSubmatch(str, -1)
+ }
+
+ // Analyze this line.
+ var currentTag, currentRegion int
+ for pos, ch := range str {
+ // Skip any color tags.
+ if currentTag < len(colorTagIndices) && pos >= colorTagIndices[currentTag][0] && pos < colorTagIndices[currentTag][1] {
+ if pos == colorTagIndices[currentTag][1]-1 {
+ currentTag++
+ }
+ continue
+ }
+
+ // Skip any regions.
+ if currentRegion < len(regionIndices) && pos >= regionIndices[currentRegion][0] && pos < regionIndices[currentRegion][1] {
+ if pos == regionIndices[currentRegion][1]-1 {
+ if currentRegionID == regionID {
+ // This is the end of the requested region. We're done.
+ return buffer.String()
+ }
+ currentRegionID = regions[currentRegion][1]
+ currentRegion++
+ }
+ continue
+ }
+
+ // Add this rune.
+ if currentRegionID == regionID {
+ buffer.WriteRune(ch)
+ }
+ }
+
+ // Add newline.
+ if currentRegionID == regionID {
+ buffer.WriteRune('\n')
+ }
+ }
+
+ return escapePattern.ReplaceAllString(buffer.String(), `[$1$2]`)
+}
+
+// Write lets us implement the io.Writer interface. Tab characters will be
+// replaced with TabSize space characters. A "\n" or "\r\n" will be interpreted
+// as a new line.
+func (t *TextView) Write(p []byte) (n int, err error) {
+ // Notify at the end.
+ if t.changed != nil {
+ defer t.changed()
+ }
+
+ t.Lock()
+ defer t.Unlock()
+
+ // Copy data over.
+ newBytes := append(t.recentBytes, p...)
+ t.recentBytes = nil
+
+ // If we have a trailing invalid UTF-8 byte, we'll wait.
+ if r, _ := utf8.DecodeLastRune(p); r == utf8.RuneError {
+ t.recentBytes = newBytes
+ return len(p), nil
+ }
+
+ // If we have a trailing open dynamic color, exclude it.
+ if t.dynamicColors {
+ openColor := regexp.MustCompile(`\[([a-zA-Z]*|#[0-9a-zA-Z]*)$`)
+ location := openColor.FindIndex(newBytes)
+ if location != nil {
+ t.recentBytes = newBytes[location[0]:]
+ newBytes = newBytes[:location[0]]
+ }
+ }
+
+ // If we have a trailing open region, exclude it.
+ if t.regions {
+ openRegion := regexp.MustCompile(`\["[a-zA-Z0-9_,;: \-\.]*"?$`)
+ location := openRegion.FindIndex(newBytes)
+ if location != nil {
+ t.recentBytes = newBytes[location[0]:]
+ newBytes = newBytes[:location[0]]
+ }
+ }
+
+ // Transform the new bytes into strings.
+ newLine := regexp.MustCompile(`\r?\n`)
+ newBytes = bytes.Replace(newBytes, []byte{'\t'}, bytes.Repeat([]byte{' '}, TabSize), -1)
+ for index, line := range newLine.Split(string(newBytes), -1) {
+ if index == 0 {
+ if len(t.buffer) == 0 {
+ t.buffer = []string{line}
+ } else {
+ t.buffer[len(t.buffer)-1] += line
+ }
+ } else {
+ t.buffer = append(t.buffer, line)
+ }
+ }
+
+ // Reset the index.
+ t.index = nil
+
+ return len(p), nil
+}
+
+// reindexBuffer re-indexes the buffer such that we can use it to easily draw
+// the buffer onto the screen. Each line in the index will contain a pointer
+// into the buffer from which on we will print text. It will also contain the
+// color with which the line starts.
+func (t *TextView) reindexBuffer(width int) {
+ if t.index != nil {
+ return // Nothing has changed. We can still use the current index.
+ }
+ t.index = nil
+ t.fromHighlight, t.toHighlight = -1, -1
+
+ // If there's no space, there's no index.
+ if width < 1 {
+ return
+ }
+
+ // Initial states.
+ regionID := ""
+ var highlighted bool
+ color := t.textColor
+
+ // Go through each line in the buffer.
+ for bufferIndex, str := range t.buffer {
+ // Find all color tags in this line. Then remove them.
+ var (
+ colorTagIndices [][]int
+ colorTags [][]string
+ )
+ if t.dynamicColors {
+ colorTagIndices = colorPattern.FindAllStringIndex(str, -1)
+ colorTags = colorPattern.FindAllStringSubmatch(str, -1)
+ str = colorPattern.ReplaceAllString(str, "")
+ }
+
+ // Find all regions in this line. Then remove them.
+ var (
+ regionIndices [][]int
+ regions [][]string
+ )
+ if t.regions {
+ regionIndices = regionPattern.FindAllStringIndex(str, -1)
+ regions = regionPattern.FindAllStringSubmatch(str, -1)
+ str = regionPattern.ReplaceAllString(str, "")
+ }
+
+ // Find all replace tags in this line. Then replace them.
+ var escapeIndices [][]int
+ if t.dynamicColors || t.regions {
+ escapeIndices = escapePattern.FindAllStringIndex(str, -1)
+ str = escapePattern.ReplaceAllString(str, "[$1$2]")
+ }
+
+ // Split the line if required.
+ var splitLines []string
+ if t.wrap && len(str) > 0 {
+ for len(str) > 0 {
+ extract := runewidth.Truncate(str, width, "")
+ if t.wordWrap && len(extract) < len(str) {
+ // Add any spaces from the next line.
+ if spaces := spacePattern.FindStringIndex(str[len(extract):]); spaces != nil && spaces[0] == 0 {
+ extract = str[:len(extract)+spaces[1]]
+ }
+
+ // Can we split before the mandatory end?
+ matches := boundaryPattern.FindAllStringIndex(extract, -1)
+ if len(matches) > 0 {
+ // Yes. Let's split there.
+ extract = extract[:matches[len(matches)-1][1]]
+ }
+ }
+ splitLines = append(splitLines, extract)
+ str = str[len(extract):]
+ }
+ } else {
+ // No need to split the line.
+ splitLines = []string{str}
+ }
+
+ // Create index from split lines.
+ var originalPos, colorPos, regionPos, escapePos int
+ for _, splitLine := range splitLines {
+ line := &textViewIndex{
+ Line: bufferIndex,
+ Pos: originalPos,
+ Color: color,
+ Region: regionID,
+ }
+
+ // Shift original position with tags.
+ lineLength := len(splitLine)
+ for {
+ if colorPos < len(colorTagIndices) && colorTagIndices[colorPos][0] <= originalPos+lineLength {
+ // Process color tags.
+ originalPos += colorTagIndices[colorPos][1] - colorTagIndices[colorPos][0]
+ color = tcell.GetColor(colorTags[colorPos][1])
+ colorPos++
+ } else if regionPos < len(regionIndices) && regionIndices[regionPos][0] <= originalPos+lineLength {
+ // Process region tags.
+ originalPos += regionIndices[regionPos][1] - regionIndices[regionPos][0]
+ regionID = regions[regionPos][1]
+ _, highlighted = t.highlights[regionID]
+
+ // Update highlight range.
+ if highlighted {
+ line := len(t.index)
+ if t.fromHighlight < 0 {
+ t.fromHighlight, t.toHighlight = line, line
+ } else if line > t.toHighlight {
+ t.toHighlight = line
+ }
+ }
+
+ regionPos++
+ } else if escapePos < len(escapeIndices) && escapeIndices[escapePos][0] <= originalPos+lineLength {
+ // Process escape tags.
+ originalPos++
+ escapePos++
+ } else {
+ break
+ }
+ }
+
+ // Advance to next line.
+ originalPos += lineLength
+
+ // Append this line.
+ line.NextPos = originalPos
+ line.Width = runewidth.StringWidth(splitLine)
+ t.index = append(t.index, line)
+ }
+
+ // Word-wrapped lines may have trailing whitespace. Remove it.
+ if t.wrap && t.wordWrap {
+ for _, line := range t.index {
+ str := t.buffer[line.Line][line.Pos:line.NextPos]
+ spaces := spacePattern.FindAllStringIndex(str, -1)
+ if spaces != nil && spaces[len(spaces)-1][1] == len(str) {
+ oldNextPos := line.NextPos
+ line.NextPos -= spaces[len(spaces)-1][1] - spaces[len(spaces)-1][0]
+ line.Width -= runewidth.StringWidth(t.buffer[line.Line][line.NextPos:oldNextPos])
+ }
+ }
+ }
+ }
+
+ // Calculate longest line.
+ t.longestLine = 0
+ for _, line := range t.index {
+ if line.Width > t.longestLine {
+ t.longestLine = line.Width
+ }
+ }
+}
+
+// Draw draws this primitive onto the screen.
+func (t *TextView) Draw(screen tcell.Screen) {
+ t.Lock()
+ defer t.Unlock()
+ t.Box.Draw(screen)
+
+ // Get the available size.
+ x, y, width, height := t.GetInnerRect()
+ t.pageSize = height
+
+ // If the width has changed, we need to reindex.
+ if width != t.lastWidth {
+ t.index = nil
+ }
+ t.lastWidth = width
+
+ // Re-index.
+ t.reindexBuffer(width)
+
+ // If we don't have an index, there's nothing to draw.
+ if t.index == nil {
+ return
+ }
+
+ // Move to highlighted regions.
+ if t.regions && t.scrollToHighlights && t.fromHighlight >= 0 {
+ // Do we fit the entire height?
+ if t.toHighlight-t.fromHighlight+1 < height {
+ // Yes, let's center the highlights.
+ t.lineOffset = (t.fromHighlight + t.toHighlight - height) / 2
+ } else {
+ // No, let's move to the start of the highlights.
+ t.lineOffset = t.fromHighlight
+ }
+ }
+ t.scrollToHighlights = false
+
+ // Adjust line offset.
+ if t.lineOffset+height > len(t.index) {
+ t.trackEnd = true
+ }
+ if t.trackEnd {
+ t.lineOffset = len(t.index) - height
+ }
+ if t.lineOffset < 0 {
+ t.lineOffset = 0
+ }
+
+ // Adjust column offset.
+ if t.align == AlignLeft {
+ if t.columnOffset+width > t.longestLine {
+ t.columnOffset = t.longestLine - width
+ }
+ if t.columnOffset < 0 {
+ t.columnOffset = 0
+ }
+ } else if t.align == AlignRight {
+ if t.columnOffset-width < -t.longestLine {
+ t.columnOffset = width - t.longestLine
+ }
+ if t.columnOffset > 0 {
+ t.columnOffset = 0
+ }
+ } else { // AlignCenter.
+ half := (t.longestLine - width) / 2
+ if half > 0 {
+ if t.columnOffset > half {
+ t.columnOffset = half
+ }
+ if t.columnOffset < -half {
+ t.columnOffset = -half
+ }
+ } else {
+ t.columnOffset = 0
+ }
+ }
+
+ // Draw the buffer.
+ for line := t.lineOffset; line < len(t.index); line++ {
+ // Are we done?
+ if line-t.lineOffset >= height {
+ break
+ }
+
+ // Get the text for this line.
+ index := t.index[line]
+ text := t.buffer[index.Line][index.Pos:index.NextPos]
+ color := index.Color
+ regionID := index.Region
+
+ // Get color tags.
+ var (
+ colorTagIndices [][]int
+ colorTags [][]string
+ )
+ if t.dynamicColors {
+ colorTagIndices = colorPattern.FindAllStringIndex(text, -1)
+ colorTags = colorPattern.FindAllStringSubmatch(text, -1)
+ }
+
+ // Get regions.
+ var (
+ regionIndices [][]int
+ regions [][]string
+ )
+ if t.regions {
+ regionIndices = regionPattern.FindAllStringIndex(text, -1)
+ regions = regionPattern.FindAllStringSubmatch(text, -1)
+ }
+
+ // Get escape tags.
+ var escapeIndices [][]int
+ if t.dynamicColors || t.regions {
+ escapeIndices = escapePattern.FindAllStringIndex(text, -1)
+ }
+
+ // Calculate the position of the line.
+ var skip, posX int
+ if t.align == AlignLeft {
+ posX = -t.columnOffset
+ } else if t.align == AlignRight {
+ posX = width - index.Width - t.columnOffset
+ } else { // AlignCenter.
+ posX = (width-index.Width)/2 - t.columnOffset
+ }
+ if posX < 0 {
+ skip = -posX
+ posX = 0
+ }
+
+ // Print the line.
+ var currentTag, currentRegion, currentEscapeTag, skipped int
+ for pos, ch := range text {
+ // Get the color.
+ if currentTag < len(colorTags) && pos >= colorTagIndices[currentTag][0] && pos < colorTagIndices[currentTag][1] {
+ if pos == colorTagIndices[currentTag][1]-1 {
+ color = tcell.GetColor(colorTags[currentTag][1])
+ currentTag++
+ }
+ continue
+ }
+
+ // Get the region.
+ if currentRegion < len(regionIndices) && pos >= regionIndices[currentRegion][0] && pos < regionIndices[currentRegion][1] {
+ if pos == regionIndices[currentRegion][1]-1 {
+ regionID = regions[currentRegion][1]
+ currentRegion++
+ }
+ continue
+ }
+
+ // Skip the second-to-last character of an escape tag.
+ if currentEscapeTag < len(escapeIndices) && pos >= escapeIndices[currentEscapeTag][0] && pos < escapeIndices[currentEscapeTag][1] {
+ if pos == escapeIndices[currentEscapeTag][1]-1 {
+ currentEscapeTag++
+ } else if pos == escapeIndices[currentEscapeTag][1]-2 {
+ continue
+ }
+ }
+
+ // Determine the width of this rune.
+ chWidth := runewidth.RuneWidth(ch)
+ if chWidth == 0 {
+ continue
+ }
+
+ // Skip to the right.
+ if !t.wrap && skipped < skip {
+ skipped += chWidth
+ continue
+ }
+
+ // Stop at the right border.
+ if posX+chWidth > width {
+ break
+ }
+
+ // Do we highlight this character?
+ style := tcell.StyleDefault.Background(t.backgroundColor).Foreground(color)
+ if len(regionID) > 0 {
+ if _, ok := t.highlights[regionID]; ok {
+ style = tcell.StyleDefault.Background(color).Foreground(t.backgroundColor)
+ }
+ }
+
+ // Draw the character.
+ for offset := 0; offset < chWidth; offset++ {
+ screen.SetContent(x+posX+offset, y+line-t.lineOffset, ch, nil, style)
+ }
+
+ // Advance.
+ posX += chWidth
+ }
+ }
+
+ // If this view is not scrollable, we'll purge the buffer of lines that have
+ // scrolled out of view.
+ if !t.scrollable && t.lineOffset > 0 {
+ t.buffer = t.buffer[t.index[t.lineOffset].Line:]
+ t.index = nil
+ }
+}
+
+// InputHandler returns the handler for this primitive.
+func (t *TextView) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) {
+ return t.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p Primitive)) {
+ key := event.Key()
+
+ if key == tcell.KeyEscape || key == tcell.KeyEnter || key == tcell.KeyTab || key == tcell.KeyBacktab {
+ if t.done != nil {
+ t.done(key)
+ }
+ return
+ }
+
+ if !t.scrollable {
+ return
+ }
+
+ switch key {
+ case tcell.KeyRune:
+ switch event.Rune() {
+ case 'g': // Home.
+ t.trackEnd = false
+ t.lineOffset = 0
+ t.columnOffset = 0
+ case 'G': // End.
+ t.trackEnd = true
+ t.columnOffset = 0
+ case 'j': // Down.
+ t.lineOffset++
+ case 'k': // Up.
+ t.trackEnd = false
+ t.lineOffset--
+ case 'h': // Left.
+ t.columnOffset--
+ case 'l': // Right.
+ t.columnOffset++
+ }
+ case tcell.KeyHome:
+ t.trackEnd = false
+ t.lineOffset = 0
+ t.columnOffset = 0
+ case tcell.KeyEnd:
+ t.trackEnd = true
+ t.columnOffset = 0
+ case tcell.KeyUp:
+ t.trackEnd = false
+ t.lineOffset--
+ case tcell.KeyDown:
+ t.lineOffset++
+ case tcell.KeyLeft:
+ t.columnOffset--
+ case tcell.KeyRight:
+ t.columnOffset++
+ case tcell.KeyPgDn, tcell.KeyCtrlF:
+ t.lineOffset += t.pageSize
+ case tcell.KeyPgUp, tcell.KeyCtrlB:
+ t.trackEnd = false
+ t.lineOffset -= t.pageSize
+ }
+ })
+}
diff --git a/vendor/maunium.net/go/tview/tview.gif b/vendor/maunium.net/go/tview/tview.gif
new file mode 100644
index 0000000..0583d7b
--- /dev/null
+++ b/vendor/maunium.net/go/tview/tview.gif
Binary files differ
diff --git a/vendor/maunium.net/go/tview/util.go b/vendor/maunium.net/go/tview/util.go
new file mode 100644
index 0000000..f49c35f
--- /dev/null
+++ b/vendor/maunium.net/go/tview/util.go
@@ -0,0 +1,456 @@
+package tview
+
+import (
+ "math"
+ "regexp"
+ "strconv"
+ "strings"
+ "unicode"
+
+ "maunium.net/go/tcell"
+ runewidth "github.com/mattn/go-runewidth"
+)
+
+// Text alignment within a box.
+const (
+ AlignLeft = iota
+ AlignCenter
+ AlignRight
+)
+
+// Semigraphical runes.
+const (
+ GraphicsHoriBar = '\u2500'
+ GraphicsVertBar = '\u2502'
+ GraphicsTopLeftCorner = '\u250c'
+ GraphicsTopRightCorner = '\u2510'
+ GraphicsBottomLeftCorner = '\u2514'
+ GraphicsBottomRightCorner = '\u2518'
+ GraphicsLeftT = '\u251c'
+ GraphicsRightT = '\u2524'
+ GraphicsTopT = '\u252c'
+ GraphicsBottomT = '\u2534'
+ GraphicsCross = '\u253c'
+ GraphicsDbVertBar = '\u2550'
+ GraphicsDbHorBar = '\u2551'
+ GraphicsDbTopLeftCorner = '\u2554'
+ GraphicsDbTopRightCorner = '\u2557'
+ GraphicsDbBottomRightCorner = '\u255d'
+ GraphicsDbBottomLeftCorner = '\u255a'
+ GraphicsEllipsis = '\u2026'
+)
+
+// joints maps combinations of two graphical runes to the rune that results
+// when joining the two in the same screen cell. The keys of this map are
+// two-rune strings where the value of the first rune is lower than the value
+// of the second rune. Identical runes are not contained.
+var joints = map[string]rune{
+ "\u2500\u2502": GraphicsCross,
+ "\u2500\u250c": GraphicsTopT,
+ "\u2500\u2510": GraphicsTopT,
+ "\u2500\u2514": GraphicsBottomT,
+ "\u2500\u2518": GraphicsBottomT,
+ "\u2500\u251c": GraphicsCross,
+ "\u2500\u2524": GraphicsCross,
+ "\u2500\u252c": GraphicsTopT,
+ "\u2500\u2534": GraphicsBottomT,
+ "\u2500\u253c": GraphicsCross,
+ "\u2502\u250c": GraphicsLeftT,
+ "\u2502\u2510": GraphicsRightT,
+ "\u2502\u2514": GraphicsLeftT,
+ "\u2502\u2518": GraphicsRightT,
+ "\u2502\u251c": GraphicsLeftT,
+ "\u2502\u2524": GraphicsRightT,
+ "\u2502\u252c": GraphicsCross,
+ "\u2502\u2534": GraphicsCross,
+ "\u2502\u253c": GraphicsCross,
+ "\u250c\u2510": GraphicsTopT,
+ "\u250c\u2514": GraphicsLeftT,
+ "\u250c\u2518": GraphicsCross,
+ "\u250c\u251c": GraphicsLeftT,
+ "\u250c\u2524": GraphicsCross,
+ "\u250c\u252c": GraphicsTopT,
+ "\u250c\u2534": GraphicsCross,
+ "\u250c\u253c": GraphicsCross,
+ "\u2510\u2514": GraphicsCross,
+ "\u2510\u2518": GraphicsRightT,
+ "\u2510\u251c": GraphicsCross,
+ "\u2510\u2524": GraphicsRightT,
+ "\u2510\u252c": GraphicsTopT,
+ "\u2510\u2534": GraphicsCross,
+ "\u2510\u253c": GraphicsCross,
+ "\u2514\u2518": GraphicsBottomT,
+ "\u2514\u251c": GraphicsLeftT,
+ "\u2514\u2524": GraphicsCross,
+ "\u2514\u252c": GraphicsCross,
+ "\u2514\u2534": GraphicsBottomT,
+ "\u2514\u253c": GraphicsCross,
+ "\u2518\u251c": GraphicsCross,
+ "\u2518\u2524": GraphicsRightT,
+ "\u2518\u252c": GraphicsCross,
+ "\u2518\u2534": GraphicsBottomT,
+ "\u2518\u253c": GraphicsCross,
+ "\u251c\u2524": GraphicsCross,
+ "\u251c\u252c": GraphicsCross,
+ "\u251c\u2534": GraphicsCross,
+ "\u251c\u253c": GraphicsCross,
+ "\u2524\u252c": GraphicsCross,
+ "\u2524\u2534": GraphicsCross,
+ "\u2524\u253c": GraphicsCross,
+ "\u252c\u2534": GraphicsCross,
+ "\u252c\u253c": GraphicsCross,
+ "\u2534\u253c": GraphicsCross,
+}
+
+// Common regular expressions.
+var (
+ colorPattern = regexp.MustCompile(`\[([a-zA-Z]+|#[0-9a-zA-Z]{6})\]`)
+ regionPattern = regexp.MustCompile(`\["([a-zA-Z0-9_,;: \-\.]*)"\]`)
+ escapePattern = regexp.MustCompile(`\[("[a-zA-Z0-9_,;: \-\.]*"|[a-zA-Z]+|#[0-9a-zA-Z]{6})\[(\[*)\]`)
+ boundaryPattern = regexp.MustCompile("([[:punct:]]\\s*|\\s+)")
+ spacePattern = regexp.MustCompile(`\s+`)
+)
+
+// Predefined InputField acceptance functions.
+var (
+ // InputFieldInteger accepts integers.
+ InputFieldInteger func(text string, ch rune) bool
+
+ // InputFieldFloat accepts floating-point numbers.
+ InputFieldFloat func(text string, ch rune) bool
+
+ // InputFieldMaxLength returns an input field accept handler which accepts
+ // input strings up to a given length. Use it like this:
+ //
+ // inputField.SetAcceptanceFunc(InputFieldMaxLength(10)) // Accept up to 10 characters.
+ InputFieldMaxLength func(maxLength int) func(text string, ch rune) bool
+)
+
+// Package initialization.
+func init() {
+ // Initialize the predefined input field handlers.
+ InputFieldInteger = func(text string, ch rune) bool {
+ if text == "-" {
+ return true
+ }
+ _, err := strconv.Atoi(text)
+ return err == nil
+ }
+ InputFieldFloat = func(text string, ch rune) bool {
+ if text == "-" || text == "." || text == "-." {
+ return true
+ }
+ _, err := strconv.ParseFloat(text, 64)
+ return err == nil
+ }
+ InputFieldMaxLength = func(maxLength int) func(text string, ch rune) bool {
+ return func(text string, ch rune) bool {
+ return len([]rune(text)) <= maxLength
+ }
+ }
+}
+
+// Print prints text onto the screen into the given box at (x,y,maxWidth,1),
+// not exceeding that box. "align" is one of AlignLeft, AlignCenter, or
+// AlignRight. The screen's background color will not be changed.
+//
+// You can change the text color mid-text by inserting a color tag. See the
+// package description for details.
+//
+// Returns the number of actual runes printed (not including color tags) and the
+// actual width used for the printed runes.
+func Print(screen tcell.Screen, text string, x, y, maxWidth, align int, color tcell.Color) (int, int) {
+ if maxWidth < 0 {
+ return 0, 0
+ }
+
+ // Get positions of color and escape tags. Remove them from original string.
+ colorIndices := colorPattern.FindAllStringIndex(text, -1)
+ colors := colorPattern.FindAllStringSubmatch(text, -1)
+ escapeIndices := escapePattern.FindAllStringIndex(text, -1)
+ strippedText := escapePattern.ReplaceAllString(colorPattern.ReplaceAllString(text, ""), "[$1$2]")
+
+ // We deal with runes, not with bytes.
+ runes := []rune(strippedText)
+
+ // This helper function takes positions for a substring of "runes" and a start
+ // color and returns the substring with the original tags and the new start
+ // color.
+ substring := func(from, to int, color tcell.Color) (string, tcell.Color) {
+ var colorPos, escapePos, runePos, startPos int
+ for pos := range text {
+ // Handle color tags.
+ if colorPos < len(colorIndices) && pos >= colorIndices[colorPos][0] && pos < colorIndices[colorPos][1] {
+ if pos == colorIndices[colorPos][1]-1 {
+ if runePos <= from {
+ color = tcell.GetColor(colors[colorPos][1])
+ }
+ colorPos++
+ }
+ continue
+ }
+
+ // Handle escape tags.
+ if escapePos < len(escapeIndices) && pos >= escapeIndices[escapePos][0] && pos < escapeIndices[escapePos][1] {
+ if pos == escapeIndices[escapePos][1]-1 {
+ escapePos++
+ } else if pos == escapeIndices[escapePos][1]-2 {
+ continue
+ }
+ }
+
+ // Check boundaries.
+ if runePos == from {
+ startPos = pos
+ } else if runePos >= to {
+ return text[startPos:pos], color
+ }
+
+ runePos++
+ }
+
+ return text[startPos:], color
+ }
+
+ // We want to reduce everything to AlignLeft.
+ if align == AlignRight {
+ width := 0
+ start := len(runes)
+ for index := start - 1; index >= 0; index-- {
+ w := runewidth.RuneWidth(runes[index])
+ if width+w > maxWidth {
+ break
+ }
+ width += w
+ start = index
+ }
+ text, color = substring(start, len(runes), color)
+ return Print(screen, text, x+maxWidth-width, y, width, AlignLeft, color)
+ } else if align == AlignCenter {
+ width := runewidth.StringWidth(strippedText)
+ if width == maxWidth {
+ // Use the exact space.
+ return Print(screen, text, x, y, maxWidth, AlignLeft, color)
+ } else if width < maxWidth {
+ // We have more space than we need.
+ half := (maxWidth - width) / 2
+ return Print(screen, text, x+half, y, maxWidth-half, AlignLeft, color)
+ } else {
+ // Chop off runes until we have a perfect fit.
+ var choppedLeft, choppedRight, leftIndex, rightIndex int
+ rightIndex = len(runes) - 1
+ for rightIndex > leftIndex && width-choppedLeft-choppedRight > maxWidth {
+ leftWidth := runewidth.RuneWidth(runes[leftIndex])
+ rightWidth := runewidth.RuneWidth(runes[rightIndex])
+ if choppedLeft < choppedRight {
+ choppedLeft += leftWidth
+ leftIndex++
+ } else {
+ choppedRight += rightWidth
+ rightIndex--
+ }
+ }
+ text, color = substring(leftIndex, rightIndex, color)
+ return Print(screen, text, x, y, maxWidth, AlignLeft, color)
+ }
+ }
+
+ // Draw text.
+ drawn := 0
+ drawnWidth := 0
+ var colorPos, escapePos int
+ for pos, ch := range text {
+ // Handle color tags.
+ if colorPos < len(colorIndices) && pos >= colorIndices[colorPos][0] && pos < colorIndices[colorPos][1] {
+ if pos == colorIndices[colorPos][1]-1 {
+ color = tcell.GetColor(colors[colorPos][1])
+ colorPos++
+ }
+ continue
+ }
+
+ // Handle escape tags.
+ if escapePos < len(escapeIndices) && pos >= escapeIndices[escapePos][0] && pos < escapeIndices[escapePos][1] {
+ if pos == escapeIndices[escapePos][1]-1 {
+ escapePos++
+ } else if pos == escapeIndices[escapePos][1]-2 {
+ continue
+ }
+ }
+
+ // Check if we have enough space for this rune.
+ chWidth := runewidth.RuneWidth(ch)
+ if drawnWidth+chWidth > maxWidth {
+ break
+ }
+ finalX := x + drawnWidth
+
+ // Print the rune.
+ _, _, style, _ := screen.GetContent(finalX, y)
+ style = style.Foreground(color)
+ for offset := 0; offset < chWidth; offset++ {
+ // To avoid undesired effects, we place the same character in all cells.
+ screen.SetContent(finalX+offset, y, ch, nil, style)
+ }
+
+ drawn++
+ drawnWidth += chWidth
+ }
+
+ return drawn, drawnWidth
+}
+
+// PrintSimple prints white text to the screen at the given position.
+func PrintSimple(screen tcell.Screen, text string, x, y int) {
+ Print(screen, text, x, y, math.MaxInt32, AlignLeft, Styles.PrimaryTextColor)
+}
+
+// StringWidth returns the width of the given string needed to print it on
+// screen. The text may contain color tags which are not counted.
+func StringWidth(text string) int {
+ return runewidth.StringWidth(escapePattern.ReplaceAllString(colorPattern.ReplaceAllString(text, ""), "[$1$2]"))
+}
+
+// WordWrap splits a text such that each resulting line does not exceed the
+// given screen width. Possible split points are after any punctuation or
+// whitespace. Whitespace after split points will be dropped.
+//
+// This function considers color tags to have no width.
+//
+// Text is always split at newline characters ('\n').
+func WordWrap(text string, width int) (lines []string) {
+ // Strip color tags.
+ strippedText := escapePattern.ReplaceAllString(colorPattern.ReplaceAllString(text, ""), "[$1$2]")
+
+ // Keep track of color tags and escape patterns so we can restore the original
+ // indices.
+ colorTagIndices := colorPattern.FindAllStringIndex(text, -1)
+ escapeIndices := escapePattern.FindAllStringIndex(text, -1)
+
+ // Find candidate breakpoints.
+ breakPoints := boundaryPattern.FindAllStringIndex(strippedText, -1)
+
+ // This helper function adds a new line to the result slice. The provided
+ // positions are in stripped index space.
+ addLine := func(from, to int) {
+ // Shift indices back to original index space.
+ var colorTagIndex, escapeIndex int
+ for colorTagIndex < len(colorTagIndices) && to >= colorTagIndices[colorTagIndex][0] ||
+ escapeIndex < len(escapeIndices) && to >= escapeIndices[escapeIndex][0] {
+ past := 0
+ if colorTagIndex < len(colorTagIndices) {
+ tagWidth := colorTagIndices[colorTagIndex][1] - colorTagIndices[colorTagIndex][0]
+ if colorTagIndices[colorTagIndex][0] < from {
+ from += tagWidth
+ to += tagWidth
+ colorTagIndex++
+ } else if colorTagIndices[colorTagIndex][0] < to {
+ to += tagWidth
+ colorTagIndex++
+ } else {
+ past++
+ }
+ } else {
+ past++
+ }
+ if escapeIndex < len(escapeIndices) {
+ tagWidth := escapeIndices[escapeIndex][1] - escapeIndices[escapeIndex][0]
+ if escapeIndices[escapeIndex][0] < from {
+ from += tagWidth
+ to += tagWidth
+ escapeIndex++
+ } else if escapeIndices[escapeIndex][0] < to {
+ to += tagWidth
+ escapeIndex++
+ } else {
+ past++
+ }
+ } else {
+ past++
+ }
+ if past == 2 {
+ break // All other indices are beyond the requested string.
+ }
+ }
+ lines = append(lines, text[from:to])
+ }
+
+ // Determine final breakpoints.
+ var start, lastEnd, newStart, breakPoint int
+ for {
+ // What's our candidate string?
+ var candidate string
+ if breakPoint < len(breakPoints) {
+ candidate = text[start:breakPoints[breakPoint][1]]
+ } else {
+ candidate = text[start:]
+ }
+ candidate = strings.TrimRightFunc(candidate, unicode.IsSpace)
+
+ if runewidth.StringWidth(candidate) >= width {
+ // We're past the available width.
+ if lastEnd > start {
+ // Use the previous candidate.
+ addLine(start, lastEnd)
+ start = newStart
+ } else {
+ // We have no previous candidate. Make a hard break.
+ var lineWidth int
+ for index, ch := range text {
+ if index < start {
+ continue
+ }
+ chWidth := runewidth.RuneWidth(ch)
+ if lineWidth > 0 && lineWidth+chWidth >= width {
+ addLine(start, index)
+ start = index
+ break
+ }
+ lineWidth += chWidth
+ }
+ }
+ } else {
+ // We haven't hit the right border yet.
+ if breakPoint >= len(breakPoints) {
+ // It's the last line. We're done.
+ if len(candidate) > 0 {
+ addLine(start, len(strippedText))
+ }
+ break
+ } else {
+ // We have a new candidate.
+ lastEnd = start + len(candidate)
+ newStart = breakPoints[breakPoint][1]
+ breakPoint++
+ }
+ }
+ }
+
+ return
+}
+
+// PrintJoinedBorder prints a border graphics rune into the screen at the given
+// position with the given color, joining it with any existing border graphics
+// rune. Background colors are preserved. At this point, only regular single
+// line borders are supported.
+func PrintJoinedBorder(screen tcell.Screen, x, y int, ch rune, color tcell.Color) {
+ previous, _, style, _ := screen.GetContent(x, y)
+ style = style.Foreground(color)
+
+ // What's the resulting rune?
+ var result rune
+ if ch == previous {
+ result = ch
+ } else {
+ if ch < previous {
+ previous, ch = ch, previous
+ }
+ result = joints[string(previous)+string(ch)]
+ }
+ if result == 0 {
+ result = ch
+ }
+
+ // We only print something if we have something.
+ screen.SetContent(x, y, result, nil, style)
+}