mirror of
https://github.com/cloudflare/cloudflared.git
synced 2025-07-27 22:39:57 +00:00
TUN-3201: Create base cloudflared UI structure
This commit is contained in:

committed by
Areg Harutyunyan

parent
0708a49848
commit
d8ebde37ca
73
vendor/github.com/rivo/tview/CODE_OF_CONDUCT.md
generated
vendored
Normal file
73
vendor/github.com/rivo/tview/CODE_OF_CONDUCT.md
generated
vendored
Normal file
@@ -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
|
35
vendor/github.com/rivo/tview/CONTRIBUTING.md
generated
vendored
Normal file
35
vendor/github.com/rivo/tview/CONTRIBUTING.md
generated
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
# 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
|
||||
|
||||
In my limited time I can spend on this project, I will always go through issues first before looking at pull requests. It takes a _lot_ of time to look at code that you submitted and I may not have that time. So be prepared to have your pull requests lying around for a long time.
|
||||
|
||||
Therefore, if you have a feature request, open an issue first before sending me a pull request, and allow for some discussion. 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. Start all sentences upper-case, as is common in English, and end them with a period.
|
||||
- A new function should be located close to related functions in the file. For example, `GetColor()` should come after (or before) `SetColor()`.
|
||||
- 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.
|
||||
- If an issue accompanies your pull request, reference it in the PR's comments, e.g. "Fixes #123", so it is closed automatically when the PR is closed.
|
21
vendor/github.com/rivo/tview/LICENSE.txt
generated
vendored
Normal file
21
vendor/github.com/rivo/tview/LICENSE.txt
generated
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2018 Oliver Kuederle
|
||||
|
||||
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.
|
62
vendor/github.com/rivo/tview/README.md
generated
vendored
Normal file
62
vendor/github.com/rivo/tview/README.md
generated
vendored
Normal file
@@ -0,0 +1,62 @@
|
||||
# Rich Interactive Widgets for Terminal UIs
|
||||
|
||||
[](https://pkg.go.dev/github.com/rivo/tview)
|
||||
[](https://goreportcard.com/report/github.com/rivo/tview)
|
||||
|
||||
This Go package provides commonly needed components for terminal based user interfaces.
|
||||
|
||||

|
||||
|
||||
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__
|
||||
- Flexible __tree 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://pkg.go.dev/github.com/rivo/tview for the package's documentation.
|
||||
|
||||
## Dependencies
|
||||
|
||||
This package is based on [github.com/gdamore/tcell](https://github.com/gdamore/tcell) (and its dependencies) as well as on [github.com/rivo/uniseg](https://github.com/rivo/uniseg).
|
||||
|
||||
## Your Feedback
|
||||
|
||||
Add your issue here on GitHub. Feel free to get in touch if you have any questions.
|
258
vendor/github.com/rivo/tview/ansi.go
generated
vendored
Normal file
258
vendor/github.com/rivo/tview/ansi.go
generated
vendored
Normal file
@@ -0,0 +1,258 @@
|
||||
package tview
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// The states of the ANSI escape code parser.
|
||||
const (
|
||||
ansiText = iota
|
||||
ansiEscape
|
||||
ansiSubstring
|
||||
ansiControlSequence
|
||||
)
|
||||
|
||||
// ansi is a io.Writer which translates ANSI escape codes into tview color
|
||||
// tags.
|
||||
type ansi struct {
|
||||
io.Writer
|
||||
|
||||
// Reusable buffers.
|
||||
buffer *bytes.Buffer // The entire output text of one Write().
|
||||
csiParameter, csiIntermediate *bytes.Buffer // Partial CSI strings.
|
||||
attributes string // The buffer's current text attributes (a tview attribute string).
|
||||
|
||||
// The current state of the parser. One of the ansi constants.
|
||||
state int
|
||||
}
|
||||
|
||||
// ANSIWriter returns an io.Writer which translates any ANSI escape codes
|
||||
// written to it into tview color tags. Other escape codes don't have an effect
|
||||
// and are simply removed. The translated text is written to the provided
|
||||
// writer.
|
||||
func ANSIWriter(writer io.Writer) io.Writer {
|
||||
return &ansi{
|
||||
Writer: writer,
|
||||
buffer: new(bytes.Buffer),
|
||||
csiParameter: new(bytes.Buffer),
|
||||
csiIntermediate: new(bytes.Buffer),
|
||||
state: ansiText,
|
||||
}
|
||||
}
|
||||
|
||||
// Write parses the given text as a string of runes, translates ANSI escape
|
||||
// codes to color tags and writes them to the output writer.
|
||||
func (a *ansi) Write(text []byte) (int, error) {
|
||||
defer func() {
|
||||
a.buffer.Reset()
|
||||
}()
|
||||
|
||||
for _, r := range string(text) {
|
||||
switch a.state {
|
||||
|
||||
// We just entered an escape sequence.
|
||||
case ansiEscape:
|
||||
switch r {
|
||||
case '[': // Control Sequence Introducer.
|
||||
a.csiParameter.Reset()
|
||||
a.csiIntermediate.Reset()
|
||||
a.state = ansiControlSequence
|
||||
case 'c': // Reset.
|
||||
fmt.Fprint(a.buffer, "[-:-:-]")
|
||||
a.state = ansiText
|
||||
case 'P', ']', 'X', '^', '_': // Substrings and commands.
|
||||
a.state = ansiSubstring
|
||||
default: // Ignore.
|
||||
a.state = ansiText
|
||||
}
|
||||
|
||||
// CSI Sequences.
|
||||
case ansiControlSequence:
|
||||
switch {
|
||||
case r >= 0x30 && r <= 0x3f: // Parameter bytes.
|
||||
if _, err := a.csiParameter.WriteRune(r); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
case r >= 0x20 && r <= 0x2f: // Intermediate bytes.
|
||||
if _, err := a.csiIntermediate.WriteRune(r); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
case r >= 0x40 && r <= 0x7e: // Final byte.
|
||||
switch r {
|
||||
case 'E': // Next line.
|
||||
count, _ := strconv.Atoi(a.csiParameter.String())
|
||||
if count == 0 {
|
||||
count = 1
|
||||
}
|
||||
fmt.Fprint(a.buffer, strings.Repeat("\n", count))
|
||||
case 'm': // Select Graphic Rendition.
|
||||
var background, foreground string
|
||||
params := a.csiParameter.String()
|
||||
fields := strings.Split(params, ";")
|
||||
if len(params) == 0 || len(fields) == 1 && fields[0] == "0" {
|
||||
// Reset.
|
||||
a.attributes = ""
|
||||
if _, err := a.buffer.WriteString("[-:-:-]"); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
break
|
||||
}
|
||||
lookupColor := func(colorNumber int) string {
|
||||
if colorNumber < 0 || colorNumber > 15 {
|
||||
return "black"
|
||||
}
|
||||
return []string{
|
||||
"black",
|
||||
"maroon",
|
||||
"green",
|
||||
"olive",
|
||||
"navy",
|
||||
"purple",
|
||||
"teal",
|
||||
"silver",
|
||||
"gray",
|
||||
"red",
|
||||
"lime",
|
||||
"yellow",
|
||||
"blue",
|
||||
"fuchsia",
|
||||
"aqua",
|
||||
"white",
|
||||
}[colorNumber]
|
||||
}
|
||||
FieldLoop:
|
||||
for index, field := range fields {
|
||||
switch field {
|
||||
case "1", "01":
|
||||
if strings.IndexRune(a.attributes, 'b') < 0 {
|
||||
a.attributes += "b"
|
||||
}
|
||||
case "2", "02":
|
||||
if strings.IndexRune(a.attributes, 'd') < 0 {
|
||||
a.attributes += "d"
|
||||
}
|
||||
case "4", "04":
|
||||
if strings.IndexRune(a.attributes, 'u') < 0 {
|
||||
a.attributes += "u"
|
||||
}
|
||||
case "5", "05":
|
||||
if strings.IndexRune(a.attributes, 'l') < 0 {
|
||||
a.attributes += "l"
|
||||
}
|
||||
case "22":
|
||||
if i := strings.IndexRune(a.attributes, 'b'); i >= 0 {
|
||||
a.attributes = a.attributes[:i] + a.attributes[i+1:]
|
||||
}
|
||||
if i := strings.IndexRune(a.attributes, 'd'); i >= 0 {
|
||||
a.attributes = a.attributes[:i] + a.attributes[i+1:]
|
||||
}
|
||||
case "24":
|
||||
if i := strings.IndexRune(a.attributes, 'u'); i >= 0 {
|
||||
a.attributes = a.attributes[:i] + a.attributes[i+1:]
|
||||
}
|
||||
case "25":
|
||||
if i := strings.IndexRune(a.attributes, 'l'); i >= 0 {
|
||||
a.attributes = a.attributes[:i] + a.attributes[i+1:]
|
||||
}
|
||||
case "30", "31", "32", "33", "34", "35", "36", "37":
|
||||
colorNumber, _ := strconv.Atoi(field)
|
||||
foreground = lookupColor(colorNumber - 30)
|
||||
case "39":
|
||||
foreground = "-"
|
||||
case "40", "41", "42", "43", "44", "45", "46", "47":
|
||||
colorNumber, _ := strconv.Atoi(field)
|
||||
background = lookupColor(colorNumber - 40)
|
||||
case "49":
|
||||
background = "-"
|
||||
case "90", "91", "92", "93", "94", "95", "96", "97":
|
||||
colorNumber, _ := strconv.Atoi(field)
|
||||
foreground = lookupColor(colorNumber - 82)
|
||||
case "100", "101", "102", "103", "104", "105", "106", "107":
|
||||
colorNumber, _ := strconv.Atoi(field)
|
||||
background = lookupColor(colorNumber - 92)
|
||||
case "38", "48":
|
||||
var color string
|
||||
if len(fields) > index+1 {
|
||||
if fields[index+1] == "5" && len(fields) > index+2 { // 8-bit colors.
|
||||
colorNumber, _ := strconv.Atoi(fields[index+2])
|
||||
if colorNumber <= 15 {
|
||||
color = lookupColor(colorNumber)
|
||||
} else if colorNumber <= 231 {
|
||||
red := (colorNumber - 16) / 36
|
||||
green := ((colorNumber - 16) / 6) % 6
|
||||
blue := (colorNumber - 16) % 6
|
||||
color = fmt.Sprintf("#%02x%02x%02x", 255*red/5, 255*green/5, 255*blue/5)
|
||||
} else if colorNumber <= 255 {
|
||||
grey := 255 * (colorNumber - 232) / 23
|
||||
color = fmt.Sprintf("#%02x%02x%02x", grey, grey, grey)
|
||||
}
|
||||
} else if fields[index+1] == "2" && len(fields) > index+4 { // 24-bit colors.
|
||||
red, _ := strconv.Atoi(fields[index+2])
|
||||
green, _ := strconv.Atoi(fields[index+3])
|
||||
blue, _ := strconv.Atoi(fields[index+4])
|
||||
color = fmt.Sprintf("#%02x%02x%02x", red, green, blue)
|
||||
}
|
||||
}
|
||||
if len(color) > 0 {
|
||||
if field == "38" {
|
||||
foreground = color
|
||||
} else {
|
||||
background = color
|
||||
}
|
||||
}
|
||||
break FieldLoop
|
||||
}
|
||||
}
|
||||
var colon string
|
||||
if len(a.attributes) > 0 {
|
||||
colon = ":"
|
||||
}
|
||||
if len(foreground) > 0 || len(background) > 0 || len(a.attributes) > 0 {
|
||||
fmt.Fprintf(a.buffer, "[%s:%s%s%s]", foreground, background, colon, a.attributes)
|
||||
}
|
||||
}
|
||||
a.state = ansiText
|
||||
default: // Undefined byte.
|
||||
a.state = ansiText // Abort CSI.
|
||||
}
|
||||
|
||||
// We just entered a substring/command sequence.
|
||||
case ansiSubstring:
|
||||
if r == 27 { // Most likely the end of the substring.
|
||||
a.state = ansiEscape
|
||||
} // Ignore all other characters.
|
||||
|
||||
// "ansiText" and all others.
|
||||
default:
|
||||
if r == 27 {
|
||||
// This is the start of an escape sequence.
|
||||
a.state = ansiEscape
|
||||
} else {
|
||||
// Just a regular rune. Send to buffer.
|
||||
if _, err := a.buffer.WriteRune(r); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Write buffer to target writer.
|
||||
n, err := a.buffer.WriteTo(a.Writer)
|
||||
if err != nil {
|
||||
return int(n), err
|
||||
}
|
||||
return len(text), nil
|
||||
}
|
||||
|
||||
// TranslateANSI replaces ANSI escape sequences found in the provided string
|
||||
// with tview's color tags and returns the resulting string.
|
||||
func TranslateANSI(text string) string {
|
||||
var buffer bytes.Buffer
|
||||
writer := ANSIWriter(&buffer)
|
||||
writer.Write([]byte(text))
|
||||
return buffer.String()
|
||||
}
|
728
vendor/github.com/rivo/tview/application.go
generated
vendored
Normal file
728
vendor/github.com/rivo/tview/application.go
generated
vendored
Normal file
@@ -0,0 +1,728 @@
|
||||
package tview
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gdamore/tcell"
|
||||
)
|
||||
|
||||
const (
|
||||
// The size of the event/update/redraw channels.
|
||||
queueSize = 100
|
||||
|
||||
// The minimum time between two consecutive redraws.
|
||||
redrawPause = 50 * time.Millisecond
|
||||
)
|
||||
|
||||
// DoubleClickInterval specifies the maximum time between clicks to register a
|
||||
// double click rather than click.
|
||||
var DoubleClickInterval = 500 * time.Millisecond
|
||||
|
||||
// MouseAction indicates one of the actions the mouse is logically doing.
|
||||
type MouseAction int16
|
||||
|
||||
// Available mouse actions.
|
||||
const (
|
||||
MouseMove MouseAction = iota
|
||||
MouseLeftDown
|
||||
MouseLeftUp
|
||||
MouseLeftClick
|
||||
MouseLeftDoubleClick
|
||||
MouseMiddleDown
|
||||
MouseMiddleUp
|
||||
MouseMiddleClick
|
||||
MouseMiddleDoubleClick
|
||||
MouseRightDown
|
||||
MouseRightUp
|
||||
MouseRightClick
|
||||
MouseRightDoubleClick
|
||||
MouseScrollUp
|
||||
MouseScrollDown
|
||||
MouseScrollLeft
|
||||
MouseScrollRight
|
||||
)
|
||||
|
||||
// queuedUpdate represented the execution of f queued by
|
||||
// Application.QueueUpdate(). The "done" channel receives exactly one element
|
||||
// after f has executed.
|
||||
type queuedUpdate struct {
|
||||
f func()
|
||||
done chan struct{}
|
||||
}
|
||||
|
||||
// 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.
|
||||
//
|
||||
// The following command displays a primitive p on the screen until Ctrl-C is
|
||||
// pressed:
|
||||
//
|
||||
// if err := tview.NewApplication().SetRoot(p, true).Run(); err != nil {
|
||||
// panic(err)
|
||||
// }
|
||||
type Application struct {
|
||||
sync.RWMutex
|
||||
|
||||
// The application's screen. Apart from Run(), this variable should never be
|
||||
// set directly. Always use the screenReplacement channel after calling
|
||||
// Fini(), to set a new screen (or nil to stop the application).
|
||||
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
|
||||
|
||||
// Set to true if mouse events are enabled.
|
||||
enableMouse 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 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)
|
||||
|
||||
// Used to send screen events from separate goroutine to main event loop
|
||||
events chan tcell.Event
|
||||
|
||||
// Functions queued from goroutines, used to serialize updates to primitives.
|
||||
updates chan queuedUpdate
|
||||
|
||||
// An object that the screen variable will be set to after Fini() was called.
|
||||
// Use this channel to set a new screen object for the application
|
||||
// (screen.Init() and draw() will be called implicitly). A value of nil will
|
||||
// stop the application.
|
||||
screenReplacement chan tcell.Screen
|
||||
|
||||
// An optional capture function which receives a mouse event and returns the
|
||||
// event to be forwarded to the default mouse handler (nil if nothing should
|
||||
// be forwarded).
|
||||
mouseCapture func(event *tcell.EventMouse, action MouseAction) (*tcell.EventMouse, MouseAction)
|
||||
|
||||
mouseCapturingPrimitive Primitive // A Primitive returned by a MouseHandler which will capture future mouse events.
|
||||
lastMouseX, lastMouseY int // The last position of the mouse.
|
||||
mouseDownX, mouseDownY int // The position of the mouse when its button was last pressed.
|
||||
lastMouseClick time.Time // The time when a mouse button was last clicked.
|
||||
lastMouseButtons tcell.ButtonMask // The last mouse button state.
|
||||
}
|
||||
|
||||
// NewApplication creates and returns a new application.
|
||||
func NewApplication() *Application {
|
||||
return &Application{
|
||||
events: make(chan tcell.Event, queueSize),
|
||||
updates: make(chan queuedUpdate, queueSize),
|
||||
screenReplacement: make(chan tcell.Screen, 1),
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
// application.
|
||||
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 mouse events (consisting of
|
||||
// the original tcell mouse event and the semantic mouse action) before they are
|
||||
// forwarded to the appropriate mouse event handler. This function can then
|
||||
// choose to forward that event (or a different one) by returning it or stop
|
||||
// the event processing by returning a nil mouse event.
|
||||
func (a *Application) SetMouseCapture(capture func(event *tcell.EventMouse, action MouseAction) (*tcell.EventMouse, MouseAction)) *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, action MouseAction) (*tcell.EventMouse, MouseAction) {
|
||||
return a.mouseCapture
|
||||
}
|
||||
|
||||
// SetScreen allows you to provide your own tcell.Screen object. For most
|
||||
// applications, this is not needed and you should be familiar with
|
||||
// tcell.Screen when using this function.
|
||||
//
|
||||
// This function is typically called before the first call to Run(). Init() need
|
||||
// not be called on the screen.
|
||||
func (a *Application) SetScreen(screen tcell.Screen) *Application {
|
||||
if screen == nil {
|
||||
return a // Invalid input. Do nothing.
|
||||
}
|
||||
|
||||
a.Lock()
|
||||
if a.screen == nil {
|
||||
// Run() has not been called yet.
|
||||
a.screen = screen
|
||||
a.Unlock()
|
||||
return a
|
||||
}
|
||||
|
||||
// Run() is already in progress. Exchange screen.
|
||||
oldScreen := a.screen
|
||||
a.Unlock()
|
||||
oldScreen.Fini()
|
||||
a.screenReplacement <- screen
|
||||
|
||||
return a
|
||||
}
|
||||
|
||||
// EnableMouse enables mouse events.
|
||||
func (a *Application) EnableMouse(enable bool) *Application {
|
||||
a.Lock()
|
||||
defer a.Unlock()
|
||||
if enable != a.enableMouse && a.screen != nil {
|
||||
if enable {
|
||||
a.screen.EnableMouse()
|
||||
} else {
|
||||
a.screen.DisableMouse()
|
||||
}
|
||||
}
|
||||
a.enableMouse = enable
|
||||
return a
|
||||
}
|
||||
|
||||
// Run starts the application and thus the event loop. This function returns
|
||||
// when Stop() was called.
|
||||
func (a *Application) Run() error {
|
||||
var (
|
||||
err error
|
||||
width, height int // The current size of the screen.
|
||||
lastRedraw time.Time // The time the screen was last redrawn.
|
||||
redrawTimer *time.Timer // A timer to schedule the next redraw.
|
||||
)
|
||||
a.Lock()
|
||||
|
||||
// Make a screen if there is none yet.
|
||||
if a.screen == nil {
|
||||
a.screen, err = tcell.NewScreen()
|
||||
if err != nil {
|
||||
a.Unlock()
|
||||
return err
|
||||
}
|
||||
if err = a.screen.Init(); err != nil {
|
||||
a.Unlock()
|
||||
return err
|
||||
}
|
||||
if a.enableMouse {
|
||||
a.screen.EnableMouse()
|
||||
}
|
||||
}
|
||||
|
||||
// We catch panics to clean up because they mess up the terminal.
|
||||
defer func() {
|
||||
if p := recover(); p != nil {
|
||||
if a.screen != nil {
|
||||
a.screen.Fini()
|
||||
}
|
||||
panic(p)
|
||||
}
|
||||
}()
|
||||
|
||||
// Draw the screen for the first time.
|
||||
a.Unlock()
|
||||
a.draw()
|
||||
|
||||
// Separate loop to wait for screen events.
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for {
|
||||
a.RLock()
|
||||
screen := a.screen
|
||||
a.RUnlock()
|
||||
if screen == nil {
|
||||
// We have no screen. Let's stop.
|
||||
a.QueueEvent(nil)
|
||||
break
|
||||
}
|
||||
|
||||
// Wait for next event and queue it.
|
||||
event := screen.PollEvent()
|
||||
if event != nil {
|
||||
// Regular event. Queue.
|
||||
a.QueueEvent(event)
|
||||
continue
|
||||
}
|
||||
|
||||
// A screen was finalized (event is nil). Wait for a new scren.
|
||||
screen = <-a.screenReplacement
|
||||
if screen == nil {
|
||||
// No new screen. We're done.
|
||||
a.QueueEvent(nil)
|
||||
return
|
||||
}
|
||||
|
||||
// We have a new screen. Keep going.
|
||||
a.Lock()
|
||||
a.screen = screen
|
||||
a.Unlock()
|
||||
|
||||
// Initialize and draw this screen.
|
||||
if err := screen.Init(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
a.draw()
|
||||
}
|
||||
}()
|
||||
|
||||
// Start event loop.
|
||||
EventLoop:
|
||||
for {
|
||||
select {
|
||||
case event := <-a.events:
|
||||
if event == nil {
|
||||
break EventLoop
|
||||
}
|
||||
|
||||
switch event := event.(type) {
|
||||
case *tcell.EventKey:
|
||||
a.RLock()
|
||||
p := a.focus
|
||||
inputCapture := a.inputCapture
|
||||
a.RUnlock()
|
||||
|
||||
// Intercept keys.
|
||||
if inputCapture != nil {
|
||||
event = inputCapture(event)
|
||||
if event == nil {
|
||||
a.draw()
|
||||
continue // Don't forward event.
|
||||
}
|
||||
}
|
||||
|
||||
// Ctrl-C closes the application.
|
||||
if event.Key() == tcell.KeyCtrlC {
|
||||
a.Stop()
|
||||
}
|
||||
|
||||
// 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.EventResize:
|
||||
if time.Since(lastRedraw) < redrawPause {
|
||||
if redrawTimer != nil {
|
||||
redrawTimer.Stop()
|
||||
}
|
||||
redrawTimer = time.AfterFunc(redrawPause, func() {
|
||||
a.events <- event
|
||||
})
|
||||
}
|
||||
a.RLock()
|
||||
screen := a.screen
|
||||
a.RUnlock()
|
||||
if screen == nil {
|
||||
continue
|
||||
}
|
||||
newWidth, newHeight := screen.Size()
|
||||
if newWidth == width && newHeight == height {
|
||||
continue
|
||||
}
|
||||
width, height = newWidth, newHeight
|
||||
lastRedraw = time.Now()
|
||||
screen.Clear()
|
||||
a.draw()
|
||||
case *tcell.EventMouse:
|
||||
consumed, isMouseDownAction := a.fireMouseActions(event)
|
||||
if consumed {
|
||||
a.draw()
|
||||
}
|
||||
a.lastMouseButtons = event.Buttons()
|
||||
if isMouseDownAction {
|
||||
a.mouseDownX, a.mouseDownY = event.Position()
|
||||
}
|
||||
}
|
||||
|
||||
// If we have updates, now is the time to execute them.
|
||||
case update := <-a.updates:
|
||||
update.f()
|
||||
update.done <- struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for the event loop to finish.
|
||||
wg.Wait()
|
||||
a.screen = nil
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// fireMouseActions analyzes the provided mouse event, derives mouse actions
|
||||
// from it and then forwards them to the corresponding primitives.
|
||||
func (a *Application) fireMouseActions(event *tcell.EventMouse) (consumed, isMouseDownAction bool) {
|
||||
// We want to relay follow-up events to the same target primitive.
|
||||
var targetPrimitive Primitive
|
||||
|
||||
// Helper function to fire a mouse action.
|
||||
fire := func(action MouseAction) {
|
||||
switch action {
|
||||
case MouseLeftDown, MouseMiddleDown, MouseRightDown:
|
||||
isMouseDownAction = true
|
||||
}
|
||||
|
||||
// Intercept event.
|
||||
if a.mouseCapture != nil {
|
||||
event, action = a.mouseCapture(event, action)
|
||||
if event == nil {
|
||||
consumed = true
|
||||
return // Don't forward event.
|
||||
}
|
||||
}
|
||||
|
||||
// Determine the target primitive.
|
||||
var primitive, capturingPrimitive Primitive
|
||||
if a.mouseCapturingPrimitive != nil {
|
||||
primitive = a.mouseCapturingPrimitive
|
||||
targetPrimitive = a.mouseCapturingPrimitive
|
||||
} else if targetPrimitive != nil {
|
||||
primitive = targetPrimitive
|
||||
} else {
|
||||
primitive = a.root
|
||||
}
|
||||
if primitive != nil {
|
||||
if handler := primitive.MouseHandler(); handler != nil {
|
||||
var wasConsumed bool
|
||||
wasConsumed, capturingPrimitive = handler(action, event, func(p Primitive) {
|
||||
a.SetFocus(p)
|
||||
})
|
||||
if wasConsumed {
|
||||
consumed = true
|
||||
}
|
||||
}
|
||||
}
|
||||
a.mouseCapturingPrimitive = capturingPrimitive
|
||||
}
|
||||
|
||||
x, y := event.Position()
|
||||
buttons := event.Buttons()
|
||||
clickMoved := x != a.mouseDownX || y != a.mouseDownY
|
||||
buttonChanges := buttons ^ a.lastMouseButtons
|
||||
|
||||
if x != a.lastMouseX || y != a.lastMouseY {
|
||||
fire(MouseMove)
|
||||
a.lastMouseX = x
|
||||
a.lastMouseY = y
|
||||
}
|
||||
|
||||
for _, buttonEvent := range []struct {
|
||||
button tcell.ButtonMask
|
||||
down, up, click, dclick MouseAction
|
||||
}{
|
||||
{tcell.Button1, MouseLeftDown, MouseLeftUp, MouseLeftClick, MouseLeftDoubleClick},
|
||||
{tcell.Button2, MouseMiddleDown, MouseMiddleUp, MouseMiddleClick, MouseMiddleDoubleClick},
|
||||
{tcell.Button3, MouseRightDown, MouseRightUp, MouseRightClick, MouseRightDoubleClick},
|
||||
} {
|
||||
if buttonChanges&buttonEvent.button != 0 {
|
||||
if buttons&buttonEvent.button != 0 {
|
||||
fire(buttonEvent.down)
|
||||
} else {
|
||||
fire(buttonEvent.up)
|
||||
if !clickMoved {
|
||||
if a.lastMouseClick.Add(DoubleClickInterval).Before(time.Now()) {
|
||||
fire(buttonEvent.click)
|
||||
a.lastMouseClick = time.Now()
|
||||
} else {
|
||||
fire(buttonEvent.dclick)
|
||||
a.lastMouseClick = time.Time{} // reset
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, wheelEvent := range []struct {
|
||||
button tcell.ButtonMask
|
||||
action MouseAction
|
||||
}{
|
||||
{tcell.WheelUp, MouseScrollUp},
|
||||
{tcell.WheelDown, MouseScrollDown},
|
||||
{tcell.WheelLeft, MouseScrollLeft},
|
||||
{tcell.WheelRight, MouseScrollRight}} {
|
||||
if buttons&wheelEvent.button != 0 {
|
||||
fire(wheelEvent.action)
|
||||
}
|
||||
}
|
||||
|
||||
return consumed, isMouseDownAction
|
||||
}
|
||||
|
||||
// Stop stops the application, causing Run() to return.
|
||||
func (a *Application) Stop() {
|
||||
a.Lock()
|
||||
defer a.Unlock()
|
||||
screen := a.screen
|
||||
if screen == nil {
|
||||
return
|
||||
}
|
||||
a.screen = nil
|
||||
screen.Fini()
|
||||
a.screenReplacement <- 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.RLock()
|
||||
screen := a.screen
|
||||
a.RUnlock()
|
||||
if screen == nil {
|
||||
return false // Screen has not yet been initialized.
|
||||
}
|
||||
|
||||
// Enter suspended mode.
|
||||
screen.Fini()
|
||||
|
||||
// Wait for "f" to return.
|
||||
f()
|
||||
|
||||
// Make a new screen.
|
||||
var err error
|
||||
screen, err = tcell.NewScreen()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
a.screenReplacement <- screen
|
||||
// One key event will get lost, see https://github.com/gdamore/tcell/issues/194
|
||||
|
||||
// Continue application loop.
|
||||
return true
|
||||
}
|
||||
|
||||
// Draw refreshes the screen (during the next update cycle). It calls the Draw()
|
||||
// function of the application's root primitive and then syncs the screen
|
||||
// buffer. It is almost never necessary to call this function. Please see
|
||||
// https://github.com/rivo/tview/wiki/Concurrency for details.
|
||||
func (a *Application) Draw() *Application {
|
||||
a.QueueUpdate(func() {
|
||||
a.draw()
|
||||
})
|
||||
return a
|
||||
}
|
||||
|
||||
// ForceDraw refreshes the screen immediately. Use this function with caution as
|
||||
// it may lead to race conditions with updates to primitives in other
|
||||
// goroutines. It is always preferrable to use Draw() instead. Never call this
|
||||
// function from a goroutine.
|
||||
//
|
||||
// It is safe to call this function during queued updates and direct event
|
||||
// handling.
|
||||
func (a *Application) ForceDraw() *Application {
|
||||
return a.draw()
|
||||
}
|
||||
|
||||
// draw actually does what Draw() promises to do.
|
||||
func (a *Application) draw() *Application {
|
||||
a.Lock()
|
||||
defer a.Unlock()
|
||||
|
||||
screen := a.screen
|
||||
root := a.root
|
||||
fullscreen := a.rootFullscreen
|
||||
before := a.beforeDraw
|
||||
after := a.afterDraw
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// QueueUpdate is used to synchronize access to primitives from non-main
|
||||
// goroutines. The provided function will be executed as part of the event loop
|
||||
// and thus will not cause race conditions with other such update functions or
|
||||
// the Draw() function.
|
||||
//
|
||||
// Note that Draw() is not implicitly called after the execution of f as that
|
||||
// may not be desirable. You can call Draw() from f if the screen should be
|
||||
// refreshed after each update. Alternatively, use QueueUpdateDraw() to follow
|
||||
// up with an immediate refresh of the screen.
|
||||
//
|
||||
// This function returns after f has executed.
|
||||
func (a *Application) QueueUpdate(f func()) *Application {
|
||||
ch := make(chan struct{})
|
||||
a.updates <- queuedUpdate{f: f, done: ch}
|
||||
<-ch
|
||||
return a
|
||||
}
|
||||
|
||||
// QueueUpdateDraw works like QueueUpdate() except it refreshes the screen
|
||||
// immediately after executing f.
|
||||
func (a *Application) QueueUpdateDraw(f func()) *Application {
|
||||
a.QueueUpdate(func() {
|
||||
f()
|
||||
a.draw()
|
||||
})
|
||||
return a
|
||||
}
|
||||
|
||||
// QueueEvent sends an event to the Application event loop.
|
||||
//
|
||||
// It is not recommended for event to be nil.
|
||||
func (a *Application) QueueEvent(event tcell.Event) *Application {
|
||||
a.events <- event
|
||||
return a
|
||||
}
|
45
vendor/github.com/rivo/tview/borders.go
generated
vendored
Normal file
45
vendor/github.com/rivo/tview/borders.go
generated
vendored
Normal file
@@ -0,0 +1,45 @@
|
||||
package tview
|
||||
|
||||
// Borders defines various borders used when primitives are drawn.
|
||||
// These may be changed to accommodate a different look and feel.
|
||||
var Borders = struct {
|
||||
Horizontal rune
|
||||
Vertical rune
|
||||
TopLeft rune
|
||||
TopRight rune
|
||||
BottomLeft rune
|
||||
BottomRight rune
|
||||
|
||||
LeftT rune
|
||||
RightT rune
|
||||
TopT rune
|
||||
BottomT rune
|
||||
Cross rune
|
||||
|
||||
HorizontalFocus rune
|
||||
VerticalFocus rune
|
||||
TopLeftFocus rune
|
||||
TopRightFocus rune
|
||||
BottomLeftFocus rune
|
||||
BottomRightFocus rune
|
||||
}{
|
||||
Horizontal: BoxDrawingsLightHorizontal,
|
||||
Vertical: BoxDrawingsLightVertical,
|
||||
TopLeft: BoxDrawingsLightDownAndRight,
|
||||
TopRight: BoxDrawingsLightDownAndLeft,
|
||||
BottomLeft: BoxDrawingsLightUpAndRight,
|
||||
BottomRight: BoxDrawingsLightUpAndLeft,
|
||||
|
||||
LeftT: BoxDrawingsLightVerticalAndRight,
|
||||
RightT: BoxDrawingsLightVerticalAndLeft,
|
||||
TopT: BoxDrawingsLightDownAndHorizontal,
|
||||
BottomT: BoxDrawingsLightUpAndHorizontal,
|
||||
Cross: BoxDrawingsLightVerticalAndHorizontal,
|
||||
|
||||
HorizontalFocus: BoxDrawingsDoubleHorizontal,
|
||||
VerticalFocus: BoxDrawingsDoubleVertical,
|
||||
TopLeftFocus: BoxDrawingsDoubleDownAndRight,
|
||||
TopRightFocus: BoxDrawingsDoubleDownAndLeft,
|
||||
BottomLeftFocus: BoxDrawingsDoubleUpAndRight,
|
||||
BottomRightFocus: BoxDrawingsDoubleUpAndLeft,
|
||||
}
|
412
vendor/github.com/rivo/tview/box.go
generated
vendored
Normal file
412
vendor/github.com/rivo/tview/box.go
generated
vendored
Normal file
@@ -0,0 +1,412 @@
|
||||
package tview
|
||||
|
||||
import (
|
||||
"github.com/gdamore/tcell"
|
||||
)
|
||||
|
||||
// Box implements the Primitive interface with an empty background and optional
|
||||
// elements such as a border and a title. Box itself does not hold any content
|
||||
// but serves as the superclass of all other primitives. Subclasses add their
|
||||
// own content, typically (but not necessarily) keeping their content within the
|
||||
// box's rectangle.
|
||||
//
|
||||
// Box provides a number of utility functions available to all primitives.
|
||||
//
|
||||
// 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 style attributes of the border.
|
||||
borderAttributes tcell.AttrMask
|
||||
|
||||
// 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
|
||||
|
||||
// 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 function which is called before the box is drawn.
|
||||
draw func(screen tcell.Screen, x, y, width, height int) (int, int, int, int)
|
||||
|
||||
// An optional capture function which receives a mouse event and returns the
|
||||
// event to be forwarded to the primitive's default mouse event handler (at
|
||||
// least one nil if nothing should be forwarded).
|
||||
mouseCapture func(action MouseAction, event *tcell.EventMouse) (MouseAction, *tcell.EventMouse)
|
||||
}
|
||||
|
||||
// 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,
|
||||
}
|
||||
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. Width and height values
|
||||
// will clamp to 0 and thus never be negative.
|
||||
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
|
||||
}
|
||||
x, y, width, height = x+b.paddingLeft,
|
||||
y+b.paddingTop,
|
||||
width-b.paddingLeft-b.paddingRight,
|
||||
height-b.paddingTop-b.paddingBottom
|
||||
if width < 0 {
|
||||
width = 0
|
||||
}
|
||||
if height < 0 {
|
||||
height = 0
|
||||
}
|
||||
return x, y, width, height
|
||||
}
|
||||
|
||||
// SetRect sets a new position of the primitive. Note that this has no effect
|
||||
// if this primitive is part of a layout (e.g. Flex, Grid) or if it was added
|
||||
// like this:
|
||||
//
|
||||
// application.SetRoot(b, true)
|
||||
func (b *Box) SetRect(x, y, width, height int) {
|
||||
b.x = x
|
||||
b.y = y
|
||||
b.width = width
|
||||
b.height = height
|
||||
b.innerX = -1 // Mark inner rect as uninitialized.
|
||||
}
|
||||
|
||||
// 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.
|
||||
//
|
||||
// Note that this function will not have an effect on primitives composed of
|
||||
// other primitives, such as Form, Flex, or Grid. Key events are only captured
|
||||
// by the primitives that have focus (e.g. InputField) and only one primitive
|
||||
// can have focus at a time. Composing primitives such as Form pass the focus on
|
||||
// to their contained primitives and thus never receive any key events
|
||||
// themselves. Therefore, they cannot intercept key events.
|
||||
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 event handler (see MouseHandler()) with the
|
||||
// functionality to capture mouse events (see SetMouseCapture()) before passing
|
||||
// them on to the provided (default) event handler.
|
||||
//
|
||||
// This is only meant to be used by subclassing primitives.
|
||||
func (b *Box) WrapMouseHandler(mouseHandler func(MouseAction, *tcell.EventMouse, func(p Primitive)) (bool, Primitive)) func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
|
||||
return func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
|
||||
if b.mouseCapture != nil {
|
||||
action, event = b.mouseCapture(action, event)
|
||||
}
|
||||
if event != nil && mouseHandler != nil {
|
||||
consumed, capture = mouseHandler(action, event, setFocus)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// MouseHandler returns nil.
|
||||
func (b *Box) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
|
||||
return b.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
|
||||
if action == MouseLeftClick && b.InRect(event.Position()) {
|
||||
setFocus(b)
|
||||
consumed = true
|
||||
}
|
||||
return
|
||||
})
|
||||
}
|
||||
|
||||
// SetMouseCapture sets a function which captures mouse events (consisting of
|
||||
// the original tcell mouse event and the semantic mouse action) before they are
|
||||
// forwarded to the primitive's default mouse event handler. This function can
|
||||
// then choose to forward that event (or a different one) by returning it or
|
||||
// returning a nil mouse event, in which case the default handler will not be
|
||||
// called.
|
||||
//
|
||||
// Providing a nil handler will remove a previously existing handler.
|
||||
func (b *Box) SetMouseCapture(capture func(action MouseAction, event *tcell.EventMouse) (MouseAction, *tcell.EventMouse)) *Box {
|
||||
b.mouseCapture = capture
|
||||
return b
|
||||
}
|
||||
|
||||
// InRect returns true if the given coordinate is within the bounds of the box's
|
||||
// rectangle.
|
||||
func (b *Box) InRect(x, y int) bool {
|
||||
rectX, rectY, width, height := b.GetRect()
|
||||
return x >= rectX && x < rectX+width && y >= rectY && y < rectY+height
|
||||
}
|
||||
|
||||
// GetMouseCapture returns the function installed with SetMouseCapture() or nil
|
||||
// if no such function has been installed.
|
||||
func (b *Box) GetMouseCapture() func(action MouseAction, event *tcell.EventMouse) (MouseAction, *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
|
||||
}
|
||||
|
||||
// SetBorderColor sets the box's border color.
|
||||
func (b *Box) SetBorderColor(color tcell.Color) *Box {
|
||||
b.borderColor = color
|
||||
return b
|
||||
}
|
||||
|
||||
// SetBorderAttributes sets the border's style attributes. You can combine
|
||||
// different attributes using bitmask operations:
|
||||
//
|
||||
// box.SetBorderAttributes(tcell.AttrUnderline | tcell.AttrBold)
|
||||
func (b *Box) SetBorderAttributes(attr tcell.AttrMask) *Box {
|
||||
b.borderAttributes = attr
|
||||
return b
|
||||
}
|
||||
|
||||
// GetBorderAttributes returns the border's style attributes.
|
||||
func (b *Box) GetBorderAttributes() tcell.AttrMask {
|
||||
return b.borderAttributes
|
||||
}
|
||||
|
||||
// GetBorderColor returns the box's border color.
|
||||
func (b *Box) GetBorderColor() tcell.Color {
|
||||
return b.borderColor
|
||||
}
|
||||
|
||||
// GetBackgroundColor returns the box's background color.
|
||||
func (b *Box) GetBackgroundColor() tcell.Color {
|
||||
return b.backgroundColor
|
||||
}
|
||||
|
||||
// SetTitle sets the box's title.
|
||||
func (b *Box) SetTitle(title string) *Box {
|
||||
b.title = title
|
||||
return b
|
||||
}
|
||||
|
||||
// GetTitle returns the box's current title.
|
||||
func (b *Box) GetTitle() string {
|
||||
return b.title
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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)
|
||||
if b.backgroundColor != tcell.ColorDefault {
|
||||
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) | tcell.Style(b.borderAttributes)
|
||||
var vertical, horizontal, topLeft, topRight, bottomLeft, bottomRight rune
|
||||
if b.focus.HasFocus() {
|
||||
horizontal = Borders.HorizontalFocus
|
||||
vertical = Borders.VerticalFocus
|
||||
topLeft = Borders.TopLeftFocus
|
||||
topRight = Borders.TopRightFocus
|
||||
bottomLeft = Borders.BottomLeftFocus
|
||||
bottomRight = Borders.BottomRightFocus
|
||||
} else {
|
||||
horizontal = Borders.Horizontal
|
||||
vertical = Borders.Vertical
|
||||
topLeft = Borders.TopLeft
|
||||
topRight = Borders.TopRight
|
||||
bottomLeft = Borders.BottomLeft
|
||||
bottomRight = Borders.BottomRight
|
||||
}
|
||||
for x := b.x + 1; x < b.x+b.width-1; x++ {
|
||||
screen.SetContent(x, b.y, horizontal, nil, border)
|
||||
screen.SetContent(x, b.y+b.height-1, horizontal, nil, border)
|
||||
}
|
||||
for y := b.y + 1; y < b.y+b.height-1; y++ {
|
||||
screen.SetContent(b.x, y, vertical, nil, border)
|
||||
screen.SetContent(b.x+b.width-1, y, vertical, 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 len(b.title)-printed > 0 && printed > 0 {
|
||||
_, _, style, _ := screen.GetContent(b.x+b.width-2, b.y)
|
||||
fg, _, _ := style.Decompose()
|
||||
Print(screen, string(SemigraphicsHorizontalEllipsis), 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()
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
157
vendor/github.com/rivo/tview/button.go
generated
vendored
Normal file
157
vendor/github.com/rivo/tview/button.go
generated
vendored
Normal file
@@ -0,0 +1,157 @@
|
||||
package tview
|
||||
|
||||
import (
|
||||
"github.com/gdamore/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, TaggedStringWidth(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)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// MouseHandler returns the mouse handler for this primitive.
|
||||
func (b *Button) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
|
||||
return b.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
|
||||
if !b.InRect(event.Position()) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Process mouse event.
|
||||
if action == MouseLeftClick {
|
||||
setFocus(b)
|
||||
if b.selected != nil {
|
||||
b.selected()
|
||||
}
|
||||
consumed = true
|
||||
}
|
||||
|
||||
return
|
||||
})
|
||||
}
|
226
vendor/github.com/rivo/tview/checkbox.go
generated
vendored
Normal file
226
vendor/github.com/rivo/tview/checkbox.go
generated
vendored
Normal file
@@ -0,0 +1,226 @@
|
||||
package tview
|
||||
|
||||
import (
|
||||
"github.com/gdamore/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 screen width of the label area. A value of 0 means use the width of
|
||||
// the label text.
|
||||
labelWidth 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
|
||||
|
||||
// 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)
|
||||
|
||||
// A callback function set by the Form class and called when the user leaves
|
||||
// this form item.
|
||||
finished 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
|
||||
}
|
||||
|
||||
// SetLabelWidth sets the screen width of the label. A value of 0 will cause the
|
||||
// primitive to use the width of the label string.
|
||||
func (c *Checkbox) SetLabelWidth(width int) *Checkbox {
|
||||
c.labelWidth = width
|
||||
return c
|
||||
}
|
||||
|
||||
// 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(labelWidth int, labelColor, bgColor, fieldTextColor, fieldBgColor tcell.Color) FormItem {
|
||||
c.labelWidth = labelWidth
|
||||
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 using the
|
||||
// checkbox. 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 sets a callback invoked when the user leaves this form item.
|
||||
func (c *Checkbox) SetFinishedFunc(handler func(key tcell.Key)) FormItem {
|
||||
c.finished = handler
|
||||
return c
|
||||
}
|
||||
|
||||
// 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.
|
||||
if c.labelWidth > 0 {
|
||||
labelWidth := c.labelWidth
|
||||
if labelWidth > rightLimit-x {
|
||||
labelWidth = rightLimit - x
|
||||
}
|
||||
Print(screen, c.label, x, y, labelWidth, AlignLeft, c.labelColor)
|
||||
x += labelWidth
|
||||
} else {
|
||||
_, 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)
|
||||
}
|
||||
if c.finished != nil {
|
||||
c.finished(key)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// MouseHandler returns the mouse handler for this primitive.
|
||||
func (c *Checkbox) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
|
||||
return c.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
|
||||
x, y := event.Position()
|
||||
_, rectY, _, _ := c.GetInnerRect()
|
||||
if !c.InRect(x, y) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Process mouse event.
|
||||
if action == MouseLeftClick && y == rectY {
|
||||
setFocus(c)
|
||||
c.checked = !c.checked
|
||||
if c.changed != nil {
|
||||
c.changed(c.checked)
|
||||
}
|
||||
consumed = true
|
||||
}
|
||||
|
||||
return
|
||||
})
|
||||
}
|
180
vendor/github.com/rivo/tview/doc.go
generated
vendored
Normal file
180
vendor/github.com/rivo/tview/doc.go
generated
vendored
Normal file
@@ -0,0 +1,180 @@
|
||||
/*
|
||||
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: A scrollable window that display multi-colored text. Text may also
|
||||
be highlighted.
|
||||
- Table: A scrollable display of tabular data. Table cells, rows, or columns
|
||||
may also be highlighted.
|
||||
- TreeView: A scrollable display for hierarchical data. Tree nodes can be
|
||||
highlighted, collapsed, expanded, and more.
|
||||
- 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.
|
||||
- Grid: A grid based layout manager.
|
||||
- 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.
|
||||
|
||||
Color tags may contain not just the foreground (text) color but also the
|
||||
background color and additional flags. In fact, the full definition of a color
|
||||
tag is as follows:
|
||||
|
||||
[<foreground>:<background>:<flags>]
|
||||
|
||||
Each of the three fields can be left blank and trailing fields can be omitted.
|
||||
(Empty square brackets "[]", however, are not considered color tags.) Colors
|
||||
that are not specified will be left unchanged. A field with just a dash ("-")
|
||||
means "reset to default".
|
||||
|
||||
You can specify the following flags (some flags may not be supported by your
|
||||
terminal):
|
||||
|
||||
l: blink
|
||||
b: bold
|
||||
d: dim
|
||||
r: reverse (switch foreground and background color)
|
||||
u: underline
|
||||
|
||||
Examples:
|
||||
|
||||
[yellow]Yellow text
|
||||
[yellow:red]Yellow text on red background
|
||||
[:red]Red background, text color unchanged
|
||||
[yellow::u]Yellow text underlined
|
||||
[::bl]Bold, blinking text
|
||||
[::-]Colors unchanged, flags reset
|
||||
[-]Reset foreground color
|
||||
[-:-:-]Reset everything
|
||||
[:]No effect
|
||||
[]Not a valid color tag, will print square brackets as they are
|
||||
|
||||
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. Note that the text inside the
|
||||
brackets will be matched less strictly than region or colors tags. I.e. any
|
||||
character that may be used in color or region tags will be recognized. Examples:
|
||||
|
||||
[red[] will be output as [red]
|
||||
["123"[] will be output as ["123"]
|
||||
[#6aff00[[] will be output as [#6aff00[]
|
||||
[a#"[[[] will be output as [a#"[[]
|
||||
[] will be output as [] (see color tags above)
|
||||
[[] will be output as [[] (not an escaped tag)
|
||||
|
||||
You can use the Escape() function to insert brackets automatically where needed.
|
||||
|
||||
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.
|
||||
|
||||
Concurrency
|
||||
|
||||
Many functions in this package are not thread-safe. For many applications, this
|
||||
may not be an issue: If your code makes changes in response to key events, it
|
||||
will execute in the main goroutine and thus will not cause any race conditions.
|
||||
|
||||
If you access your primitives from other goroutines, however, you will need to
|
||||
synchronize execution. The easiest way to do this is to call
|
||||
Application.QueueUpdate() or Application.QueueUpdateDraw() (see the function
|
||||
documentation for details):
|
||||
|
||||
go func() {
|
||||
app.QueueUpdateDraw(func() {
|
||||
table.SetCellSimple(0, 0, "Foo bar")
|
||||
})
|
||||
}()
|
||||
|
||||
One exception to this is the io.Writer interface implemented by TextView. You
|
||||
can safely write to a TextView from any goroutine. See the TextView
|
||||
documentation for details.
|
||||
|
||||
You can also call Application.Draw() from any goroutine without having to wrap
|
||||
it in QueueUpdate(). And, as mentioned above, key event callbacks are executed
|
||||
in the main goroutine and thus should not use QueueUpdate() as that may lead to
|
||||
deadlocks.
|
||||
|
||||
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://github.com/gdamore/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
|
547
vendor/github.com/rivo/tview/dropdown.go
generated
vendored
Normal file
547
vendor/github.com/rivo/tview/dropdown.go
generated
vendored
Normal file
@@ -0,0 +1,547 @@
|
||||
package tview
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/gdamore/tcell"
|
||||
)
|
||||
|
||||
// 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
|
||||
|
||||
// Strings to be placed before and after each drop-down option.
|
||||
optionPrefix, optionSuffix string
|
||||
|
||||
// The index of the currently selected option. Negative if no option is
|
||||
// currently selected.
|
||||
currentOption int
|
||||
|
||||
// Strings to be placed beefore and after the current option.
|
||||
currentOptionPrefix, currentOptionSuffix string
|
||||
|
||||
// The text to be displayed when no option has yet been selected.
|
||||
noSelection string
|
||||
|
||||
// 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 label area. A value of 0 means use the width of
|
||||
// the label text.
|
||||
labelWidth int
|
||||
|
||||
// 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)
|
||||
|
||||
// A callback function set by the Form class and called when the user leaves
|
||||
// this form item.
|
||||
finished func(tcell.Key)
|
||||
|
||||
// A callback function which is called when the user changes the drop-down's
|
||||
// selection.
|
||||
selected func(text string, index int)
|
||||
|
||||
dragging bool // Set to true when mouse dragging is in progress.
|
||||
}
|
||||
|
||||
// NewDropDown returns a new drop-down.
|
||||
func NewDropDown() *DropDown {
|
||||
list := NewList()
|
||||
list.ShowSecondaryText(false).
|
||||
SetMainTextColor(Styles.PrimitiveBackgroundColor).
|
||||
SetSelectedTextColor(Styles.PrimitiveBackgroundColor).
|
||||
SetSelectedBackgroundColor(Styles.PrimaryTextColor).
|
||||
SetHighlightFullLine(true).
|
||||
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. Calling
|
||||
// this function will also trigger the "selected" callback (if there is one).
|
||||
func (d *DropDown) SetCurrentOption(index int) *DropDown {
|
||||
if index >= 0 && index < len(d.options) {
|
||||
d.currentOption = index
|
||||
d.list.SetCurrentItem(index)
|
||||
if d.selected != nil {
|
||||
d.selected(d.options[index].Text, index)
|
||||
}
|
||||
if d.options[index].Selected != nil {
|
||||
d.options[index].Selected()
|
||||
}
|
||||
} else {
|
||||
d.currentOption = -1
|
||||
d.list.SetCurrentItem(0) // Set to 0 because -1 means "last item".
|
||||
if d.selected != nil {
|
||||
d.selected("", -1)
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
// SetTextOptions sets the text to be placed before and after each drop-down
|
||||
// option (prefix/suffix), the text placed before and after the currently
|
||||
// selected option (currentPrefix/currentSuffix) as well as the text to be
|
||||
// displayed when no option is currently selected. Per default, all of these
|
||||
// strings are empty.
|
||||
func (d *DropDown) SetTextOptions(prefix, suffix, currentPrefix, currentSuffix, noSelection string) *DropDown {
|
||||
d.currentOptionPrefix = currentPrefix
|
||||
d.currentOptionSuffix = currentSuffix
|
||||
d.noSelection = noSelection
|
||||
d.optionPrefix = prefix
|
||||
d.optionSuffix = suffix
|
||||
for index := 0; index < d.list.GetItemCount(); index++ {
|
||||
d.list.SetItemText(index, prefix+d.options[index].Text+suffix, "")
|
||||
}
|
||||
return d
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// SetLabelWidth sets the screen width of the label. A value of 0 will cause the
|
||||
// primitive to use the width of the label string.
|
||||
func (d *DropDown) SetLabelWidth(width int) *DropDown {
|
||||
d.labelWidth = width
|
||||
return d
|
||||
}
|
||||
|
||||
// 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(labelWidth int, labelColor, bgColor, fieldTextColor, fieldBgColor tcell.Color) FormItem {
|
||||
d.labelWidth = labelWidth
|
||||
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 := TaggedStringWidth(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(d.optionPrefix+text+d.optionSuffix, "", 0, nil)
|
||||
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, nil)
|
||||
}(text, index)
|
||||
}
|
||||
d.selected = selected
|
||||
return d
|
||||
}
|
||||
|
||||
// SetSelectedFunc sets a handler which is called when the user changes the
|
||||
// drop-down's option. This handler will be called in addition and prior to
|
||||
// an option's optional individual handler. The handler is provided with the
|
||||
// selected option's text and index. If "no option" was selected, these values
|
||||
// are an empty string and -1.
|
||||
func (d *DropDown) SetSelectedFunc(handler func(text string, index int)) *DropDown {
|
||||
d.selected = handler
|
||||
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 sets a callback invoked when the user leaves this form item.
|
||||
func (d *DropDown) SetFinishedFunc(handler func(key tcell.Key)) FormItem {
|
||||
d.finished = handler
|
||||
return d
|
||||
}
|
||||
|
||||
// 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.
|
||||
if d.labelWidth > 0 {
|
||||
labelWidth := d.labelWidth
|
||||
if labelWidth > rightLimit-x {
|
||||
labelWidth = rightLimit - x
|
||||
}
|
||||
Print(screen, d.label, x, y, labelWidth, AlignLeft, d.labelColor)
|
||||
x += labelWidth
|
||||
} else {
|
||||
_, drawnWidth := Print(screen, d.label, x, y, rightLimit-x, AlignLeft, d.labelColor)
|
||||
x += drawnWidth
|
||||
}
|
||||
|
||||
// What's the longest option text?
|
||||
maxWidth := 0
|
||||
optionWrapWidth := TaggedStringWidth(d.optionPrefix + d.optionSuffix)
|
||||
for _, option := range d.options {
|
||||
strWidth := TaggedStringWidth(option.Text) + optionWrapWidth
|
||||
if strWidth > maxWidth {
|
||||
maxWidth = strWidth
|
||||
}
|
||||
}
|
||||
|
||||
// Draw selection area.
|
||||
fieldWidth := d.fieldWidth
|
||||
if fieldWidth == 0 {
|
||||
fieldWidth = maxWidth
|
||||
if d.currentOption < 0 {
|
||||
noSelectionWidth := TaggedStringWidth(d.noSelection)
|
||||
if noSelectionWidth > fieldWidth {
|
||||
fieldWidth = noSelectionWidth
|
||||
}
|
||||
} else if d.currentOption < len(d.options) {
|
||||
currentOptionWidth := TaggedStringWidth(d.currentOptionPrefix + d.options[d.currentOption].Text + d.currentOptionSuffix)
|
||||
if currentOptionWidth > fieldWidth {
|
||||
fieldWidth = currentOptionWidth
|
||||
}
|
||||
}
|
||||
}
|
||||
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.
|
||||
currentOptionPrefixWidth := TaggedStringWidth(d.currentOptionPrefix)
|
||||
prefixWidth := stringWidth(d.prefix)
|
||||
listItemText := d.options[d.list.GetCurrentItem()].Text
|
||||
Print(screen, d.currentOptionPrefix, x, y, fieldWidth, AlignLeft, d.fieldTextColor)
|
||||
Print(screen, d.prefix, x+currentOptionPrefixWidth, y, fieldWidth-currentOptionPrefixWidth, AlignLeft, d.prefixTextColor)
|
||||
if len(d.prefix) < len(listItemText) {
|
||||
Print(screen, listItemText[len(d.prefix):]+d.currentOptionSuffix, x+prefixWidth+currentOptionPrefixWidth, y, fieldWidth-prefixWidth-currentOptionPrefixWidth, AlignLeft, d.fieldTextColor)
|
||||
}
|
||||
} else {
|
||||
color := d.fieldTextColor
|
||||
text := d.noSelection
|
||||
if d.currentOption >= 0 && d.currentOption < len(d.options) {
|
||||
text = d.currentOptionPrefix + d.options[d.currentOption].Text + d.currentOptionSuffix
|
||||
}
|
||||
// Just show the current selection.
|
||||
if d.GetFocusable().HasFocus() && !d.open {
|
||||
color = d.fieldBackgroundColor
|
||||
}
|
||||
Print(screen, 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)) {
|
||||
// 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)
|
||||
d.evalPrefix()
|
||||
}
|
||||
|
||||
d.openList(setFocus)
|
||||
case tcell.KeyEscape, tcell.KeyTab, tcell.KeyBacktab:
|
||||
if d.done != nil {
|
||||
d.done(key)
|
||||
}
|
||||
if d.finished != nil {
|
||||
d.finished(key)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// evalPrefix selects an item in the drop-down list based on the current prefix.
|
||||
func (d *DropDown) evalPrefix() {
|
||||
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])
|
||||
}
|
||||
}
|
||||
|
||||
// openList hands control over to the embedded List primitive.
|
||||
func (d *DropDown) openList(setFocus func(Primitive)) {
|
||||
d.open = true
|
||||
optionBefore := d.currentOption
|
||||
|
||||
d.list.SetSelectedFunc(func(index int, mainText, secondaryText string, shortcut rune) {
|
||||
if d.dragging {
|
||||
return // If we're dragging the mouse, we don't want to trigger any events.
|
||||
}
|
||||
|
||||
// An option was selected. Close the list again.
|
||||
d.currentOption = index
|
||||
d.closeList(setFocus)
|
||||
|
||||
// Trigger "selected" event.
|
||||
if d.selected != nil {
|
||||
d.selected(d.options[d.currentOption].Text, d.currentOption)
|
||||
}
|
||||
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())
|
||||
d.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])
|
||||
}
|
||||
d.evalPrefix()
|
||||
} else if event.Key() == tcell.KeyEscape {
|
||||
d.currentOption = optionBefore
|
||||
d.closeList(setFocus)
|
||||
} else {
|
||||
d.prefix = ""
|
||||
}
|
||||
|
||||
return event
|
||||
})
|
||||
|
||||
setFocus(d.list)
|
||||
}
|
||||
|
||||
// closeList closes the embedded List element by hiding it and removing focus
|
||||
// from it.
|
||||
func (d *DropDown) closeList(setFocus func(Primitive)) {
|
||||
d.open = false
|
||||
if d.list.HasFocus() {
|
||||
setFocus(d)
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// MouseHandler returns the mouse handler for this primitive.
|
||||
func (d *DropDown) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
|
||||
return d.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
|
||||
// Was the mouse event in the drop-down box itself (or on its label)?
|
||||
x, y := event.Position()
|
||||
_, rectY, _, _ := d.GetInnerRect()
|
||||
inRect := y == rectY
|
||||
if !d.open && !inRect {
|
||||
return d.InRect(x, y), nil // No, and it's not expanded either. Ignore.
|
||||
}
|
||||
|
||||
// Handle dragging. Clicks are implicitly handled by this logic.
|
||||
switch action {
|
||||
case MouseLeftDown:
|
||||
consumed = d.open || inRect
|
||||
capture = d
|
||||
if !d.open {
|
||||
d.openList(setFocus)
|
||||
d.dragging = true
|
||||
} else if consumed, _ := d.list.MouseHandler()(MouseLeftClick, event, setFocus); !consumed {
|
||||
d.closeList(setFocus) // Close drop-down if clicked outside of it.
|
||||
}
|
||||
case MouseMove:
|
||||
if d.dragging {
|
||||
// We pretend it's a left click so we can see the selection during
|
||||
// dragging. Because we don't act upon it, it's not a problem.
|
||||
d.list.MouseHandler()(MouseLeftClick, event, setFocus)
|
||||
consumed = true
|
||||
capture = d
|
||||
}
|
||||
case MouseLeftUp:
|
||||
if d.dragging {
|
||||
d.dragging = false
|
||||
d.list.MouseHandler()(MouseLeftClick, event, setFocus)
|
||||
consumed = true
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
})
|
||||
}
|
225
vendor/github.com/rivo/tview/flex.go
generated
vendored
Normal file
225
vendor/github.com/rivo/tview/flex.go
generated
vendored
Normal file
@@ -0,0 +1,225 @@
|
||||
package tview
|
||||
|
||||
import (
|
||||
"github.com/gdamore/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, Flex 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().
|
||||
//
|
||||
// Note that Box, the superclass of Flex, will have its background color set to
|
||||
// transparent so that any nil flex items will leave their background unchanged.
|
||||
// To clear a Flex's background before any items are drawn, set it to the
|
||||
// desired color:
|
||||
//
|
||||
// flex.SetBackgroundColor(tview.Styles.PrimitiveBackgroundColor)
|
||||
func NewFlex() *Flex {
|
||||
f := &Flex{
|
||||
Box: NewBox().SetBackgroundColor(tcell.ColorDefault),
|
||||
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. The proportion 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
|
||||
}
|
||||
|
||||
// Clear removes all items from the container.
|
||||
func (f *Flex) Clear() *Flex {
|
||||
f.items = nil
|
||||
return f
|
||||
}
|
||||
|
||||
// ResizeItem sets a new size for the item(s) with the given primitive. If there
|
||||
// are multiple Flex items with the same primitive, they will all receive the
|
||||
// same size. For details regarding the size parameters, see AddItem().
|
||||
func (f *Flex) ResizeItem(p Primitive, fixedSize, proportion int) *Flex {
|
||||
for _, item := range f.items {
|
||||
if item.Item == p {
|
||||
item.FixedSize = fixedSize
|
||||
item.Proportion = proportion
|
||||
}
|
||||
}
|
||||
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 {
|
||||
if proportionSum > 0 {
|
||||
size = distSize * item.Proportion / proportionSum
|
||||
distSize -= size
|
||||
proportionSum -= item.Proportion
|
||||
} else {
|
||||
size = 0
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
// MouseHandler returns the mouse handler for this primitive.
|
||||
func (f *Flex) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
|
||||
return f.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
|
||||
if !f.InRect(event.Position()) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Pass mouse events along to the first child item that takes it.
|
||||
for _, item := range f.items {
|
||||
if item.Item == nil {
|
||||
continue
|
||||
}
|
||||
consumed, capture = item.Item.MouseHandler()(action, event, setFocus)
|
||||
if consumed {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
})
|
||||
}
|
8
vendor/github.com/rivo/tview/focusable.go
generated
vendored
Normal file
8
vendor/github.com/rivo/tview/focusable.go
generated
vendored
Normal file
@@ -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
|
||||
}
|
663
vendor/github.com/rivo/tview/form.go
generated
vendored
Normal file
663
vendor/github.com/rivo/tview/form.go
generated
vendored
Normal file
@@ -0,0 +1,663 @@
|
||||
package tview
|
||||
|
||||
import (
|
||||
"github.com/gdamore/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(labelWidth int, 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
|
||||
|
||||
// SetFinishedFunc 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.) This is only used when the form itself receives
|
||||
// focus so that the last element that had focus keeps it.
|
||||
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
|
||||
}
|
||||
|
||||
// SetFocus shifts the focus to the form element with the given index, counting
|
||||
// non-button items first and buttons last. Note that this index is only used
|
||||
// when the form itself receives focus.
|
||||
func (f *Form) SetFocus(index int) *Form {
|
||||
if index < 0 {
|
||||
f.focusedElement = 0
|
||||
} else if index >= len(f.items)+len(f.buttons) {
|
||||
f.focusedElement = len(f.items) + len(f.buttons)
|
||||
} else {
|
||||
f.focusedElement = index
|
||||
}
|
||||
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).
|
||||
SetOptions(options, selected).
|
||||
SetCurrentOption(initialOption))
|
||||
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
|
||||
}
|
||||
|
||||
// GetButton returns the button at the specified 0-based index. Note that
|
||||
// buttons have been specially prepared for this form and modifying some of
|
||||
// their attributes may have unintended side effects.
|
||||
func (f *Form) GetButton(index int) *Button {
|
||||
return f.buttons[index]
|
||||
}
|
||||
|
||||
// RemoveButton removes the button at the specified position, starting with 0
|
||||
// for the button that was added first.
|
||||
func (f *Form) RemoveButton(index int) *Form {
|
||||
f.buttons = append(f.buttons[:index], f.buttons[index+1:]...)
|
||||
return f
|
||||
}
|
||||
|
||||
// GetButtonCount returns the number of buttons in this form.
|
||||
func (f *Form) GetButtonCount() int {
|
||||
return len(f.buttons)
|
||||
}
|
||||
|
||||
// GetButtonIndex returns the index of the button with the given label, starting
|
||||
// with 0 for the button that was added first. If no such label was found, -1
|
||||
// is returned.
|
||||
func (f *Form) GetButtonIndex(label string) int {
|
||||
for index, button := range f.buttons {
|
||||
if button.GetLabel() == label {
|
||||
return index
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
// 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.ClearButtons()
|
||||
}
|
||||
f.focusedElement = 0
|
||||
return f
|
||||
}
|
||||
|
||||
// ClearButtons removes all buttons from the form.
|
||||
func (f *Form) ClearButtons() *Form {
|
||||
f.buttons = nil
|
||||
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. Specifically, these
|
||||
// are:
|
||||
//
|
||||
// - The label width
|
||||
// - The label color
|
||||
// - The background color
|
||||
// - The field text color
|
||||
// - The field background color
|
||||
func (f *Form) AddFormItem(item FormItem) *Form {
|
||||
f.items = append(f.items, item)
|
||||
return f
|
||||
}
|
||||
|
||||
// GetFormItemCount returns the number of items in the form (not including the
|
||||
// buttons).
|
||||
func (f *Form) GetFormItemCount() int {
|
||||
return len(f.items)
|
||||
}
|
||||
|
||||
// GetFormItem returns the form item 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]
|
||||
}
|
||||
|
||||
// RemoveFormItem removes 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) RemoveFormItem(index int) *Form {
|
||||
f.items = append(f.items[:index], f.items[index+1:]...)
|
||||
return f
|
||||
}
|
||||
|
||||
// GetFormItemByLabel returns the first form element with the given label. If
|
||||
// no such element is found, nil is returned. Buttons are not searched and will
|
||||
// therefore not be returned.
|
||||
func (f *Form) GetFormItemByLabel(label string) FormItem {
|
||||
for _, item := range f.items {
|
||||
if item.GetLabel() == label {
|
||||
return item
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetFormItemIndex returns the index of the first form element with the given
|
||||
// label. If no such element is found, -1 is returned. Buttons are not searched
|
||||
// and will therefore not be returned.
|
||||
func (f *Form) GetFormItemIndex(label string) int {
|
||||
for index, item := range f.items {
|
||||
if item.GetLabel() == label {
|
||||
return index
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
// GetFocusedItemIndex returns the indices of the form element or button which
|
||||
// currently has focus. If they don't, -1 is returned resepectively.
|
||||
func (f *Form) GetFocusedItemIndex() (formItem, button int) {
|
||||
index := f.focusIndex()
|
||||
if index < 0 {
|
||||
return -1, -1
|
||||
}
|
||||
if index < len(f.items) {
|
||||
return index, -1
|
||||
}
|
||||
return -1, index - len(f.items)
|
||||
}
|
||||
|
||||
// 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 actual item that has focus.
|
||||
if index := f.focusIndex(); index >= 0 {
|
||||
f.focusedElement = index
|
||||
}
|
||||
|
||||
// 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 {
|
||||
labelWidth := TaggedStringWidth(item.GetLabel())
|
||||
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.
|
||||
labelWidth := TaggedStringWidth(item.GetLabel())
|
||||
var itemWidth int
|
||||
if f.horizontal {
|
||||
fieldWidth := item.GetFieldWidth()
|
||||
if fieldWidth == 0 {
|
||||
fieldWidth = DefaultFormFieldWidth
|
||||
}
|
||||
labelWidth++
|
||||
itemWidth = labelWidth + fieldWidth
|
||||
} else {
|
||||
// We want all fields to align vertically.
|
||||
labelWidth = maxLabelWidth
|
||||
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(
|
||||
labelWidth,
|
||||
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 := TaggedStringWidth(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 {
|
||||
f.hasFocus = true
|
||||
return
|
||||
}
|
||||
f.hasFocus = false
|
||||
|
||||
// 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 {
|
||||
if f.hasFocus {
|
||||
return true
|
||||
}
|
||||
return f.focusIndex() >= 0
|
||||
}
|
||||
|
||||
// focusIndex returns the index of the currently focused item, counting form
|
||||
// items first, then buttons. A negative value indicates that no containeed item
|
||||
// has focus.
|
||||
func (f *Form) focusIndex() int {
|
||||
for index, item := range f.items {
|
||||
if item.GetFocusable().HasFocus() {
|
||||
return index
|
||||
}
|
||||
}
|
||||
for index, button := range f.buttons {
|
||||
if button.focus.HasFocus() {
|
||||
return len(f.items) + index
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
// MouseHandler returns the mouse handler for this primitive.
|
||||
func (f *Form) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
|
||||
return f.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
|
||||
if !f.InRect(event.Position()) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// At the end, update f.focusedElement and prepare current item/button.
|
||||
defer func() {
|
||||
if consumed {
|
||||
index := f.focusIndex()
|
||||
if index >= 0 {
|
||||
f.focusedElement = index
|
||||
}
|
||||
f.Focus(setFocus)
|
||||
}
|
||||
}()
|
||||
|
||||
// Determine items to pass mouse events to.
|
||||
for _, item := range f.items {
|
||||
consumed, capture = item.MouseHandler()(action, event, setFocus)
|
||||
if consumed {
|
||||
return
|
||||
}
|
||||
}
|
||||
for _, button := range f.buttons {
|
||||
consumed, capture = button.MouseHandler()(action, event, setFocus)
|
||||
if consumed {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// A mouse click anywhere else will return the focus to the last selected
|
||||
// element.
|
||||
if action == MouseLeftClick {
|
||||
consumed = true
|
||||
}
|
||||
|
||||
return
|
||||
})
|
||||
}
|
169
vendor/github.com/rivo/tview/frame.go
generated
vendored
Normal file
169
vendor/github.com/rivo/tview/frame.go
generated
vendored
Normal file
@@ -0,0 +1,169 @@
|
||||
package tview
|
||||
|
||||
import (
|
||||
"github.com/gdamore/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 space around another primitive. In addition,
|
||||
// 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
|
||||
}
|
||||
|
||||
// MouseHandler returns the mouse handler for this primitive.
|
||||
func (f *Frame) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
|
||||
return f.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
|
||||
if !f.InRect(event.Position()) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Pass mouse events on to contained primitive.
|
||||
return f.primitive.MouseHandler()(action, event, setFocus)
|
||||
})
|
||||
}
|
12
vendor/github.com/rivo/tview/go.mod
generated
vendored
Normal file
12
vendor/github.com/rivo/tview/go.mod
generated
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
module github.com/rivo/tview
|
||||
|
||||
go 1.12
|
||||
|
||||
require (
|
||||
github.com/gdamore/tcell v1.3.0
|
||||
github.com/lucasb-eyer/go-colorful v1.0.3
|
||||
github.com/mattn/go-runewidth v0.0.8
|
||||
github.com/rivo/uniseg v0.1.0
|
||||
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4 // indirect
|
||||
golang.org/x/text v0.3.2 // indirect
|
||||
)
|
25
vendor/github.com/rivo/tview/go.sum
generated
vendored
Normal file
25
vendor/github.com/rivo/tview/go.sum
generated
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
github.com/DATA-DOG/go-sqlmock v1.3.3 h1:CWUqKXe0s8A2z6qCgkP4Kru7wC11YoAnoupUKFDnH08=
|
||||
github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
|
||||
github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko=
|
||||
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
|
||||
github.com/gdamore/tcell v1.3.0 h1:r35w0JBADPZCVQijYebl6YMWWtHRqVEGt7kL2eBADRM=
|
||||
github.com/gdamore/tcell v1.3.0/go.mod h1:Hjvr+Ofd+gLglo7RYKxxnzCBmev3BzsS67MebKS4zMM=
|
||||
github.com/lucasb-eyer/go-colorful v1.0.2 h1:mCMFu6PgSozg9tDNMMK3g18oJBX7oYGrC09mS6CXfO4=
|
||||
github.com/lucasb-eyer/go-colorful v1.0.2/go.mod h1:0MS4r+7BZKSJ5mw4/S5MPN+qHFF1fYclkSPilDOKW0s=
|
||||
github.com/lucasb-eyer/go-colorful v1.0.3 h1:QIbQXiugsb+q10B+MI+7DI1oQLdmnep86tWFlaaUAac=
|
||||
github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/mattn/go-runewidth v0.0.4 h1:2BvfKmzob6Bmd4YsL0zygOqfdFnK7GR4QL06Do4/p7Y=
|
||||
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
|
||||
github.com/mattn/go-runewidth v0.0.8 h1:3tS41NlGYSmhhe/8fhGRzc+z3AYCw1Fe1WAyLuujKs0=
|
||||
github.com/mattn/go-runewidth v0.0.8/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||
github.com/rivo/uniseg v0.1.0 h1:+2KBaVoUmb9XzDsrx/Ct0W/EYOSFf/nWTauy++DprtY=
|
||||
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756 h1:9nuHUbU8dRnRRfj9KjWUVrJeoexdbeMjttk6Oh1rD10=
|
||||
golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4 h1:sfkvUWPNGwSV+8/fNqctR5lS2AqCSqYwXdrjCxp/dXo=
|
||||
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
684
vendor/github.com/rivo/tview/grid.go
generated
vendored
Normal file
684
vendor/github.com/rivo/tview/grid.go
generated
vendored
Normal file
@@ -0,0 +1,684 @@
|
||||
package tview
|
||||
|
||||
import (
|
||||
"math"
|
||||
|
||||
"github.com/gdamore/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.
|
||||
//
|
||||
// Note that Box, the superclass of Grid, will have its background color set to
|
||||
// transparent so that any grid areas not covered by any primitives will leave
|
||||
// their background unchanged. To clear a Grid's background before any items are
|
||||
// drawn, set it to the desired color:
|
||||
//
|
||||
// grid.SetBackgroundColor(tview.Styles.PrimitiveBackgroundColor)
|
||||
func NewGrid() *Grid {
|
||||
g := &Grid{
|
||||
Box: NewBox().SetBackgroundColor(tcell.ColorDefault),
|
||||
bordersColor: Styles.GraphicsColor,
|
||||
}
|
||||
g.focus = g
|
||||
return g
|
||||
}
|
||||
|
||||
// SetColumns defines how the columns of the grid are distributed. Each value
|
||||
// defines the size of one column, starting with the leftmost column. Values
|
||||
// greater 0 represent absolute column widths (gaps not included). Values less
|
||||
// or equal 0 represent proportional column widths or fractions of the remaining
|
||||
// free space, where 0 is treated the same as -1. That is, a column with a value
|
||||
// of -3 will have three times the width of a column with a value of -1 (or 0).
|
||||
// The minimum width set with SetMinSize() is always observed.
|
||||
//
|
||||
// Primitives may extend beyond the columns defined explicitly with this
|
||||
// function. A value of 0 is assumed for any undefined column. In fact, if you
|
||||
// never call this function, all columns occupied by primitives will have the
|
||||
// same width. On the other hand, unoccupied columns 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 columns with widths of 30, 10, 15, 15, and 30
|
||||
// cells:
|
||||
//
|
||||
// grid.Setcolumns(30, 10, -1, -1, -2)
|
||||
//
|
||||
// If a primitive were then placed in the 6th and 7th column, 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) SetColumns(columns ...int) *Grid {
|
||||
g.columns = columns
|
||||
return g
|
||||
}
|
||||
|
||||
// SetRows defines how the rows of the grid are distributed. These values behave
|
||||
// the same as the column values provided with SetColumns(), see there for a
|
||||
// definition and examples.
|
||||
//
|
||||
// The provided values correspond to row heights, the first value defining
|
||||
// the height of the topmost row.
|
||||
func (g *Grid) SetRows(rows ...int) *Grid {
|
||||
g.rows = rows
|
||||
return g
|
||||
}
|
||||
|
||||
// SetSize is a shortcut for SetRows() and SetColumns() where all row and column
|
||||
// values are set to the given size values. See SetColumns() 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 "rowSpan" rows and "colSpan" columns.
|
||||
// For example, for a primitive to occupy rows 2, 3, and 4 and columns 5 and 6:
|
||||
//
|
||||
// grid.AddItem(p, 2, 5, 3, 2, 0, 0, true)
|
||||
//
|
||||
// If rowSpan or colSpan 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, rowSpan, colSpan, minGridHeight, minGridWidth int, focus bool) *Grid {
|
||||
g.items = append(g.items, &gridItem{
|
||||
Item: p,
|
||||
Row: row,
|
||||
Column: column,
|
||||
Height: rowSpan,
|
||||
Width: colSpan,
|
||||
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 remain 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 g.hasFocus
|
||||
}
|
||||
|
||||
// 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()
|
||||
screenWidth, screenHeight := screen.Size()
|
||||
|
||||
// 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.MinGridWidth < previousItem.MinGridWidth && item.MinGridHeight < previousItem.MinGridHeight {
|
||||
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.
|
||||
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
|
||||
}
|
||||
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
|
||||
}
|
||||
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
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
// Calculate row/column positions.
|
||||
var columnX, rowY int
|
||||
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 int
|
||||
add := 1
|
||||
if !g.borders {
|
||||
add = g.gapRows
|
||||
}
|
||||
for index, height := range rowHeight {
|
||||
if index >= g.rowOffset {
|
||||
break
|
||||
}
|
||||
offsetY += height + add
|
||||
}
|
||||
if !g.borders {
|
||||
add = g.gapColumns
|
||||
}
|
||||
for index, width := range columnWidth {
|
||||
if index >= g.columnOffset {
|
||||
break
|
||||
}
|
||||
offsetX += width + add
|
||||
}
|
||||
|
||||
// Line up the last row/column with the end of the available area.
|
||||
var border int
|
||||
if g.borders {
|
||||
border = 1
|
||||
}
|
||||
last := len(rowPos) - 1
|
||||
if rowPos[last]+rowHeight[last]+border-offsetY < height {
|
||||
offsetY = rowPos[last] - height + rowHeight[last] + border
|
||||
}
|
||||
last = len(columnPos) - 1
|
||||
if columnPos[last]+columnWidth[last]+border-offsetX < width {
|
||||
offsetX = columnPos[last] - width + columnWidth[last] + border
|
||||
}
|
||||
|
||||
// The focused item must be within the visible area.
|
||||
if focus != nil {
|
||||
if focus.y+focus.h-offsetY >= height {
|
||||
offsetY = focus.y - height + focus.h
|
||||
}
|
||||
if focus.y-offsetY < 0 {
|
||||
offsetY = focus.y
|
||||
}
|
||||
if focus.x+focus.w-offsetX >= width {
|
||||
offsetX = focus.x - width + focus.w
|
||||
}
|
||||
if focus.x-offsetX < 0 {
|
||||
offsetX = focus.x
|
||||
}
|
||||
}
|
||||
|
||||
// Adjust row/column offsets based on this value.
|
||||
var from, to int
|
||||
for index, pos := range rowPos {
|
||||
if pos-offsetY < 0 {
|
||||
from = index + 1
|
||||
}
|
||||
if pos-offsetY < height {
|
||||
to = index
|
||||
}
|
||||
}
|
||||
if g.rowOffset < from {
|
||||
g.rowOffset = from
|
||||
}
|
||||
if g.rowOffset > to {
|
||||
g.rowOffset = to
|
||||
}
|
||||
from, to = 0, 0
|
||||
for index, pos := range columnPos {
|
||||
if pos-offsetX < 0 {
|
||||
from = index + 1
|
||||
}
|
||||
if pos-offsetX < width {
|
||||
to = index
|
||||
}
|
||||
}
|
||||
if g.columnOffset < from {
|
||||
g.columnOffset = from
|
||||
}
|
||||
if g.columnOffset > to {
|
||||
g.columnOffset = to
|
||||
}
|
||||
|
||||
// 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 >= width || item.x+item.w <= 0 || item.y >= height || item.y+item.h <= 0 {
|
||||
item.visible = false
|
||||
continue
|
||||
}
|
||||
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
|
||||
}
|
||||
item.x += x
|
||||
item.y += y
|
||||
primitive.SetRect(item.x, 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 >= screenWidth {
|
||||
continue
|
||||
}
|
||||
by := item.y - 1
|
||||
if by >= 0 && by < screenHeight {
|
||||
PrintJoinedSemigraphics(screen, bx, by, Borders.Horizontal, g.bordersColor)
|
||||
}
|
||||
by = item.y + item.h
|
||||
if by >= 0 && by < screenHeight {
|
||||
PrintJoinedSemigraphics(screen, bx, by, Borders.Horizontal, g.bordersColor)
|
||||
}
|
||||
}
|
||||
for by := item.y; by < item.y+item.h; by++ { // Left/right lines.
|
||||
if by < 0 || by >= screenHeight {
|
||||
continue
|
||||
}
|
||||
bx := item.x - 1
|
||||
if bx >= 0 && bx < screenWidth {
|
||||
PrintJoinedSemigraphics(screen, bx, by, Borders.Vertical, g.bordersColor)
|
||||
}
|
||||
bx = item.x + item.w
|
||||
if bx >= 0 && bx < screenWidth {
|
||||
PrintJoinedSemigraphics(screen, bx, by, Borders.Vertical, g.bordersColor)
|
||||
}
|
||||
}
|
||||
bx, by := item.x-1, item.y-1 // Top-left corner.
|
||||
if bx >= 0 && bx < screenWidth && by >= 0 && by < screenHeight {
|
||||
PrintJoinedSemigraphics(screen, bx, by, Borders.TopLeft, g.bordersColor)
|
||||
}
|
||||
bx, by = item.x+item.w, item.y-1 // Top-right corner.
|
||||
if bx >= 0 && bx < screenWidth && by >= 0 && by < screenHeight {
|
||||
PrintJoinedSemigraphics(screen, bx, by, Borders.TopRight, g.bordersColor)
|
||||
}
|
||||
bx, by = item.x-1, item.y+item.h // Bottom-left corner.
|
||||
if bx >= 0 && bx < screenWidth && by >= 0 && by < screenHeight {
|
||||
PrintJoinedSemigraphics(screen, bx, by, Borders.BottomLeft, g.bordersColor)
|
||||
}
|
||||
bx, by = item.x+item.w, item.y+item.h // Bottom-right corner.
|
||||
if bx >= 0 && bx < screenWidth && by >= 0 && by < screenHeight {
|
||||
PrintJoinedSemigraphics(screen, bx, by, Borders.BottomRight, g.bordersColor)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MouseHandler returns the mouse handler for this primitive.
|
||||
func (g *Grid) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
|
||||
return g.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
|
||||
if !g.InRect(event.Position()) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Pass mouse events along to the first child item that takes it.
|
||||
for _, item := range g.items {
|
||||
if item.Item == nil {
|
||||
continue
|
||||
}
|
||||
consumed, capture = item.Item.MouseHandler()(action, event, setFocus)
|
||||
if consumed {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
})
|
||||
}
|
625
vendor/github.com/rivo/tview/inputfield.go
generated
vendored
Normal file
625
vendor/github.com/rivo/tview/inputfield.go
generated
vendored
Normal file
@@ -0,0 +1,625 @@
|
||||
package tview
|
||||
|
||||
import (
|
||||
"math"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/gdamore/tcell"
|
||||
)
|
||||
|
||||
// InputField is a one-line box (three lines if there is a title) where the
|
||||
// user can enter text. Use SetAcceptanceFunc() to accept or reject input,
|
||||
// SetChangedFunc() to listen for changes, and SetMaskCharacter() to hide input
|
||||
// from onlookers (e.g. for password input).
|
||||
//
|
||||
// The following keys can be used for navigation and editing:
|
||||
//
|
||||
// - Left arrow: Move left by one character.
|
||||
// - Right arrow: Move right by one character.
|
||||
// - Home, Ctrl-A, Alt-a: Move to the beginning of the line.
|
||||
// - End, Ctrl-E, Alt-e: Move to the end of the line.
|
||||
// - Alt-left, Alt-b: Move left by one word.
|
||||
// - Alt-right, Alt-f: Move right by one word.
|
||||
// - Backspace: Delete the character before the cursor.
|
||||
// - Delete: Delete the character after the cursor.
|
||||
// - Ctrl-K: Delete from the cursor to the end of the line.
|
||||
// - Ctrl-W: Delete the last word before the cursor.
|
||||
// - Ctrl-U: Delete the entire line.
|
||||
//
|
||||
// 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 label area. A value of 0 means use the width of
|
||||
// the label text.
|
||||
labelWidth int
|
||||
|
||||
// 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
|
||||
|
||||
// The cursor position as a byte index into the text string.
|
||||
cursorPos int
|
||||
|
||||
// An optional autocomplete function which receives the current text of the
|
||||
// input field and returns a slice of strings to be displayed in a drop-down
|
||||
// selection.
|
||||
autocomplete func(text string) []string
|
||||
|
||||
// The List object which shows the selectable autocomplete entries. If not
|
||||
// nil, the list's main texts represent the current autocomplete entries.
|
||||
autocompleteList *List
|
||||
autocompleteListMutex sync.Mutex
|
||||
|
||||
// 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)
|
||||
|
||||
// A callback function set by the Form class and called when the user leaves
|
||||
// this form item.
|
||||
finished func(tcell.Key)
|
||||
|
||||
fieldX int // The x-coordinate of the input field as determined during the last call to Draw().
|
||||
offset int // The number of bytes of the text string skipped ahead while drawing.
|
||||
}
|
||||
|
||||
// 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
|
||||
i.cursorPos = len(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
|
||||
}
|
||||
|
||||
// SetLabelWidth sets the screen width of the label. A value of 0 will cause the
|
||||
// primitive to use the width of the label string.
|
||||
func (i *InputField) SetLabelWidth(width int) *InputField {
|
||||
i.labelWidth = width
|
||||
return i
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// SetPlaceholderTextColor sets the text color of placeholder text.
|
||||
func (i *InputField) SetPlaceholderTextColor(color tcell.Color) *InputField {
|
||||
i.placeholderTextColor = color
|
||||
return i
|
||||
}
|
||||
|
||||
// SetFormAttributes sets attributes shared by all form items.
|
||||
func (i *InputField) SetFormAttributes(labelWidth int, labelColor, bgColor, fieldTextColor, fieldBgColor tcell.Color) FormItem {
|
||||
i.labelWidth = labelWidth
|
||||
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
|
||||
}
|
||||
|
||||
// SetAutocompleteFunc sets an autocomplete callback function which may return
|
||||
// strings to be selected from a drop-down based on the current text of the
|
||||
// input field. The drop-down appears only if len(entries) > 0. The callback is
|
||||
// invoked in this function and whenever the current text changes or when
|
||||
// Autocomplete() is called. Entries are cleared when the user selects an entry
|
||||
// or presses Escape.
|
||||
func (i *InputField) SetAutocompleteFunc(callback func(currentText string) (entries []string)) *InputField {
|
||||
i.autocomplete = callback
|
||||
i.Autocomplete()
|
||||
return i
|
||||
}
|
||||
|
||||
// Autocomplete invokes the autocomplete callback (if there is one). If the
|
||||
// length of the returned autocomplete entries slice is greater than 0, the
|
||||
// input field will present the user with a corresponding drop-down list the
|
||||
// next time the input field is drawn.
|
||||
//
|
||||
// It is safe to call this function from any goroutine. Note that the input
|
||||
// field is not redrawn automatically unless called from the main goroutine
|
||||
// (e.g. in response to events).
|
||||
func (i *InputField) Autocomplete() *InputField {
|
||||
i.autocompleteListMutex.Lock()
|
||||
defer i.autocompleteListMutex.Unlock()
|
||||
if i.autocomplete == nil {
|
||||
return i
|
||||
}
|
||||
|
||||
// Do we have any autocomplete entries?
|
||||
entries := i.autocomplete(i.text)
|
||||
if len(entries) == 0 {
|
||||
// No entries, no list.
|
||||
i.autocompleteList = nil
|
||||
return i
|
||||
}
|
||||
|
||||
// Make a list if we have none.
|
||||
if i.autocompleteList == nil {
|
||||
i.autocompleteList = NewList()
|
||||
i.autocompleteList.ShowSecondaryText(false).
|
||||
SetMainTextColor(Styles.PrimitiveBackgroundColor).
|
||||
SetSelectedTextColor(Styles.PrimitiveBackgroundColor).
|
||||
SetSelectedBackgroundColor(Styles.PrimaryTextColor).
|
||||
SetHighlightFullLine(true).
|
||||
SetBackgroundColor(Styles.MoreContrastBackgroundColor)
|
||||
}
|
||||
|
||||
// Fill it with the entries.
|
||||
currentEntry := -1
|
||||
i.autocompleteList.Clear()
|
||||
for index, entry := range entries {
|
||||
i.autocompleteList.AddItem(entry, "", 0, nil)
|
||||
if currentEntry < 0 && entry == i.text {
|
||||
currentEntry = index
|
||||
}
|
||||
}
|
||||
|
||||
// Set the selection if we have one.
|
||||
if currentEntry >= 0 {
|
||||
i.autocompleteList.SetCurrentItem(currentEntry)
|
||||
}
|
||||
|
||||
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 sets a callback invoked when the user leaves this form item.
|
||||
func (i *InputField) SetFinishedFunc(handler func(key tcell.Key)) FormItem {
|
||||
i.finished = handler
|
||||
return i
|
||||
}
|
||||
|
||||
// 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.
|
||||
if i.labelWidth > 0 {
|
||||
labelWidth := i.labelWidth
|
||||
if labelWidth > rightLimit-x {
|
||||
labelWidth = rightLimit - x
|
||||
}
|
||||
Print(screen, i.label, x, y, labelWidth, AlignLeft, i.labelColor)
|
||||
x += labelWidth
|
||||
} else {
|
||||
_, drawnWidth := Print(screen, i.label, x, y, rightLimit-x, AlignLeft, i.labelColor)
|
||||
x += drawnWidth
|
||||
}
|
||||
|
||||
// Draw input area.
|
||||
i.fieldX = x
|
||||
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)
|
||||
}
|
||||
|
||||
// Text.
|
||||
var cursorScreenPos int
|
||||
text := i.text
|
||||
if text == "" && i.placeholder != "" {
|
||||
// Draw placeholder text.
|
||||
Print(screen, Escape(i.placeholder), x, y, fieldWidth, AlignLeft, i.placeholderTextColor)
|
||||
i.offset = 0
|
||||
} else {
|
||||
// Draw entered text.
|
||||
if i.maskCharacter > 0 {
|
||||
text = strings.Repeat(string(i.maskCharacter), utf8.RuneCountInString(i.text))
|
||||
}
|
||||
if fieldWidth >= stringWidth(text) {
|
||||
// We have enough space for the full text.
|
||||
Print(screen, Escape(text), x, y, fieldWidth, AlignLeft, i.fieldTextColor)
|
||||
i.offset = 0
|
||||
iterateString(text, func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool {
|
||||
if textPos >= i.cursorPos {
|
||||
return true
|
||||
}
|
||||
cursorScreenPos += screenWidth
|
||||
return false
|
||||
})
|
||||
} else {
|
||||
// The text doesn't fit. Where is the cursor?
|
||||
if i.cursorPos < 0 {
|
||||
i.cursorPos = 0
|
||||
} else if i.cursorPos > len(text) {
|
||||
i.cursorPos = len(text)
|
||||
}
|
||||
// Shift the text so the cursor is inside the field.
|
||||
var shiftLeft int
|
||||
if i.offset > i.cursorPos {
|
||||
i.offset = i.cursorPos
|
||||
} else if subWidth := stringWidth(text[i.offset:i.cursorPos]); subWidth > fieldWidth-1 {
|
||||
shiftLeft = subWidth - fieldWidth + 1
|
||||
}
|
||||
currentOffset := i.offset
|
||||
iterateString(text, func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool {
|
||||
if textPos >= currentOffset {
|
||||
if shiftLeft > 0 {
|
||||
i.offset = textPos + textWidth
|
||||
shiftLeft -= screenWidth
|
||||
} else {
|
||||
if textPos+textWidth > i.cursorPos {
|
||||
return true
|
||||
}
|
||||
cursorScreenPos += screenWidth
|
||||
}
|
||||
}
|
||||
return false
|
||||
})
|
||||
Print(screen, Escape(text[i.offset:]), x, y, fieldWidth, AlignLeft, i.fieldTextColor)
|
||||
}
|
||||
}
|
||||
|
||||
// Draw autocomplete list.
|
||||
i.autocompleteListMutex.Lock()
|
||||
defer i.autocompleteListMutex.Unlock()
|
||||
if i.autocompleteList != nil {
|
||||
// How much space do we need?
|
||||
lheight := i.autocompleteList.GetItemCount()
|
||||
lwidth := 0
|
||||
for index := 0; index < lheight; index++ {
|
||||
entry, _ := i.autocompleteList.GetItemText(index)
|
||||
width := TaggedStringWidth(entry)
|
||||
if width > lwidth {
|
||||
lwidth = width
|
||||
}
|
||||
}
|
||||
|
||||
// We prefer to drop down but if there is no space, maybe drop up?
|
||||
lx := x
|
||||
ly := y + 1
|
||||
_, 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
|
||||
}
|
||||
i.autocompleteList.SetRect(lx, ly, lwidth, lheight)
|
||||
i.autocompleteList.Draw(screen)
|
||||
}
|
||||
|
||||
// Set cursor.
|
||||
if i.focus.HasFocus() {
|
||||
screen.ShowCursor(x+cursorScreenPos, 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.Autocomplete()
|
||||
if i.changed != nil {
|
||||
i.changed(i.text)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Movement functions.
|
||||
home := func() { i.cursorPos = 0 }
|
||||
end := func() { i.cursorPos = len(i.text) }
|
||||
moveLeft := func() {
|
||||
iterateStringReverse(i.text[:i.cursorPos], func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool {
|
||||
i.cursorPos -= textWidth
|
||||
return true
|
||||
})
|
||||
}
|
||||
moveRight := func() {
|
||||
iterateString(i.text[i.cursorPos:], func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool {
|
||||
i.cursorPos += textWidth
|
||||
return true
|
||||
})
|
||||
}
|
||||
moveWordLeft := func() {
|
||||
i.cursorPos = len(regexp.MustCompile(`\S+\s*$`).ReplaceAllString(i.text[:i.cursorPos], ""))
|
||||
}
|
||||
moveWordRight := func() {
|
||||
i.cursorPos = len(i.text) - len(regexp.MustCompile(`^\s*\S+\s*`).ReplaceAllString(i.text[i.cursorPos:], ""))
|
||||
}
|
||||
|
||||
// Add character function. Returns whether or not the rune character is
|
||||
// accepted.
|
||||
add := func(r rune) bool {
|
||||
newText := i.text[:i.cursorPos] + string(r) + i.text[i.cursorPos:]
|
||||
if i.accept != nil && !i.accept(newText, r) {
|
||||
return false
|
||||
}
|
||||
i.text = newText
|
||||
i.cursorPos += len(string(r))
|
||||
return true
|
||||
}
|
||||
|
||||
// Finish up.
|
||||
finish := func(key tcell.Key) {
|
||||
if i.done != nil {
|
||||
i.done(key)
|
||||
}
|
||||
if i.finished != nil {
|
||||
i.finished(key)
|
||||
}
|
||||
}
|
||||
|
||||
// Process key event.
|
||||
i.autocompleteListMutex.Lock()
|
||||
defer i.autocompleteListMutex.Unlock()
|
||||
switch key := event.Key(); key {
|
||||
case tcell.KeyRune: // Regular character.
|
||||
if event.Modifiers()&tcell.ModAlt > 0 {
|
||||
// We accept some Alt- key combinations.
|
||||
switch event.Rune() {
|
||||
case 'a': // Home.
|
||||
home()
|
||||
case 'e': // End.
|
||||
end()
|
||||
case 'b': // Move word left.
|
||||
moveWordLeft()
|
||||
case 'f': // Move word right.
|
||||
moveWordRight()
|
||||
default:
|
||||
if !add(event.Rune()) {
|
||||
return
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Other keys are simply accepted as regular characters.
|
||||
if !add(event.Rune()) {
|
||||
return
|
||||
}
|
||||
}
|
||||
case tcell.KeyCtrlU: // Delete all.
|
||||
i.text = ""
|
||||
i.cursorPos = 0
|
||||
case tcell.KeyCtrlK: // Delete until the end of the line.
|
||||
i.text = i.text[:i.cursorPos]
|
||||
case tcell.KeyCtrlW: // Delete last word.
|
||||
lastWord := regexp.MustCompile(`\S+\s*$`)
|
||||
newText := lastWord.ReplaceAllString(i.text[:i.cursorPos], "") + i.text[i.cursorPos:]
|
||||
i.cursorPos -= len(i.text) - len(newText)
|
||||
i.text = newText
|
||||
case tcell.KeyBackspace, tcell.KeyBackspace2: // Delete character before the cursor.
|
||||
iterateStringReverse(i.text[:i.cursorPos], func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool {
|
||||
i.text = i.text[:textPos] + i.text[textPos+textWidth:]
|
||||
i.cursorPos -= textWidth
|
||||
return true
|
||||
})
|
||||
if i.offset >= i.cursorPos {
|
||||
i.offset = 0
|
||||
}
|
||||
case tcell.KeyDelete: // Delete character after the cursor.
|
||||
iterateString(i.text[i.cursorPos:], func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool {
|
||||
i.text = i.text[:i.cursorPos] + i.text[i.cursorPos+textWidth:]
|
||||
return true
|
||||
})
|
||||
case tcell.KeyLeft:
|
||||
if event.Modifiers()&tcell.ModAlt > 0 {
|
||||
moveWordLeft()
|
||||
} else {
|
||||
moveLeft()
|
||||
}
|
||||
case tcell.KeyRight:
|
||||
if event.Modifiers()&tcell.ModAlt > 0 {
|
||||
moveWordRight()
|
||||
} else {
|
||||
moveRight()
|
||||
}
|
||||
case tcell.KeyHome, tcell.KeyCtrlA:
|
||||
home()
|
||||
case tcell.KeyEnd, tcell.KeyCtrlE:
|
||||
end()
|
||||
case tcell.KeyEnter, tcell.KeyEscape: // We might be done.
|
||||
if i.autocompleteList != nil {
|
||||
i.autocompleteList = nil
|
||||
} else {
|
||||
finish(key)
|
||||
}
|
||||
case tcell.KeyDown, tcell.KeyTab: // Autocomplete selection.
|
||||
if i.autocompleteList != nil {
|
||||
count := i.autocompleteList.GetItemCount()
|
||||
newEntry := i.autocompleteList.GetCurrentItem() + 1
|
||||
if newEntry >= count {
|
||||
newEntry = 0
|
||||
}
|
||||
i.autocompleteList.SetCurrentItem(newEntry)
|
||||
currentText, _ = i.autocompleteList.GetItemText(newEntry) // Don't trigger changed function twice.
|
||||
i.SetText(currentText)
|
||||
} else {
|
||||
finish(key)
|
||||
}
|
||||
case tcell.KeyUp, tcell.KeyBacktab: // Autocomplete selection.
|
||||
if i.autocompleteList != nil {
|
||||
newEntry := i.autocompleteList.GetCurrentItem() - 1
|
||||
if newEntry < 0 {
|
||||
newEntry = i.autocompleteList.GetItemCount() - 1
|
||||
}
|
||||
i.autocompleteList.SetCurrentItem(newEntry)
|
||||
currentText, _ = i.autocompleteList.GetItemText(newEntry) // Don't trigger changed function twice.
|
||||
i.SetText(currentText)
|
||||
} else {
|
||||
finish(key)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// MouseHandler returns the mouse handler for this primitive.
|
||||
func (i *InputField) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
|
||||
return i.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
|
||||
x, y := event.Position()
|
||||
_, rectY, _, _ := i.GetInnerRect()
|
||||
if !i.InRect(x, y) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Process mouse event.
|
||||
if action == MouseLeftClick && y == rectY {
|
||||
// Determine where to place the cursor.
|
||||
if x >= i.fieldX {
|
||||
if !iterateString(i.text[i.offset:], func(main rune, comb []rune, textPos int, textWidth int, screenPos int, screenWidth int) bool {
|
||||
if x-i.fieldX < screenPos+screenWidth {
|
||||
i.cursorPos = textPos + i.offset
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}) {
|
||||
i.cursorPos = len(i.text)
|
||||
}
|
||||
}
|
||||
setFocus(i)
|
||||
consumed = true
|
||||
}
|
||||
|
||||
return
|
||||
})
|
||||
}
|
628
vendor/github.com/rivo/tview/list.go
generated
vendored
Normal file
628
vendor/github.com/rivo/tview/list.go
generated
vendored
Normal file
@@ -0,0 +1,628 @@
|
||||
package tview
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/gdamore/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
|
||||
|
||||
// If true, the selection is only shown when the list has focus.
|
||||
selectedFocusOnly bool
|
||||
|
||||
// If true, the entire row is highlighted when selected.
|
||||
highlightFullLine bool
|
||||
|
||||
// Whether or not navigating the list will wrap around.
|
||||
wrapAround bool
|
||||
|
||||
// The number of list items skipped at the top before the first item is drawn.
|
||||
offset int
|
||||
|
||||
// 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,
|
||||
wrapAround: 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, starting at 0
|
||||
// for the first item. If a negative index is provided, items are referred to
|
||||
// from the back (-1 = last item, -2 = second-to-last item, and so on). Out of
|
||||
// range indices are clamped to the beginning/end.
|
||||
//
|
||||
// Calling this function triggers a "changed" event if the selection changes.
|
||||
func (l *List) SetCurrentItem(index int) *List {
|
||||
if index < 0 {
|
||||
index = len(l.items) + index
|
||||
}
|
||||
if index >= len(l.items) {
|
||||
index = len(l.items) - 1
|
||||
}
|
||||
if index < 0 {
|
||||
index = 0
|
||||
}
|
||||
|
||||
if index != l.currentItem && l.changed != nil {
|
||||
item := l.items[index]
|
||||
l.changed(index, item.MainText, item.SecondaryText, item.Shortcut)
|
||||
}
|
||||
|
||||
l.currentItem = index
|
||||
|
||||
return l
|
||||
}
|
||||
|
||||
// GetCurrentItem returns the index of the currently selected list item,
|
||||
// starting at 0 for the first item.
|
||||
func (l *List) GetCurrentItem() int {
|
||||
return l.currentItem
|
||||
}
|
||||
|
||||
// RemoveItem removes the item with the given index (starting at 0) from the
|
||||
// list. If a negative index is provided, items are referred to from the back
|
||||
// (-1 = last item, -2 = second-to-last item, and so on). Out of range indices
|
||||
// are clamped to the beginning/end, i.e. unless the list is empty, an item is
|
||||
// always removed.
|
||||
//
|
||||
// The currently selected item is shifted accordingly. If it is the one that is
|
||||
// removed, a "changed" event is fired.
|
||||
func (l *List) RemoveItem(index int) *List {
|
||||
if len(l.items) == 0 {
|
||||
return l
|
||||
}
|
||||
|
||||
// Adjust index.
|
||||
if index < 0 {
|
||||
index = len(l.items) + index
|
||||
}
|
||||
if index >= len(l.items) {
|
||||
index = len(l.items) - 1
|
||||
}
|
||||
if index < 0 {
|
||||
index = 0
|
||||
}
|
||||
|
||||
// Remove item.
|
||||
l.items = append(l.items[:index], l.items[index+1:]...)
|
||||
|
||||
// If there is nothing left, we're done.
|
||||
if len(l.items) == 0 {
|
||||
return l
|
||||
}
|
||||
|
||||
// Shift current item.
|
||||
previousCurrentItem := l.currentItem
|
||||
if l.currentItem >= index {
|
||||
l.currentItem--
|
||||
}
|
||||
|
||||
// Fire "changed" event for removed items.
|
||||
if previousCurrentItem == index && l.changed != nil {
|
||||
item := l.items[l.currentItem]
|
||||
l.changed(l.currentItem, item.MainText, item.SecondaryText, item.Shortcut)
|
||||
}
|
||||
|
||||
return l
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// SetSelectedFocusOnly sets a flag which determines when the currently selected
|
||||
// list item is highlighted. If set to true, selected items are only highlighted
|
||||
// when the list has focus. If set to false, they are always highlighted.
|
||||
func (l *List) SetSelectedFocusOnly(focusOnly bool) *List {
|
||||
l.selectedFocusOnly = focusOnly
|
||||
return l
|
||||
}
|
||||
|
||||
// SetHighlightFullLine sets a flag which determines whether the colored
|
||||
// background of selected items spans the entire width of the view. If set to
|
||||
// true, the highlight spans the entire view. If set to false, only the text of
|
||||
// the selected item from beginning to end is highlighted.
|
||||
func (l *List) SetHighlightFullLine(highlight bool) *List {
|
||||
l.highlightFullLine = highlight
|
||||
return l
|
||||
}
|
||||
|
||||
// ShowSecondaryText determines whether or not to show secondary item texts.
|
||||
func (l *List) ShowSecondaryText(show bool) *List {
|
||||
l.showSecondaryText = show
|
||||
return l
|
||||
}
|
||||
|
||||
// SetWrapAround sets the flag that determines whether navigating the list will
|
||||
// wrap around. That is, navigating downwards on the last item will move the
|
||||
// selection to the first item (similarly in the other direction). If set to
|
||||
// false, the selection won't change when navigating downwards on the last item
|
||||
// or navigating upwards on the first item.
|
||||
func (l *List) SetWrapAround(wrapAround bool) *List {
|
||||
l.wrapAround = wrapAround
|
||||
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(index int, mainText string, secondaryText string, shortcut 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 calls InsertItem() with an index of -1.
|
||||
func (l *List) AddItem(mainText, secondaryText string, shortcut rune, selected func()) *List {
|
||||
l.InsertItem(-1, mainText, secondaryText, shortcut, selected)
|
||||
return l
|
||||
}
|
||||
|
||||
// InsertItem adds a new item to the list at the specified index. An index of 0
|
||||
// will insert the item at the beginning, an index of 1 before the second item,
|
||||
// and so on. An index of GetItemCount() or higher will insert the item at the
|
||||
// end of the list. Negative indices are also allowed: An index of -1 will
|
||||
// insert the item at the end of the list, an index of -2 before the last item,
|
||||
// and so on. An index of -GetItemCount()-1 or lower will insert the item at the
|
||||
// beginning.
|
||||
//
|
||||
// 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 callback is needed or if all events are handled
|
||||
// through the selected callback set with SetSelectedFunc().
|
||||
//
|
||||
// The currently selected item will shift its position accordingly. If the list
|
||||
// was previously empty, a "changed" event is fired because the new item becomes
|
||||
// selected.
|
||||
func (l *List) InsertItem(index int, mainText, secondaryText string, shortcut rune, selected func()) *List {
|
||||
item := &listItem{
|
||||
MainText: mainText,
|
||||
SecondaryText: secondaryText,
|
||||
Shortcut: shortcut,
|
||||
Selected: selected,
|
||||
}
|
||||
|
||||
// Shift index to range.
|
||||
if index < 0 {
|
||||
index = len(l.items) + index + 1
|
||||
}
|
||||
if index < 0 {
|
||||
index = 0
|
||||
} else if index > len(l.items) {
|
||||
index = len(l.items)
|
||||
}
|
||||
|
||||
// Shift current item.
|
||||
if l.currentItem < len(l.items) && l.currentItem >= index {
|
||||
l.currentItem++
|
||||
}
|
||||
|
||||
// Insert item (make space for the new item, then shift and insert).
|
||||
l.items = append(l.items, nil)
|
||||
if index < len(l.items)-1 { // -1 because l.items has already grown by one item.
|
||||
copy(l.items[index+1:], l.items[index:])
|
||||
}
|
||||
l.items[index] = item
|
||||
|
||||
// Fire a "change" event for the first item in the list.
|
||||
if len(l.items) == 1 && l.changed != nil {
|
||||
item := l.items[0]
|
||||
l.changed(0, item.MainText, item.SecondaryText, item.Shortcut)
|
||||
}
|
||||
|
||||
return l
|
||||
}
|
||||
|
||||
// GetItemCount returns the number of items in the list.
|
||||
func (l *List) GetItemCount() int {
|
||||
return len(l.items)
|
||||
}
|
||||
|
||||
// GetItemText returns an item's texts (main and secondary). Panics if the index
|
||||
// is out of range.
|
||||
func (l *List) GetItemText(index int) (main, secondary string) {
|
||||
return l.items[index].MainText, l.items[index].SecondaryText
|
||||
}
|
||||
|
||||
// SetItemText sets an item's main and secondary text. Panics if the index is
|
||||
// out of range.
|
||||
func (l *List) SetItemText(index int, main, secondary string) *List {
|
||||
item := l.items[index]
|
||||
item.MainText = main
|
||||
item.SecondaryText = secondary
|
||||
return l
|
||||
}
|
||||
|
||||
// FindItems searches the main and secondary texts for the given strings and
|
||||
// returns a list of item indices in which those strings are found. One of the
|
||||
// two search strings may be empty, it will then be ignored. Indices are always
|
||||
// returned in ascending order.
|
||||
//
|
||||
// If mustContainBoth is set to true, mainSearch must be contained in the main
|
||||
// text AND secondarySearch must be contained in the secondary text. If it is
|
||||
// false, only one of the two search strings must be contained.
|
||||
//
|
||||
// Set ignoreCase to true for case-insensitive search.
|
||||
func (l *List) FindItems(mainSearch, secondarySearch string, mustContainBoth, ignoreCase bool) (indices []int) {
|
||||
if mainSearch == "" && secondarySearch == "" {
|
||||
return
|
||||
}
|
||||
|
||||
if ignoreCase {
|
||||
mainSearch = strings.ToLower(mainSearch)
|
||||
secondarySearch = strings.ToLower(secondarySearch)
|
||||
}
|
||||
|
||||
for index, item := range l.items {
|
||||
mainText := item.MainText
|
||||
secondaryText := item.SecondaryText
|
||||
if ignoreCase {
|
||||
mainText = strings.ToLower(mainText)
|
||||
secondaryText = strings.ToLower(secondaryText)
|
||||
}
|
||||
|
||||
// strings.Contains() always returns true for a "" search.
|
||||
mainContained := strings.Contains(mainText, mainSearch)
|
||||
secondaryContained := strings.Contains(secondaryText, secondarySearch)
|
||||
if mustContainBoth && mainContained && secondaryContained ||
|
||||
!mustContainBoth && (mainText != "" && mainContained || secondaryText != "" && secondaryContained) {
|
||||
indices = append(indices, index)
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// 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
|
||||
_, totalHeight := screen.Size()
|
||||
if bottomLimit > totalHeight {
|
||||
bottomLimit = totalHeight
|
||||
}
|
||||
|
||||
// Do we show any shortcuts?
|
||||
var showShortcuts bool
|
||||
for _, item := range l.items {
|
||||
if item.Shortcut != 0 {
|
||||
showShortcuts = true
|
||||
x += 4
|
||||
width -= 4
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Adjust offset to keep the current selection in view.
|
||||
if l.currentItem < l.offset {
|
||||
l.offset = l.currentItem
|
||||
} else if l.showSecondaryText {
|
||||
if 2*(l.currentItem-l.offset) >= height-1 {
|
||||
l.offset = (2*l.currentItem + 3 - height) / 2
|
||||
}
|
||||
} else {
|
||||
if l.currentItem-l.offset >= height {
|
||||
l.offset = l.currentItem + 1 - height
|
||||
}
|
||||
}
|
||||
|
||||
// Draw the list items.
|
||||
for index, item := range l.items {
|
||||
if index < l.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 && (!l.selectedFocusOnly || l.HasFocus()) {
|
||||
textWidth := width
|
||||
if !l.highlightFullLine {
|
||||
if w := TaggedStringWidth(item.MainText); w < textWidth {
|
||||
textWidth = w
|
||||
}
|
||||
}
|
||||
|
||||
for bx := 0; bx < textWidth; 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)) {
|
||||
if event.Key() == tcell.KeyEscape {
|
||||
if l.done != nil {
|
||||
l.done()
|
||||
}
|
||||
return
|
||||
} else if len(l.items) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
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:
|
||||
_, _, _, height := l.GetInnerRect()
|
||||
l.currentItem += height
|
||||
case tcell.KeyPgUp:
|
||||
_, _, _, height := l.GetInnerRect()
|
||||
l.currentItem -= height
|
||||
case tcell.KeyEnter:
|
||||
if l.currentItem >= 0 && l.currentItem < len(l.items) {
|
||||
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.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 {
|
||||
if l.wrapAround {
|
||||
l.currentItem = len(l.items) - 1
|
||||
} else {
|
||||
l.currentItem = 0
|
||||
}
|
||||
} else if l.currentItem >= len(l.items) {
|
||||
if l.wrapAround {
|
||||
l.currentItem = 0
|
||||
} else {
|
||||
l.currentItem = len(l.items) - 1
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// indexAtPoint returns the index of the list item found at the given position
|
||||
// or a negative value if there is no such list item.
|
||||
func (l *List) indexAtPoint(x, y int) int {
|
||||
rectX, rectY, width, height := l.GetInnerRect()
|
||||
if rectX < 0 || rectX >= rectX+width || y < rectY || y >= rectY+height {
|
||||
return -1
|
||||
}
|
||||
|
||||
index := y - rectY
|
||||
if l.showSecondaryText {
|
||||
index /= 2
|
||||
}
|
||||
index += l.offset
|
||||
|
||||
if index >= len(l.items) {
|
||||
return -1
|
||||
}
|
||||
return index
|
||||
}
|
||||
|
||||
// MouseHandler returns the mouse handler for this primitive.
|
||||
func (l *List) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
|
||||
return l.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
|
||||
if !l.InRect(event.Position()) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Process mouse event.
|
||||
switch action {
|
||||
case MouseLeftClick:
|
||||
setFocus(l)
|
||||
index := l.indexAtPoint(event.Position())
|
||||
if index != -1 {
|
||||
item := l.items[index]
|
||||
if item.Selected != nil {
|
||||
item.Selected()
|
||||
}
|
||||
if l.selected != nil {
|
||||
l.selected(index, item.MainText, item.SecondaryText, item.Shortcut)
|
||||
}
|
||||
if index != l.currentItem && l.changed != nil {
|
||||
l.changed(index, item.MainText, item.SecondaryText, item.Shortcut)
|
||||
}
|
||||
l.currentItem = index
|
||||
}
|
||||
consumed = true
|
||||
case MouseScrollUp:
|
||||
if l.offset > 0 {
|
||||
l.offset--
|
||||
}
|
||||
consumed = true
|
||||
case MouseScrollDown:
|
||||
lines := len(l.items) - l.offset
|
||||
if l.showSecondaryText {
|
||||
lines *= 2
|
||||
}
|
||||
if _, _, _, height := l.GetInnerRect(); lines > height {
|
||||
l.offset++
|
||||
}
|
||||
consumed = true
|
||||
}
|
||||
|
||||
return
|
||||
})
|
||||
}
|
190
vendor/github.com/rivo/tview/modal.go
generated
vendored
Normal file
190
vendor/github.com/rivo/tview/modal.go
generated
vendored
Normal file
@@ -0,0 +1,190 @@
|
||||
package tview
|
||||
|
||||
import (
|
||||
"github.com/gdamore/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 frame 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.form.SetCancelFunc(func() {
|
||||
if m.done != nil {
|
||||
m.done(-1, "")
|
||||
}
|
||||
})
|
||||
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
|
||||
}
|
||||
|
||||
// SetBackgroundColor sets the color of the modal frame background.
|
||||
func (m *Modal) SetBackgroundColor(color tcell.Color) *Modal {
|
||||
m.form.SetBackgroundColor(color)
|
||||
m.frame.SetBackgroundColor(color)
|
||||
return m
|
||||
}
|
||||
|
||||
// SetTextColor sets the color of the message text.
|
||||
func (m *Modal) SetTextColor(color tcell.Color) *Modal {
|
||||
m.textColor = color
|
||||
return m
|
||||
}
|
||||
|
||||
// SetButtonBackgroundColor sets the background color of the buttons.
|
||||
func (m *Modal) SetButtonBackgroundColor(color tcell.Color) *Modal {
|
||||
m.form.SetButtonBackgroundColor(color)
|
||||
return m
|
||||
}
|
||||
|
||||
// SetButtonTextColor sets the color of the button texts.
|
||||
func (m *Modal) SetButtonTextColor(color tcell.Color) *Modal {
|
||||
m.form.SetButtonTextColor(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)
|
||||
}
|
||||
})
|
||||
button := m.form.GetButton(m.form.GetButtonCount() - 1)
|
||||
button.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
|
||||
switch event.Key() {
|
||||
case tcell.KeyDown, tcell.KeyRight:
|
||||
return tcell.NewEventKey(tcell.KeyTab, 0, tcell.ModNone)
|
||||
case tcell.KeyUp, tcell.KeyLeft:
|
||||
return tcell.NewEventKey(tcell.KeyBacktab, 0, tcell.ModNone)
|
||||
}
|
||||
return event
|
||||
})
|
||||
}(index, label)
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
// ClearButtons removes all buttons from the window.
|
||||
func (m *Modal) ClearButtons() *Modal {
|
||||
m.form.ClearButtons()
|
||||
return m
|
||||
}
|
||||
|
||||
// SetFocus shifts the focus to the button with the given index.
|
||||
func (m *Modal) SetFocus(index int) *Modal {
|
||||
m.form.SetFocus(index)
|
||||
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 += TaggedStringWidth(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)
|
||||
}
|
||||
|
||||
// MouseHandler returns the mouse handler for this primitive.
|
||||
func (m *Modal) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
|
||||
return m.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
|
||||
// Pass mouse events on to the form.
|
||||
consumed, capture = m.form.MouseHandler()(action, event, setFocus)
|
||||
if !consumed && action == MouseLeftClick && m.InRect(event.Position()) {
|
||||
setFocus(m)
|
||||
consumed = true
|
||||
}
|
||||
return
|
||||
})
|
||||
}
|
302
vendor/github.com/rivo/tview/pages.go
generated
vendored
Normal file
302
vendor/github.com/rivo/tview/pages.go
generated
vendored
Normal file
@@ -0,0 +1,302 @@
|
||||
package tview
|
||||
|
||||
import (
|
||||
"github.com/gdamore/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. (Visible) pages are drawn from back to front.
|
||||
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
|
||||
}
|
||||
|
||||
// GetPageCount returns the number of pages currently stored in this object.
|
||||
func (p *Pages) GetPageCount() int {
|
||||
return len(p.pages)
|
||||
}
|
||||
|
||||
// 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 so always specify a non-empty
|
||||
// name.
|
||||
//
|
||||
// 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 {
|
||||
hasFocus := p.HasFocus()
|
||||
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 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. If that page was the only
|
||||
// visible page, visibility is assigned to the last page.
|
||||
func (p *Pages) RemovePage(name string) *Pages {
|
||||
var isVisible bool
|
||||
hasFocus := p.HasFocus()
|
||||
for index, page := range p.pages {
|
||||
if page.Name == name {
|
||||
isVisible = page.Visible
|
||||
p.pages = append(p.pages[:index], p.pages[index+1:]...)
|
||||
if page.Visible && p.changed != nil {
|
||||
p.changed()
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
if isVisible {
|
||||
for index, page := range p.pages {
|
||||
if index < len(p.pages)-1 {
|
||||
if page.Visible {
|
||||
break // There is a remaining visible page.
|
||||
}
|
||||
} else {
|
||||
page.Visible = true // We need at least one visible page.
|
||||
}
|
||||
}
|
||||
}
|
||||
if hasFocus {
|
||||
p.Focus(p.setFocus)
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
// GetFrontPage returns the front-most visible page. If there are no visible
|
||||
// pages, ("", nil) is returned.
|
||||
func (p *Pages) GetFrontPage() (name string, item Primitive) {
|
||||
for index := len(p.pages) - 1; index >= 0; index-- {
|
||||
if p.pages[index].Visible {
|
||||
return p.pages[index].Name, p.pages[index].Item
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
|
||||
// MouseHandler returns the mouse handler for this primitive.
|
||||
func (p *Pages) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
|
||||
return p.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
|
||||
if !p.InRect(event.Position()) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Pass mouse events along to the last visible page item that takes it.
|
||||
for index := len(p.pages) - 1; index >= 0; index-- {
|
||||
page := p.pages[index]
|
||||
if page.Visible {
|
||||
consumed, capture = page.Item.MouseHandler()(action, event, setFocus)
|
||||
if consumed {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
})
|
||||
}
|
57
vendor/github.com/rivo/tview/primitive.go
generated
vendored
Normal file
57
vendor/github.com/rivo/tview/primitive.go
generated
vendored
Normal file
@@ -0,0 +1,57 @@
|
||||
package tview
|
||||
|
||||
import "github.com/gdamore/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))
|
||||
|
||||
// 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
|
||||
|
||||
// MouseHandler returns a handler which receives mouse events.
|
||||
// It is called by the Application class.
|
||||
//
|
||||
// A value of nil may also be returned to stop the downward propagation of
|
||||
// mouse events.
|
||||
//
|
||||
// The Box class provides functionality to intercept mouse events. If you
|
||||
// subclass from Box, it is recommended that you wrap your handler using
|
||||
// Box.WrapMouseHandler() so you inherit that functionality.
|
||||
MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive)
|
||||
}
|
296
vendor/github.com/rivo/tview/semigraphics.go
generated
vendored
Normal file
296
vendor/github.com/rivo/tview/semigraphics.go
generated
vendored
Normal file
@@ -0,0 +1,296 @@
|
||||
package tview
|
||||
|
||||
import "github.com/gdamore/tcell"
|
||||
|
||||
// Semigraphics provides an easy way to access unicode characters for drawing.
|
||||
//
|
||||
// Named like the unicode characters, 'Semigraphics'-prefix used if unicode block
|
||||
// isn't prefixed itself.
|
||||
const (
|
||||
// Block: General Punctation U+2000-U+206F (http://unicode.org/charts/PDF/U2000.pdf)
|
||||
SemigraphicsHorizontalEllipsis rune = '\u2026' // …
|
||||
|
||||
// Block: Box Drawing U+2500-U+257F (http://unicode.org/charts/PDF/U2500.pdf)
|
||||
BoxDrawingsLightHorizontal rune = '\u2500' // ─
|
||||
BoxDrawingsHeavyHorizontal rune = '\u2501' // ━
|
||||
BoxDrawingsLightVertical rune = '\u2502' // │
|
||||
BoxDrawingsHeavyVertical rune = '\u2503' // ┃
|
||||
BoxDrawingsLightTripleDashHorizontal rune = '\u2504' // ┄
|
||||
BoxDrawingsHeavyTripleDashHorizontal rune = '\u2505' // ┅
|
||||
BoxDrawingsLightTripleDashVertical rune = '\u2506' // ┆
|
||||
BoxDrawingsHeavyTripleDashVertical rune = '\u2507' // ┇
|
||||
BoxDrawingsLightQuadrupleDashHorizontal rune = '\u2508' // ┈
|
||||
BoxDrawingsHeavyQuadrupleDashHorizontal rune = '\u2509' // ┉
|
||||
BoxDrawingsLightQuadrupleDashVertical rune = '\u250a' // ┊
|
||||
BoxDrawingsHeavyQuadrupleDashVertical rune = '\u250b' // ┋
|
||||
BoxDrawingsLightDownAndRight rune = '\u250c' // ┌
|
||||
BoxDrawingsDownLighAndRightHeavy rune = '\u250d' // ┍
|
||||
BoxDrawingsDownHeavyAndRightLight rune = '\u250e' // ┎
|
||||
BoxDrawingsHeavyDownAndRight rune = '\u250f' // ┏
|
||||
BoxDrawingsLightDownAndLeft rune = '\u2510' // ┐
|
||||
BoxDrawingsDownLighAndLeftHeavy rune = '\u2511' // ┑
|
||||
BoxDrawingsDownHeavyAndLeftLight rune = '\u2512' // ┒
|
||||
BoxDrawingsHeavyDownAndLeft rune = '\u2513' // ┓
|
||||
BoxDrawingsLightUpAndRight rune = '\u2514' // └
|
||||
BoxDrawingsUpLightAndRightHeavy rune = '\u2515' // ┕
|
||||
BoxDrawingsUpHeavyAndRightLight rune = '\u2516' // ┖
|
||||
BoxDrawingsHeavyUpAndRight rune = '\u2517' // ┗
|
||||
BoxDrawingsLightUpAndLeft rune = '\u2518' // ┘
|
||||
BoxDrawingsUpLightAndLeftHeavy rune = '\u2519' // ┙
|
||||
BoxDrawingsUpHeavyAndLeftLight rune = '\u251a' // ┚
|
||||
BoxDrawingsHeavyUpAndLeft rune = '\u251b' // ┛
|
||||
BoxDrawingsLightVerticalAndRight rune = '\u251c' // ├
|
||||
BoxDrawingsVerticalLightAndRightHeavy rune = '\u251d' // ┝
|
||||
BoxDrawingsUpHeavyAndRightDownLight rune = '\u251e' // ┞
|
||||
BoxDrawingsDownHeacyAndRightUpLight rune = '\u251f' // ┟
|
||||
BoxDrawingsVerticalHeavyAndRightLight rune = '\u2520' // ┠
|
||||
BoxDrawingsDownLightAnbdRightUpHeavy rune = '\u2521' // ┡
|
||||
BoxDrawingsUpLightAndRightDownHeavy rune = '\u2522' // ┢
|
||||
BoxDrawingsHeavyVerticalAndRight rune = '\u2523' // ┣
|
||||
BoxDrawingsLightVerticalAndLeft rune = '\u2524' // ┤
|
||||
BoxDrawingsVerticalLightAndLeftHeavy rune = '\u2525' // ┥
|
||||
BoxDrawingsUpHeavyAndLeftDownLight rune = '\u2526' // ┦
|
||||
BoxDrawingsDownHeavyAndLeftUpLight rune = '\u2527' // ┧
|
||||
BoxDrawingsVerticalheavyAndLeftLight rune = '\u2528' // ┨
|
||||
BoxDrawingsDownLightAndLeftUpHeavy rune = '\u2529' // ┨
|
||||
BoxDrawingsUpLightAndLeftDownHeavy rune = '\u252a' // ┪
|
||||
BoxDrawingsHeavyVerticalAndLeft rune = '\u252b' // ┫
|
||||
BoxDrawingsLightDownAndHorizontal rune = '\u252c' // ┬
|
||||
BoxDrawingsLeftHeavyAndRightDownLight rune = '\u252d' // ┭
|
||||
BoxDrawingsRightHeavyAndLeftDownLight rune = '\u252e' // ┮
|
||||
BoxDrawingsDownLightAndHorizontalHeavy rune = '\u252f' // ┯
|
||||
BoxDrawingsDownHeavyAndHorizontalLight rune = '\u2530' // ┰
|
||||
BoxDrawingsRightLightAndLeftDownHeavy rune = '\u2531' // ┱
|
||||
BoxDrawingsLeftLightAndRightDownHeavy rune = '\u2532' // ┲
|
||||
BoxDrawingsHeavyDownAndHorizontal rune = '\u2533' // ┳
|
||||
BoxDrawingsLightUpAndHorizontal rune = '\u2534' // ┴
|
||||
BoxDrawingsLeftHeavyAndRightUpLight rune = '\u2535' // ┵
|
||||
BoxDrawingsRightHeavyAndLeftUpLight rune = '\u2536' // ┶
|
||||
BoxDrawingsUpLightAndHorizontalHeavy rune = '\u2537' // ┷
|
||||
BoxDrawingsUpHeavyAndHorizontalLight rune = '\u2538' // ┸
|
||||
BoxDrawingsRightLightAndLeftUpHeavy rune = '\u2539' // ┹
|
||||
BoxDrawingsLeftLightAndRightUpHeavy rune = '\u253a' // ┺
|
||||
BoxDrawingsHeavyUpAndHorizontal rune = '\u253b' // ┻
|
||||
BoxDrawingsLightVerticalAndHorizontal rune = '\u253c' // ┼
|
||||
BoxDrawingsLeftHeavyAndRightVerticalLight rune = '\u253d' // ┽
|
||||
BoxDrawingsRightHeavyAndLeftVerticalLight rune = '\u253e' // ┾
|
||||
BoxDrawingsVerticalLightAndHorizontalHeavy rune = '\u253f' // ┿
|
||||
BoxDrawingsUpHeavyAndDownHorizontalLight rune = '\u2540' // ╀
|
||||
BoxDrawingsDownHeavyAndUpHorizontalLight rune = '\u2541' // ╁
|
||||
BoxDrawingsVerticalHeavyAndHorizontalLight rune = '\u2542' // ╂
|
||||
BoxDrawingsLeftUpHeavyAndRightDownLight rune = '\u2543' // ╃
|
||||
BoxDrawingsRightUpHeavyAndLeftDownLight rune = '\u2544' // ╄
|
||||
BoxDrawingsLeftDownHeavyAndRightUpLight rune = '\u2545' // ╅
|
||||
BoxDrawingsRightDownHeavyAndLeftUpLight rune = '\u2546' // ╆
|
||||
BoxDrawingsDownLightAndUpHorizontalHeavy rune = '\u2547' // ╇
|
||||
BoxDrawingsUpLightAndDownHorizontalHeavy rune = '\u2548' // ╈
|
||||
BoxDrawingsRightLightAndLeftVerticalHeavy rune = '\u2549' // ╉
|
||||
BoxDrawingsLeftLightAndRightVerticalHeavy rune = '\u254a' // ╊
|
||||
BoxDrawingsHeavyVerticalAndHorizontal rune = '\u254b' // ╋
|
||||
BoxDrawingsLightDoubleDashHorizontal rune = '\u254c' // ╌
|
||||
BoxDrawingsHeavyDoubleDashHorizontal rune = '\u254d' // ╍
|
||||
BoxDrawingsLightDoubleDashVertical rune = '\u254e' // ╎
|
||||
BoxDrawingsHeavyDoubleDashVertical rune = '\u254f' // ╏
|
||||
BoxDrawingsDoubleHorizontal rune = '\u2550' // ═
|
||||
BoxDrawingsDoubleVertical rune = '\u2551' // ║
|
||||
BoxDrawingsDownSingleAndRightDouble rune = '\u2552' // ╒
|
||||
BoxDrawingsDownDoubleAndRightSingle rune = '\u2553' // ╓
|
||||
BoxDrawingsDoubleDownAndRight rune = '\u2554' // ╔
|
||||
BoxDrawingsDownSingleAndLeftDouble rune = '\u2555' // ╕
|
||||
BoxDrawingsDownDoubleAndLeftSingle rune = '\u2556' // ╖
|
||||
BoxDrawingsDoubleDownAndLeft rune = '\u2557' // ╗
|
||||
BoxDrawingsUpSingleAndRightDouble rune = '\u2558' // ╘
|
||||
BoxDrawingsUpDoubleAndRightSingle rune = '\u2559' // ╙
|
||||
BoxDrawingsDoubleUpAndRight rune = '\u255a' // ╚
|
||||
BoxDrawingsUpSingleAndLeftDouble rune = '\u255b' // ╛
|
||||
BoxDrawingsUpDobuleAndLeftSingle rune = '\u255c' // ╜
|
||||
BoxDrawingsDoubleUpAndLeft rune = '\u255d' // ╝
|
||||
BoxDrawingsVerticalSingleAndRightDouble rune = '\u255e' // ╞
|
||||
BoxDrawingsVerticalDoubleAndRightSingle rune = '\u255f' // ╟
|
||||
BoxDrawingsDoubleVerticalAndRight rune = '\u2560' // ╠
|
||||
BoxDrawingsVerticalSingleAndLeftDouble rune = '\u2561' // ╡
|
||||
BoxDrawingsVerticalDoubleAndLeftSingle rune = '\u2562' // ╢
|
||||
BoxDrawingsDoubleVerticalAndLeft rune = '\u2563' // ╣
|
||||
BoxDrawingsDownSingleAndHorizontalDouble rune = '\u2564' // ╤
|
||||
BoxDrawingsDownDoubleAndHorizontalSingle rune = '\u2565' // ╥
|
||||
BoxDrawingsDoubleDownAndHorizontal rune = '\u2566' // ╦
|
||||
BoxDrawingsUpSingleAndHorizontalDouble rune = '\u2567' // ╧
|
||||
BoxDrawingsUpDoubleAndHorizontalSingle rune = '\u2568' // ╨
|
||||
BoxDrawingsDoubleUpAndHorizontal rune = '\u2569' // ╩
|
||||
BoxDrawingsVerticalSingleAndHorizontalDouble rune = '\u256a' // ╪
|
||||
BoxDrawingsVerticalDoubleAndHorizontalSingle rune = '\u256b' // ╫
|
||||
BoxDrawingsDoubleVerticalAndHorizontal rune = '\u256c' // ╬
|
||||
BoxDrawingsLightArcDownAndRight rune = '\u256d' // ╭
|
||||
BoxDrawingsLightArcDownAndLeft rune = '\u256e' // ╮
|
||||
BoxDrawingsLightArcUpAndLeft rune = '\u256f' // ╯
|
||||
BoxDrawingsLightArcUpAndRight rune = '\u2570' // ╰
|
||||
BoxDrawingsLightDiagonalUpperRightToLowerLeft rune = '\u2571' // ╱
|
||||
BoxDrawingsLightDiagonalUpperLeftToLowerRight rune = '\u2572' // ╲
|
||||
BoxDrawingsLightDiagonalCross rune = '\u2573' // ╳
|
||||
BoxDrawingsLightLeft rune = '\u2574' // ╴
|
||||
BoxDrawingsLightUp rune = '\u2575' // ╵
|
||||
BoxDrawingsLightRight rune = '\u2576' // ╶
|
||||
BoxDrawingsLightDown rune = '\u2577' // ╷
|
||||
BoxDrawingsHeavyLeft rune = '\u2578' // ╸
|
||||
BoxDrawingsHeavyUp rune = '\u2579' // ╹
|
||||
BoxDrawingsHeavyRight rune = '\u257a' // ╺
|
||||
BoxDrawingsHeavyDown rune = '\u257b' // ╻
|
||||
BoxDrawingsLightLeftAndHeavyRight rune = '\u257c' // ╼
|
||||
BoxDrawingsLightUpAndHeavyDown rune = '\u257d' // ╽
|
||||
BoxDrawingsHeavyLeftAndLightRight rune = '\u257e' // ╾
|
||||
BoxDrawingsHeavyUpAndLightDown rune = '\u257f' // ╿
|
||||
)
|
||||
|
||||
// SemigraphicJoints is a map for joining semigraphic (or otherwise) runes.
|
||||
// So far only light lines are supported but if you want to change the border
|
||||
// styling you need to provide the joints, too.
|
||||
// The matching will be sorted ascending by rune value, so you don't need to
|
||||
// provide all rune combinations,
|
||||
// e.g. (─) + (│) = (┼) will also match (│) + (─) = (┼)
|
||||
var SemigraphicJoints = map[string]rune{
|
||||
// (─) + (│) = (┼)
|
||||
string([]rune{BoxDrawingsLightHorizontal, BoxDrawingsLightVertical}): BoxDrawingsLightVerticalAndHorizontal,
|
||||
// (─) + (┌) = (┬)
|
||||
string([]rune{BoxDrawingsLightHorizontal, BoxDrawingsLightDownAndRight}): BoxDrawingsLightDownAndHorizontal,
|
||||
// (─) + (┐) = (┬)
|
||||
string([]rune{BoxDrawingsLightHorizontal, BoxDrawingsLightDownAndLeft}): BoxDrawingsLightDownAndHorizontal,
|
||||
// (─) + (└) = (┴)
|
||||
string([]rune{BoxDrawingsLightHorizontal, BoxDrawingsLightUpAndRight}): BoxDrawingsLightUpAndHorizontal,
|
||||
// (─) + (┘) = (┴)
|
||||
string([]rune{BoxDrawingsLightHorizontal, BoxDrawingsLightUpAndLeft}): BoxDrawingsLightUpAndHorizontal,
|
||||
// (─) + (├) = (┼)
|
||||
string([]rune{BoxDrawingsLightHorizontal, BoxDrawingsLightVerticalAndRight}): BoxDrawingsLightVerticalAndHorizontal,
|
||||
// (─) + (┤) = (┼)
|
||||
string([]rune{BoxDrawingsLightHorizontal, BoxDrawingsLightVerticalAndLeft}): BoxDrawingsLightVerticalAndHorizontal,
|
||||
// (─) + (┬) = (┬)
|
||||
string([]rune{BoxDrawingsLightHorizontal, BoxDrawingsLightDownAndHorizontal}): BoxDrawingsLightDownAndHorizontal,
|
||||
// (─) + (┴) = (┴)
|
||||
string([]rune{BoxDrawingsLightHorizontal, BoxDrawingsLightUpAndHorizontal}): BoxDrawingsLightUpAndHorizontal,
|
||||
// (─) + (┼) = (┼)
|
||||
string([]rune{BoxDrawingsLightHorizontal, BoxDrawingsLightVerticalAndHorizontal}): BoxDrawingsLightVerticalAndHorizontal,
|
||||
|
||||
// (│) + (┌) = (├)
|
||||
string([]rune{BoxDrawingsLightVertical, BoxDrawingsLightDownAndRight}): BoxDrawingsLightVerticalAndRight,
|
||||
// (│) + (┐) = (┤)
|
||||
string([]rune{BoxDrawingsLightVertical, BoxDrawingsLightDownAndLeft}): BoxDrawingsLightVerticalAndLeft,
|
||||
// (│) + (└) = (├)
|
||||
string([]rune{BoxDrawingsLightVertical, BoxDrawingsLightUpAndRight}): BoxDrawingsLightVerticalAndRight,
|
||||
// (│) + (┘) = (┤)
|
||||
string([]rune{BoxDrawingsLightVertical, BoxDrawingsLightUpAndLeft}): BoxDrawingsLightVerticalAndLeft,
|
||||
// (│) + (├) = (├)
|
||||
string([]rune{BoxDrawingsLightVertical, BoxDrawingsLightVerticalAndRight}): BoxDrawingsLightVerticalAndRight,
|
||||
// (│) + (┤) = (┤)
|
||||
string([]rune{BoxDrawingsLightVertical, BoxDrawingsLightVerticalAndLeft}): BoxDrawingsLightVerticalAndLeft,
|
||||
// (│) + (┬) = (┼)
|
||||
string([]rune{BoxDrawingsLightVertical, BoxDrawingsLightDownAndHorizontal}): BoxDrawingsLightVerticalAndHorizontal,
|
||||
// (│) + (┴) = (┼)
|
||||
string([]rune{BoxDrawingsLightVertical, BoxDrawingsLightUpAndHorizontal}): BoxDrawingsLightVerticalAndHorizontal,
|
||||
// (│) + (┼) = (┼)
|
||||
string([]rune{BoxDrawingsLightVertical, BoxDrawingsLightVerticalAndHorizontal}): BoxDrawingsLightVerticalAndHorizontal,
|
||||
|
||||
// (┌) + (┐) = (┬)
|
||||
string([]rune{BoxDrawingsLightDownAndRight, BoxDrawingsLightDownAndLeft}): BoxDrawingsLightDownAndHorizontal,
|
||||
// (┌) + (└) = (├)
|
||||
string([]rune{BoxDrawingsLightDownAndRight, BoxDrawingsLightUpAndRight}): BoxDrawingsLightVerticalAndRight,
|
||||
// (┌) + (┘) = (┼)
|
||||
string([]rune{BoxDrawingsLightDownAndRight, BoxDrawingsLightUpAndLeft}): BoxDrawingsLightVerticalAndHorizontal,
|
||||
// (┌) + (├) = (├)
|
||||
string([]rune{BoxDrawingsLightDownAndRight, BoxDrawingsLightVerticalAndRight}): BoxDrawingsLightVerticalAndRight,
|
||||
// (┌) + (┤) = (┼)
|
||||
string([]rune{BoxDrawingsLightDownAndRight, BoxDrawingsLightVerticalAndLeft}): BoxDrawingsLightVerticalAndHorizontal,
|
||||
// (┌) + (┬) = (┬)
|
||||
string([]rune{BoxDrawingsLightDownAndRight, BoxDrawingsLightDownAndHorizontal}): BoxDrawingsLightDownAndHorizontal,
|
||||
// (┌) + (┴) = (┼)
|
||||
string([]rune{BoxDrawingsLightDownAndRight, BoxDrawingsLightUpAndHorizontal}): BoxDrawingsLightVerticalAndHorizontal,
|
||||
// (┌) + (┴) = (┼)
|
||||
string([]rune{BoxDrawingsLightDownAndRight, BoxDrawingsLightVerticalAndHorizontal}): BoxDrawingsLightVerticalAndHorizontal,
|
||||
|
||||
// (┐) + (└) = (┼)
|
||||
string([]rune{BoxDrawingsLightDownAndLeft, BoxDrawingsLightUpAndRight}): BoxDrawingsLightVerticalAndHorizontal,
|
||||
// (┐) + (┘) = (┤)
|
||||
string([]rune{BoxDrawingsLightDownAndLeft, BoxDrawingsLightUpAndLeft}): BoxDrawingsLightVerticalAndLeft,
|
||||
// (┐) + (├) = (┼)
|
||||
string([]rune{BoxDrawingsLightDownAndLeft, BoxDrawingsLightVerticalAndRight}): BoxDrawingsLightVerticalAndHorizontal,
|
||||
// (┐) + (┤) = (┤)
|
||||
string([]rune{BoxDrawingsLightDownAndLeft, BoxDrawingsLightVerticalAndLeft}): BoxDrawingsLightVerticalAndLeft,
|
||||
// (┐) + (┬) = (┬)
|
||||
string([]rune{BoxDrawingsLightDownAndLeft, BoxDrawingsLightDownAndHorizontal}): BoxDrawingsLightDownAndHorizontal,
|
||||
// (┐) + (┴) = (┼)
|
||||
string([]rune{BoxDrawingsLightDownAndLeft, BoxDrawingsLightUpAndHorizontal}): BoxDrawingsLightVerticalAndHorizontal,
|
||||
// (┐) + (┼) = (┼)
|
||||
string([]rune{BoxDrawingsLightDownAndLeft, BoxDrawingsLightVerticalAndHorizontal}): BoxDrawingsLightVerticalAndHorizontal,
|
||||
|
||||
// (└) + (┘) = (┴)
|
||||
string([]rune{BoxDrawingsLightUpAndRight, BoxDrawingsLightUpAndLeft}): BoxDrawingsLightUpAndHorizontal,
|
||||
// (└) + (├) = (├)
|
||||
string([]rune{BoxDrawingsLightUpAndRight, BoxDrawingsLightVerticalAndRight}): BoxDrawingsLightVerticalAndRight,
|
||||
// (└) + (┤) = (┼)
|
||||
string([]rune{BoxDrawingsLightUpAndRight, BoxDrawingsLightVerticalAndLeft}): BoxDrawingsLightVerticalAndHorizontal,
|
||||
// (└) + (┬) = (┼)
|
||||
string([]rune{BoxDrawingsLightUpAndRight, BoxDrawingsLightDownAndHorizontal}): BoxDrawingsLightVerticalAndHorizontal,
|
||||
// (└) + (┴) = (┴)
|
||||
string([]rune{BoxDrawingsLightUpAndRight, BoxDrawingsLightUpAndHorizontal}): BoxDrawingsLightUpAndHorizontal,
|
||||
// (└) + (┼) = (┼)
|
||||
string([]rune{BoxDrawingsLightUpAndRight, BoxDrawingsLightVerticalAndHorizontal}): BoxDrawingsLightVerticalAndHorizontal,
|
||||
|
||||
// (┘) + (├) = (┼)
|
||||
string([]rune{BoxDrawingsLightUpAndLeft, BoxDrawingsLightVerticalAndRight}): BoxDrawingsLightVerticalAndHorizontal,
|
||||
// (┘) + (┤) = (┤)
|
||||
string([]rune{BoxDrawingsLightUpAndLeft, BoxDrawingsLightVerticalAndLeft}): BoxDrawingsLightVerticalAndLeft,
|
||||
// (┘) + (┬) = (┼)
|
||||
string([]rune{BoxDrawingsLightUpAndLeft, BoxDrawingsLightDownAndHorizontal}): BoxDrawingsLightVerticalAndHorizontal,
|
||||
// (┘) + (┴) = (┴)
|
||||
string([]rune{BoxDrawingsLightUpAndLeft, BoxDrawingsLightUpAndHorizontal}): BoxDrawingsLightUpAndHorizontal,
|
||||
// (┘) + (┼) = (┼)
|
||||
string([]rune{BoxDrawingsLightUpAndLeft, BoxDrawingsLightVerticalAndHorizontal}): BoxDrawingsLightVerticalAndHorizontal,
|
||||
|
||||
// (├) + (┤) = (┼)
|
||||
string([]rune{BoxDrawingsLightVerticalAndRight, BoxDrawingsLightVerticalAndLeft}): BoxDrawingsLightVerticalAndHorizontal,
|
||||
// (├) + (┬) = (┼)
|
||||
string([]rune{BoxDrawingsLightVerticalAndRight, BoxDrawingsLightDownAndHorizontal}): BoxDrawingsLightVerticalAndHorizontal,
|
||||
// (├) + (┴) = (┼)
|
||||
string([]rune{BoxDrawingsLightVerticalAndRight, BoxDrawingsLightUpAndHorizontal}): BoxDrawingsLightVerticalAndHorizontal,
|
||||
// (├) + (┼) = (┼)
|
||||
string([]rune{BoxDrawingsLightVerticalAndRight, BoxDrawingsLightVerticalAndHorizontal}): BoxDrawingsLightVerticalAndHorizontal,
|
||||
|
||||
// (┤) + (┬) = (┼)
|
||||
string([]rune{BoxDrawingsLightVerticalAndLeft, BoxDrawingsLightDownAndHorizontal}): BoxDrawingsLightVerticalAndHorizontal,
|
||||
// (┤) + (┴) = (┼)
|
||||
string([]rune{BoxDrawingsLightVerticalAndLeft, BoxDrawingsLightUpAndHorizontal}): BoxDrawingsLightVerticalAndHorizontal,
|
||||
// (┤) + (┼) = (┼)
|
||||
string([]rune{BoxDrawingsLightVerticalAndLeft, BoxDrawingsLightVerticalAndHorizontal}): BoxDrawingsLightVerticalAndHorizontal,
|
||||
|
||||
// (┬) + (┴) = (┼)
|
||||
string([]rune{BoxDrawingsLightDownAndHorizontal, BoxDrawingsLightUpAndHorizontal}): BoxDrawingsLightVerticalAndHorizontal,
|
||||
// (┬) + (┼) = (┼)
|
||||
string([]rune{BoxDrawingsLightDownAndHorizontal, BoxDrawingsLightVerticalAndHorizontal}): BoxDrawingsLightVerticalAndHorizontal,
|
||||
|
||||
// (┴) + (┼) = (┼)
|
||||
string([]rune{BoxDrawingsLightUpAndHorizontal, BoxDrawingsLightVerticalAndHorizontal}): BoxDrawingsLightVerticalAndHorizontal,
|
||||
}
|
||||
|
||||
// PrintJoinedSemigraphics prints a semigraphics rune into the screen at the given
|
||||
// position with the given color, joining it with any existing semigraphics
|
||||
// rune. Background colors are preserved. At this point, only regular single
|
||||
// line borders are supported.
|
||||
func PrintJoinedSemigraphics(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 = SemigraphicJoints[string([]rune{previous, ch})]
|
||||
}
|
||||
if result == 0 {
|
||||
result = ch
|
||||
}
|
||||
|
||||
// We only print something if we have something.
|
||||
screen.SetContent(x, y, result, nil, style)
|
||||
}
|
35
vendor/github.com/rivo/tview/styles.go
generated
vendored
Normal file
35
vendor/github.com/rivo/tview/styles.go
generated
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
package tview
|
||||
|
||||
import "github.com/gdamore/tcell"
|
||||
|
||||
// Theme defines the colors used when primitives are initialized.
|
||||
type Theme 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.
|
||||
}
|
||||
|
||||
// Styles defines the theme for applications. The default is for a black
|
||||
// background and some basic colors: black, white, yellow, green, cyan, and
|
||||
// blue.
|
||||
var Styles = Theme{
|
||||
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,
|
||||
}
|
1266
vendor/github.com/rivo/tview/table.go
generated
vendored
Normal file
1266
vendor/github.com/rivo/tview/table.go
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1189
vendor/github.com/rivo/tview/textview.go
generated
vendored
Normal file
1189
vendor/github.com/rivo/tview/textview.go
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
775
vendor/github.com/rivo/tview/treeview.go
generated
vendored
Normal file
775
vendor/github.com/rivo/tview/treeview.go
generated
vendored
Normal file
@@ -0,0 +1,775 @@
|
||||
package tview
|
||||
|
||||
import (
|
||||
"github.com/gdamore/tcell"
|
||||
)
|
||||
|
||||
// Tree navigation events.
|
||||
const (
|
||||
treeNone int = iota
|
||||
treeHome
|
||||
treeEnd
|
||||
treeUp
|
||||
treeDown
|
||||
treePageUp
|
||||
treePageDown
|
||||
)
|
||||
|
||||
// TreeNode represents one node in a tree view.
|
||||
type TreeNode struct {
|
||||
// The reference object.
|
||||
reference interface{}
|
||||
|
||||
// This node's child nodes.
|
||||
children []*TreeNode
|
||||
|
||||
// The item's text.
|
||||
text string
|
||||
|
||||
// The text color.
|
||||
color tcell.Color
|
||||
|
||||
// Whether or not this node can be selected.
|
||||
selectable bool
|
||||
|
||||
// Whether or not this node's children should be displayed.
|
||||
expanded bool
|
||||
|
||||
// The additional horizontal indent of this node's text.
|
||||
indent int
|
||||
|
||||
// An optional function which is called when the user selects this node.
|
||||
selected func()
|
||||
|
||||
// Temporary member variables.
|
||||
parent *TreeNode // The parent node (nil for the root).
|
||||
level int // The hierarchy level (0 for the root, 1 for its children, and so on).
|
||||
graphicsX int // The x-coordinate of the left-most graphics rune.
|
||||
textX int // The x-coordinate of the first rune of the text.
|
||||
}
|
||||
|
||||
// NewTreeNode returns a new tree node.
|
||||
func NewTreeNode(text string) *TreeNode {
|
||||
return &TreeNode{
|
||||
text: text,
|
||||
color: Styles.PrimaryTextColor,
|
||||
indent: 2,
|
||||
expanded: true,
|
||||
selectable: true,
|
||||
}
|
||||
}
|
||||
|
||||
// Walk traverses this node's subtree in depth-first, pre-order (NLR) order and
|
||||
// calls the provided callback function on each traversed node (which includes
|
||||
// this node) with the traversed node and its parent node (nil for this node).
|
||||
// The callback returns whether traversal should continue with the traversed
|
||||
// node's child nodes (true) or not recurse any deeper (false).
|
||||
func (n *TreeNode) Walk(callback func(node, parent *TreeNode) bool) *TreeNode {
|
||||
n.parent = nil
|
||||
nodes := []*TreeNode{n}
|
||||
for len(nodes) > 0 {
|
||||
// Pop the top node and process it.
|
||||
node := nodes[len(nodes)-1]
|
||||
nodes = nodes[:len(nodes)-1]
|
||||
if !callback(node, node.parent) {
|
||||
// Don't add any children.
|
||||
continue
|
||||
}
|
||||
|
||||
// Add children in reverse order.
|
||||
for index := len(node.children) - 1; index >= 0; index-- {
|
||||
node.children[index].parent = node
|
||||
nodes = append(nodes, node.children[index])
|
||||
}
|
||||
}
|
||||
|
||||
return n
|
||||
}
|
||||
|
||||
// SetReference allows you to store a reference of any type in this node. This
|
||||
// will allow you to establish a mapping between the TreeView hierarchy and your
|
||||
// internal tree structure.
|
||||
func (n *TreeNode) SetReference(reference interface{}) *TreeNode {
|
||||
n.reference = reference
|
||||
return n
|
||||
}
|
||||
|
||||
// GetReference returns this node's reference object.
|
||||
func (n *TreeNode) GetReference() interface{} {
|
||||
return n.reference
|
||||
}
|
||||
|
||||
// SetChildren sets this node's child nodes.
|
||||
func (n *TreeNode) SetChildren(childNodes []*TreeNode) *TreeNode {
|
||||
n.children = childNodes
|
||||
return n
|
||||
}
|
||||
|
||||
// GetText returns this node's text.
|
||||
func (n *TreeNode) GetText() string {
|
||||
return n.text
|
||||
}
|
||||
|
||||
// GetChildren returns this node's children.
|
||||
func (n *TreeNode) GetChildren() []*TreeNode {
|
||||
return n.children
|
||||
}
|
||||
|
||||
// ClearChildren removes all child nodes from this node.
|
||||
func (n *TreeNode) ClearChildren() *TreeNode {
|
||||
n.children = nil
|
||||
return n
|
||||
}
|
||||
|
||||
// AddChild adds a new child node to this node.
|
||||
func (n *TreeNode) AddChild(node *TreeNode) *TreeNode {
|
||||
n.children = append(n.children, node)
|
||||
return n
|
||||
}
|
||||
|
||||
// SetSelectable sets a flag indicating whether this node can be selected by
|
||||
// the user.
|
||||
func (n *TreeNode) SetSelectable(selectable bool) *TreeNode {
|
||||
n.selectable = selectable
|
||||
return n
|
||||
}
|
||||
|
||||
// SetSelectedFunc sets a function which is called when the user selects this
|
||||
// node by hitting Enter when it is selected.
|
||||
func (n *TreeNode) SetSelectedFunc(handler func()) *TreeNode {
|
||||
n.selected = handler
|
||||
return n
|
||||
}
|
||||
|
||||
// SetExpanded sets whether or not this node's child nodes should be displayed.
|
||||
func (n *TreeNode) SetExpanded(expanded bool) *TreeNode {
|
||||
n.expanded = expanded
|
||||
return n
|
||||
}
|
||||
|
||||
// Expand makes the child nodes of this node appear.
|
||||
func (n *TreeNode) Expand() *TreeNode {
|
||||
n.expanded = true
|
||||
return n
|
||||
}
|
||||
|
||||
// Collapse makes the child nodes of this node disappear.
|
||||
func (n *TreeNode) Collapse() *TreeNode {
|
||||
n.expanded = false
|
||||
return n
|
||||
}
|
||||
|
||||
// ExpandAll expands this node and all descendent nodes.
|
||||
func (n *TreeNode) ExpandAll() *TreeNode {
|
||||
n.Walk(func(node, parent *TreeNode) bool {
|
||||
node.expanded = true
|
||||
return true
|
||||
})
|
||||
return n
|
||||
}
|
||||
|
||||
// CollapseAll collapses this node and all descendent nodes.
|
||||
func (n *TreeNode) CollapseAll() *TreeNode {
|
||||
n.Walk(func(node, parent *TreeNode) bool {
|
||||
n.expanded = false
|
||||
return true
|
||||
})
|
||||
return n
|
||||
}
|
||||
|
||||
// IsExpanded returns whether the child nodes of this node are visible.
|
||||
func (n *TreeNode) IsExpanded() bool {
|
||||
return n.expanded
|
||||
}
|
||||
|
||||
// SetText sets the node's text which is displayed.
|
||||
func (n *TreeNode) SetText(text string) *TreeNode {
|
||||
n.text = text
|
||||
return n
|
||||
}
|
||||
|
||||
// GetColor returns the node's color.
|
||||
func (n *TreeNode) GetColor() tcell.Color {
|
||||
return n.color
|
||||
}
|
||||
|
||||
// SetColor sets the node's text color.
|
||||
func (n *TreeNode) SetColor(color tcell.Color) *TreeNode {
|
||||
n.color = color
|
||||
return n
|
||||
}
|
||||
|
||||
// SetIndent sets an additional indentation for this node's text. A value of 0
|
||||
// keeps the text as far left as possible with a minimum of line graphics. Any
|
||||
// value greater than that moves the text to the right.
|
||||
func (n *TreeNode) SetIndent(indent int) *TreeNode {
|
||||
n.indent = indent
|
||||
return n
|
||||
}
|
||||
|
||||
// TreeView displays tree structures. A tree consists of nodes (TreeNode
|
||||
// objects) where each node has zero or more child nodes and exactly one parent
|
||||
// node (except for the root node which has no parent node).
|
||||
//
|
||||
// The SetRoot() function is used to specify the root of the tree. Other nodes
|
||||
// are added locally to the root node or any of its descendents. See the
|
||||
// TreeNode documentation for details on node attributes. (You can use
|
||||
// SetReference() to store a reference to nodes of your own tree structure.)
|
||||
//
|
||||
// Nodes can be selected by calling SetCurrentNode(). The user can navigate the
|
||||
// selection or the tree by using the following keys:
|
||||
//
|
||||
// - j, down arrow, right arrow: Move (the selection) down by one node.
|
||||
// - k, up arrow, left arrow: Move (the selection) up by one node.
|
||||
// - g, home: Move (the selection) to the top.
|
||||
// - G, end: Move (the selection) to the bottom.
|
||||
// - Ctrl-F, page down: Move (the selection) down by one page.
|
||||
// - Ctrl-B, page up: Move (the selection) up by one page.
|
||||
//
|
||||
// Selected nodes can trigger the "selected" callback when the user hits Enter.
|
||||
//
|
||||
// The root node corresponds to level 0, its children correspond to level 1,
|
||||
// their children to level 2, and so on. Per default, the first level that is
|
||||
// displayed is 0, i.e. the root node. You can call SetTopLevel() to hide
|
||||
// levels.
|
||||
//
|
||||
// If graphics are turned on (see SetGraphics()), lines indicate the tree's
|
||||
// hierarchy. Alternative (or additionally), you can set different prefixes
|
||||
// using SetPrefixes() for different levels, for example to display hierarchical
|
||||
// bullet point lists.
|
||||
//
|
||||
// See https://github.com/rivo/tview/wiki/TreeView for an example.
|
||||
type TreeView struct {
|
||||
*Box
|
||||
|
||||
// The root node.
|
||||
root *TreeNode
|
||||
|
||||
// The currently selected node or nil if no node is selected.
|
||||
currentNode *TreeNode
|
||||
|
||||
// The movement to be performed during the call to Draw(), one of the
|
||||
// constants defined above.
|
||||
movement int
|
||||
|
||||
// The top hierarchical level shown. (0 corresponds to the root level.)
|
||||
topLevel int
|
||||
|
||||
// Strings drawn before the nodes, based on their level.
|
||||
prefixes []string
|
||||
|
||||
// Vertical scroll offset.
|
||||
offsetY int
|
||||
|
||||
// If set to true, all node texts will be aligned horizontally.
|
||||
align bool
|
||||
|
||||
// If set to true, the tree structure is drawn using lines.
|
||||
graphics bool
|
||||
|
||||
// The color of the lines.
|
||||
graphicsColor tcell.Color
|
||||
|
||||
// An optional function which is called when the user has navigated to a new
|
||||
// tree node.
|
||||
changed func(node *TreeNode)
|
||||
|
||||
// An optional function which is called when a tree item was selected.
|
||||
selected func(node *TreeNode)
|
||||
|
||||
// An optional function which is called when the user moves away from this
|
||||
// primitive.
|
||||
done func(key tcell.Key)
|
||||
|
||||
// The visible nodes, top-down, as set by process().
|
||||
nodes []*TreeNode
|
||||
}
|
||||
|
||||
// NewTreeView returns a new tree view.
|
||||
func NewTreeView() *TreeView {
|
||||
return &TreeView{
|
||||
Box: NewBox(),
|
||||
graphics: true,
|
||||
graphicsColor: Styles.GraphicsColor,
|
||||
}
|
||||
}
|
||||
|
||||
// SetRoot sets the root node of the tree.
|
||||
func (t *TreeView) SetRoot(root *TreeNode) *TreeView {
|
||||
t.root = root
|
||||
return t
|
||||
}
|
||||
|
||||
// GetRoot returns the root node of the tree. If no such node was previously
|
||||
// set, nil is returned.
|
||||
func (t *TreeView) GetRoot() *TreeNode {
|
||||
return t.root
|
||||
}
|
||||
|
||||
// SetCurrentNode sets the currently selected node. Provide nil to clear all
|
||||
// selections. Selected nodes must be visible and selectable, or else the
|
||||
// selection will be changed to the top-most selectable and visible node.
|
||||
//
|
||||
// This function does NOT trigger the "changed" callback.
|
||||
func (t *TreeView) SetCurrentNode(node *TreeNode) *TreeView {
|
||||
t.currentNode = node
|
||||
return t
|
||||
}
|
||||
|
||||
// GetCurrentNode returns the currently selected node or nil of no node is
|
||||
// currently selected.
|
||||
func (t *TreeView) GetCurrentNode() *TreeNode {
|
||||
return t.currentNode
|
||||
}
|
||||
|
||||
// SetTopLevel sets the first tree level that is visible with 0 referring to the
|
||||
// root, 1 to the root's child nodes, and so on. Nodes above the top level are
|
||||
// not displayed.
|
||||
func (t *TreeView) SetTopLevel(topLevel int) *TreeView {
|
||||
t.topLevel = topLevel
|
||||
return t
|
||||
}
|
||||
|
||||
// SetPrefixes defines the strings drawn before the nodes' texts. This is a
|
||||
// slice of strings where each element corresponds to a node's hierarchy level,
|
||||
// i.e. 0 for the root, 1 for the root's children, and so on (levels will
|
||||
// cycle).
|
||||
//
|
||||
// For example, to display a hierarchical list with bullet points:
|
||||
//
|
||||
// treeView.SetGraphics(false).
|
||||
// SetPrefixes([]string{"* ", "- ", "x "})
|
||||
func (t *TreeView) SetPrefixes(prefixes []string) *TreeView {
|
||||
t.prefixes = prefixes
|
||||
return t
|
||||
}
|
||||
|
||||
// SetAlign controls the horizontal alignment of the node texts. If set to true,
|
||||
// all texts except that of top-level nodes will be placed in the same column.
|
||||
// If set to false, they will indent with the hierarchy.
|
||||
func (t *TreeView) SetAlign(align bool) *TreeView {
|
||||
t.align = align
|
||||
return t
|
||||
}
|
||||
|
||||
// SetGraphics sets a flag which determines whether or not line graphics are
|
||||
// drawn to illustrate the tree's hierarchy.
|
||||
func (t *TreeView) SetGraphics(showGraphics bool) *TreeView {
|
||||
t.graphics = showGraphics
|
||||
return t
|
||||
}
|
||||
|
||||
// SetGraphicsColor sets the colors of the lines used to draw the tree structure.
|
||||
func (t *TreeView) SetGraphicsColor(color tcell.Color) *TreeView {
|
||||
t.graphicsColor = color
|
||||
return t
|
||||
}
|
||||
|
||||
// SetChangedFunc sets the function which is called when the user navigates to
|
||||
// a new tree node.
|
||||
func (t *TreeView) SetChangedFunc(handler func(node *TreeNode)) *TreeView {
|
||||
t.changed = handler
|
||||
return t
|
||||
}
|
||||
|
||||
// SetSelectedFunc sets the function which is called when the user selects a
|
||||
// node by pressing Enter on the current selection.
|
||||
func (t *TreeView) SetSelectedFunc(handler func(node *TreeNode)) *TreeView {
|
||||
t.selected = handler
|
||||
return t
|
||||
}
|
||||
|
||||
// SetDoneFunc sets a handler which is called whenever the user presses the
|
||||
// Escape, Tab, or Backtab key.
|
||||
func (t *TreeView) SetDoneFunc(handler func(key tcell.Key)) *TreeView {
|
||||
t.done = handler
|
||||
return t
|
||||
}
|
||||
|
||||
// GetScrollOffset returns the number of node rows that were skipped at the top
|
||||
// of the tree view. Note that when the user navigates the tree view, this value
|
||||
// is only updated after the tree view has been redrawn.
|
||||
func (t *TreeView) GetScrollOffset() int {
|
||||
return t.offsetY
|
||||
}
|
||||
|
||||
// GetRowCount returns the number of "visible" nodes. This includes nodes which
|
||||
// fall outside the tree view's box but notably does not include the children
|
||||
// of collapsed nodes. Note that this value is only up to date after the tree
|
||||
// view has been drawn.
|
||||
func (t *TreeView) GetRowCount() int {
|
||||
return len(t.nodes)
|
||||
}
|
||||
|
||||
// process builds the visible tree, populates the "nodes" slice, and processes
|
||||
// pending selection actions.
|
||||
func (t *TreeView) process() {
|
||||
_, _, _, height := t.GetInnerRect()
|
||||
|
||||
// Determine visible nodes and their placement.
|
||||
var graphicsOffset, maxTextX int
|
||||
t.nodes = nil
|
||||
if t.root == nil {
|
||||
return
|
||||
}
|
||||
selectedIndex := -1
|
||||
topLevelGraphicsX := -1
|
||||
if t.graphics {
|
||||
graphicsOffset = 1
|
||||
}
|
||||
t.root.Walk(func(node, parent *TreeNode) bool {
|
||||
// Set node attributes.
|
||||
node.parent = parent
|
||||
if parent == nil {
|
||||
node.level = 0
|
||||
node.graphicsX = 0
|
||||
node.textX = 0
|
||||
} else {
|
||||
node.level = parent.level + 1
|
||||
node.graphicsX = parent.textX
|
||||
node.textX = node.graphicsX + graphicsOffset + node.indent
|
||||
}
|
||||
if !t.graphics && t.align {
|
||||
// Without graphics, we align nodes on the first column.
|
||||
node.textX = 0
|
||||
}
|
||||
if node.level == t.topLevel {
|
||||
// No graphics for top level nodes.
|
||||
node.graphicsX = 0
|
||||
node.textX = 0
|
||||
}
|
||||
|
||||
// Add the node to the list.
|
||||
if node.level >= t.topLevel {
|
||||
// This node will be visible.
|
||||
if node.textX > maxTextX {
|
||||
maxTextX = node.textX
|
||||
}
|
||||
if node == t.currentNode && node.selectable {
|
||||
selectedIndex = len(t.nodes)
|
||||
}
|
||||
|
||||
// Maybe we want to skip this level.
|
||||
if t.topLevel == node.level && (topLevelGraphicsX < 0 || node.graphicsX < topLevelGraphicsX) {
|
||||
topLevelGraphicsX = node.graphicsX
|
||||
}
|
||||
|
||||
t.nodes = append(t.nodes, node)
|
||||
}
|
||||
|
||||
// Recurse if desired.
|
||||
return node.expanded
|
||||
})
|
||||
|
||||
// Post-process positions.
|
||||
for _, node := range t.nodes {
|
||||
// If text must align, we correct the positions.
|
||||
if t.align && node.level > t.topLevel {
|
||||
node.textX = maxTextX
|
||||
}
|
||||
|
||||
// If we skipped levels, shift to the left.
|
||||
if topLevelGraphicsX > 0 {
|
||||
node.graphicsX -= topLevelGraphicsX
|
||||
node.textX -= topLevelGraphicsX
|
||||
}
|
||||
}
|
||||
|
||||
// Process selection. (Also trigger events if necessary.)
|
||||
if selectedIndex >= 0 {
|
||||
// Move the selection.
|
||||
newSelectedIndex := selectedIndex
|
||||
MovementSwitch:
|
||||
switch t.movement {
|
||||
case treeUp:
|
||||
for newSelectedIndex > 0 {
|
||||
newSelectedIndex--
|
||||
if t.nodes[newSelectedIndex].selectable {
|
||||
break MovementSwitch
|
||||
}
|
||||
}
|
||||
newSelectedIndex = selectedIndex
|
||||
case treeDown:
|
||||
for newSelectedIndex < len(t.nodes)-1 {
|
||||
newSelectedIndex++
|
||||
if t.nodes[newSelectedIndex].selectable {
|
||||
break MovementSwitch
|
||||
}
|
||||
}
|
||||
newSelectedIndex = selectedIndex
|
||||
case treeHome:
|
||||
for newSelectedIndex = 0; newSelectedIndex < len(t.nodes); newSelectedIndex++ {
|
||||
if t.nodes[newSelectedIndex].selectable {
|
||||
break MovementSwitch
|
||||
}
|
||||
}
|
||||
newSelectedIndex = selectedIndex
|
||||
case treeEnd:
|
||||
for newSelectedIndex = len(t.nodes) - 1; newSelectedIndex >= 0; newSelectedIndex-- {
|
||||
if t.nodes[newSelectedIndex].selectable {
|
||||
break MovementSwitch
|
||||
}
|
||||
}
|
||||
newSelectedIndex = selectedIndex
|
||||
case treePageDown:
|
||||
if newSelectedIndex+height < len(t.nodes) {
|
||||
newSelectedIndex += height
|
||||
} else {
|
||||
newSelectedIndex = len(t.nodes) - 1
|
||||
}
|
||||
for ; newSelectedIndex < len(t.nodes); newSelectedIndex++ {
|
||||
if t.nodes[newSelectedIndex].selectable {
|
||||
break MovementSwitch
|
||||
}
|
||||
}
|
||||
newSelectedIndex = selectedIndex
|
||||
case treePageUp:
|
||||
if newSelectedIndex >= height {
|
||||
newSelectedIndex -= height
|
||||
} else {
|
||||
newSelectedIndex = 0
|
||||
}
|
||||
for ; newSelectedIndex >= 0; newSelectedIndex-- {
|
||||
if t.nodes[newSelectedIndex].selectable {
|
||||
break MovementSwitch
|
||||
}
|
||||
}
|
||||
newSelectedIndex = selectedIndex
|
||||
}
|
||||
t.currentNode = t.nodes[newSelectedIndex]
|
||||
if newSelectedIndex != selectedIndex {
|
||||
t.movement = treeNone
|
||||
if t.changed != nil {
|
||||
t.changed(t.currentNode)
|
||||
}
|
||||
}
|
||||
selectedIndex = newSelectedIndex
|
||||
|
||||
// Move selection into viewport.
|
||||
if selectedIndex-t.offsetY >= height {
|
||||
t.offsetY = selectedIndex - height + 1
|
||||
}
|
||||
if selectedIndex < t.offsetY {
|
||||
t.offsetY = selectedIndex
|
||||
}
|
||||
} else {
|
||||
// If selection is not visible or selectable, select the first candidate.
|
||||
if t.currentNode != nil {
|
||||
for index, node := range t.nodes {
|
||||
if node.selectable {
|
||||
selectedIndex = index
|
||||
t.currentNode = node
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if selectedIndex < 0 {
|
||||
t.currentNode = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Draw draws this primitive onto the screen.
|
||||
func (t *TreeView) Draw(screen tcell.Screen) {
|
||||
t.Box.Draw(screen)
|
||||
if t.root == nil {
|
||||
return
|
||||
}
|
||||
_, totalHeight := screen.Size()
|
||||
|
||||
t.process()
|
||||
|
||||
// Scroll the tree.
|
||||
x, y, width, height := t.GetInnerRect()
|
||||
switch t.movement {
|
||||
case treeUp:
|
||||
t.offsetY--
|
||||
case treeDown:
|
||||
t.offsetY++
|
||||
case treeHome:
|
||||
t.offsetY = 0
|
||||
case treeEnd:
|
||||
t.offsetY = len(t.nodes)
|
||||
case treePageUp:
|
||||
t.offsetY -= height
|
||||
case treePageDown:
|
||||
t.offsetY += height
|
||||
}
|
||||
t.movement = treeNone
|
||||
|
||||
// Fix invalid offsets.
|
||||
if t.offsetY >= len(t.nodes)-height {
|
||||
t.offsetY = len(t.nodes) - height
|
||||
}
|
||||
if t.offsetY < 0 {
|
||||
t.offsetY = 0
|
||||
}
|
||||
|
||||
// Draw the tree.
|
||||
posY := y
|
||||
lineStyle := tcell.StyleDefault.Background(t.backgroundColor).Foreground(t.graphicsColor)
|
||||
for index, node := range t.nodes {
|
||||
// Skip invisible parts.
|
||||
if posY >= y+height+1 || posY >= totalHeight {
|
||||
break
|
||||
}
|
||||
if index < t.offsetY {
|
||||
continue
|
||||
}
|
||||
|
||||
// Draw the graphics.
|
||||
if t.graphics {
|
||||
// Draw ancestor branches.
|
||||
ancestor := node.parent
|
||||
for ancestor != nil && ancestor.parent != nil && ancestor.parent.level >= t.topLevel {
|
||||
if ancestor.graphicsX >= width {
|
||||
continue
|
||||
}
|
||||
|
||||
// Draw a branch if this ancestor is not a last child.
|
||||
if ancestor.parent.children[len(ancestor.parent.children)-1] != ancestor {
|
||||
if posY-1 >= y && ancestor.textX > ancestor.graphicsX {
|
||||
PrintJoinedSemigraphics(screen, x+ancestor.graphicsX, posY-1, Borders.Vertical, t.graphicsColor)
|
||||
}
|
||||
if posY < y+height {
|
||||
screen.SetContent(x+ancestor.graphicsX, posY, Borders.Vertical, nil, lineStyle)
|
||||
}
|
||||
}
|
||||
ancestor = ancestor.parent
|
||||
}
|
||||
|
||||
if node.textX > node.graphicsX && node.graphicsX < width {
|
||||
// Connect to the node above.
|
||||
if posY-1 >= y && t.nodes[index-1].graphicsX <= node.graphicsX && t.nodes[index-1].textX > node.graphicsX {
|
||||
PrintJoinedSemigraphics(screen, x+node.graphicsX, posY-1, Borders.TopLeft, t.graphicsColor)
|
||||
}
|
||||
|
||||
// Join this node.
|
||||
if posY < y+height {
|
||||
screen.SetContent(x+node.graphicsX, posY, Borders.BottomLeft, nil, lineStyle)
|
||||
for pos := node.graphicsX + 1; pos < node.textX && pos < width; pos++ {
|
||||
screen.SetContent(x+pos, posY, Borders.Horizontal, nil, lineStyle)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Draw the prefix and the text.
|
||||
if node.textX < width && posY < y+height {
|
||||
// Prefix.
|
||||
var prefixWidth int
|
||||
if len(t.prefixes) > 0 {
|
||||
_, prefixWidth = Print(screen, t.prefixes[(node.level-t.topLevel)%len(t.prefixes)], x+node.textX, posY, width-node.textX, AlignLeft, node.color)
|
||||
}
|
||||
|
||||
// Text.
|
||||
if node.textX+prefixWidth < width {
|
||||
style := tcell.StyleDefault.Foreground(node.color)
|
||||
if node == t.currentNode {
|
||||
style = tcell.StyleDefault.Background(node.color).Foreground(t.backgroundColor)
|
||||
}
|
||||
printWithStyle(screen, node.text, x+node.textX+prefixWidth, posY, width-node.textX-prefixWidth, AlignLeft, style)
|
||||
}
|
||||
}
|
||||
|
||||
// Advance.
|
||||
posY++
|
||||
}
|
||||
}
|
||||
|
||||
// InputHandler returns the handler for this primitive.
|
||||
func (t *TreeView) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) {
|
||||
return t.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p Primitive)) {
|
||||
selectNode := func() {
|
||||
node := t.currentNode
|
||||
if node != nil {
|
||||
if t.selected != nil {
|
||||
t.selected(node)
|
||||
}
|
||||
if node.selected != nil {
|
||||
node.selected()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Because the tree is flattened into a list only at drawing time, we also
|
||||
// postpone the (selection) movement to drawing time.
|
||||
switch key := event.Key(); key {
|
||||
case tcell.KeyTab, tcell.KeyBacktab, tcell.KeyEscape:
|
||||
if t.done != nil {
|
||||
t.done(key)
|
||||
}
|
||||
case tcell.KeyDown, tcell.KeyRight:
|
||||
t.movement = treeDown
|
||||
case tcell.KeyUp, tcell.KeyLeft:
|
||||
t.movement = treeUp
|
||||
case tcell.KeyHome:
|
||||
t.movement = treeHome
|
||||
case tcell.KeyEnd:
|
||||
t.movement = treeEnd
|
||||
case tcell.KeyPgDn, tcell.KeyCtrlF:
|
||||
t.movement = treePageDown
|
||||
case tcell.KeyPgUp, tcell.KeyCtrlB:
|
||||
t.movement = treePageUp
|
||||
case tcell.KeyRune:
|
||||
switch event.Rune() {
|
||||
case 'g':
|
||||
t.movement = treeHome
|
||||
case 'G':
|
||||
t.movement = treeEnd
|
||||
case 'j':
|
||||
t.movement = treeDown
|
||||
case 'k':
|
||||
t.movement = treeUp
|
||||
case ' ':
|
||||
selectNode()
|
||||
}
|
||||
case tcell.KeyEnter:
|
||||
selectNode()
|
||||
}
|
||||
|
||||
t.process()
|
||||
})
|
||||
}
|
||||
|
||||
// MouseHandler returns the mouse handler for this primitive.
|
||||
func (t *TreeView) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
|
||||
return t.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
|
||||
x, y := event.Position()
|
||||
if !t.InRect(x, y) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
switch action {
|
||||
case MouseLeftClick:
|
||||
setFocus(t)
|
||||
_, rectY, _, _ := t.GetInnerRect()
|
||||
y -= rectY
|
||||
if y >= 0 && y < len(t.nodes) {
|
||||
node := t.nodes[y]
|
||||
if node.selectable {
|
||||
previousNode := t.currentNode
|
||||
t.currentNode = node
|
||||
if previousNode != node && t.changed != nil {
|
||||
t.changed(node)
|
||||
}
|
||||
if t.selected != nil {
|
||||
t.selected(node)
|
||||
}
|
||||
if node.selected != nil {
|
||||
node.selected()
|
||||
}
|
||||
}
|
||||
}
|
||||
consumed = true
|
||||
case MouseScrollUp:
|
||||
t.movement = treeUp
|
||||
consumed = true
|
||||
case MouseScrollDown:
|
||||
t.movement = treeDown
|
||||
consumed = true
|
||||
}
|
||||
|
||||
return
|
||||
})
|
||||
}
|
BIN
vendor/github.com/rivo/tview/tview.gif
generated
vendored
Normal file
BIN
vendor/github.com/rivo/tview/tview.gif
generated
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.1 MiB |
630
vendor/github.com/rivo/tview/util.go
generated
vendored
Normal file
630
vendor/github.com/rivo/tview/util.go
generated
vendored
Normal file
@@ -0,0 +1,630 @@
|
||||
package tview
|
||||
|
||||
import (
|
||||
"math"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
|
||||
"github.com/gdamore/tcell"
|
||||
runewidth "github.com/mattn/go-runewidth"
|
||||
"github.com/rivo/uniseg"
|
||||
)
|
||||
|
||||
// Text alignment within a box.
|
||||
const (
|
||||
AlignLeft = iota
|
||||
AlignCenter
|
||||
AlignRight
|
||||
)
|
||||
|
||||
// Common regular expressions.
|
||||
var (
|
||||
colorPattern = regexp.MustCompile(`\[([a-zA-Z]+|#[0-9a-zA-Z]{6}|\-)?(:([a-zA-Z]+|#[0-9a-zA-Z]{6}|\-)?(:([lbdru]+|\-)?)?)?\]`)
|
||||
regionPattern = regexp.MustCompile(`\["([a-zA-Z0-9_,;: \-\.]*)"\]`)
|
||||
escapePattern = regexp.MustCompile(`\[([a-zA-Z0-9_,;: \-\."#]+)\[(\[*)\]`)
|
||||
nonEscapePattern = regexp.MustCompile(`(\[[a-zA-Z0-9_,;: \-\."#]+\[*)\]`)
|
||||
boundaryPattern = regexp.MustCompile(`(([,\.\-:;!\?&#+]|\n)[ \t\f\r]*|([ \t\f\r]+))`)
|
||||
spacePattern = regexp.MustCompile(`\s+`)
|
||||
)
|
||||
|
||||
// Positions of substrings in regular expressions.
|
||||
const (
|
||||
colorForegroundPos = 1
|
||||
colorBackgroundPos = 3
|
||||
colorFlagPos = 5
|
||||
)
|
||||
|
||||
// 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() {
|
||||
// We'll use zero width joiners.
|
||||
runewidth.ZeroWidthJoiner = true
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// styleFromTag takes the given style, defined by a foreground color (fgColor),
|
||||
// a background color (bgColor), and style attributes, and modifies it based on
|
||||
// the substrings (tagSubstrings) extracted by the regular expression for color
|
||||
// tags. The new colors and attributes are returned where empty strings mean
|
||||
// "don't modify" and a dash ("-") means "reset to default".
|
||||
func styleFromTag(fgColor, bgColor, attributes string, tagSubstrings []string) (newFgColor, newBgColor, newAttributes string) {
|
||||
if tagSubstrings[colorForegroundPos] != "" {
|
||||
color := tagSubstrings[colorForegroundPos]
|
||||
if color == "-" {
|
||||
fgColor = "-"
|
||||
} else if color != "" {
|
||||
fgColor = color
|
||||
}
|
||||
}
|
||||
|
||||
if tagSubstrings[colorBackgroundPos-1] != "" {
|
||||
color := tagSubstrings[colorBackgroundPos]
|
||||
if color == "-" {
|
||||
bgColor = "-"
|
||||
} else if color != "" {
|
||||
bgColor = color
|
||||
}
|
||||
}
|
||||
|
||||
if tagSubstrings[colorFlagPos-1] != "" {
|
||||
flags := tagSubstrings[colorFlagPos]
|
||||
if flags == "-" {
|
||||
attributes = "-"
|
||||
} else if flags != "" {
|
||||
attributes = flags
|
||||
}
|
||||
}
|
||||
|
||||
return fgColor, bgColor, attributes
|
||||
}
|
||||
|
||||
// overlayStyle mixes a background color with a foreground color (fgColor),
|
||||
// a (possibly new) background color (bgColor), and style attributes, and
|
||||
// returns the resulting style. For a definition of the colors and attributes,
|
||||
// see styleFromTag(). Reset instructions cause the corresponding part of the
|
||||
// default style to be used.
|
||||
func overlayStyle(background tcell.Color, defaultStyle tcell.Style, fgColor, bgColor, attributes string) tcell.Style {
|
||||
defFg, defBg, defAttr := defaultStyle.Decompose()
|
||||
style := defaultStyle.Background(background)
|
||||
|
||||
style = style.Foreground(defFg)
|
||||
if fgColor != "" {
|
||||
if fgColor == "-" {
|
||||
style = style.Foreground(defFg)
|
||||
} else {
|
||||
style = style.Foreground(tcell.GetColor(fgColor))
|
||||
}
|
||||
}
|
||||
|
||||
if bgColor == "-" || bgColor == "" && defBg != tcell.ColorDefault {
|
||||
style = style.Background(defBg)
|
||||
} else if bgColor != "" {
|
||||
style = style.Background(tcell.GetColor(bgColor))
|
||||
}
|
||||
|
||||
if attributes == "-" {
|
||||
style = style.Bold(defAttr&tcell.AttrBold > 0)
|
||||
style = style.Blink(defAttr&tcell.AttrBlink > 0)
|
||||
style = style.Reverse(defAttr&tcell.AttrReverse > 0)
|
||||
style = style.Underline(defAttr&tcell.AttrUnderline > 0)
|
||||
style = style.Dim(defAttr&tcell.AttrDim > 0)
|
||||
} else if attributes != "" {
|
||||
style = style.Normal()
|
||||
for _, flag := range attributes {
|
||||
switch flag {
|
||||
case 'l':
|
||||
style = style.Blink(true)
|
||||
case 'b':
|
||||
style = style.Bold(true)
|
||||
case 'd':
|
||||
style = style.Dim(true)
|
||||
case 'r':
|
||||
style = style.Reverse(true)
|
||||
case 'u':
|
||||
style = style.Underline(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return style
|
||||
}
|
||||
|
||||
// decomposeString returns information about a string which may contain color
|
||||
// tags or region tags, depending on which ones are requested to be found. It
|
||||
// returns the indices of the color tags (as returned by
|
||||
// re.FindAllStringIndex()), the color tags themselves (as returned by
|
||||
// re.FindAllStringSubmatch()), the indices of region tags and the region tags
|
||||
// themselves, the indices of an escaped tags (only if at least color tags or
|
||||
// region tags are requested), the string stripped by any tags and escaped, and
|
||||
// the screen width of the stripped string.
|
||||
func decomposeString(text string, findColors, findRegions bool) (colorIndices [][]int, colors [][]string, regionIndices [][]int, regions [][]string, escapeIndices [][]int, stripped string, width int) {
|
||||
// Shortcut for the trivial case.
|
||||
if !findColors && !findRegions {
|
||||
return nil, nil, nil, nil, nil, text, stringWidth(text)
|
||||
}
|
||||
|
||||
// Get positions of any tags.
|
||||
if findColors {
|
||||
colorIndices = colorPattern.FindAllStringIndex(text, -1)
|
||||
colors = colorPattern.FindAllStringSubmatch(text, -1)
|
||||
}
|
||||
if findRegions {
|
||||
regionIndices = regionPattern.FindAllStringIndex(text, -1)
|
||||
regions = regionPattern.FindAllStringSubmatch(text, -1)
|
||||
}
|
||||
escapeIndices = escapePattern.FindAllStringIndex(text, -1)
|
||||
|
||||
// Because the color pattern detects empty tags, we need to filter them out.
|
||||
for i := len(colorIndices) - 1; i >= 0; i-- {
|
||||
if colorIndices[i][1]-colorIndices[i][0] == 2 {
|
||||
colorIndices = append(colorIndices[:i], colorIndices[i+1:]...)
|
||||
colors = append(colors[:i], colors[i+1:]...)
|
||||
}
|
||||
}
|
||||
|
||||
// Make a (sorted) list of all tags.
|
||||
allIndices := make([][3]int, 0, len(colorIndices)+len(regionIndices)+len(escapeIndices))
|
||||
for indexType, index := range [][][]int{colorIndices, regionIndices, escapeIndices} {
|
||||
for _, tag := range index {
|
||||
allIndices = append(allIndices, [3]int{tag[0], tag[1], indexType})
|
||||
}
|
||||
}
|
||||
sort.Slice(allIndices, func(i int, j int) bool {
|
||||
return allIndices[i][0] < allIndices[j][0]
|
||||
})
|
||||
|
||||
// Remove the tags from the original string.
|
||||
var from int
|
||||
buf := make([]byte, 0, len(text))
|
||||
for _, indices := range allIndices {
|
||||
if indices[2] == 2 { // Escape sequences are not simply removed.
|
||||
buf = append(buf, []byte(text[from:indices[1]-2])...)
|
||||
buf = append(buf, ']')
|
||||
from = indices[1]
|
||||
} else {
|
||||
buf = append(buf, []byte(text[from:indices[0]])...)
|
||||
from = indices[1]
|
||||
}
|
||||
}
|
||||
buf = append(buf, text[from:]...)
|
||||
stripped = string(buf)
|
||||
|
||||
// Get the width of the stripped string.
|
||||
width = stringWidth(stripped)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// 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 colors and text styles mid-text by inserting a color tag.
|
||||
// See the package description for details.
|
||||
//
|
||||
// Returns the number of actual bytes of the text printed (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) {
|
||||
return printWithStyle(screen, text, x, y, maxWidth, align, tcell.StyleDefault.Foreground(color))
|
||||
}
|
||||
|
||||
// printWithStyle works like Print() but it takes a style instead of just a
|
||||
// foreground color.
|
||||
func printWithStyle(screen tcell.Screen, text string, x, y, maxWidth, align int, style tcell.Style) (int, int) {
|
||||
totalWidth, totalHeight := screen.Size()
|
||||
if maxWidth <= 0 || len(text) == 0 || y < 0 || y >= totalHeight {
|
||||
return 0, 0
|
||||
}
|
||||
|
||||
// Decompose the text.
|
||||
colorIndices, colors, _, _, escapeIndices, strippedText, strippedWidth := decomposeString(text, true, false)
|
||||
|
||||
// We want to reduce all alignments to AlignLeft.
|
||||
if align == AlignRight {
|
||||
if strippedWidth <= maxWidth {
|
||||
// There's enough space for the entire text.
|
||||
return printWithStyle(screen, text, x+maxWidth-strippedWidth, y, maxWidth, AlignLeft, style)
|
||||
}
|
||||
// Trim characters off the beginning.
|
||||
var (
|
||||
bytes, width, colorPos, escapePos, tagOffset int
|
||||
foregroundColor, backgroundColor, attributes string
|
||||
)
|
||||
_, originalBackground, _ := style.Decompose()
|
||||
iterateString(strippedText, func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool {
|
||||
// Update color/escape tag offset and style.
|
||||
if colorPos < len(colorIndices) && textPos+tagOffset >= colorIndices[colorPos][0] && textPos+tagOffset < colorIndices[colorPos][1] {
|
||||
foregroundColor, backgroundColor, attributes = styleFromTag(foregroundColor, backgroundColor, attributes, colors[colorPos])
|
||||
style = overlayStyle(originalBackground, style, foregroundColor, backgroundColor, attributes)
|
||||
tagOffset += colorIndices[colorPos][1] - colorIndices[colorPos][0]
|
||||
colorPos++
|
||||
}
|
||||
if escapePos < len(escapeIndices) && textPos+tagOffset >= escapeIndices[escapePos][0] && textPos+tagOffset < escapeIndices[escapePos][1] {
|
||||
tagOffset++
|
||||
escapePos++
|
||||
}
|
||||
if strippedWidth-screenPos < maxWidth {
|
||||
// We chopped off enough.
|
||||
if escapePos > 0 && textPos+tagOffset-1 >= escapeIndices[escapePos-1][0] && textPos+tagOffset-1 < escapeIndices[escapePos-1][1] {
|
||||
// Unescape open escape sequences.
|
||||
escapeCharPos := escapeIndices[escapePos-1][1] - 2
|
||||
text = text[:escapeCharPos] + text[escapeCharPos+1:]
|
||||
}
|
||||
// Print and return.
|
||||
bytes, width = printWithStyle(screen, text[textPos+tagOffset:], x, y, maxWidth, AlignLeft, style)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
return bytes, width
|
||||
} else if align == AlignCenter {
|
||||
if strippedWidth == maxWidth {
|
||||
// Use the exact space.
|
||||
return printWithStyle(screen, text, x, y, maxWidth, AlignLeft, style)
|
||||
} else if strippedWidth < maxWidth {
|
||||
// We have more space than we need.
|
||||
half := (maxWidth - strippedWidth) / 2
|
||||
return printWithStyle(screen, text, x+half, y, maxWidth-half, AlignLeft, style)
|
||||
} else {
|
||||
// Chop off runes until we have a perfect fit.
|
||||
var choppedLeft, choppedRight, leftIndex, rightIndex int
|
||||
rightIndex = len(strippedText)
|
||||
for rightIndex-1 > leftIndex && strippedWidth-choppedLeft-choppedRight > maxWidth {
|
||||
if choppedLeft < choppedRight {
|
||||
// Iterate on the left by one character.
|
||||
iterateString(strippedText[leftIndex:], func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool {
|
||||
choppedLeft += screenWidth
|
||||
leftIndex += textWidth
|
||||
return true
|
||||
})
|
||||
} else {
|
||||
// Iterate on the right by one character.
|
||||
iterateStringReverse(strippedText[leftIndex:rightIndex], func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool {
|
||||
choppedRight += screenWidth
|
||||
rightIndex -= textWidth
|
||||
return true
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Add tag offsets and determine start style.
|
||||
var (
|
||||
colorPos, escapePos, tagOffset int
|
||||
foregroundColor, backgroundColor, attributes string
|
||||
)
|
||||
_, originalBackground, _ := style.Decompose()
|
||||
for index := range strippedText {
|
||||
// We only need the offset of the left index.
|
||||
if index > leftIndex {
|
||||
// We're done.
|
||||
if escapePos > 0 && leftIndex+tagOffset-1 >= escapeIndices[escapePos-1][0] && leftIndex+tagOffset-1 < escapeIndices[escapePos-1][1] {
|
||||
// Unescape open escape sequences.
|
||||
escapeCharPos := escapeIndices[escapePos-1][1] - 2
|
||||
text = text[:escapeCharPos] + text[escapeCharPos+1:]
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
// Update color/escape tag offset.
|
||||
if colorPos < len(colorIndices) && index+tagOffset >= colorIndices[colorPos][0] && index+tagOffset < colorIndices[colorPos][1] {
|
||||
if index <= leftIndex {
|
||||
foregroundColor, backgroundColor, attributes = styleFromTag(foregroundColor, backgroundColor, attributes, colors[colorPos])
|
||||
style = overlayStyle(originalBackground, style, foregroundColor, backgroundColor, attributes)
|
||||
}
|
||||
tagOffset += colorIndices[colorPos][1] - colorIndices[colorPos][0]
|
||||
colorPos++
|
||||
}
|
||||
if escapePos < len(escapeIndices) && index+tagOffset >= escapeIndices[escapePos][0] && index+tagOffset < escapeIndices[escapePos][1] {
|
||||
tagOffset++
|
||||
escapePos++
|
||||
}
|
||||
}
|
||||
return printWithStyle(screen, text[leftIndex+tagOffset:], x, y, maxWidth, AlignLeft, style)
|
||||
}
|
||||
}
|
||||
|
||||
// Draw text.
|
||||
var (
|
||||
drawn, drawnWidth, colorPos, escapePos, tagOffset int
|
||||
foregroundColor, backgroundColor, attributes string
|
||||
)
|
||||
iterateString(strippedText, func(main rune, comb []rune, textPos, length, screenPos, screenWidth int) bool {
|
||||
// Only continue if there is still space.
|
||||
if drawnWidth+screenWidth > maxWidth || x+drawnWidth >= totalWidth {
|
||||
return true
|
||||
}
|
||||
|
||||
// Handle color tags.
|
||||
for colorPos < len(colorIndices) && textPos+tagOffset >= colorIndices[colorPos][0] && textPos+tagOffset < colorIndices[colorPos][1] {
|
||||
foregroundColor, backgroundColor, attributes = styleFromTag(foregroundColor, backgroundColor, attributes, colors[colorPos])
|
||||
tagOffset += colorIndices[colorPos][1] - colorIndices[colorPos][0]
|
||||
colorPos++
|
||||
}
|
||||
|
||||
// Handle scape tags.
|
||||
if escapePos < len(escapeIndices) && textPos+tagOffset >= escapeIndices[escapePos][0] && textPos+tagOffset < escapeIndices[escapePos][1] {
|
||||
if textPos+tagOffset == escapeIndices[escapePos][1]-2 {
|
||||
tagOffset++
|
||||
escapePos++
|
||||
}
|
||||
}
|
||||
|
||||
// Print the rune sequence.
|
||||
finalX := x + drawnWidth
|
||||
_, _, finalStyle, _ := screen.GetContent(finalX, y)
|
||||
_, background, _ := finalStyle.Decompose()
|
||||
finalStyle = overlayStyle(background, style, foregroundColor, backgroundColor, attributes)
|
||||
for offset := screenWidth - 1; offset >= 0; offset-- {
|
||||
// To avoid undesired effects, we populate all cells.
|
||||
if offset == 0 {
|
||||
screen.SetContent(finalX+offset, y, main, comb, finalStyle)
|
||||
} else {
|
||||
screen.SetContent(finalX+offset, y, ' ', nil, finalStyle)
|
||||
}
|
||||
}
|
||||
|
||||
// Advance.
|
||||
drawn += length
|
||||
drawnWidth += screenWidth
|
||||
|
||||
return false
|
||||
})
|
||||
|
||||
return drawn + tagOffset + len(escapeIndices), 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)
|
||||
}
|
||||
|
||||
// TaggedStringWidth returns the width of the given string needed to print it on
|
||||
// screen. The text may contain color tags which are not counted.
|
||||
func TaggedStringWidth(text string) int {
|
||||
_, _, _, _, _, _, width := decomposeString(text, true, false)
|
||||
return width
|
||||
}
|
||||
|
||||
// stringWidth returns the number of horizontal cells needed to print the given
|
||||
// text. It splits the text into its grapheme clusters, calculates each
|
||||
// cluster's width, and adds them up to a total.
|
||||
func stringWidth(text string) (width int) {
|
||||
g := uniseg.NewGraphemes(text)
|
||||
for g.Next() {
|
||||
var chWidth int
|
||||
for _, r := range g.Runes() {
|
||||
chWidth = runewidth.RuneWidth(r)
|
||||
if chWidth > 0 {
|
||||
break // Our best guess at this point is to use the width of the first non-zero-width rune.
|
||||
}
|
||||
}
|
||||
width += chWidth
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 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) {
|
||||
colorTagIndices, _, _, _, escapeIndices, strippedText, _ := decomposeString(text, true, false)
|
||||
|
||||
// Find candidate breakpoints.
|
||||
breakpoints := boundaryPattern.FindAllStringSubmatchIndex(strippedText, -1)
|
||||
// Results in one entry for each candidate. Each entry is an array a of
|
||||
// indices into strippedText where a[6] < 0 for newline/punctuation matches
|
||||
// and a[4] < 0 for whitespace matches.
|
||||
|
||||
// Process stripped text one character at a time.
|
||||
var (
|
||||
colorPos, escapePos, breakpointPos, tagOffset int
|
||||
lastBreakpoint, lastContinuation, currentLineStart int
|
||||
lineWidth, overflow int
|
||||
forceBreak bool
|
||||
)
|
||||
unescape := func(substr string, startIndex int) string {
|
||||
// A helper function to unescape escaped tags.
|
||||
for index := escapePos; index >= 0; index-- {
|
||||
if index < len(escapeIndices) && startIndex > escapeIndices[index][0] && startIndex < escapeIndices[index][1]-1 {
|
||||
pos := escapeIndices[index][1] - 2 - startIndex
|
||||
return substr[:pos] + substr[pos+1:]
|
||||
}
|
||||
}
|
||||
return substr
|
||||
}
|
||||
iterateString(strippedText, func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool {
|
||||
// Handle tags.
|
||||
for {
|
||||
if colorPos < len(colorTagIndices) && textPos+tagOffset >= colorTagIndices[colorPos][0] && textPos+tagOffset < colorTagIndices[colorPos][1] {
|
||||
// Colour tags.
|
||||
tagOffset += colorTagIndices[colorPos][1] - colorTagIndices[colorPos][0]
|
||||
colorPos++
|
||||
} else if escapePos < len(escapeIndices) && textPos+tagOffset == escapeIndices[escapePos][1]-2 {
|
||||
// Escape tags.
|
||||
tagOffset++
|
||||
escapePos++
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Is this a breakpoint?
|
||||
if breakpointPos < len(breakpoints) && textPos+tagOffset == breakpoints[breakpointPos][0] {
|
||||
// Yes, it is. Set up breakpoint infos depending on its type.
|
||||
lastBreakpoint = breakpoints[breakpointPos][0] + tagOffset
|
||||
lastContinuation = breakpoints[breakpointPos][1] + tagOffset
|
||||
overflow = 0
|
||||
forceBreak = main == '\n'
|
||||
if breakpoints[breakpointPos][6] < 0 && !forceBreak {
|
||||
lastBreakpoint++ // Don't skip punctuation.
|
||||
}
|
||||
breakpointPos++
|
||||
}
|
||||
|
||||
// Check if a break is warranted.
|
||||
if forceBreak || lineWidth > 0 && lineWidth+screenWidth > width {
|
||||
breakpoint := lastBreakpoint
|
||||
continuation := lastContinuation
|
||||
if forceBreak {
|
||||
breakpoint = textPos + tagOffset
|
||||
continuation = textPos + tagOffset + 1
|
||||
lastBreakpoint = 0
|
||||
overflow = 0
|
||||
} else if lastBreakpoint <= currentLineStart {
|
||||
breakpoint = textPos + tagOffset
|
||||
continuation = textPos + tagOffset
|
||||
overflow = 0
|
||||
}
|
||||
lines = append(lines, unescape(text[currentLineStart:breakpoint], currentLineStart))
|
||||
currentLineStart, lineWidth, forceBreak = continuation, overflow, false
|
||||
}
|
||||
|
||||
// Remember the characters since the last breakpoint.
|
||||
if lastBreakpoint > 0 && lastContinuation <= textPos+tagOffset {
|
||||
overflow += screenWidth
|
||||
}
|
||||
|
||||
// Advance.
|
||||
lineWidth += screenWidth
|
||||
|
||||
// But if we're still inside a breakpoint, skip next character (whitespace).
|
||||
if textPos+tagOffset < currentLineStart {
|
||||
lineWidth -= screenWidth
|
||||
}
|
||||
|
||||
return false
|
||||
})
|
||||
|
||||
// Flush the rest.
|
||||
if currentLineStart < len(text) {
|
||||
lines = append(lines, unescape(text[currentLineStart:], currentLineStart))
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Escape escapes the given text such that color and/or region tags are not
|
||||
// recognized and substituted by the print functions of this package. For
|
||||
// example, to include a tag-like string in a box title or in a TextView:
|
||||
//
|
||||
// box.SetTitle(tview.Escape("[squarebrackets]"))
|
||||
// fmt.Fprint(textView, tview.Escape(`["quoted"]`))
|
||||
func Escape(text string) string {
|
||||
return nonEscapePattern.ReplaceAllString(text, "$1[]")
|
||||
}
|
||||
|
||||
// iterateString iterates through the given string one printed character at a
|
||||
// time. For each such character, the callback function is called with the
|
||||
// Unicode code points of the character (the first rune and any combining runes
|
||||
// which may be nil if there aren't any), the starting position (in bytes)
|
||||
// within the original string, its length in bytes, the screen position of the
|
||||
// character, and the screen width of it. The iteration stops if the callback
|
||||
// returns true. This function returns true if the iteration was stopped before
|
||||
// the last character.
|
||||
func iterateString(text string, callback func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool) bool {
|
||||
var screenPos int
|
||||
|
||||
gr := uniseg.NewGraphemes(text)
|
||||
for gr.Next() {
|
||||
r := gr.Runes()
|
||||
from, to := gr.Positions()
|
||||
width := stringWidth(gr.Str())
|
||||
var comb []rune
|
||||
if len(r) > 1 {
|
||||
comb = r[1:]
|
||||
}
|
||||
|
||||
if callback(r[0], comb, from, to-from, screenPos, width) {
|
||||
return true
|
||||
}
|
||||
|
||||
screenPos += width
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// iterateStringReverse iterates through the given string in reverse, starting
|
||||
// from the end of the string, one printed character at a time. For each such
|
||||
// character, the callback function is called with the Unicode code points of
|
||||
// the character (the first rune and any combining runes which may be nil if
|
||||
// there aren't any), the starting position (in bytes) within the original
|
||||
// string, its length in bytes, the screen position of the character, and the
|
||||
// screen width of it. The iteration stops if the callback returns true. This
|
||||
// function returns true if the iteration was stopped before the last character.
|
||||
func iterateStringReverse(text string, callback func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool) bool {
|
||||
type cluster struct {
|
||||
main rune
|
||||
comb []rune
|
||||
textPos, textWidth, screenPos, screenWidth int
|
||||
}
|
||||
|
||||
// Create the grapheme clusters.
|
||||
var clusters []cluster
|
||||
iterateString(text, func(main rune, comb []rune, textPos int, textWidth int, screenPos int, screenWidth int) bool {
|
||||
clusters = append(clusters, cluster{
|
||||
main: main,
|
||||
comb: comb,
|
||||
textPos: textPos,
|
||||
textWidth: textWidth,
|
||||
screenPos: screenPos,
|
||||
screenWidth: screenWidth,
|
||||
})
|
||||
return false
|
||||
})
|
||||
|
||||
// Iterate in reverse.
|
||||
for index := len(clusters) - 1; index >= 0; index-- {
|
||||
if callback(
|
||||
clusters[index].main,
|
||||
clusters[index].comb,
|
||||
clusters[index].textPos,
|
||||
clusters[index].textWidth,
|
||||
clusters[index].screenPos,
|
||||
clusters[index].screenWidth,
|
||||
) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
21
vendor/github.com/rivo/uniseg/LICENSE.txt
generated
vendored
Normal file
21
vendor/github.com/rivo/uniseg/LICENSE.txt
generated
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2019 Oliver Kuederle
|
||||
|
||||
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.
|
62
vendor/github.com/rivo/uniseg/README.md
generated
vendored
Normal file
62
vendor/github.com/rivo/uniseg/README.md
generated
vendored
Normal file
@@ -0,0 +1,62 @@
|
||||
# Unicode Text Segmentation for Go
|
||||
|
||||
[](https://godoc.org/github.com/rivo/uniseg)
|
||||
[](https://goreportcard.com/report/github.com/rivo/uniseg)
|
||||
|
||||
This Go package implements Unicode Text Segmentation according to [Unicode Standard Annex #29](http://unicode.org/reports/tr29/) (Unicode version 12.0.0).
|
||||
|
||||
At this point, only the determination of grapheme cluster boundaries is implemented.
|
||||
|
||||
## Background
|
||||
|
||||
In Go, [strings are read-only slices of bytes](https://blog.golang.org/strings). They can be turned into Unicode code points using the `for` loop or by casting: `[]rune(str)`. However, multiple code points may be combined into one user-perceived character or what the Unicode specification calls "grapheme cluster". Here are some examples:
|
||||
|
||||
|String|Bytes (UTF-8)|Code points (runes)|Grapheme clusters|
|
||||
|-|-|-|-|
|
||||
|Käse|6 bytes: `4b 61 cc 88 73 65`|5 code points: `4b 61 308 73 65`|4 clusters: `[4b],[61 308],[73],[65]`|
|
||||
|🏳️🌈|14 bytes: `f0 9f 8f b3 ef b8 8f e2 80 8d f0 9f 8c 88`|4 code points: `1f3f3 fe0f 200d 1f308`|1 cluster: `[1f3f3 fe0f 200d 1f308]`|
|
||||
|🇩🇪|8 bytes: `f0 9f 87 a9 f0 9f 87 aa`|2 code points: `1f1e9 1f1ea`|1 cluster: `[1f1e9 1f1ea]`|
|
||||
|
||||
This package provides a tool to iterate over these grapheme clusters. This may be used to determine the number of user-perceived characters, to split strings in their intended places, or to extract individual characters which form a unit.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
go get github.com/rivo/uniseg
|
||||
```
|
||||
|
||||
## Basic Example
|
||||
|
||||
```go
|
||||
package uniseg
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/rivo/uniseg"
|
||||
)
|
||||
|
||||
func main() {
|
||||
gr := uniseg.NewGraphemes("👍🏼!")
|
||||
for gr.Next() {
|
||||
fmt.Printf("%x ", gr.Runes())
|
||||
}
|
||||
// Output: [1f44d 1f3fc] [21]
|
||||
}
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
Refer to https://godoc.org/github.com/rivo/uniseg for the package's documentation.
|
||||
|
||||
## Dependencies
|
||||
|
||||
This package does not depend on any packages outside the standard library.
|
||||
|
||||
## Your Feedback
|
||||
|
||||
Add your issue here on GitHub. Feel free to get in touch if you have any questions.
|
||||
|
||||
## Version
|
||||
|
||||
Version tags will be introduced once Golang modules are official. Consider this version 0.1.
|
8
vendor/github.com/rivo/uniseg/doc.go
generated
vendored
Normal file
8
vendor/github.com/rivo/uniseg/doc.go
generated
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
/*
|
||||
Package uniseg implements Unicode Text Segmentation according to Unicode
|
||||
Standard Annex #29 (http://unicode.org/reports/tr29/).
|
||||
|
||||
At this point, only the determination of grapheme cluster boundaries is
|
||||
implemented.
|
||||
*/
|
||||
package uniseg
|
3
vendor/github.com/rivo/uniseg/go.mod
generated
vendored
Normal file
3
vendor/github.com/rivo/uniseg/go.mod
generated
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
module github.com/rivo/uniseg
|
||||
|
||||
go 1.12
|
258
vendor/github.com/rivo/uniseg/grapheme.go
generated
vendored
Normal file
258
vendor/github.com/rivo/uniseg/grapheme.go
generated
vendored
Normal file
@@ -0,0 +1,258 @@
|
||||
package uniseg
|
||||
|
||||
// The states of the grapheme cluster parser.
|
||||
const (
|
||||
grAny = iota
|
||||
grCR
|
||||
grControlLF
|
||||
grL
|
||||
grLVV
|
||||
grLVTT
|
||||
grPrepend
|
||||
grExtendedPictographic
|
||||
grExtendedPictographicZWJ
|
||||
grRIOdd
|
||||
grRIEven
|
||||
)
|
||||
|
||||
// The grapheme cluster parser's breaking instructions.
|
||||
const (
|
||||
grNoBoundary = iota
|
||||
grBoundary
|
||||
)
|
||||
|
||||
// The grapheme cluster parser's state transitions. Maps (state, property) to
|
||||
// (new state, breaking instruction, rule number). The breaking instruction
|
||||
// always refers to the boundary between the last and next code point.
|
||||
//
|
||||
// This map is queried as follows:
|
||||
//
|
||||
// 1. Find specific state + specific property. Stop if found.
|
||||
// 2. Find specific state + any property.
|
||||
// 3. Find any state + specific property.
|
||||
// 4. If only (2) or (3) (but not both) was found, stop.
|
||||
// 5. If both (2) and (3) were found, use state and breaking instruction from
|
||||
// the transition with the lower rule number, prefer (3) if rule numbers
|
||||
// are equal. Stop.
|
||||
// 6. Assume grAny and grBoundary.
|
||||
var grTransitions = map[[2]int][3]int{
|
||||
// GB5
|
||||
{grAny, prCR}: {grCR, grBoundary, 50},
|
||||
{grAny, prLF}: {grControlLF, grBoundary, 50},
|
||||
{grAny, prControl}: {grControlLF, grBoundary, 50},
|
||||
|
||||
// GB4
|
||||
{grCR, prAny}: {grAny, grBoundary, 40},
|
||||
{grControlLF, prAny}: {grAny, grBoundary, 40},
|
||||
|
||||
// GB3.
|
||||
{grCR, prLF}: {grAny, grNoBoundary, 30},
|
||||
|
||||
// GB6.
|
||||
{grAny, prL}: {grL, grBoundary, 9990},
|
||||
{grL, prL}: {grL, grNoBoundary, 60},
|
||||
{grL, prV}: {grLVV, grNoBoundary, 60},
|
||||
{grL, prLV}: {grLVV, grNoBoundary, 60},
|
||||
{grL, prLVT}: {grLVTT, grNoBoundary, 60},
|
||||
|
||||
// GB7.
|
||||
{grAny, prLV}: {grLVV, grBoundary, 9990},
|
||||
{grAny, prV}: {grLVV, grBoundary, 9990},
|
||||
{grLVV, prV}: {grLVV, grNoBoundary, 70},
|
||||
{grLVV, prT}: {grLVTT, grNoBoundary, 70},
|
||||
|
||||
// GB8.
|
||||
{grAny, prLVT}: {grLVTT, grBoundary, 9990},
|
||||
{grAny, prT}: {grLVTT, grBoundary, 9990},
|
||||
{grLVTT, prT}: {grLVTT, grNoBoundary, 80},
|
||||
|
||||
// GB9.
|
||||
{grAny, prExtend}: {grAny, grNoBoundary, 90},
|
||||
{grAny, prZWJ}: {grAny, grNoBoundary, 90},
|
||||
|
||||
// GB9a.
|
||||
{grAny, prSpacingMark}: {grAny, grNoBoundary, 91},
|
||||
|
||||
// GB9b.
|
||||
{grAny, prPreprend}: {grPrepend, grBoundary, 9990},
|
||||
{grPrepend, prAny}: {grAny, grNoBoundary, 92},
|
||||
|
||||
// GB11.
|
||||
{grAny, prExtendedPictographic}: {grExtendedPictographic, grBoundary, 9990},
|
||||
{grExtendedPictographic, prExtend}: {grExtendedPictographic, grNoBoundary, 110},
|
||||
{grExtendedPictographic, prZWJ}: {grExtendedPictographicZWJ, grNoBoundary, 110},
|
||||
{grExtendedPictographicZWJ, prExtendedPictographic}: {grExtendedPictographic, grNoBoundary, 110},
|
||||
|
||||
// GB12 / GB13.
|
||||
{grAny, prRegionalIndicator}: {grRIOdd, grBoundary, 9990},
|
||||
{grRIOdd, prRegionalIndicator}: {grRIEven, grNoBoundary, 120},
|
||||
{grRIEven, prRegionalIndicator}: {grRIOdd, grBoundary, 120},
|
||||
}
|
||||
|
||||
// Graphemes implements an iterator over Unicode extended grapheme clusters,
|
||||
// specified in the Unicode Standard Annex #29. Grapheme clusters correspond to
|
||||
// "user-perceived characters". These characters often consist of multiple
|
||||
// code points (e.g. the "woman kissing woman" emoji consists of 8 code points:
|
||||
// woman + ZWJ + heavy black heart (2 code points) + ZWJ + kiss mark + ZWJ +
|
||||
// woman) and the rules described in Annex #29 must be applied to group those
|
||||
// code points into clusters perceived by the user as one character.
|
||||
type Graphemes struct {
|
||||
// The code points over which this class iterates.
|
||||
codePoints []rune
|
||||
|
||||
// The (byte-based) indices of the code points into the original string plus
|
||||
// len(original string). Thus, len(indices) = len(codePoints) + 1.
|
||||
indices []int
|
||||
|
||||
// The current grapheme cluster to be returned. These are indices into
|
||||
// codePoints/indices. If start == end, we either haven't started iterating
|
||||
// yet (0) or the iteration has already completed (1).
|
||||
start, end int
|
||||
|
||||
// The index of the next code point to be parsed.
|
||||
pos int
|
||||
|
||||
// The current state of the code point parser.
|
||||
state int
|
||||
}
|
||||
|
||||
// NewGraphemes returns a new grapheme cluster iterator.
|
||||
func NewGraphemes(s string) *Graphemes {
|
||||
g := &Graphemes{}
|
||||
for index, codePoint := range s {
|
||||
g.codePoints = append(g.codePoints, codePoint)
|
||||
g.indices = append(g.indices, index)
|
||||
}
|
||||
g.indices = append(g.indices, len(s))
|
||||
g.Next() // Parse ahead.
|
||||
return g
|
||||
}
|
||||
|
||||
// Next advances the iterator by one grapheme cluster and returns false if no
|
||||
// clusters are left. This function must be called before the first cluster is
|
||||
// accessed.
|
||||
func (g *Graphemes) Next() bool {
|
||||
g.start = g.end
|
||||
|
||||
// The state transition gives us a boundary instruction BEFORE the next code
|
||||
// point so we always need to stay ahead by one code point.
|
||||
|
||||
// Parse the next code point.
|
||||
for g.pos <= len(g.codePoints) {
|
||||
// GB2.
|
||||
if g.pos == len(g.codePoints) {
|
||||
g.end = g.pos
|
||||
g.pos++
|
||||
break
|
||||
}
|
||||
|
||||
// Determine the property of the next character.
|
||||
nextProperty := property(g.codePoints[g.pos])
|
||||
g.pos++
|
||||
|
||||
// Find the applicable transition.
|
||||
var boundary bool
|
||||
transition, ok := grTransitions[[2]int{g.state, nextProperty}]
|
||||
if ok {
|
||||
// We have a specific transition. We'll use it.
|
||||
g.state = transition[0]
|
||||
boundary = transition[1] == grBoundary
|
||||
} else {
|
||||
// No specific transition found. Try the less specific ones.
|
||||
transAnyProp, okAnyProp := grTransitions[[2]int{g.state, prAny}]
|
||||
transAnyState, okAnyState := grTransitions[[2]int{grAny, nextProperty}]
|
||||
if okAnyProp && okAnyState {
|
||||
// Both apply. We'll use a mix (see comments for grTransitions).
|
||||
g.state = transAnyState[0]
|
||||
boundary = transAnyState[1] == grBoundary
|
||||
if transAnyProp[2] < transAnyState[2] {
|
||||
g.state = transAnyProp[0]
|
||||
boundary = transAnyProp[1] == grBoundary
|
||||
}
|
||||
} else if okAnyProp {
|
||||
// We only have a specific state.
|
||||
g.state = transAnyProp[0]
|
||||
boundary = transAnyProp[1] == grBoundary
|
||||
// This branch will probably never be reached because okAnyState will
|
||||
// always be true given the current transition map. But we keep it here
|
||||
// for future modifications to the transition map where this may not be
|
||||
// true anymore.
|
||||
} else if okAnyState {
|
||||
// We only have a specific property.
|
||||
g.state = transAnyState[0]
|
||||
boundary = transAnyState[1] == grBoundary
|
||||
} else {
|
||||
// No known transition. GB999: Any x Any.
|
||||
g.state = grAny
|
||||
boundary = true
|
||||
}
|
||||
}
|
||||
|
||||
// If we found a cluster boundary, let's stop here. The current cluster will
|
||||
// be the one that just ended.
|
||||
if g.pos-1 == 0 /* GB1 */ || boundary {
|
||||
g.end = g.pos - 1
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return g.start != g.end
|
||||
}
|
||||
|
||||
// Runes returns a slice of runes (code points) which corresponds to the current
|
||||
// grapheme cluster. If the iterator is already past the end or Next() has not
|
||||
// yet been called, nil is returned.
|
||||
func (g *Graphemes) Runes() []rune {
|
||||
if g.start == g.end {
|
||||
return nil
|
||||
}
|
||||
return g.codePoints[g.start:g.end]
|
||||
}
|
||||
|
||||
// Str returns a substring of the original string which corresponds to the
|
||||
// current grapheme cluster. If the iterator is already past the end or Next()
|
||||
// has not yet been called, an empty string is returned.
|
||||
func (g *Graphemes) Str() string {
|
||||
if g.start == g.end {
|
||||
return ""
|
||||
}
|
||||
return string(g.codePoints[g.start:g.end])
|
||||
}
|
||||
|
||||
// Bytes returns a byte slice which corresponds to the current grapheme cluster.
|
||||
// If the iterator is already past the end or Next() has not yet been called,
|
||||
// nil is returned.
|
||||
func (g *Graphemes) Bytes() []byte {
|
||||
if g.start == g.end {
|
||||
return nil
|
||||
}
|
||||
return []byte(string(g.codePoints[g.start:g.end]))
|
||||
}
|
||||
|
||||
// Positions returns the interval of the current grapheme cluster as byte
|
||||
// positions into the original string. The first returned value "from" indexes
|
||||
// the first byte and the second returned value "to" indexes the first byte that
|
||||
// is not included anymore, i.e. str[from:to] is the current grapheme cluster of
|
||||
// the original string "str". If Next() has not yet been called, both values are
|
||||
// 0. If the iterator is already past the end, both values are 1.
|
||||
func (g *Graphemes) Positions() (int, int) {
|
||||
return g.indices[g.start], g.indices[g.end]
|
||||
}
|
||||
|
||||
// Reset puts the iterator into its initial state such that the next call to
|
||||
// Next() sets it to the first grapheme cluster again.
|
||||
func (g *Graphemes) Reset() {
|
||||
g.start, g.end, g.pos, g.state = 0, 0, 0, grAny
|
||||
g.Next() // Parse ahead again.
|
||||
}
|
||||
|
||||
// GraphemeClusterCount returns the number of user-perceived characters
|
||||
// (grapheme clusters) for the given string. To calculate this number, it
|
||||
// iterates through the string using the Graphemes iterator.
|
||||
func GraphemeClusterCount(s string) (n int) {
|
||||
g := NewGraphemes(s)
|
||||
for g.Next() {
|
||||
n++
|
||||
}
|
||||
return
|
||||
}
|
1658
vendor/github.com/rivo/uniseg/properties.go
generated
vendored
Normal file
1658
vendor/github.com/rivo/uniseg/properties.go
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user