chore(deps): 🚀 update module dependencies

* Add new dependencies for terminal handling and color management.
* Include updates for tcell, go-colorful, tview, and uniseg.
* Update golang.org/x/sys and golang.org/x/term for improved compatibility.
* Ensure all dependencies are explicitly listed with their versions.
This commit is contained in:
2026-01-04 18:29:11 +02:00
parent 5d3c86119e
commit 355f0f918f
559 changed files with 279307 additions and 4 deletions

35
vendor/github.com/rivo/tview/CONTRIBUTING.md generated vendored Normal file
View 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. Comments in their own lines must not exceed the 80 character border. Break over if necessary.
- 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
View 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.

171
vendor/github.com/rivo/tview/README.md generated vendored Normal file
View File

@@ -0,0 +1,171 @@
# Rich Interactive Widgets for Terminal UIs
[![PkgGoDev](https://pkg.go.dev/badge/github.com/rivo/tview)](https://pkg.go.dev/github.com/rivo/tview)
[![Go Report](https://img.shields.io/badge/go%20report-A%2B-brightgreen.svg)](https://goreportcard.com/report/github.com/rivo/tview)
This Go package provides commonly used components for terminal based user interfaces.
![Screenshot](tview.gif)
Among these components are:
- __Input forms__ (including __text input__, __selections__, __checkboxes__, and __buttons__)
- Navigable multi-color __text views__
- Editable multi-line __text areas__
- Sophisticated navigable __table views__
- Flexible __tree views__
- Selectable __lists__
- __Images__
- __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.
## Usage
To add this package to your project:
```bash
go get github.com/rivo/tview@master
```
## 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.
## Projects using `tview`
- [K9s - Kubernetes CLI](https://github.com/derailed/k9s)
- [IRCCloud Terminal Client](https://github.com/termoose/irccloud)
- [Window manager for `tview`](https://github.com/epiclabs-io/winman)
- [CLI bookmark manager](https://github.com/Endi1/drawer)
- [A caving database interface written in Go](https://github.com/IdlePhysicist/cave-logger)
- [Interactive file browse and exec any command.](https://github.com/bannzai/itree)
- [A complete TUI for LDAP](https://github.com/Macmod/godap)
- [A simple CRM](https://github.com/broadcastle/crm)
- [Terminal UI for todist](https://github.com/cyberdummy/todoista)
- [Graphical kubectl wrapper](https://github.com/dcaiafa/kpick)
- [Decred Decentralized Exchange ](https://github.com/decred/dcrdex)
- [A CLI file browser for Raspberry PI](https://github.com/destinmoulton/pixi)
- [A tool to manage projects.](https://github.com/divramod/dp)
- [A simple app for BMI monitoring](https://github.com/erleene/go-bmi)
- [Stream TIDAL from command line](https://github.com/godsic/vibe)
- [Secure solution for fully decentralized password management](https://github.com/guillaumemichel/passtor/)
- [A growing collection of convenient little tools to work with systemd services](https://github.com/muesli/service-tools/)
- [A terminal based browser for Redis written in Go](https://github.com/nitishm/redis-terminal)
- [First project for the Computer Networks course.](https://github.com/pablogadhi/XMPPClient)
- [Test your typing speed in the terminal!](https://github.com/shilangyu/typer-go)
- [TUI Client for Docker](https://github.com/skanehira/docui)
- [SSH client using certificates signed by HashiCorp Vault](https://github.com/stephane-martin/vssh)
- [VMware vCenter Text UI](https://github.com/thebsdbox/vctui)
- [Bookmarks on terminal](https://github.com/tryffel/bookmarker)
- [A UDP testing utility](https://github.com/vaelen/udp-tester)
- [A simple Kanban board for your terminal](https://github.com/witchard/toukan)
- [The personal information dashboard for your terminal. ](https://github.com/wtfutil/wtf)
- [MySQL database to Golang struct](https://github.com/xxjwxc/gormt)
- [Discord, TUI and SIXEL.](https://gitlab.com/diamondburned/6cord)
- [A CLI Audio Player](https://www.github.com/dhulihan/grump)
- [GLab, a GitLab CLI tool](https://gitlab.com/profclems/glab)
- [Browse your AWS ECS Clusters in the Terminal](https://github.com/swartzrock/ecsview)
- [The CLI Task Manager for Geeks](https://github.com/ajaxray/geek-life)
- [Fast disk usage analyzer written in Go](https://github.com/dundee/gdu)
- [Multiplayer Chess On Terminal](https://github.com/qnkhuat/gochess)
- [Scriptable TUI music player](https://github.com/issadarkthing/gomu)
- [MangaDesk : TUI Client for downloading manga to your computer](https://github.com/darylhjd/mangadesk)
- [Go How Much? a Crypto coin price tracking from terminal](https://github.com/ledongthuc/gohowmuch)
- [dbui: Universal CLI for Database Connections](https://github.com/KenanBek/dbui)
- [ssmbrowse: Simple and elegant cli AWS SSM parameter browser](https://github.com/bnaydenov/ssmbrowse)
- [gobit: binance intelligence terminal](https://github.com/infl00p/gobit)
- [viddy: A modern watch command](https://github.com/sachaos/viddy)
- [s3surfer: CLI tool for browsing S3 bucket and download objects interactively](https://github.com/hirose31/s3surfer)
- [libgen-tui: A terminal UI for downloading books from Library Genesis](https://github.com/audstanley/libgen-tui)
- [kubectl-lazy: kubectl plugin to easy to view pod](https://github.com/togettoyou/kubectl-lazy)
- [podman-tui: podman user interface](https://github.com/containers/podman-tui)
- [tvxwidgets: tview extra widgets](https://github.com/navidys/tvxwidgets)
- [Domino card game on terminal](https://github.com/gusti-andika/card-domino.git)
- [goaround: Query stackoverflow API and get results on terminal](https://github.com/glendsoza/goaround)
- [resto: a CLI app can send pretty HTTP & API requests with TUI](https://github.com/abdfnx/resto)
- [twad: a WAD launcher for the terminal](https://github.com/zmnpl/twad)
- [pacseek: A TUI for searching and installing Arch Linux packages](https://github.com/moson-mo/pacseek)
- [7GUIs demo](https://github.com/letientai299/7guis/tree/master/tui)
- [tuihub: A utility hub/dashboard for personal use](https://github.com/ashis0013/tuihub)
- [l'oggo: A terminal app for structured log streaming (GCP stack driver, k8s, local streaming)](https://github.com/aurc/loggo)
- [reminder: Terminal based interactive app for organising tasks with minimal efforts.](https://github.com/goyalmunish/reminder)
- [tufw: A terminal UI for ufw.](https://github.com/peltho/tufw)
- [gh: the GitHub CLI](https://github.com/cli/cli)
- [piptui: Terminal UI to manage pip packages](https://github.com/glendsoza/piptui/)
- [cross-clipboard: A cross-platform clipboard sharing](https://github.com/ntsd/cross-clipboard)
- [tui-deck: nextcloud deck frontend](https://github.com/mebitek/tui-deck)
- [ktop: A top-like tool for your Kubernetes clusters](https://github.com/vladimirvivien/ktop)
- [blimp: UI for weather, network latency, application status, & more](https://github.com/merlinfuchs/blimp)
- [Curly - A simple TUI leveraging curl to test endpoints](https://github.com/migcaraballo/curly)
- [amtui: Alertmanager TUI](https://github.com/pehlicd/amtui)
- [A TUI CLI manager](https://github.com/costa86/cli-manager)
- [PrivateBTC](https://github.com/adrianbrad/privatebtc)
- [play: A TUI playground to experiment with your favorite programs, such as grep, sed, awk, jq and yq](https://github.com/paololazzari/play)
- [gorest: Enjoy making HTTP requests in your terminal, just like you do in Insomnia.](https://github.com/NathanFirmo/gorest)
- [Terminal-based application to listen Radio Stations around the world!](https://github.com/vergonha/garden-tui)
- [ntui: A TUI to manage Hashicorp Nomad clusters](https://github.com/SHAPPY0/ntui)
- [lazysql: A cross-platform TUI database management tool written in Go](https://github.com/jorgerojas26/lazysql)
- [redis-tui: A Redis Text-based UI client in CLI](https://github.com/mylxsw/redis-tui)
- [fen: File manager](https://github.com/kivattt/fen)
- [sqltui: A terminal UI to operate sql and nosql databases](https://github.com/LinPr/sqltui)
- [DBee: Simple database browser](https://github.com/murat-cileli/dbee)
- [oddshub: A TUI for sports betting odds](https://github.com/dos-2/oddshub)
- [envolve: Terminal based interactive app for manage enviroment variables](https://github.com/erdemkosk/envolve)
- [zfs-file-history: Terminal UI for inspecting and restoring file history on ZFS snapshots](https://github.com/markusressel/zfs-file-history)
- [fan2go-tui: Terminal UI for fan2go](https://github.com/markusressel/fan2go-tui)
- [NatsDash: Terminal UI for NATS Jetstream](https://nats-dash-gui.returnzero.win/)
- [tuissh: A terminal UI to manage ssh connections](https://github.com/linuxexam/tuissh)
- [chiko: Ultimate Beauty TUI gRPC Client](https://github.com/felangga/chiko)
- [kmip-explorer: Browse & manage your KMIP objects from the terminal](https://github.com/phsym/kmip-explorer)
- [stui: Slurm TUI for managing HPC clusters](https://github.com/antvirf/stui)
- [nerdlog: Fast, remote-first, multi-host log viewer with timeline histogram](https://github.com/dimonomid/nerdlog)
## Documentation
Refer to https://pkg.go.dev/github.com/rivo/tview for the package's documentation. Also check out the [Wiki](https://github.com/rivo/tview/wiki).
## 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).
## Sponsor this Project
[Become a Sponsor on GitHub](https://github.com/sponsors/rivo?metadata_source=tview_readme) to further this project!
## Backwards-Compatibility
I try really hard to keep this project backwards compatible. Your software should not break when you upgrade `tview`. But this also means that some of its shortcomings that were present in the initial versions will remain. Having said that, backwards compatibility may still break when:
- a new version of an imported package (most likely [`tcell`](https://github.com/gdamore/tcell)) changes in such a way that forces me to make changes in `tview` as well,
- I fix something that I consider a bug, rather than a feature, something that does not work as originally intended,
- I make changes to "internal" interfaces such as [`Primitive`](https://pkg.go.dev/github.com/rivo/tview#Primitive). You shouldn't need these interfaces unless you're writing your own primitives for `tview`. (Yes, I realize these are public interfaces. This has advantages as well as disadvantages. For the time being, it is what it is.)
## Your Feedback
Add your issue here on GitHub. Feel free to get in touch if you have any questions.
## Code of Conduct
We follow Golang's Code of Conduct which you can find [here](https://golang.org/conduct).

283
vendor/github.com/rivo/tview/ansi.go generated vendored Normal file
View File

@@ -0,0 +1,283 @@
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 style 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 style 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 || fields[0] == "" || fields[0] == "0" {
// Reset.
foreground = "-"
background = "-"
a.attributes = "-"
}
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.ContainsRune(a.attributes, 'b') {
a.attributes += "b"
}
case "2", "02":
if !strings.ContainsRune(a.attributes, 'd') {
a.attributes += "d"
}
case "3", "03":
if !strings.ContainsRune(a.attributes, 'i') {
a.attributes += "i"
}
case "4", "04":
if !strings.ContainsRune(a.attributes, 'u') {
a.attributes += "u"
}
case "5", "05":
if !strings.ContainsRune(a.attributes, 'l') {
a.attributes += "l"
}
case "7", "07":
if !strings.ContainsRune(a.attributes, 'r') {
a.attributes += "r"
}
case "9", "09":
if !strings.ContainsRune(a.attributes, 's') {
a.attributes += "s"
}
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 "23":
if i := strings.IndexRune(a.attributes, 'i'); 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 "27":
if i := strings.IndexRune(a.attributes, 'r'); i >= 0 {
a.attributes = a.attributes[:i] + a.attributes[i+1:]
}
case "29":
if i := strings.IndexRune(a.attributes, 's'); 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) > 1 && a.attributes[0] == '-' {
a.attributes = a.attributes[1:]
}
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 style 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()
}

899
vendor/github.com/rivo/tview/application.go generated vendored Normal file
View File

@@ -0,0 +1,899 @@
package tview
import (
"strings"
"sync"
"time"
"github.com/gdamore/tcell/v2"
)
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
// The following special value will not be provided as a mouse action but
// indicate that an overridden mouse event was consumed. See
// [Box.SetMouseCapture] for details.
MouseConsumed
)
// queuedUpdate represented the execution of f queued by
// Application.QueueUpdate(). If "done" is not nil, it 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 application's title. If not empty, it will be set on every new screen
// that is added.
title string
// 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
// Set to true if paste events are enabled.
enablePaste 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.
//
// The only default global key event is Ctrl-C which stops the application. It
// requires special handling:
//
// - If you do not wish to change the default behavior, return the original
// event object passed to your input capture function.
// - If you wish to block Ctrl-C from any functionality, return nil.
// - If you do not wish Ctrl-C to stop the application but still want to
// forward the Ctrl-C event to primitives down the hierarchy, return a new
// key event with the same key and modifiers, e.g.
// tcell.NewEventKey(tcell.KeyCtrlC, 0, tcell.ModNone).
//
// Pasted key events are not forwarded to the input capture function if pasting
// is enabled (see [Application.EnablePaste]).
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. In such a case, the
// event is considered consumed and the screen will be redrawn.
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. As the tcell.Screen interface may
// change in the future, you may need to update your code when this package
// updates to a new tcell version.
//
// 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()
screen.Init()
return a
}
// Run() is already in progress. Exchange screen.
oldScreen := a.screen
a.Unlock()
oldScreen.Fini()
a.screenReplacement <- screen
return a
}
// SetTitle sets the title of the terminal window, to the extent that the
// terminal supports it. A non-empty title will be set on every new tcell.Screen
// that is created by or added to this application.
func (a *Application) SetTitle(title string) *Application {
a.Lock()
defer a.Unlock()
a.title = title
if a.screen != nil {
a.screen.SetTitle(title)
}
return a
}
// EnableMouse enables mouse events or disables them (if "false" is provided).
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
}
// EnablePaste enables the capturing of paste events or disables them (if
// "false" is provided). This must be supported by the terminal.
//
// Widgets won't interpret paste events for navigation or selection purposes.
// Paste events are typically only used to insert a block of text into an
// [InputField] or a [TextArea].
func (a *Application) EnablePaste(enable bool) *Application {
a.Lock()
defer a.Unlock()
if enable != a.enablePaste && a.screen != nil {
if enable {
a.screen.EnablePaste()
} else {
a.screen.DisablePaste()
}
}
a.enablePaste = enable
return a
}
// Run starts the application and thus the event loop. This function returns
// when [Application.Stop] was called.
//
// Note that while an application is running, it fully claims stdin, stdout, and
// stderr. If you use these standard streams, they may not work as expected.
// Consider stopping the application first or suspending it (using
// [Application.Suspend]) if you have to interact with the standard streams, for
// example when needing to print a call stack during a panic.
func (a *Application) Run() error {
var (
err, appErr error
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()
} else {
a.screen.DisableMouse()
}
if a.enablePaste {
a.screen.EnablePaste()
} else {
a.screen.DisablePaste()
}
if a.title != "" {
a.screen.SetTitle(a.title)
}
}
// 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 screen.
screen = <-a.screenReplacement
if screen == nil {
// No new screen. We're done.
a.QueueEvent(nil) // Stop the event loop.
return
}
// We have a new screen. Keep going.
a.Lock()
a.screen = screen
enableMouse := a.enableMouse
enablePaste := a.enablePaste
a.Unlock()
// Initialize and draw this screen.
if err := screen.Init(); err != nil {
panic(err)
}
if enableMouse {
screen.EnableMouse()
} else {
screen.DisableMouse()
}
if enablePaste {
screen.EnablePaste()
} else {
screen.DisablePaste()
}
if a.title != "" {
screen.SetTitle(a.title)
}
a.draw()
}
}()
// Start event loop.
var (
pasteBuffer strings.Builder
pasting bool // Set to true while we receive paste key events.
)
EventLoop:
for {
select {
// If we received an event, handle it.
case event := <-a.events:
if event == nil {
break EventLoop
}
switch event := event.(type) {
case *tcell.EventKey:
// If we are pasting, collect runes, nothing else.
if pasting {
switch event.Key() {
case tcell.KeyRune:
pasteBuffer.WriteRune(event.Rune())
case tcell.KeyEnter:
pasteBuffer.WriteRune('\n')
case tcell.KeyTab:
pasteBuffer.WriteRune('\t')
}
break
}
a.RLock()
root := a.root
inputCapture := a.inputCapture
a.RUnlock()
// Intercept keys.
var draw bool
originalEvent := event
if inputCapture != nil {
event = inputCapture(event)
if event == nil {
a.draw()
break // Don't forward event.
}
draw = true
}
// Ctrl-C closes the application.
if event == originalEvent && event.Key() == tcell.KeyCtrlC {
a.Stop()
break
}
// Pass other key events to the root primitive.
if root != nil && root.HasFocus() {
if handler := root.InputHandler(); handler != nil {
handler(event, func(p Primitive) {
a.SetFocus(p)
})
draw = true
}
}
// Redraw.
if draw {
a.draw()
}
case *tcell.EventPaste:
if !a.enablePaste {
break
}
if event.Start() {
pasting = true
pasteBuffer.Reset()
} else if event.End() {
pasting = false
a.RLock()
root := a.root
a.RUnlock()
if root != nil && root.HasFocus() && pasteBuffer.Len() > 0 {
// Pass paste event to the root primitive.
if handler := root.PasteHandler(); handler != nil {
handler(pasteBuffer.String(), func(p Primitive) {
a.SetFocus(p)
})
}
// Redraw.
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 {
break
}
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()
}
case *tcell.EventError:
appErr = event
a.Stop()
}
// If we have updates, now is the time to execute them.
case update := <-a.updates:
update.f()
if update.done != nil {
update.done <- struct{}{}
}
}
}
// Wait for the event loop to finish.
wg.Wait()
a.screen = nil
return appErr
}
// 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.ButtonPrimary, MouseLeftDown, MouseLeftUp, MouseLeftClick, MouseLeftDoubleClick},
{tcell.ButtonMiddle, MouseMiddleDown, MouseMiddleUp, MouseMiddleClick, MouseMiddleDoubleClick},
{tcell.ButtonSecondary, MouseRightDown, MouseRightUp, MouseRightClick, MouseRightDoubleClick},
} {
if buttonChanges&buttonEvent.button != 0 {
if buttons&buttonEvent.button != 0 {
fire(buttonEvent.down)
} else {
fire(buttonEvent.up) // A user override might set event to nil.
if !clickMoved && event != nil {
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.
if err := screen.Suspend(); err != nil {
return false // Suspension failed.
}
// Wait for "f" to return.
f()
// If the screen object has changed in the meantime, we need to do more.
a.RLock()
defer a.RUnlock()
if a.screen != screen {
// Calling Stop() while in suspend mode currently still leads to a
// panic, see https://github.com/gdamore/tcell/issues/440.
screen.Fini()
if a.screen == nil {
return true // If stop was called (a.screen is nil), we're done already.
}
} else {
// It hasn't changed. Resume.
screen.Resume() // Not much we can do in case of an error.
}
// 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. It can actually
// deadlock your application if you call it from the main thread (e.g. in a
// callback function of a widget). 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 preferable to call [Application.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 is not nil here.
width, height := screen.Size()
root.SetRect(0, 0, width, height)
}
// Clear screen to remove unwanted artifacts from the previous cycle.
screen.Clear()
// 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
}
// Sync forces a full re-sync of the screen buffer with the actual screen during
// the next event cycle. This is useful for when the terminal screen is
// corrupted so you may want to offer your users a keyboard shortcut to refresh
// the screen.
func (a *Application) Sync() *Application {
a.updates <- queuedUpdate{f: func() {
a.RLock()
screen := a.screen
a.RUnlock()
if screen == nil {
return
}
screen.Sync()
}}
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 to a new primitive. All key events will be directed
// down the hierarchy (starting at the root) until a primitive handles them,
// which per default goes towards the focused primitive.
//
// 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
View 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,
}

486
vendor/github.com/rivo/tview/box.go generated vendored Normal file
View File

@@ -0,0 +1,486 @@
package tview
import (
"github.com/gdamore/tcell/v2"
)
// 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
// If set to true, the background of this box is not cleared while drawing.
dontClear bool
// Whether or not a border is drawn, reducing the box's space for content by
// two in width and height.
border bool
// The border style.
borderStyle tcell.Style
// 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
// Whether or not this box has focus. This is typically ignored for
// container primitives (e.g. Flex, Grid, Pages), as they will delegate
// focus to their children.
hasFocus bool
// Optional callback functions invoked when the primitive receives or loses
// focus.
focus, blur func()
// 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,
borderStyle: tcell.StyleDefault.Foreground(Styles.BorderColor).Background(Styles.PrimitiveBackgroundColor),
titleColor: Styles.TitleColor,
titleAlign: AlignCenter,
}
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(p, 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 [Box.InputHandler]) with the
// functionality to capture input (see [Box.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. Box has no default input handling.
func (b *Box) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) {
return b.WrapInputHandler(nil)
}
// WrapPasteHandler wraps a paste handler (see [Box.PasteHandler]).
func (b *Box) WrapPasteHandler(pasteHandler func(string, func(p Primitive))) func(string, func(p Primitive)) {
return func(text string, setFocus func(p Primitive)) {
if pasteHandler != nil {
pasteHandler(text, setFocus)
}
}
}
// PasteHandler returns nil. Box has no default paste handling.
func (b *Box) PasteHandler() func(pastedText string, setFocus func(p Primitive)) {
return b.WrapPasteHandler(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.
//
// This function can also be used on container primitives (like Flex, Grid, or
// Form) as keyboard events will be handed down until they are handled.
//
// Pasted key events are not forwarded to the input capture function if pasting
// is enabled (see [Application.EnablePaste]).
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 [Box.MouseHandler]) with the
// functionality to capture mouse events (see [Box.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 {
if action == MouseConsumed {
consumed = true
}
} else if mouseHandler != nil {
consumed, capture = mouseHandler(action, event, setFocus)
}
return
}
}
// MouseHandler returns nil. Box has no default mouse handling.
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 == MouseLeftDown && 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.
//
// When a nil event is returned, the returned mouse action value may be set to
// [MouseConsumed] to indicate that the event was consumed and the screen should
// be redrawn. Any other value will not cause a redraw.
//
// Providing a nil handler will remove a previously existing handler.
//
// Note that mouse events are ignored completely if the application has not been
// enabled for mouse events (see [Application.EnableMouse]), which is the
// default.
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
}
// InInnerRect returns true if the given coordinate is within the bounds of the
// box's inner rectangle (within the border and padding).
func (b *Box) InInnerRect(x, y int) bool {
rectX, rectY, width, height := b.GetInnerRect()
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
b.borderStyle = b.borderStyle.Background(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
}
// SetBorderStyle sets the box's border style.
func (b *Box) SetBorderStyle(style tcell.Style) *Box {
b.borderStyle = style
return b
}
// SetBorderColor sets the box's border color.
func (b *Box) SetBorderColor(color tcell.Color) *Box {
b.borderStyle = b.borderStyle.Foreground(color)
return b
}
// SetBorderAttributes sets the border's style attributes. You can combine
// different attributes using bitmask operations:
//
// box.SetBorderAttributes(tcell.AttrItalic | tcell.AttrBold)
func (b *Box) SetBorderAttributes(attr tcell.AttrMask) *Box {
b.borderStyle = b.borderStyle.Attributes(attr)
return b
}
// GetBorderAttributes returns the border's style attributes.
func (b *Box) GetBorderAttributes() tcell.AttrMask {
_, _, attr := b.borderStyle.Decompose()
return attr
}
// GetBorderColor returns the box's border color.
func (b *Box) GetBorderColor() tcell.Color {
color, _, _ := b.borderStyle.Decompose()
return color
}
// 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) {
b.DrawForSubclass(screen, b)
}
// DrawForSubclass draws this box under the assumption that primitive p is a
// subclass of this box. This is needed e.g. to draw proper box frames which
// depend on the subclass's focus.
//
// Only call this function from your own custom primitives. It is not needed in
// applications that have no custom primitives.
func (b *Box) DrawForSubclass(screen tcell.Screen, p Primitive) {
// Don't draw anything if there is no space.
if b.width <= 0 || b.height <= 0 {
return
}
// Fill background.
background := tcell.StyleDefault.Background(b.backgroundColor)
if !b.dontClear {
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 {
var vertical, horizontal, topLeft, topRight, bottomLeft, bottomRight rune
if p.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, b.borderStyle)
screen.SetContent(x, b.y+b.height-1, horizontal, nil, b.borderStyle)
}
for y := b.y + 1; y < b.y+b.height-1; y++ {
screen.SetContent(b.x, y, vertical, nil, b.borderStyle)
screen.SetContent(b.x+b.width-1, y, vertical, nil, b.borderStyle)
}
screen.SetContent(b.x, b.y, topLeft, nil, b.borderStyle)
screen.SetContent(b.x+b.width-1, b.y, topRight, nil, b.borderStyle)
screen.SetContent(b.x, b.y+b.height-1, bottomLeft, nil, b.borderStyle)
screen.SetContent(b.x+b.width-1, b.y+b.height-1, bottomRight, nil, b.borderStyle)
// 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 {
xEllipsis := b.x + b.width - 2
if b.titleAlign == AlignRight {
xEllipsis = b.x + 1
}
_, _, style, _ := screen.GetContent(xEllipsis, b.y)
fg, _, _ := style.Decompose()
Print(screen, string(SemigraphicsHorizontalEllipsis), xEllipsis, 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()
}
}
// SetFocusFunc sets a callback function which is invoked when this primitive
// receives focus. Container primitives such as [Flex] or [Grid] may not be
// notified if one of their descendents receive focus directly.
//
// Set to nil to remove the callback function.
func (b *Box) SetFocusFunc(callback func()) *Box {
b.focus = callback
return b
}
// SetBlurFunc sets a callback function which is invoked when this primitive
// loses focus. This does not apply to container primitives such as [Flex] or
// [Grid].
//
// Set to nil to remove the callback function.
func (b *Box) SetBlurFunc(callback func()) *Box {
b.blur = callback
return b
}
// Focus is called when this primitive receives focus.
func (b *Box) Focus(delegate func(p Primitive)) {
b.hasFocus = true
if b.focus != nil {
b.focus()
}
}
// Blur is called when this primitive loses focus.
func (b *Box) Blur() {
if b.blur != nil {
b.blur()
}
b.hasFocus = false
}
// HasFocus returns whether or not this primitive has focus.
func (b *Box) HasFocus() bool {
return b.hasFocus
}

199
vendor/github.com/rivo/tview/button.go generated vendored Normal file
View File

@@ -0,0 +1,199 @@
package tview
import (
"github.com/gdamore/tcell/v2"
)
// 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
// If set to true, the button cannot be activated.
disabled bool
// The text to be displayed inside the button.
text string
// The button's style (when deactivated).
style tcell.Style
// The button's style (when activated).
activatedStyle tcell.Style
// The button's style (when disabled).
disabledStyle tcell.Style
// 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).
exit func(tcell.Key)
}
// NewButton returns a new input field.
func NewButton(label string) *Button {
box := NewBox()
box.SetRect(0, 0, TaggedStringWidth(label)+4, 1)
return &Button{
Box: box,
text: label,
style: tcell.StyleDefault.Background(Styles.ContrastBackgroundColor).Foreground(Styles.PrimaryTextColor),
activatedStyle: tcell.StyleDefault.Background(Styles.PrimaryTextColor).Foreground(Styles.InverseTextColor),
disabledStyle: tcell.StyleDefault.Background(Styles.ContrastBackgroundColor).Foreground(Styles.ContrastSecondaryTextColor),
}
}
// SetLabel sets the button text.
func (b *Button) SetLabel(label string) *Button {
b.text = label
return b
}
// GetLabel returns the button text.
func (b *Button) GetLabel() string {
return b.text
}
// SetLabelColor sets the color of the button text.
func (b *Button) SetLabelColor(color tcell.Color) *Button {
b.style = b.style.Foreground(color)
return b
}
// SetStyle sets the style of the button used when it is not focused.
func (b *Button) SetStyle(style tcell.Style) *Button {
b.style = style
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.activatedStyle = b.activatedStyle.Foreground(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.activatedStyle = b.activatedStyle.Background(color)
return b
}
// SetActivatedStyle sets the style of the button used when it is focused.
func (b *Button) SetActivatedStyle(style tcell.Style) *Button {
b.activatedStyle = style
return b
}
// SetDisabledStyle sets the style of the button used when it is disabled.
func (b *Button) SetDisabledStyle(style tcell.Style) *Button {
b.disabledStyle = style
return b
}
// SetDisabled sets whether or not the button is disabled. Disabled buttons
// cannot be activated.
//
// If the button is part of a form, you should set focus to the form itself
// after calling this function to set focus to the next non-disabled form item.
func (b *Button) SetDisabled(disabled bool) *Button {
b.disabled = disabled
return b
}
// IsDisabled returns whether or not the button is disabled.
func (b *Button) IsDisabled() bool {
return b.disabled
}
// SetSelectedFunc sets a handler which is called when the button was selected.
func (b *Button) SetSelectedFunc(handler func()) *Button {
b.selected = handler
return b
}
// SetExitFunc 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) SetExitFunc(handler func(key tcell.Key)) *Button {
b.exit = handler
return b
}
// Draw draws this primitive onto the screen.
func (b *Button) Draw(screen tcell.Screen) {
// Draw the box.
style := b.style
if b.disabled {
style = b.disabledStyle
}
if b.HasFocus() && !b.disabled {
style = b.activatedStyle
}
_, backgroundColor, _ := style.Decompose()
b.SetBackgroundColor(backgroundColor)
b.Box.DrawForSubclass(screen, b)
// Draw label.
x, y, width, height := b.GetInnerRect()
if width > 0 && height > 0 {
y = y + height/2
printWithStyle(screen, b.text, x, y, 0, width, AlignCenter, style, true)
}
}
// 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)) {
if b.disabled {
return
}
// 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.exit != nil {
b.exit(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.disabled {
return false, nil
}
if !b.InRect(event.Position()) {
return false, nil
}
// Process mouse event.
if action == MouseLeftDown {
setFocus(b)
consumed = true
} else if action == MouseLeftClick {
if b.selected != nil {
b.selected()
}
consumed = true
}
return
})
}

338
vendor/github.com/rivo/tview/checkbox.go generated vendored Normal file
View File

@@ -0,0 +1,338 @@
package tview
import (
"github.com/gdamore/tcell/v2"
)
// 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 checkbox is disabled/read-only.
disabled bool
// 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 style.
labelStyle tcell.Style
// The style of the unchecked checkbox.
uncheckedStyle tcell.Style
// The style of the checked checkbox.
checkedStyle tcell.Style
// Teh style of the checkbox when it is currently focused.
focusStyle tcell.Style
// The string used to display an unchecked box.
uncheckedString string
// The string used to display a checked box.
checkedString string
// 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(),
labelStyle: tcell.StyleDefault.Foreground(Styles.SecondaryTextColor),
uncheckedStyle: tcell.StyleDefault.Background(Styles.ContrastBackgroundColor).Foreground(Styles.PrimaryTextColor),
checkedStyle: tcell.StyleDefault.Background(Styles.ContrastBackgroundColor).Foreground(Styles.PrimaryTextColor),
focusStyle: tcell.StyleDefault.Background(Styles.PrimaryTextColor).Foreground(Styles.ContrastBackgroundColor),
uncheckedString: " ",
checkedString: "X",
}
}
// SetChecked sets the state of the checkbox. This also triggers the "changed"
// callback if the state changes with this call.
func (c *Checkbox) SetChecked(checked bool) *Checkbox {
if c.checked != checked {
if c.changed != nil {
c.changed(checked)
}
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.labelStyle = c.labelStyle.Foreground(color)
return c
}
// SetLabelStyle sets the style of the label.
func (c *Checkbox) SetLabelStyle(style tcell.Style) *Checkbox {
c.labelStyle = style
return c
}
// SetFieldBackgroundColor sets the background color of the input area.
func (c *Checkbox) SetFieldBackgroundColor(color tcell.Color) *Checkbox {
c.uncheckedStyle = c.uncheckedStyle.Background(color)
c.checkedStyle = c.checkedStyle.Background(color)
c.focusStyle = c.focusStyle.Foreground(color)
return c
}
// SetFieldTextColor sets the text color of the input area.
func (c *Checkbox) SetFieldTextColor(color tcell.Color) *Checkbox {
c.uncheckedStyle = c.uncheckedStyle.Foreground(color)
c.checkedStyle = c.checkedStyle.Foreground(color)
c.focusStyle = c.focusStyle.Background(color)
return c
}
// SetUncheckedStyle sets the style of the unchecked checkbox.
func (c *Checkbox) SetUncheckedStyle(style tcell.Style) *Checkbox {
c.uncheckedStyle = style
return c
}
// SetCheckedStyle sets the style of the checked checkbox.
func (c *Checkbox) SetCheckedStyle(style tcell.Style) *Checkbox {
c.checkedStyle = style
return c
}
// SetActivatedStyle sets the style of the checkbox when it is currently
// focused.
func (c *Checkbox) SetActivatedStyle(style tcell.Style) *Checkbox {
c.focusStyle = style
return c
}
// SetCheckedString sets the string to be displayed when the checkbox is
// checked (defaults to "X"). The string may contain color tags (consider
// adapting the checkbox's various styles accordingly). See [Escape] in
// case you want to display square brackets.
func (c *Checkbox) SetCheckedString(checked string) *Checkbox {
c.checkedString = checked
return c
}
// SetUncheckedString sets the string to be displayed when the checkbox is
// not checked (defaults to the empty space " "). The string may contain color
// tags (consider adapting the checkbox's various styles accordingly). See
// [Escape] in case you want to display square brackets.
func (c *Checkbox) SetUncheckedString(unchecked string) *Checkbox {
c.uncheckedString = unchecked
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.SetLabelColor(labelColor)
c.backgroundColor = bgColor
c.SetFieldTextColor(fieldTextColor)
c.SetFieldBackgroundColor(fieldBgColor)
return c
}
// GetFieldWidth returns this primitive's field width.
func (c *Checkbox) GetFieldWidth() int {
return 1
}
// GetFieldHeight returns this primitive's field height.
func (c *Checkbox) GetFieldHeight() int {
return 1
}
// SetDisabled sets whether or not the item is disabled / read-only.
func (c *Checkbox) SetDisabled(disabled bool) FormItem {
c.disabled = disabled
if c.finished != nil {
c.finished(-1)
}
return c
}
// SetChangedFunc sets a handler which is called when the checked state of this
// checkbox was changed. 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
}
// Focus is called when this primitive receives focus.
func (c *Checkbox) Focus(delegate func(p Primitive)) {
// If we're part of a form and this item is disabled, there's nothing the
// user can do here so we're finished.
if c.finished != nil && c.disabled {
c.finished(-1)
return
}
c.Box.Focus(delegate)
}
// Draw draws this primitive onto the screen.
func (c *Checkbox) Draw(screen tcell.Screen) {
c.Box.DrawForSubclass(screen, c)
// Prepare
x, y, width, height := c.GetInnerRect()
rightLimit := x + width
if height < 1 || rightLimit <= x {
return
}
// Draw label.
_, labelBg, _ := c.labelStyle.Decompose()
if c.labelWidth > 0 {
labelWidth := c.labelWidth
if labelWidth > width {
labelWidth = width
}
printWithStyle(screen, c.label, x, y, 0, labelWidth, AlignLeft, c.labelStyle, labelBg == tcell.ColorDefault)
x += labelWidth
width -= labelWidth
} else {
_, _, drawnWidth := printWithStyle(screen, c.label, x, y, 0, width, AlignLeft, c.labelStyle, labelBg == tcell.ColorDefault)
x += drawnWidth
width -= drawnWidth
}
// Draw checkbox.
str := c.uncheckedString
style := c.uncheckedStyle
if c.checked {
str = c.checkedString
style = c.checkedStyle
}
if c.disabled {
style = style.Background(c.backgroundColor)
}
if c.HasFocus() {
style = c.focusStyle
}
printWithStyle(screen, str, x, y, 0, width, AlignLeft, style, c.disabled)
}
// 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)) {
if c.disabled {
return
}
// 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) {
if c.disabled {
return false, nil
}
x, y := event.Position()
_, rectY, _, _ := c.GetInnerRect()
if !c.InRect(x, y) {
return false, nil
}
// Process mouse event.
if y == rectY {
if action == MouseLeftDown {
setFocus(c)
consumed = true
} else if action == MouseLeftClick {
c.checked = !c.checked
if c.changed != nil {
c.changed(c.checked)
}
consumed = true
}
}
return
})
}

224
vendor/github.com/rivo/tview/doc.go generated vendored Normal file
View File

@@ -0,0 +1,224 @@
/*
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.
- [TextArea]: An editable multi-line text area.
- [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.
- [Image]: Displays images.
- [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 [Application.Stop] function is called
or when Ctrl-C is pressed.
# 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.
# Styles, Colors, and Hyperlinks
Throughout this package, styles are specified using the [tcell.Style] type.
Styles specify colors with 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. The [tcell.Style] type also
allows you to specify text attributes such as "bold" or "italic" or a URL
which some terminals use to display hyperlinks.
Almost all strings which are displayed may contain style tags. A style tag's
content is always wrapped in square brackets. In its simplest form, a style tag
specifies the foreground color of the text. Colors in these tags are W3C color
names or six hexadecimal digits following a hash tag. Examples:
This is a [red]warning[white]!
The sky is [#8080ff]blue[#ffffff].
A style tag changes the style of the characters following that style tag. There
is no style stack and no nesting of style tags.
Style tags are used in 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.
A style tag's full format looks like this:
[<foreground>:<background>:<attribute flags>:<url>]
Each of the four fields can be left blank and trailing fields can be omitted.
(Empty square brackets "[]", however, are not considered style tags.) Fields
that are not specified will be left unchanged. A field with just a dash ("-")
means "reset to default".
You can specify the following flags to turn on certain attributes (some flags
may not be supported by your terminal):
l: blink
b: bold
i: italic
d: dim
r: reverse (switch foreground and background color)
u: underline
s: strike-through
Use uppercase letters to turn off the corresponding attribute, for example,
"B" to turn off bold. Uppercase letters have no effect if the attribute was not
previously set.
Setting a URL allows you to turn a piece of text into a hyperlink in some
terminals. Specify a dash ("-") to specify the end of the hyperlink. Hyperlinks
must only contain single-byte characters (e.g. ASCII) and they may not contain
bracket characters ("[" or "]").
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
[::i]Italic and [::I]not italic
Click [:::https://example.com]here[:::-] for example.com.
Send an email to [:::mailto:her@example.com]her/[:::mail:him@example.com]him/[:::mail:them@example.com]them[:::-].
[-:-:-:-]Reset everything
[:]No effect
[]Not a valid style 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 style 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.
Note that most terminals will not report information about their color theme.
This package therefore does not support using the terminal's color theme. The
default style is a dark theme and you must change the [Styles] variable to
switch to a light (or other) theme.
# Unicode Support
This package supports all unicode characters supported by your terminal.
# Mouse Support
If your terminal supports mouse events, you can enable mouse support for your
application by calling [Application.EnableMouse]. Note that this may interfere
with your terminal's default mouse behavior. Mouse support is disabled by
default.
# Concurrency
Many functions in this package are not thread-safe. For many applications, this
is not an issue: If your code makes changes in response to key events, the
corresponding callback function will execute in the main goroutine and thus will
not cause any race conditions. (Exceptions to this are documented.)
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 [Application.QueueUpdate]. And, as mentioned above, key event callbacks
are executed in the main goroutine and thus should not use
[Application.QueueUpdate] as that may lead to deadlocks. It is also not
necessary to call [Application.Draw] from such callbacks as it will be called
automatically.
# Type Hierarchy
All widgets listed above contain the [Box] type. All of [Box]'s functions are
therefore available for all widgets, too. Please note that if you are using the
functions of [Box] on a subclass, they will return a *Box, not the subclass.
This is a Golang limitation. So while tview supports method chaining in many
places, these chains must be broken when using [Box]'s functions. Example:
// This will cause "textArea" to be an empty Box.
textArea := tview.NewTextArea().
SetMaxLength(256).
SetPlaceholder("Enter text here").
SetBorder(true)
You will need to call [Box.SetBorder] separately:
textArea := tview.NewTextArea().
SetMaxLength(256).
SetPlaceholder("Enter text here")
texArea.SetBorder(true)
All widgets also implement the [Primitive] interface.
The tview package's rendering is based on version 2 of
https://github.com/gdamore/tcell. It uses types and constants from that package
(e.g. colors, styles, and keyboard values).
*/
package tview

741
vendor/github.com/rivo/tview/dropdown.go generated vendored Normal file
View File

@@ -0,0 +1,741 @@
package tview
import (
"regexp"
"strings"
"github.com/gdamore/tcell/v2"
)
// 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
// Whether or not this drop-down is disabled/read-only.
disabled bool
// 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 before 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 input field containing the entered prefix for the current selection.
// This is only visible when the drop-down is open. It never receives focus,
// however. And it only receives events, we never call its Draw method.
prefix *InputField
// The list element for the options.
list *List
// The text to be displayed before the input area.
label string
// The label style.
labelStyle tcell.Style
// The field style.
fieldStyle tcell.Style
// The style of the field when it is focused and the drop-down is closed.
focusedStyle tcell.Style
// The style of the field when it is disabled.
disabledStyle tcell.Style
// The style of the prefix.
prefixStyle tcell.Style
// 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).
SetMainTextStyle(tcell.StyleDefault.Background(Styles.MoreContrastBackgroundColor).Foreground(Styles.PrimitiveBackgroundColor)).
SetSelectedStyle(tcell.StyleDefault.Background(Styles.PrimaryTextColor).Foreground(Styles.PrimitiveBackgroundColor)).
SetHighlightFullLine(true).
SetBackgroundColor(Styles.MoreContrastBackgroundColor)
prefix := NewInputField()
box := NewBox()
d := &DropDown{
Box: box,
currentOption: -1,
list: list,
prefix: prefix,
labelStyle: tcell.StyleDefault.Foreground(Styles.SecondaryTextColor),
fieldStyle: tcell.StyleDefault.Background(Styles.ContrastBackgroundColor).Foreground(Styles.PrimaryTextColor),
focusedStyle: tcell.StyleDefault.Background(Styles.PrimaryTextColor).Foreground(Styles.ContrastBackgroundColor),
disabledStyle: tcell.StyleDefault.Background(box.backgroundColor).Foreground(Styles.SecondaryTextColor),
prefixStyle: tcell.StyleDefault.Background(Styles.PrimaryTextColor).Foreground(Styles.ContrastBackgroundColor),
}
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
}
// SetUseStyleTags sets a flag that determines whether tags found in the option
// texts are interpreted as tview tags. By default, this flag is enabled (for
// backwards compatibility reasons).
func (d *DropDown) SetUseStyleTags(useStyleTags bool) *DropDown {
d.list.SetUseStyleTags(useStyleTags, useStyleTags)
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.labelStyle = d.labelStyle.Foreground(color)
return d
}
// SetLabelStyle sets the style of the label.
func (d *DropDown) SetLabelStyle(style tcell.Style) *DropDown {
d.labelStyle = style
return d
}
// SetFieldBackgroundColor sets the background color of the selected field.
// This also overrides the prefix background color.
func (d *DropDown) SetFieldBackgroundColor(color tcell.Color) *DropDown {
d.fieldStyle = d.fieldStyle.Background(color)
d.prefix.SetFieldBackgroundColor(color)
return d
}
// SetFieldTextColor sets the text color of the options area.
func (d *DropDown) SetFieldTextColor(color tcell.Color) *DropDown {
d.fieldStyle = d.fieldStyle.Foreground(color)
return d
}
// SetFieldStyle sets the style of the options area.
func (d *DropDown) SetFieldStyle(style tcell.Style) *DropDown {
d.fieldStyle = style
return d
}
// SetFocusedStyle sets the style of the options area when the drop-down is
// focused and closed.
func (d *DropDown) SetFocusedStyle(style tcell.Style) *DropDown {
d.focusedStyle = style
return d
}
// SetDisabledStyle sets the style of the options area when the drop-down is
// disabled.
func (d *DropDown) SetDisabledStyle(style tcell.Style) *DropDown {
d.disabledStyle = style
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.prefixStyle = d.prefixStyle.Foreground(color)
return d
}
// SetPrefixStyle sets the style 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) SetPrefixStyle(style tcell.Style) *DropDown {
d.prefixStyle = style
return d
}
// SetListStyles sets the styles of the items in the drop-down list (unselected
// as well as selected items). Style attributes are currently ignored but may be
// used in the future.
func (d *DropDown) SetListStyles(unselected, selected tcell.Style) *DropDown {
d.list.SetMainTextStyle(unselected).SetSelectedStyle(selected)
_, bg, _ := unselected.Decompose()
d.list.SetBackgroundColor(bg)
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.SetLabelColor(labelColor)
d.SetBackgroundColor(bgColor)
d.SetFieldStyle(tcell.StyleDefault.Foreground(fieldTextColor).Background(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
}
// GetFieldHeight returns this primitive's field height.
func (d *DropDown) GetFieldHeight() int {
return 1
}
// SetDisabled sets whether or not the item is disabled / read-only.
func (d *DropDown) SetDisabled(disabled bool) FormItem {
d.disabled = disabled
if d.finished != nil {
d.finished(-1)
}
return d
}
// 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 _, text := range texts {
d.AddOption(text, nil)
}
d.selected = selected
return d
}
// GetOptionCount returns the number of options in the drop-down.
func (d *DropDown) GetOptionCount() int {
return len(d.options)
}
// RemoveOption removes the specified option from the drop-down. Panics if the
// index is out of range. If the currently selected option is removed, no option
// will be selected.
func (d *DropDown) RemoveOption(index int) *DropDown {
if index == d.currentOption {
d.currentOption = -1
}
d.options = append(d.options[:index], d.options[index+1:]...)
d.list.RemoveItem(index)
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.DrawForSubclass(screen, d)
// Prepare.
x, y, width, height := d.GetInnerRect()
rightLimit := x + width
if height < 1 || rightLimit <= x {
return
}
useStyleTags, _ := d.list.GetUseStyleTags()
// Draw label.
if d.labelWidth > 0 {
labelWidth := d.labelWidth
if labelWidth > rightLimit-x {
labelWidth = rightLimit - x
}
printWithStyle(screen, d.label, x, y, 0, labelWidth, AlignLeft, d.labelStyle, true)
x += labelWidth
} else {
_, _, drawnWidth := printWithStyle(screen, d.label, x, y, 0, rightLimit-x, AlignLeft, d.labelStyle, true)
x += drawnWidth
}
// What's the longest option text?
maxWidth := 0
for _, option := range d.options {
str := d.optionPrefix + option.Text + d.optionSuffix
if !useStyleTags {
str = Escape(str)
}
strWidth := TaggedStringWidth(str)
if strWidth > maxWidth {
maxWidth = strWidth
}
str = d.currentOptionPrefix + option.Text + d.currentOptionSuffix
if !useStyleTags {
str = Escape(str)
}
strWidth = TaggedStringWidth(str)
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 := d.fieldStyle
if d.disabled {
fieldStyle = d.disabledStyle
} else if d.HasFocus() && !d.open {
fieldStyle = d.focusedStyle
}
for index := 0; index < fieldWidth; index++ {
screen.SetContent(x+index, y, ' ', nil, fieldStyle)
}
// Draw selected text.
prefix := Escape(d.prefix.GetText())
if d.HasFocus() && d.open && len(prefix) > 0 {
// The drop-down is open and we have an input prefix.
// Draw current option prefix first.
currentOptionPrefix := d.currentOptionPrefix
currentOptionSuffix := d.currentOptionSuffix
if !useStyleTags {
currentOptionPrefix = Escape(currentOptionPrefix)
currentOptionSuffix = Escape(currentOptionSuffix)
}
_, _, copWidth := printWithStyle(screen, currentOptionPrefix, x, y, 0, fieldWidth, AlignLeft, d.fieldStyle, false)
if copWidth < fieldWidth {
// Then draw the prefix.
_, _, prefixWidth := printWithStyle(screen, prefix, x+copWidth, y, 0, fieldWidth-copWidth, AlignLeft, d.prefixStyle, false)
if copWidth+prefixWidth < fieldWidth {
// Then the current option remainder.
var corWidth int
currentItem := d.list.GetCurrentItem()
if currentItem >= 0 && currentItem < len(d.options) {
text := d.options[currentItem].Text
if !useStyleTags {
text = Escape(text)
}
_, _, corWidth = printWithStyle(screen, text, x+copWidth+prefixWidth, y, prefixWidth, fieldWidth-copWidth-prefixWidth, AlignLeft, d.fieldStyle, false)
}
if copWidth+prefixWidth+corWidth < fieldWidth {
// And finally the current option suffix.
printWithStyle(screen, currentOptionSuffix, x+copWidth+prefixWidth+corWidth, y, 0, fieldWidth-copWidth-prefixWidth-corWidth, AlignLeft, d.fieldStyle, false)
}
}
}
} else {
// The drop-down is closed. Just draw the selected option.
text := d.noSelection
if d.currentOption >= 0 && d.currentOption < len(d.options) {
text = d.currentOptionPrefix + d.options[d.currentOption].Text + d.currentOptionSuffix
}
if !useStyleTags {
text = Escape(text)
}
printWithStyle(screen, text, x, y, 0, fieldWidth, AlignLeft, fieldStyle, false)
}
// Draw options list.
if d.HasFocus() && d.open {
lx := x
ly := y + 1
lwidth := maxWidth
lheight := len(d.options)
swidth, sheight := screen.Size()
// We prefer to align the left sides of the list and the main widget, but
// if there is no space to the right, then shift the list to the left.
if lx+lwidth >= swidth {
lx = swidth - lwidth
if lx < 0 {
lx = 0
}
}
// We prefer to drop down but if there is no space, maybe drop up?
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)) {
if d.disabled {
return
}
// Process key event.
switch key := event.Key(); key {
case tcell.KeyDown, tcell.KeyUp, tcell.KeyHome, tcell.KeyEnd, tcell.KeyPgDn, tcell.KeyPgUp:
// Open the list and forward the event to it.
d.openList(setFocus)
if handler := d.list.InputHandler(); handler != nil {
handler(event, setFocus)
}
d.prefix.SetText("")
case tcell.KeyEnter:
// If the list is closed, open it. Otherwise, forward the event to
// it.
if !d.open {
d.openList(setFocus)
} else if handler := d.list.InputHandler(); handler != nil {
handler(event, setFocus)
}
case tcell.KeyEscape, tcell.KeyTab, tcell.KeyBacktab:
// Done selecting.
if d.done != nil {
d.done(key)
}
if d.finished != nil {
d.finished(key)
}
d.closeList(setFocus)
default:
// Pass other key events to the input field.
if handler := d.prefix.InputHandler(); handler != nil {
handler(event, setFocus)
}
d.evalPrefix()
d.openList(setFocus)
}
})
}
// evalPrefix selects an item in the drop-down list based on the current prefix.
func (d *DropDown) evalPrefix() {
prefix := strings.ToLower(d.prefix.GetText())
if len(prefix) == 0 {
return
}
useStyleTags, _ := d.list.GetUseStyleTags()
for index, option := range d.options {
text := option.Text
if useStyleTags {
text = stripTags(text)
}
if strings.HasPrefix(strings.ToLower(text), prefix) {
d.list.SetCurrentItem(index)
return
}
}
}
// openList hands control over to the embedded List primitive.
func (d *DropDown) openList(setFocus func(Primitive)) {
if d.open {
return
}
d.open = true
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)
// Clear the prefix input field.
d.prefix.SetText("")
// Trigger "selected" event.
currentOption := d.options[d.currentOption]
if d.selected != nil {
d.selected(currentOption.Text, d.currentOption)
}
if currentOption.Selected != nil {
currentOption.Selected()
}
}).SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
switch key := event.Key(); key {
case tcell.KeyDown, tcell.KeyUp, tcell.KeyPgDn, tcell.KeyPgUp, tcell.KeyHome, tcell.KeyEnd, tcell.KeyEnter: // Basic list navigation.
break
case tcell.KeyEscape: // Abort selection.
d.closeList(setFocus)
return nil
default: // All other keys are passed to the input field.
if handler := d.prefix.InputHandler(); handler != nil {
handler(event, setFocus)
}
return nil
}
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)
}
}
// IsOpen returns true if the drop-down list is currently open.
func (d *DropDown) IsOpen() bool {
return d.open
}
// Focus is called by the application when the primitive receives focus.
func (d *DropDown) Focus(delegate func(p Primitive)) {
// If we're part of a form and this item is disabled, there's nothing the
// user can do here so we're finished.
if d.finished != nil && d.disabled {
d.finished(-1)
return
}
if d.open {
delegate(d.list)
} else {
d.Box.Focus(delegate)
}
}
// HasFocus returns whether or not this primitive has focus.
func (d *DropDown) HasFocus() bool {
if d.open {
return d.list.HasFocus()
}
return d.Box.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) {
if d.disabled {
return false, nil
}
// Was the mouse event in the drop-down box itself (or on its label)?
x, y := event.Position()
inRect := d.InInnerRect(x, y)
if !d.open && !inRect {
return d.InRect(x, y), nil // No, and it's not expanded either. Ignore.
}
// As long as the drop-down is open, we capture all mouse events.
if d.open {
capture = d
}
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
}
case MouseLeftUp:
if d.dragging {
d.dragging = false
d.list.MouseHandler()(MouseLeftClick, event, setFocus)
consumed = true
}
}
return
})
}
// PasteHandler returns the handler for this primitive.
func (d *DropDown) PasteHandler() func(pastedText string, setFocus func(p Primitive)) {
return d.WrapPasteHandler(func(pastedText string, setFocus func(p Primitive)) {
if !d.open || d.disabled {
return
}
// Strip any newline characters (simple version).
pastedText = regexp.MustCompile(`\r?\n`).ReplaceAllString(pastedText, "")
// Forward the pasted text to the input field.
d.prefix.PasteHandler()(pastedText, setFocus)
})
}

275
vendor/github.com/rivo/tview/flex.go generated vendored Normal file
View File

@@ -0,0 +1,275 @@
package tview
import (
"github.com/gdamore/tcell/v2"
)
// Flex directions.
const (
// One item per row.
FlexRow = 0
// One item per column.
FlexColumn = 1
// As defined in CSS, items distributed along a row.
FlexRowCSS = 1
// As defined in CSS, items distributed within a column.
FlexColumnCSS = 0
)
// 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 not clear its contents 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 a box with the desired
// color:
//
// flex.Box = NewBox()
func NewFlex() *Flex {
f := &Flex{
direction: FlexColumn,
}
f.Box = NewBox()
f.Box.dontClear = true
return f
}
// SetDirection sets the direction in which the contained primitives are
// distributed. This can be either FlexColumn (default) or FlexRow. Note that
// these are the opposite of what you would expect coming from CSS. You may also
// use FlexColumnCSS or FlexRowCSS, to remain in line with the CSS definition.
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
}
// GetItemCount returns the number of items in this container.
func (f *Flex) GetItemCount() int {
return len(f.items)
}
// GetItem returns the primitive at the given index, starting with 0 for the
// first primitive in this container.
//
// This function will panic for out of range indices.
func (f *Flex) GetItem(index int) Primitive {
return f.items[index].Item
}
// 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.DrawForSubclass(screen, f)
// 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.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
}
}
f.Box.Focus(delegate)
}
// 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.HasFocus() {
return true
}
}
return f.Box.HasFocus()
}
// 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
})
}
// InputHandler returns the handler for this primitive.
func (f *Flex) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) {
return f.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p Primitive)) {
for _, item := range f.items {
if item.Item != nil && item.Item.HasFocus() {
if handler := item.Item.InputHandler(); handler != nil {
handler(event, setFocus)
return
}
}
}
})
}
// PasteHandler returns the handler for this primitive.
func (f *Flex) PasteHandler() func(pastedText string, setFocus func(p Primitive)) {
return f.WrapPasteHandler(func(pastedText string, setFocus func(p Primitive)) {
for _, item := range f.items {
if item.Item != nil && item.Item.HasFocus() {
if handler := item.Item.PasteHandler(); handler != nil {
handler(pastedText, setFocus)
return
}
}
}
})
}

896
vendor/github.com/rivo/tview/form.go generated vendored Normal file
View File

@@ -0,0 +1,896 @@
package tview
import (
"image"
"github.com/gdamore/tcell/v2"
)
var (
// 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.
DefaultFormFieldWidth = 10
// DefaultFormFieldHeight is the default field height of multi-line form
// elements whose field height is flexible (0).
DefaultFormFieldHeight = 5
)
// 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 field width is flexible and may use as much space as
// required.
GetFieldWidth() int
// GetFieldHeight returns the height of the form item's field (the area which
// is manipulated by the user). This value must be greater than 0.
GetFieldHeight() 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), the Backtab key (move to previous field), or a negative
// value, indicating that the action for the last known key should be
// repeated.
SetFinishedFunc(handler func(key tcell.Key)) FormItem
// SetDisabled sets whether or not the item is disabled / read-only. A form
// must have at least one item that is not disabled.
SetDisabled(disabled bool) 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 cells 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 style of the input area.
fieldStyle tcell.Style
// The style of the buttons when they are not focused.
buttonStyle tcell.Style
// The style of the buttons when they are focused.
buttonActivatedStyle tcell.Style
// The style of the buttons when they are disabled.
buttonDisabledStyle tcell.Style
// The last (valid) key that wsa sent to a "finished" handler or -1 if no
// such key is known yet.
lastFinishedKey tcell.Key
// 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,
fieldStyle: tcell.StyleDefault.Background(Styles.ContrastBackgroundColor).Foreground(Styles.PrimaryTextColor),
buttonStyle: tcell.StyleDefault.Background(Styles.ContrastBackgroundColor).Foreground(Styles.PrimaryTextColor),
buttonActivatedStyle: tcell.StyleDefault.Background(Styles.PrimaryTextColor).Foreground(Styles.ContrastBackgroundColor),
buttonDisabledStyle: tcell.StyleDefault.Background(Styles.ContrastBackgroundColor).Foreground(Styles.ContrastSecondaryTextColor),
lastFinishedKey: tcell.KeyTab, // To skip over inactive elements at the beginning of the form.
}
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.fieldStyle = f.fieldStyle.Background(color)
return f
}
// SetFieldTextColor sets the text color of the input areas.
func (f *Form) SetFieldTextColor(color tcell.Color) *Form {
f.fieldStyle = f.fieldStyle.Foreground(color)
return f
}
// SetFieldStyle sets the style of the input areas. Attributes are currently
// still ignored to maintain backwards compatibility.
func (f *Form) SetFieldStyle(style tcell.Style) *Form {
f.fieldStyle = style
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. This is
// also the text color of the buttons when they are focused.
func (f *Form) SetButtonBackgroundColor(color tcell.Color) *Form {
f.buttonStyle = f.buttonStyle.Background(color)
f.buttonActivatedStyle = f.buttonActivatedStyle.Foreground(color)
return f
}
// SetButtonTextColor sets the color of the button texts. This is also the
// background of the buttons when they are focused.
func (f *Form) SetButtonTextColor(color tcell.Color) *Form {
f.buttonStyle = f.buttonStyle.Foreground(color)
f.buttonActivatedStyle = f.buttonActivatedStyle.Background(color)
return f
}
// SetButtonStyle sets the style of the buttons when they are not focused.
func (f *Form) SetButtonStyle(style tcell.Style) *Form {
f.buttonStyle = style
return f
}
// SetButtonActivatedStyle sets the style of the buttons when they are focused.
func (f *Form) SetButtonActivatedStyle(style tcell.Style) *Form {
f.buttonActivatedStyle = style
return f
}
// SetButtonDisabledStyle sets the style of the buttons when they are disabled.
func (f *Form) SetButtonDisabledStyle(style tcell.Style) *Form {
f.buttonDisabledStyle = style
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 {
var current, future int
for itemIndex, item := range f.items {
if itemIndex == index {
future = itemIndex
}
if item.HasFocus() {
current = itemIndex
}
}
for buttonIndex, button := range f.buttons {
if buttonIndex+len(f.items) == index {
future = buttonIndex + len(f.items)
}
if button.HasFocus() {
current = buttonIndex + len(f.items)
}
}
var focus func(p Primitive)
focus = func(p Primitive) {
p.Focus(focus)
}
if current != future {
if current >= 0 && current < len(f.items) {
f.items[current].Blur()
} else if current >= len(f.items) && current < len(f.items)+len(f.buttons) {
f.buttons[current-len(f.items)].Blur()
}
if future >= 0 && future < len(f.items) {
focus(f.items[future])
} else if future >= len(f.items) && future < len(f.items)+len(f.buttons) {
focus(f.buttons[future-len(f.items)])
}
}
f.focusedElement = future
return f
}
// AddTextArea adds a text area to the form. It has a label, an optional initial
// text, a size (width and height) referring to the actual input area (a
// fieldWidth of 0 extends it as far right as possible, a fieldHeight of 0 will
// cause it to be [DefaultFormFieldHeight]), and a maximum number of bytes of
// text allowed (0 means no limit).
//
// The optional callback function is invoked when the content of the text area
// has changed. Note that especially for larger texts, this is an expensive
// operation due to technical constraints of the [TextArea] primitive (every key
// stroke leads to a new reallocation of the entire text).
func (f *Form) AddTextArea(label, text string, fieldWidth, fieldHeight, maxLength int, changed func(text string)) *Form {
if fieldHeight == 0 {
fieldHeight = DefaultFormFieldHeight
}
textArea := NewTextArea().
SetLabel(label).
SetSize(fieldHeight, fieldWidth).
SetMaxLength(maxLength)
if text != "" {
textArea.SetText(text, true)
}
if changed != nil {
textArea.SetChangedFunc(func() {
changed(textArea.GetText())
})
}
f.items = append(f.items, textArea)
return f
}
// AddTextView adds a text view to the form. It has a label and text, a size
// (width and height) referring to the actual text element (a fieldWidth of 0
// extends it as far right as possible, a fieldHeight of 0 will cause it to be
// [DefaultFormFieldHeight]), a flag to turn on/off dynamic colors, and a flag
// to turn on/off scrolling. If scrolling is turned off, the text view will not
// receive focus.
func (f *Form) AddTextView(label, text string, fieldWidth, fieldHeight int, dynamicColors, scrollable bool) *Form {
if fieldHeight == 0 {
fieldHeight = DefaultFormFieldHeight
}
textArea := NewTextView().
SetLabel(label).
SetSize(fieldHeight, fieldWidth).
SetDynamicColors(dynamicColors).
SetScrollable(scrollable).
SetText(text)
f.items = append(f.items, textArea)
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
}
// AddImage adds an image to the form. It has a label and the image will fit in
// the specified width and height (its aspect ratio is preserved). See
// [Image.SetColors] for a description of the "colors" parameter. Images are not
// interactive and are skipped over in a form. The "width" value may be 0
// (adjust dynamically) but "height" should generally be a positive value.
func (f *Form) AddImage(label string, image image.Image, width, height, colors int) *Form {
f.items = append(f.items, NewImage().
SetLabel(label).
SetImage(image).
SetSize(height, width).
SetAlign(AlignTop, AlignLeft).
SetColors(colors))
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 respectively.
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.DrawForSubclass(screen, f)
// 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.
type position struct{ x, y, width, height int }
positions := make([]position, len(f.items)+len(f.buttons))
var (
focusedPosition position
lineHeight = 1
)
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
}
itemHeight := item.GetFieldHeight()
if itemHeight <= 0 {
itemHeight = DefaultFormFieldHeight
}
// Advance to next line if there is no space.
if f.horizontal && x+labelWidth+1 >= rightLimit {
x = startX
y += lineHeight + 1
lineHeight = itemHeight
}
// Update line height.
if itemHeight > lineHeight {
lineHeight = itemHeight
}
// Adjust the item's attributes.
if x+itemWidth >= rightLimit {
itemWidth = rightLimit - x
}
fieldTextColor, fieldBackgroundColor, _ := f.fieldStyle.Decompose()
item.SetFormAttributes(
labelWidth,
f.labelColor,
f.backgroundColor,
fieldTextColor,
fieldBackgroundColor,
)
// Save position.
positions[index].x = x
positions[index].y = y
positions[index].width = itemWidth
positions[index].height = itemHeight
if item.HasFocus() {
focusedPosition = positions[index]
}
// Advance to next item.
if f.horizontal {
x += itemWidth + f.itemPadding
} else {
y += itemHeight + 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 += lineHeight + 1
space = width
lineHeight = 1
}
} else {
if space < 1 {
break // No space for this button anymore.
}
}
if buttonWidth > space {
buttonWidth = space
}
button.SetStyle(f.buttonStyle).
SetActivatedStyle(f.buttonActivatedStyle).
SetDisabledStyle(f.buttonDisabledStyle)
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.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)) {
// 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
}
var handler func(key tcell.Key)
handler = func(key tcell.Key) {
if key >= 0 {
f.lastFinishedKey = 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)
}
default:
if key < 0 && f.lastFinishedKey >= 0 {
// Repeat the last action.
handler(f.lastFinishedKey)
}
}
}
// Track whether a form item has focus.
var itemFocused bool
f.hasFocus = false
// Set the handler and focus for all items and buttons.
for index, button := range f.buttons {
button.SetExitFunc(handler)
if f.focusedElement == index+len(f.items) {
if button.IsDisabled() {
f.focusedElement++
if f.focusedElement >= len(f.items)+len(f.buttons) {
f.focusedElement = 0
}
continue
}
itemFocused = true
func(b *Button) { // Wrapping might not be necessary anymore in future Go versions.
defer delegate(b)
}(button)
}
}
for index, item := range f.items {
item.SetFinishedFunc(handler)
if f.focusedElement == index {
itemFocused = true
func(i FormItem) { // Wrapping might not be necessary anymore in future Go versions.
defer delegate(i)
}(item)
}
}
// If no item was focused, focus the form itself.
if !itemFocused {
f.Box.Focus(delegate)
}
}
// HasFocus returns whether or not this primitive has focus.
func (f *Form) HasFocus() bool {
if f.focusIndex() >= 0 {
return true
}
return f.Box.HasFocus()
}
// 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.HasFocus() {
return index
}
}
for index, button := range f.buttons {
if button.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) {
// At the end, update f.focusedElement and prepare current item/button.
defer func() {
if consumed {
index := f.focusIndex()
if index >= 0 {
f.focusedElement = index
}
}
}()
// Determine items to pass mouse events to.
for _, item := range f.items {
// Exclude TextView items from mouse-down events as they are
// read-only items and thus should not be focused.
if _, ok := item.(*TextView); ok && action == MouseLeftDown {
continue
}
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 down anywhere else will return the focus to the last selected
// element.
if action == MouseLeftDown && f.InRect(event.Position()) {
f.Focus(setFocus)
consumed = true
}
return
})
}
// InputHandler returns the handler for this primitive.
func (f *Form) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) {
return f.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p Primitive)) {
for _, item := range f.items {
if item != nil && item.HasFocus() {
if handler := item.InputHandler(); handler != nil {
handler(event, setFocus)
return
}
}
}
for _, button := range f.buttons {
if button.HasFocus() {
if handler := button.InputHandler(); handler != nil {
handler(event, setFocus)
return
}
}
}
})
}
// PasteHandler returns the handler for this primitive.
func (f *Form) PasteHandler() func(pastedText string, setFocus func(p Primitive)) {
return f.WrapPasteHandler(func(pastedText string, setFocus func(p Primitive)) {
for _, item := range f.items {
if item != nil && item.HasFocus() {
if handler := item.PasteHandler(); handler != nil {
handler(pastedText, setFocus)
return
}
}
}
for _, button := range f.buttons {
if button.HasFocus() {
if handler := button.PasteHandler(); handler != nil {
handler(pastedText, setFocus)
return
}
}
}
})
}

235
vendor/github.com/rivo/tview/frame.go generated vendored Normal file
View File

@@ -0,0 +1,235 @@
package tview
import (
"github.com/gdamore/tcell/v2"
)
// 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. May be nil.
primitive Primitive
// The lines of text to be displayed.
text []*frameText
// Border spacing.
top, bottom, header, footer, left, right int
// Keep a reference in case we need it when we change the primitive.
setFocus func(p Primitive)
}
// NewFrame returns a new frame around the given primitive. The primitive's
// size will be changed to fit within this frame. The primitive may be nil, in
// which case no other primitive is embedded in the 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,
}
return f
}
// SetPrimitive replaces the contained primitive with the given one. To remove
// a primitive, set it to nil.
func (f *Frame) SetPrimitive(p Primitive) *Frame {
var hasFocus bool
if f.primitive != nil {
hasFocus = f.primitive.HasFocus()
}
f.primitive = p
if hasFocus && f.setFocus != nil {
f.setFocus(p) // Restore focus.
}
return f
}
// GetPrimitive returns the primitive contained in this frame.
func (f *Frame) GetPrimitive() Primitive {
return f.primitive
}
// 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.DrawForSubclass(screen, f)
// 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 f.primitive != nil {
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)) {
f.setFocus = delegate
if f.primitive != nil {
delegate(f.primitive)
} else {
f.Box.Focus(delegate)
}
}
// HasFocus returns whether or not this primitive has focus.
func (f *Frame) HasFocus() bool {
if f.primitive == nil {
return f.Box.HasFocus()
}
return f.primitive.HasFocus()
}
// 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.
if f.primitive != nil {
consumed, capture = f.primitive.MouseHandler()(action, event, setFocus)
if consumed {
return true, capture
}
}
// Clicking on the frame parts.
if action == MouseLeftDown {
setFocus(f)
consumed = true
}
return
})
}
// InputHandler returns the handler for this primitive.
func (f *Frame) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) {
return f.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p Primitive)) {
if f.primitive == nil {
return
}
if handler := f.primitive.InputHandler(); handler != nil {
handler(event, setFocus)
return
}
})
}
// PasteHandler returns the handler for this primitive.
func (f *Frame) PasteHandler() func(pastedText string, setFocus func(p Primitive)) {
return f.WrapPasteHandler(func(pastedText string, setFocus func(p Primitive)) {
if f.primitive == nil {
return
}
if handler := f.primitive.PasteHandler(); handler != nil {
handler(pastedText, setFocus)
return
}
})
}

717
vendor/github.com/rivo/tview/grid.go generated vendored Normal file
View File

@@ -0,0 +1,717 @@
package tview
import (
"math"
"github.com/gdamore/tcell/v2"
)
// 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
// [Grid.SetRows] / [Grid.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 be 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, reset its Box to one
// with the desired color:
//
// grid.Box = NewBox()
func NewGrid() *Grid {
g := &Grid{
bordersColor: Styles.GraphicsColor,
}
g.Box = NewBox()
g.Box.dontClear = true
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 than 0 represent absolute column widths (gaps not included). Values
// less than or equal to 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 [Grid.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 [Grid.SetRows] and [Grid.SetColumns] where
// all row and column values are set to the given size values. See
// [Grid.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 with the highest minimum value (width or height,
// whatever is higher) 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.Box.Focus(delegate)
}
// HasFocus returns whether or not this primitive has focus.
func (g *Grid) HasFocus() bool {
for _, item := range g.items {
if item.visible && item.Item.HasFocus() {
return true
}
}
return g.Box.HasFocus()
}
// Draw draws this primitive onto the screen.
func (g *Grid) Draw(screen tcell.Screen) {
g.Box.DrawForSubclass(screen, g)
x, y, width, height := g.GetInnerRect()
screenWidth, screenHeight := screen.Size()
// Make a list of items which apply.
items := make([]*gridItem, 0, len(g.items))
ItemLoop:
for _, item := range g.items {
item.visible = false
if item.Item == nil || item.Width <= 0 || item.Height <= 0 || width < item.MinGridWidth || height < item.MinGridHeight {
continue // Disqualified.
}
// Check for overlaps and multiple layouts of the same item.
for index, existing := range items {
// Do they overlap or are identical?
if item.Item != existing.Item &&
(item.Row >= existing.Row+existing.Height || item.Row+item.Height <= existing.Row ||
item.Column >= existing.Column+existing.Width || item.Column+item.Width <= existing.Column) {
continue // They don't and aren't.
}
// What's their minimum size?
itemMin := item.MinGridWidth
if item.MinGridHeight > itemMin {
itemMin = item.MinGridHeight
}
existingMin := existing.MinGridWidth
if existing.MinGridHeight > existingMin {
existingMin = existing.MinGridHeight
}
// Which one is more important?
if itemMin < existingMin {
continue ItemLoop // This one isn't. Drop it.
}
items[index] = item // This one is. Replace the other.
continue ItemLoop
}
// This item will be visible.
items = append(items, 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 {
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 {
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 _, 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 item.Item.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
}
// 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.
borderStyle := tcell.StyleDefault.Background(g.backgroundColor).Foreground(g.bordersColor)
for _, 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
item.Item.SetRect(item.x, item.y, item.w, item.h)
// Draw primitive.
if item == focus {
defer item.Item.Draw(screen)
} else {
item.Item.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, borderStyle)
}
by = item.y + item.h
if by >= 0 && by < screenHeight {
PrintJoinedSemigraphics(screen, bx, by, Borders.Horizontal, borderStyle)
}
}
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, borderStyle)
}
bx = item.x + item.w
if bx >= 0 && bx < screenWidth {
PrintJoinedSemigraphics(screen, bx, by, Borders.Vertical, borderStyle)
}
}
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, borderStyle)
}
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, borderStyle)
}
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, borderStyle)
}
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, borderStyle)
}
}
}
}
// 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
})
}
// 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)) {
if !g.hasFocus {
// Pass event on to child primitive.
for _, item := range g.items {
if item != nil && item.Item.HasFocus() {
if handler := item.Item.InputHandler(); handler != nil {
handler(event, setFocus)
return
}
}
}
return
}
// Process our own key events if we have direct focus.
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++
}
})
}
// PasteHandler returns the handler for this primitive.
func (g *Grid) PasteHandler() func(pastedText string, setFocus func(p Primitive)) {
return g.WrapPasteHandler(func(pastedText string, setFocus func(p Primitive)) {
for _, item := range g.items {
if item != nil && item.Item.HasFocus() {
if handler := item.Item.PasteHandler(); handler != nil {
handler(pastedText, setFocus)
return
}
}
}
})
}

764
vendor/github.com/rivo/tview/image.go generated vendored Normal file
View File

@@ -0,0 +1,764 @@
package tview
import (
"image"
"math"
"github.com/gdamore/tcell/v2"
)
// Types of dithering applied to images.
const (
DitheringNone = iota // No dithering.
DitheringFloydSteinberg // Floyd-Steinberg dithering (the default).
)
// The number of colors supported by true color terminals (R*G*B = 256*256*256).
const TrueColor = 16777216
// This map describes what each block element looks like. A 1 bit represents a
// pixel that is drawn, a 0 bit represents a pixel that is not drawn. The least
// significant bit is the top left pixel, the most significant bit is the bottom
// right pixel, moving row by row from left to right, top to bottom.
var blockElements = map[rune]uint64{
BlockLowerOneEighthBlock: 0b1111111100000000000000000000000000000000000000000000000000000000,
BlockLowerOneQuarterBlock: 0b1111111111111111000000000000000000000000000000000000000000000000,
BlockLowerThreeEighthsBlock: 0b1111111111111111111111110000000000000000000000000000000000000000,
BlockLowerHalfBlock: 0b1111111111111111111111111111111100000000000000000000000000000000,
BlockLowerFiveEighthsBlock: 0b1111111111111111111111111111111111111111000000000000000000000000,
BlockLowerThreeQuartersBlock: 0b1111111111111111111111111111111111111111111111110000000000000000,
BlockLowerSevenEighthsBlock: 0b1111111111111111111111111111111111111111111111111111111100000000,
BlockLeftSevenEighthsBlock: 0b0111111101111111011111110111111101111111011111110111111101111111,
BlockLeftThreeQuartersBlock: 0b0011111100111111001111110011111100111111001111110011111100111111,
BlockLeftFiveEighthsBlock: 0b0001111100011111000111110001111100011111000111110001111100011111,
BlockLeftHalfBlock: 0b0000111100001111000011110000111100001111000011110000111100001111,
BlockLeftThreeEighthsBlock: 0b0000011100000111000001110000011100000111000001110000011100000111,
BlockLeftOneQuarterBlock: 0b0000001100000011000000110000001100000011000000110000001100000011,
BlockLeftOneEighthBlock: 0b0000000100000001000000010000000100000001000000010000000100000001,
BlockQuadrantLowerLeft: 0b0000111100001111000011110000111100000000000000000000000000000000,
BlockQuadrantLowerRight: 0b1111000011110000111100001111000000000000000000000000000000000000,
BlockQuadrantUpperLeft: 0b0000000000000000000000000000000000001111000011110000111100001111,
BlockQuadrantUpperRight: 0b0000000000000000000000000000000011110000111100001111000011110000,
BlockQuadrantUpperLeftAndLowerRight: 0b1111000011110000111100001111000000001111000011110000111100001111,
}
// pixel represents a character on screen used to draw part of an image.
type pixel struct {
style tcell.Style
element rune // The block element.
}
// Image implements a widget that displays one image. The original image
// (specified with [Image.SetImage]) is resized according to the specified size
// (see [Image.SetSize]), using the specified number of colors (see
// [Image.SetColors]), while applying dithering if necessary (see
// [Image.SetDithering]).
//
// Images are approximated by graphical characters in the terminal. The
// resolution is therefore limited by the number and type of characters that can
// be drawn in the terminal and the colors available in the terminal. The
// quality of the final image also depends on the terminal's font and spacing
// settings, none of which are under the control of this package. Results may
// vary.
type Image struct {
*Box
// The image to be displayed. If nil, the widget will be empty.
image image.Image
// The size of the image. If a value is 0, the corresponding size is chosen
// automatically based on the other size while preserving the image's aspect
// ratio. If both are 0, the image uses as much space as possible. A
// negative value represents a percentage, e.g. -50 means 50% of the
// available space.
width, height int
// The number of colors to use. If 0, the number of colors is chosen based
// on the terminal's capabilities.
colors int
// The dithering algorithm to use, one of the constants starting with
// "ImageDithering".
dithering int
// The width of a terminal's cell divided by its height.
aspectRatio float64
// Horizontal and vertical alignment, one of the "Align" constants.
alignHorizontal, alignVertical int
// The text to be displayed before the image.
label string
// The label style.
labelStyle tcell.Style
// The screen width of the label area. A value of 0 means use the width of
// the label text.
labelWidth int
// The actual image size (in cells) when it was drawn the last time.
lastWidth, lastHeight int
// The actual image (in cells) when it was drawn the last time. The size of
// this slice is lastWidth * lastHeight, indexed by y*lastWidth + x.
pixels []pixel
// A callback function set by the Form class and called when the user leaves
// this form item.
finished func(tcell.Key)
}
// NewImage returns a new image widget with an empty image (use [Image.SetImage]
// to specify the image to be displayed). The image will use the widget's entire
// available space. The dithering algorithm is set to Floyd-Steinberg dithering.
// The terminal's cell aspect ratio defaults to 0.5.
func NewImage() *Image {
return &Image{
Box: NewBox(),
dithering: DitheringFloydSteinberg,
aspectRatio: 0.5,
alignHorizontal: AlignCenter,
alignVertical: AlignCenter,
}
}
// SetImage sets the image to be displayed. If nil, the widget will be empty.
func (i *Image) SetImage(image image.Image) *Image {
i.image = image
i.lastWidth, i.lastHeight = 0, 0
return i
}
// SetSize sets the size of the image. Positive values refer to cells in the
// terminal. Negative values refer to a percentage of the available space (e.g.
// -50 means 50%). A value of 0 means that the corresponding size is chosen
// automatically based on the other size while preserving the image's aspect
// ratio. If both are 0, the image uses as much space as possible while still
// preserving the aspect ratio.
func (i *Image) SetSize(rows, columns int) *Image {
i.width = columns
i.height = rows
return i
}
// SetColors sets the number of colors to use. This should be the number of
// colors supported by the terminal. If 0, the number of colors is chosen based
// on the TERM environment variable (which may or may not be reliable).
//
// Only the values 0, 2, 8, 256, and 16777216 ([TrueColor]) are supported. Other
// values will be rounded up to the next supported value, to a maximum of
// 16777216.
//
// The effect of using more colors than supported by the terminal is undefined.
func (i *Image) SetColors(colors int) *Image {
i.colors = colors
i.lastWidth, i.lastHeight = 0, 0
return i
}
// GetColors returns the number of colors that will be used while drawing the
// image. This is one of the values listed in [Image.SetColors], except 0 which
// will be replaced by the actual number of colors used.
func (i *Image) GetColors() int {
switch {
case i.colors == 0:
return availableColors
case i.colors <= 2:
return 2
case i.colors <= 8:
return 8
case i.colors <= 256:
return 256
}
return TrueColor
}
// SetDithering sets the dithering algorithm to use, one of the constants
// starting with "Dithering", for example [DitheringFloydSteinberg] (the
// default). Dithering is not applied when rendering in true-color.
func (i *Image) SetDithering(dithering int) *Image {
i.dithering = dithering
i.lastWidth, i.lastHeight = 0, 0
return i
}
// SetAspectRatio sets the width of a terminal's cell divided by its height.
// You may change the default of 0.5 if your terminal / font has a different
// aspect ratio. This is used to calculate the size of the image if the
// specified width or height is 0. The function will panic if the aspect ratio
// is 0 or less.
func (i *Image) SetAspectRatio(aspectRatio float64) *Image {
if aspectRatio <= 0 {
panic("aspect ratio must be greater than 0")
}
i.aspectRatio = aspectRatio
i.lastWidth, i.lastHeight = 0, 0
return i
}
// SetAlign sets the vertical and horizontal alignment of the image within the
// widget's space. The possible values are [AlignTop], [AlignCenter], and
// [AlignBottom] for vertical alignment and [AlignLeft], [AlignCenter], and
// [AlignRight] for horizontal alignment. The default is [AlignCenter] for both
// (or [AlignTop] and [AlignLeft] if the image is part of a [Form]).
func (i *Image) SetAlign(vertical, horizontal int) *Image {
i.alignHorizontal = horizontal
i.alignVertical = vertical
return i
}
// SetLabel sets the text to be displayed before the image.
func (i *Image) SetLabel(label string) *Image {
i.label = label
return i
}
// GetLabel returns the text to be displayed before the image.
func (i *Image) 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 *Image) SetLabelWidth(width int) *Image {
i.labelWidth = width
return i
}
// GetFieldWidth returns this primitive's field width. This is the image's width
// or, if the width is 0 or less, the proportional width of the image based on
// its height as returned by [Image.GetFieldHeight]. If there is no image, 0 is
// returned.
func (i *Image) GetFieldWidth() int {
if i.width <= 0 {
if i.image == nil {
return 0
}
bounds := i.image.Bounds()
height := i.GetFieldHeight()
return bounds.Dx() * height / bounds.Dy()
}
return i.width
}
// GetFieldHeight returns this primitive's field height. This is the image's
// height or 8 if the height is 0 or less.
func (i *Image) GetFieldHeight() int {
if i.height <= 0 {
return 8
}
return i.height
}
// SetDisabled sets whether or not the item is disabled / read-only.
func (i *Image) SetDisabled(disabled bool) FormItem {
return i // Images are always read-only.
}
// SetFormAttributes sets attributes shared by all form items.
func (i *Image) SetFormAttributes(labelWidth int, labelColor, bgColor, fieldTextColor, fieldBgColor tcell.Color) FormItem {
i.labelWidth = labelWidth
i.backgroundColor = bgColor
i.SetLabelStyle(tcell.StyleDefault.Foreground(labelColor).Background(bgColor))
i.lastWidth, i.lastHeight = 0, 0
return i
}
// SetLabelStyle sets the style of the label.
func (i *Image) SetLabelStyle(style tcell.Style) *Image {
i.labelStyle = style
return i
}
// GetLabelStyle returns the style of the label.
func (i *Image) GetLabelStyle() tcell.Style {
return i.labelStyle
}
// SetFinishedFunc sets a callback invoked when the user leaves this form item.
func (i *Image) SetFinishedFunc(handler func(key tcell.Key)) FormItem {
i.finished = handler
return i
}
// Focus is called when this primitive receives focus.
func (i *Image) Focus(delegate func(p Primitive)) {
// If we're part of a form, there's nothing the user can do here so we're
// finished.
if i.finished != nil {
i.finished(-1)
return
}
i.Box.Focus(delegate)
}
// render re-populates the [Image.pixels] slice based on the current settings,
// if [Image.lastWidth] and [Image.lastHeight] don't match the current image's
// size. It also sets the new image size in these two variables.
func (i *Image) render() {
// If there is no image, there are no pixels.
if i.image == nil {
i.pixels = nil
return
}
// Calculate the new (terminal-space) image size.
bounds := i.image.Bounds()
imageWidth, imageHeight := bounds.Dx(), bounds.Dy()
if i.aspectRatio != 1.0 {
imageWidth = int(float64(imageWidth) / i.aspectRatio)
}
width, height := i.width, i.height
_, _, innerWidth, innerHeight := i.GetInnerRect()
if i.labelWidth > 0 {
innerWidth -= i.labelWidth
} else {
innerWidth -= TaggedStringWidth(i.label)
}
if innerWidth <= 0 {
i.pixels = nil
return
}
if width == 0 && height == 0 {
// Use all available space.
width, height = innerWidth, innerHeight
if adjustedWidth := imageWidth * height / imageHeight; adjustedWidth < width {
width = adjustedWidth
} else {
height = imageHeight * width / imageWidth
}
} else {
// Turn percentages into absolute values.
if width < 0 {
width = innerWidth * -width / 100
}
if height < 0 {
height = innerHeight * -height / 100
}
if width == 0 {
// Adjust the width.
width = imageWidth * height / imageHeight
} else if height == 0 {
// Adjust the height.
height = imageHeight * width / imageWidth
}
}
if width <= 0 || height <= 0 {
i.pixels = nil
return
}
// If nothing has changed, we're done.
if i.lastWidth == width && i.lastHeight == height {
return
}
i.lastWidth, i.lastHeight = width, height // This could still be larger than the available space but that's ok for now.
// Generate the initial pixels by resizing the image (8x8 per cell).
pixels := i.resize()
// Turn them into block elements with background/foreground colors.
i.stamp(pixels)
}
// resize resizes the image to the current size and returns the result as a
// slice of pixels. It is assumed that [Image.lastWidth] (w) and
// [Image.lastHeight] (h) are positive, non-zero values, and the slice has a
// size of 64*w*h, with each pixel being represented by 3 float64 values in the
// range of 0-1. The factor of 64 is due to the fact that we calculate 8x8
// pixels per cell.
func (i *Image) resize() [][3]float64 {
// Because most of the time, we will be downsizing the image, we don't even
// attempt to do any fancy interpolation. For each target pixel, we
// calculate a weighted average of the source pixels using their coverage
// area.
bounds := i.image.Bounds()
srcWidth, srcHeight := bounds.Dx(), bounds.Dy()
tgtWidth, tgtHeight := i.lastWidth*8, i.lastHeight*8
coverageWidth, coverageHeight := float64(tgtWidth)/float64(srcWidth), float64(tgtHeight)/float64(srcHeight)
pixels := make([][3]float64, tgtWidth*tgtHeight)
weights := make([]float64, tgtWidth*tgtHeight)
for srcY := bounds.Min.Y; srcY < bounds.Max.Y; srcY++ {
for srcX := bounds.Min.X; srcX < bounds.Max.X; srcX++ {
r32, g32, b32, _ := i.image.At(srcX, srcY).RGBA()
r, g, b := float64(r32)/0xffff, float64(g32)/0xffff, float64(b32)/0xffff
// Iterate over all target pixels. Outer loop is Y.
startY := float64(srcY-bounds.Min.Y) * coverageHeight
endY := startY + coverageHeight
fromY, toY := int(startY), int(endY)
for tgtY := fromY; tgtY <= toY && tgtY < tgtHeight; tgtY++ {
coverageY := 1.0
if tgtY == fromY {
coverageY -= math.Mod(startY, 1.0)
}
if tgtY == toY {
coverageY -= 1.0 - math.Mod(endY, 1.0)
}
// Inner loop is X.
startX := float64(srcX-bounds.Min.X) * coverageWidth
endX := startX + coverageWidth
fromX, toX := int(startX), int(endX)
for tgtX := fromX; tgtX <= toX && tgtX < tgtWidth; tgtX++ {
coverageX := 1.0
if tgtX == fromX {
coverageX -= math.Mod(startX, 1.0)
}
if tgtX == toX {
coverageX -= 1.0 - math.Mod(endX, 1.0)
}
// Add a weighted contribution to the target pixel.
index := tgtY*tgtWidth + tgtX
coverage := coverageX * coverageY
pixels[index][0] += r * coverage
pixels[index][1] += g * coverage
pixels[index][2] += b * coverage
weights[index] += coverage
}
}
}
}
// Normalize the pixels.
for index, weight := range weights {
if weight > 0 {
pixels[index][0] /= weight
pixels[index][1] /= weight
pixels[index][2] /= weight
}
}
return pixels
}
// stamp takes the pixels generated by [Image.resize] and populates the
// [Image.pixels] slice accordingly.
func (i *Image) stamp(resized [][3]float64) {
// For each 8x8 pixel block, we find the best block element to represent it,
// given the available colors.
i.pixels = make([]pixel, i.lastWidth*i.lastHeight)
colors := i.GetColors()
for row := 0; row < i.lastHeight; row++ {
for col := 0; col < i.lastWidth; col++ {
// Calculate an error for each potential block element + color. Keep
// the one with the lowest error.
// Note that the values in "resize" may lie outside [0, 1] due to
// the error distribution during dithering.
minMSE := math.MaxFloat64 // Mean squared error.
var final [64][3]float64 // The final pixel values.
for element, bits := range blockElements {
// Calculate the average color for the pixels covered by the set
// bits and unset bits.
var (
bg, fg [3]float64
setBits float64
bit uint64 = 1
)
for y := 0; y < 8; y++ {
for x := 0; x < 8; x++ {
index := (row*8+y)*i.lastWidth*8 + (col*8 + x)
if bits&bit != 0 {
fg[0] += resized[index][0]
fg[1] += resized[index][1]
fg[2] += resized[index][2]
setBits++
} else {
bg[0] += resized[index][0]
bg[1] += resized[index][1]
bg[2] += resized[index][2]
}
bit <<= 1
}
}
for ch := 0; ch < 3; ch++ {
fg[ch] /= setBits
if fg[ch] < 0 {
fg[ch] = 0
} else if fg[ch] > 1 {
fg[ch] = 1
}
bg[ch] /= 64 - setBits
if bg[ch] < 0 {
bg[ch] = 0
}
if bg[ch] > 1 {
bg[ch] = 1
}
}
// Quantize to the nearest acceptable color.
for _, color := range []*[3]float64{&fg, &bg} {
if colors == 2 {
// Monochrome. The following weights correspond better
// to human perception than the arithmetic mean.
gray := 0.299*color[0] + 0.587*color[1] + 0.114*color[2]
if gray < 0.5 {
*color = [3]float64{0, 0, 0}
} else {
*color = [3]float64{1, 1, 1}
}
} else {
for index, ch := range color {
switch {
case colors == 8:
// Colors vary wildly for each terminal. Expect
// suboptimal results.
if ch < 0.5 {
color[index] = 0
} else {
color[index] = 1
}
case colors == 256:
color[index] = math.Round(ch*6) / 6
}
}
}
}
// Calculate the error (and the final pixel values).
var (
mse float64
values [64][3]float64
valuesIndex int
)
bit = 1
for y := 0; y < 8; y++ {
for x := 0; x < 8; x++ {
if bits&bit != 0 {
values[valuesIndex] = fg
} else {
values[valuesIndex] = bg
}
index := (row*8+y)*i.lastWidth*8 + (col*8 + x)
for ch := 0; ch < 3; ch++ {
err := resized[index][ch] - values[valuesIndex][ch]
mse += err * err
}
bit <<= 1
valuesIndex++
}
}
// Do we have a better match?
if mse < minMSE {
// Yes. Save it.
minMSE = mse
final = values
index := row*i.lastWidth + col
i.pixels[index].element = element
i.pixels[index].style = tcell.StyleDefault.
Foreground(tcell.NewRGBColor(int32(math.Min(255, fg[0]*255)), int32(math.Min(255, fg[1]*255)), int32(math.Min(255, fg[2]*255)))).
Background(tcell.NewRGBColor(int32(math.Min(255, bg[0]*255)), int32(math.Min(255, bg[1]*255)), int32(math.Min(255, bg[2]*255))))
}
}
// Check if there is a shade block which results in a smaller error.
// What's the overall average color?
var avg [3]float64
for y := 0; y < 8; y++ {
for x := 0; x < 8; x++ {
index := (row*8+y)*i.lastWidth*8 + (col*8 + x)
for ch := 0; ch < 3; ch++ {
avg[ch] += resized[index][ch] / 64
}
}
}
for ch := 0; ch < 3; ch++ {
if avg[ch] < 0 {
avg[ch] = 0
} else if avg[ch] > 1 {
avg[ch] = 1
}
}
// Quantize and choose shade element.
element := BlockFullBlock
var fg, bg tcell.Color
shades := []rune{' ', BlockLightShade, BlockMediumShade, BlockDarkShade, BlockFullBlock}
if colors == 2 {
// Monochrome.
gray := 0.299*avg[0] + 0.587*avg[1] + 0.114*avg[2] // See above for details.
shade := int(math.Round(gray * 4))
element = shades[shade]
for ch := 0; ch < 3; ch++ {
avg[ch] = float64(shade) / 4
}
bg = tcell.ColorBlack
fg = tcell.ColorWhite
} else if colors == TrueColor {
// True color.
fg = tcell.NewRGBColor(int32(math.Min(255, avg[0]*255)), int32(math.Min(255, avg[1]*255)), int32(math.Min(255, avg[2]*255)))
bg = fg
} else {
// 8 or 256 colors.
steps := 1.0
if colors == 256 {
steps = 6.0
}
var (
lo, hi, pos [3]float64
shade float64
)
for ch := 0; ch < 3; ch++ {
lo[ch] = math.Floor(avg[ch]*steps) / steps
hi[ch] = math.Ceil(avg[ch]*steps) / steps
if r := hi[ch] - lo[ch]; r > 0 {
pos[ch] = (avg[ch] - lo[ch]) / r
if math.Abs(pos[ch]-0.5) < math.Abs(shade-0.5) {
shade = pos[ch]
}
}
}
shade = math.Round(shade * 4)
element = shades[int(shade)]
shade /= 4
for ch := 0; ch < 3; ch++ { // Find the closest channel value.
best := math.Abs(avg[ch] - (lo[ch] + (hi[ch]-lo[ch])*shade)) // Start shade from lo to hi.
if value := math.Abs(avg[ch] - (hi[ch] - (hi[ch]-lo[ch])*shade)); value < best {
best = value // Swap lo and hi.
lo[ch], hi[ch] = hi[ch], lo[ch]
}
if value := math.Abs(avg[ch] - lo[ch]); value < best {
best = value // Use lo.
hi[ch] = lo[ch]
}
if value := math.Abs(avg[ch] - hi[ch]); value < best {
lo[ch] = hi[ch] // Use hi.
}
avg[ch] = lo[ch] + (hi[ch]-lo[ch])*shade // Quantize.
}
bg = tcell.NewRGBColor(int32(math.Min(255, lo[0]*255)), int32(math.Min(255, lo[1]*255)), int32(math.Min(255, lo[2]*255)))
fg = tcell.NewRGBColor(int32(math.Min(255, hi[0]*255)), int32(math.Min(255, hi[1]*255)), int32(math.Min(255, hi[2]*255)))
}
// Calculate the error (and the final pixel values).
var (
mse float64
values [64][3]float64
valuesIndex int
)
for y := 0; y < 8; y++ {
for x := 0; x < 8; x++ {
index := (row*8+y)*i.lastWidth*8 + (col*8 + x)
for ch := 0; ch < 3; ch++ {
err := resized[index][ch] - avg[ch]
mse += err * err
}
values[valuesIndex] = avg
valuesIndex++
}
}
// Is this shade element better than the block element?
if mse < minMSE {
// Yes. Save it.
final = values
index := row*i.lastWidth + col
i.pixels[index].element = element
i.pixels[index].style = tcell.StyleDefault.Foreground(fg).Background(bg)
}
// Apply dithering.
if colors < TrueColor && i.dithering == DitheringFloydSteinberg {
// The dithering mask determines how the error is distributed.
// Each element has three values: dx, dy, and weight (in 16th).
var mask = [4][3]int{
{1, 0, 7},
{-1, 1, 3},
{0, 1, 5},
{1, 1, 1},
}
// We dither the 8x8 block as a 2x2 block, transferring errors
// to its 2x2 neighbors.
for ch := 0; ch < 3; ch++ {
for y := 0; y < 2; y++ {
for x := 0; x < 2; x++ {
// What's the error for this 4x4 block?
var err float64
for dy := 0; dy < 4; dy++ {
for dx := 0; dx < 4; dx++ {
err += (final[(y*4+dy)*8+(x*4+dx)][ch] - resized[(row*8+(y*4+dy))*i.lastWidth*8+(col*8+(x*4+dx))][ch]) / 16
}
}
// Distribute it to the 2x2 neighbors.
for _, dist := range mask {
for dy := 0; dy < 4; dy++ {
for dx := 0; dx < 4; dx++ {
targetX, targetY := (x+dist[0])*4+dx, (y+dist[1])*4+dy
if targetX < 0 || col*8+targetX >= i.lastWidth*8 || targetY < 0 || row*8+targetY >= i.lastHeight*8 {
continue
}
resized[(row*8+targetY)*i.lastWidth*8+(col*8+targetX)][ch] -= err * float64(dist[2]) / 16
}
}
}
}
}
}
}
}
}
}
// Draw draws this primitive onto the screen.
func (i *Image) Draw(screen tcell.Screen) {
i.DrawForSubclass(screen, i)
// Regenerate image if necessary.
i.render()
// Draw label.
viewX, viewY, viewWidth, viewHeight := i.GetInnerRect()
_, labelBg, _ := i.labelStyle.Decompose()
if i.labelWidth > 0 {
labelWidth := i.labelWidth
if labelWidth > viewWidth {
labelWidth = viewWidth
}
printWithStyle(screen, i.label, viewX, viewY, 0, labelWidth, AlignLeft, i.labelStyle, labelBg == tcell.ColorDefault)
viewX += labelWidth
viewWidth -= labelWidth
} else {
_, _, drawnWidth := printWithStyle(screen, i.label, viewX, viewY, 0, viewWidth, AlignLeft, i.labelStyle, labelBg == tcell.ColorDefault)
viewX += drawnWidth
viewWidth -= drawnWidth
}
// Determine image placement.
x, y, width, height := viewX, viewY, i.lastWidth, i.lastHeight
if i.alignHorizontal == AlignCenter {
x += (viewWidth - width) / 2
} else if i.alignHorizontal == AlignRight {
x += viewWidth - width
}
if i.alignVertical == AlignCenter {
y += (viewHeight - height) / 2
} else if i.alignVertical == AlignBottom {
y += viewHeight - height
}
// Draw the image.
for row := 0; row < height; row++ {
if y+row < viewY || y+row >= viewY+viewHeight {
continue
}
for col := 0; col < width; col++ {
if x+col < viewX || x+col >= viewX+viewWidth {
continue
}
index := row*width + col
screen.SetContent(x+col, y+row, i.pixels[index].element, nil, i.pixels[index].style)
}
}
}

717
vendor/github.com/rivo/tview/inputfield.go generated vendored Normal file
View File

@@ -0,0 +1,717 @@
package tview
import (
"math"
"strconv"
"strings"
"sync"
"github.com/gdamore/tcell/v2"
"github.com/rivo/uniseg"
)
const (
AutocompletedNavigate = iota // The user navigated the autocomplete list (using the errow keys).
AutocompletedTab // The user selected an autocomplete entry using the tab key.
AutocompletedEnter // The user selected an autocomplete entry using the enter key.
AutocompletedClick // The user selected an autocomplete entry by clicking the mouse button on it.
)
// Predefined InputField acceptance functions.
var (
// InputFieldInteger accepts integers.
InputFieldInteger = func(text string, ch rune) bool {
if text == "-" {
return true
}
_, err := strconv.Atoi(text)
return err == nil
}
// InputFieldFloat accepts floating-point numbers.
InputFieldFloat = func(text string, ch rune) bool {
if text == "-" || text == "." || text == "-." {
return true
}
_, err := strconv.ParseFloat(text, 64)
return err == nil
}
// 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 {
return func(text string, ch rune) bool {
return len([]rune(text)) <= maxLength
}
}
)
// InputField is a one-line box into which the user can enter text. Use
// [InputField.SetAcceptanceFunc] to accept or reject input,
// [InputField.SetChangedFunc] to listen for changes, and
// [InputField.SetMaskCharacter] to hide input from onlookers (e.g. for password
// input).
//
// The input field also has an optional autocomplete feature. It is initialized
// by the [InputField.SetAutocompleteFunc] function. For more control over the
// autocomplete drop-down's behavior, you can also set the
// [InputField.SetAutocompletedFunc].
//
// Navigation and editing is the same as for a [TextArea], with the following
// exceptions:
//
// - Tab, BackTab, Enter, Escape: Finish editing.
//
// Note that while pressing Tab or Enter is intercepted by the input field, it
// is possible to paste such characters into the input field, possibly resulting
// in multi-line input. You can use [InputField.SetAcceptanceFunc] to prevent
// this.
//
// If autocomplete functionality is configured:
//
// - Down arrow: Open the autocomplete drop-down.
// - Tab, Enter: Select the current autocomplete entry.
//
// See https://github.com/rivo/tview/wiki/InputField for an example.
type InputField struct {
*Box
// The text area providing the core functionality of the input field.
textArea *TextArea
// The screen width of the input area. A value of 0 means extend as much as
// possible.
fieldWidth 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
// The styles of the autocomplete entries.
autocompleteStyles struct {
main tcell.Style
selected tcell.Style
background tcell.Color
useTags bool
}
// An optional function which is called when the user selects an
// autocomplete entry. The text and index of the selected entry (within the
// list) is provided, as well as the user action causing the selection (one
// of the "Autocompleted" values). The function should return true if the
// autocomplete list should be closed. If nil, the input field will be
// updated automatically when the user navigates the autocomplete list.
autocompleted func(text string, index int, source int) bool
// 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)
}
// NewInputField returns a new input field.
func NewInputField() *InputField {
i := &InputField{
Box: NewBox(),
textArea: NewTextArea().SetWrap(false),
}
i.textArea.SetChangedFunc(func() {
if i.changed != nil {
i.changed(i.textArea.GetText())
}
}).SetFocusFunc(func() {
// Forward focus event to the input field.
if i.Box.focus != nil {
i.Box.focus()
}
})
i.textArea.textStyle = tcell.StyleDefault.Background(Styles.ContrastBackgroundColor).Foreground(Styles.PrimaryTextColor)
i.textArea.placeholderStyle = tcell.StyleDefault.Background(Styles.ContrastBackgroundColor).Foreground(Styles.ContrastSecondaryTextColor)
i.autocompleteStyles.main = tcell.StyleDefault.Background(Styles.MoreContrastBackgroundColor).Foreground(Styles.PrimitiveBackgroundColor)
i.autocompleteStyles.selected = tcell.StyleDefault.Background(Styles.PrimaryTextColor).Foreground(Styles.PrimitiveBackgroundColor)
i.autocompleteStyles.background = Styles.MoreContrastBackgroundColor
i.autocompleteStyles.useTags = true
return i
}
// SetText sets the current text of the input field. This can be undone by the
// user. Calling this function will also trigger a "changed" event.
func (i *InputField) SetText(text string) *InputField {
i.textArea.Replace(0, i.textArea.GetTextLength(), text)
return i
}
// GetText returns the current text of the input field.
func (i *InputField) GetText() string {
return i.textArea.GetText()
}
// SetLabel sets the text to be displayed before the input area.
func (i *InputField) SetLabel(label string) *InputField {
i.textArea.SetLabel(label)
return i
}
// GetLabel returns the text to be displayed before the input area.
func (i *InputField) GetLabel() string {
return i.textArea.GetLabel()
}
// 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.textArea.SetLabelWidth(width)
return i
}
// SetPlaceholder sets the text to be displayed when the input text is empty.
func (i *InputField) SetPlaceholder(text string) *InputField {
i.textArea.SetPlaceholder(text)
return i
}
// SetLabelColor sets the text color of the label.
func (i *InputField) SetLabelColor(color tcell.Color) *InputField {
i.textArea.SetLabelStyle(i.textArea.GetLabelStyle().Foreground(color))
return i
}
// SetLabelStyle sets the style of the label.
func (i *InputField) SetLabelStyle(style tcell.Style) *InputField {
i.textArea.SetLabelStyle(style)
return i
}
// GetLabelStyle returns the style of the label.
func (i *InputField) GetLabelStyle() tcell.Style {
return i.textArea.GetLabelStyle()
}
// SetFieldBackgroundColor sets the background color of the input area.
func (i *InputField) SetFieldBackgroundColor(color tcell.Color) *InputField {
i.textArea.SetTextStyle(i.textArea.GetTextStyle().Background(color))
return i
}
// SetFieldTextColor sets the text color of the input area.
func (i *InputField) SetFieldTextColor(color tcell.Color) *InputField {
i.textArea.SetTextStyle(i.textArea.GetTextStyle().Foreground(color))
return i
}
// SetFieldStyle sets the style of the input area (when no placeholder is
// shown).
func (i *InputField) SetFieldStyle(style tcell.Style) *InputField {
i.textArea.SetTextStyle(style)
return i
}
// GetFieldStyle returns the style of the input area (when no placeholder is
// shown).
func (i *InputField) GetFieldStyle() tcell.Style {
return i.textArea.GetTextStyle()
}
// SetPlaceholderTextColor sets the text color of placeholder text.
func (i *InputField) SetPlaceholderTextColor(color tcell.Color) *InputField {
i.textArea.SetPlaceholderStyle(i.textArea.GetPlaceholderStyle().Foreground(color))
return i
}
// SetPlaceholderStyle sets the style of the input area (when a placeholder is
// shown).
func (i *InputField) SetPlaceholderStyle(style tcell.Style) *InputField {
i.textArea.SetPlaceholderStyle(style)
return i
}
// GetPlaceholderStyle returns the style of the input area (when a placeholder
// is shown).
func (i *InputField) GetPlaceholderStyle() tcell.Style {
return i.textArea.GetPlaceholderStyle()
}
// SetAutocompleteStyles sets the colors and style of the autocomplete entries.
// For details, see [List.SetMainTextStyle], [List.SetSelectedStyle], and
// [Box.SetBackgroundColor].
func (i *InputField) SetAutocompleteStyles(background tcell.Color, main, selected tcell.Style) *InputField {
i.autocompleteStyles.background = background
i.autocompleteStyles.main = main
i.autocompleteStyles.selected = selected
return i
}
// SetAutocompleteUseTags sets whether or not the autocomplete entries may
// contain style tags affecting their appearance. The default is true.
func (i *InputField) SetAutocompleteUseTags(useTags bool) *InputField {
i.autocompleteStyles.useTags = useTags
return i
}
// SetFormAttributes sets attributes shared by all form items.
func (i *InputField) SetFormAttributes(labelWidth int, labelColor, bgColor, fieldTextColor, fieldBgColor tcell.Color) FormItem {
i.textArea.SetFormAttributes(labelWidth, labelColor, bgColor, fieldTextColor, 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
}
// GetFieldHeight returns this primitive's field height.
func (i *InputField) GetFieldHeight() int {
return 1
}
// SetDisabled sets whether or not the item is disabled / read-only.
func (i *InputField) SetDisabled(disabled bool) FormItem {
i.textArea.SetDisabled(disabled)
if i.finished != nil {
i.finished(-1)
}
return i
}
// SetMaskCharacter sets a character that masks user input on a screen. A value
// of 0 disables masking.
func (i *InputField) SetMaskCharacter(mask rune) *InputField {
if mask == 0 {
i.textArea.setTransform(nil)
return i
}
maskStr := string(mask)
maskWidth := uniseg.StringWidth(maskStr)
i.textArea.setTransform(func(cluster, rest string, boundaries int) (newCluster string, newBoundaries int) {
return maskStr, maskWidth << uniseg.ShiftWidth
})
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
// [InputField.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
}
// SetAutocompletedFunc sets a callback function which is invoked when the user
// selects an entry from the autocomplete drop-down list. The function is passed
// the text of the selected entry (stripped of any style tags), the index of the
// entry, and the user action that caused the selection, for example
// [AutocompletedNavigate]. It returns true if the autocomplete drop-down should
// be closed after the callback returns or false if it should remain open, in
// which case [InputField.Autocomplete] is called to update the drop-down's
// contents.
//
// If no such callback is set (or nil is provided), the input field will be
// updated with the selection any time the user navigates the autocomplete
// drop-down list. So this function essentially gives you more control over the
// autocomplete functionality.
func (i *InputField) SetAutocompletedFunc(autocompleted func(text string, index int, source int) bool) *InputField {
i.autocompleted = autocompleted
return i
}
// Autocomplete invokes the autocomplete callback (if there is one, see
// [InputField.SetAutocompleteFunc]). 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?
text := i.textArea.GetText()
entries := i.autocomplete(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).
SetMainTextStyle(i.autocompleteStyles.main).
SetSelectedStyle(i.autocompleteStyles.selected).
SetUseStyleTags(i.autocompleteStyles.useTags, i.autocompleteStyles.useTags).
SetHighlightFullLine(true).
SetBackgroundColor(i.autocompleteStyles.background)
}
// Fill it with the entries.
currentIndex := i.autocompleteList.GetCurrentItem()
var currentSelection string
if currentIndex >= 0 && currentIndex < i.autocompleteList.GetItemCount() {
currentSelection, _ = i.autocompleteList.GetItemText(currentIndex)
}
currentEntry := -1
suffixLength := math.MaxInt
i.autocompleteList.Clear()
for index, entry := range entries {
i.autocompleteList.AddItem(entry, "", 0, nil)
if currentSelection != "" && entry == currentSelection {
currentEntry = index
}
if currentSelection == "" && strings.HasPrefix(entry, text) && len(entry)-len(text) < suffixLength {
currentEntry = index
suffixLength = len(text) - len(entry)
}
}
// 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. The handler receives the text as it would be
// after the change and the last character entered. If the handler is nil, all
// input is accepted. The function is only called when a single rune is inserted
// at the current cursor position.
//
// This package defines a number of variables prefixed with InputField which may
// be used for common input (e.g. numbers, maximum text length). See for example
// [InputFieldInteger].
//
// When text is pasted, lastChar is 0.
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
}
// Focus is called when this primitive receives focus.
func (i *InputField) Focus(delegate func(p Primitive)) {
// If we're part of a form and this item is disabled, there's nothing the
// user can do here so we're finished.
if i.finished != nil && i.textArea.GetDisabled() {
i.finished(-1)
return
}
i.Box.Focus(delegate)
}
// HasFocus returns whether or not this primitive has focus.
func (i *InputField) HasFocus() bool {
return i.textArea.HasFocus() || i.Box.HasFocus()
}
// Blur is called when this primitive loses focus.
func (i *InputField) Blur() {
i.textArea.Blur()
i.Box.Blur()
i.autocompleteList = nil // Hide the autocomplete drop-down.
}
// Draw draws this primitive onto the screen.
func (i *InputField) Draw(screen tcell.Screen) {
i.Box.DrawForSubclass(screen, i)
// Prepare
x, y, width, height := i.GetInnerRect()
if height < 1 || width < 1 {
return
}
// Resize text area.
labelWidth := i.textArea.GetLabelWidth()
if labelWidth == 0 {
labelWidth = TaggedStringWidth(i.textArea.GetLabel())
}
fieldWidth := i.fieldWidth
if fieldWidth == 0 {
fieldWidth = width - labelWidth
}
i.textArea.SetRect(x, y, labelWidth+fieldWidth, 1)
i.textArea.setMinCursorPadding(fieldWidth-1, 1)
// Draw text area.
i.textArea.hasFocus = i.HasFocus() // Force cursor positioning.
i.textArea.Draw(screen)
// Draw autocomplete list.
i.autocompleteListMutex.Lock()
defer i.autocompleteListMutex.Unlock()
if i.autocompleteList != nil && i.HasFocus() {
// 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 + labelWidth
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)
}
}
// 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)) {
if i.textArea.GetDisabled() {
return
}
// Trigger changed events.
var skipAutocomplete bool
currentText := i.textArea.GetText()
defer func() {
if skipAutocomplete {
return
}
if i.textArea.GetText() != currentText {
i.Autocomplete()
}
}()
// If we have an autocomplete list, there are certain keys we will
// forward to it.
i.autocompleteListMutex.Lock()
defer i.autocompleteListMutex.Unlock()
if i.autocompleteList != nil {
i.autocompleteList.SetChangedFunc(nil)
i.autocompleteList.SetSelectedFunc(nil)
switch key := event.Key(); key {
case tcell.KeyEscape: // Close the list.
i.autocompleteList = nil
return
case tcell.KeyEnter, tcell.KeyTab: // Intentional selection.
index := i.autocompleteList.GetCurrentItem()
text, _ := i.autocompleteList.GetItemText(index)
if i.autocompleted != nil {
source := AutocompletedEnter
if key == tcell.KeyTab {
source = AutocompletedTab
}
if i.autocompleted(stripTags(text), index, source) {
i.autocompleteList = nil
currentText = i.GetText()
}
} else {
i.SetText(text)
skipAutocomplete = true
i.autocompleteList = nil
}
return
case tcell.KeyDown, tcell.KeyUp, tcell.KeyPgDn, tcell.KeyPgUp:
i.autocompleteList.SetChangedFunc(func(index int, text, secondaryText string, shortcut rune) {
text = stripTags(text)
if i.autocompleted != nil {
if i.autocompleted(text, index, AutocompletedNavigate) {
i.autocompleteList = nil
currentText = i.GetText()
}
} else {
i.SetText(text)
currentText = stripTags(text) // We want to keep the autocomplete list open and unchanged.
}
})
i.autocompleteList.InputHandler()(event, setFocus)
return
}
}
// Finish up.
finish := func(key tcell.Key) {
if i.done != nil {
i.done(key)
}
if i.finished != nil {
i.finished(key)
}
}
// Process special key events for the input field.
switch key := event.Key(); key {
case tcell.KeyDown:
i.autocompleteListMutex.Unlock() // We're still holding a lock.
i.Autocomplete()
i.autocompleteListMutex.Lock()
case tcell.KeyEnter, tcell.KeyEscape, tcell.KeyTab, tcell.KeyBacktab:
finish(key)
case tcell.KeyCtrlV:
if i.accept != nil && !i.accept(i.textArea.getTextBeforeCursor()+i.textArea.GetClipboardText()+i.textArea.getTextAfterCursor(), 0) {
return
}
i.textArea.InputHandler()(event, setFocus)
case tcell.KeyRune:
if event.Modifiers()&tcell.ModAlt == 0 && i.accept != nil {
// Check if this rune is accepted.
r := event.Rune()
if !i.accept(i.textArea.getTextBeforeCursor()+string(r)+i.textArea.getTextAfterCursor(), r) {
return
}
}
fallthrough
default:
// Forward other key events to the text area.
i.textArea.InputHandler()(event, setFocus)
}
})
}
// 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) {
if i.textArea.GetDisabled() {
return false, nil
}
var skipAutocomplete bool
currentText := i.GetText()
defer func() {
if skipAutocomplete {
return
}
if i.textArea.GetText() != currentText {
i.Autocomplete()
}
}()
// If we have an autocomplete list, forward the mouse event to it.
i.autocompleteListMutex.Lock()
defer i.autocompleteListMutex.Unlock()
if i.autocompleteList != nil {
i.autocompleteList.SetChangedFunc(nil)
i.autocompleteList.SetSelectedFunc(func(index int, text, secondaryText string, shortcut rune) {
text = stripTags(text)
if i.autocompleted != nil {
if i.autocompleted(text, index, AutocompletedClick) {
i.autocompleteList = nil
currentText = i.GetText()
}
return
}
i.SetText(text)
skipAutocomplete = true
i.autocompleteList = nil
})
if consumed, _ = i.autocompleteList.MouseHandler()(action, event, setFocus); consumed {
setFocus(i)
return
}
}
// Is mouse event within the input field?
x, y := event.Position()
if !i.InRect(x, y) {
return false, nil
}
// Forward mouse event to the text area.
consumed, capture = i.textArea.MouseHandler()(action, event, setFocus)
return
})
}
// PasteHandler returns the handler for this primitive.
func (i *InputField) PasteHandler() func(pastedText string, setFocus func(p Primitive)) {
return i.WrapPasteHandler(func(pastedText string, setFocus func(p Primitive)) {
// Input field may be disabled.
if i.textArea.GetDisabled() {
return
}
// The autocomplete drop down may be open.
i.autocompleteListMutex.Lock()
defer i.autocompleteListMutex.Unlock()
if i.autocompleteList != nil {
return
}
// We may not accept this text.
if i.accept != nil && !i.accept(i.textArea.getTextBeforeCursor()+pastedText+i.textArea.getTextAfterCursor(), 0) {
return
}
// Forward the pasted text to the text area.
i.textArea.PasteHandler()(pastedText, setFocus)
})
}

779
vendor/github.com/rivo/tview/list.go generated vendored Normal file
View File

@@ -0,0 +1,779 @@
package tview
import (
"fmt"
"strings"
"github.com/gdamore/tcell/v2"
)
// 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. List items can be
// shown as a single line or as two lines. They can be selected by pressing
// their assigned shortcut key, navigating to them and pressing Enter, or
// clicking on them with the mouse. The following key binds are available:
//
// - Down arrow / tab: Move down one item.
// - Up arrow / backtab: Move up one item.
// - Home: Move to the first item.
// - End: Move to the last item.
// - Page down: Move down one page.
// - Page up: Move up one page.
// - Enter / Space: Select the current item.
// - Right / left: Scroll horizontally. Only if the list is wider than the
// available space.
//
// By default, list item texts can contain style tags. Use
// [List.SetUseStyleTags] to disable this feature.
//
// See [List.SetChangedFunc] for a way to be notified when the user navigates
// to a list item. See [List.SetSelectedFunc] for a way to be notified when a
// list item was 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 style.
mainTextStyle tcell.Style
// The item secondary text style.
secondaryTextStyle tcell.Style
// The item shortcut text style.
shortcutStyle tcell.Style
// The style for selected items.
selectedStyle tcell.Style
// 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 style tags can be used in the main text.
mainStyleTags bool
// Whether or not style tags can be used in the secondary text.
secondaryStyleTags 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.
itemOffset int
// The number of cells skipped on the left side of an item text. Shortcuts
// are not affected.
horizontalOffset 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 list.
func NewList() *List {
return &List{
Box: NewBox(),
showSecondaryText: true,
wrapAround: true,
mainTextStyle: tcell.StyleDefault.Foreground(Styles.PrimaryTextColor).Background(Styles.PrimitiveBackgroundColor),
secondaryTextStyle: tcell.StyleDefault.Foreground(Styles.TertiaryTextColor).Background(Styles.PrimitiveBackgroundColor),
shortcutStyle: tcell.StyleDefault.Foreground(Styles.SecondaryTextColor).Background(Styles.PrimitiveBackgroundColor),
selectedStyle: tcell.StyleDefault.Foreground(Styles.PrimitiveBackgroundColor).Background(Styles.PrimaryTextColor),
mainStyleTags: true,
secondaryStyleTags: true,
}
}
// 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
}
// SetOffset sets the number of items to be skipped (vertically) as well as the
// number of cells skipped horizontally when the list is drawn. Note that one
// item corresponds to two rows when there are secondary texts. Shortcuts are
// always drawn.
//
// These values may change when the list is drawn to ensure the currently
// selected item is visible and item texts move out of view. Users can also
// modify these values by interacting with the list.
func (l *List) SetOffset(items, horizontal int) *List {
l.itemOffset = items
l.horizontalOffset = horizontal
return l
}
// GetOffset returns the number of items skipped while drawing, as well as the
// number of cells item text is moved to the left. See also SetOffset() for more
// information on these values.
func (l *List) GetOffset() (int, int) {
return l.itemOffset, l.horizontalOffset
}
// 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, unless no items are left.
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 == len(l.items) {
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.mainTextStyle = l.mainTextStyle.Foreground(color)
return l
}
// SetMainTextStyle sets the style of the items' main text. Note that the
// background color is ignored in order not to override the background color of
// the list itself.
func (l *List) SetMainTextStyle(style tcell.Style) *List {
l.mainTextStyle = style
return l
}
// SetSecondaryTextColor sets the color of the items' secondary text.
func (l *List) SetSecondaryTextColor(color tcell.Color) *List {
l.secondaryTextStyle = l.secondaryTextStyle.Foreground(color)
return l
}
// SetSecondaryTextStyle sets the style of the items' secondary text. Note that
// the background color is ignored in order not to override the background color
// of the list itself.
func (l *List) SetSecondaryTextStyle(style tcell.Style) *List {
l.secondaryTextStyle = style
return l
}
// SetShortcutColor sets the color of the items' shortcut.
func (l *List) SetShortcutColor(color tcell.Color) *List {
l.shortcutStyle = l.shortcutStyle.Foreground(color)
return l
}
// SetShortcutStyle sets the style of the items' shortcut. Note that the
// background color is ignored in order not to override the background color of
// the list itself.
func (l *List) SetShortcutStyle(style tcell.Style) *List {
l.shortcutStyle = style
return l
}
// SetSelectedTextColor sets the text color of selected items. Note that the
// color of main text characters that are different from the main text color
// (e.g. style tags) is maintained.
func (l *List) SetSelectedTextColor(color tcell.Color) *List {
l.selectedStyle = l.selectedStyle.Foreground(color)
return l
}
// SetSelectedBackgroundColor sets the background color of selected items.
func (l *List) SetSelectedBackgroundColor(color tcell.Color) *List {
l.selectedStyle = l.selectedStyle.Background(color)
return l
}
// SetSelectedStyle sets the style of the selected items. Note that the color of
// main text characters that are different from the main text color (e.g. color
// tags) is maintained.
func (l *List) SetSelectedStyle(style tcell.Style) *List {
l.selectedStyle = style
return l
}
// SetUseStyleTags sets a flag which determines whether style tags are used in
// the main and secondary texts. The default is true.
func (l *List) SetUseStyleTags(mainStyleTags, secondaryStyleTags bool) *List {
l.mainStyleTags = mainStyleTags
l.secondaryStyleTags = secondaryStyleTags
return l
}
// GetUseStyleTags returns whether style tags are used in the main and secondary
// texts.
func (l *List) GetUseStyleTags() (mainStyleTags, secondaryStyleTags bool) {
return l.mainStyleTags, l.secondaryStyleTags
}
// 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
}
// GetSelectedFunc returns the function set with [List.SetSelectedFunc] or nil
// if no such function was set.
func (l *List) GetSelectedFunc() func(int, string, string, rune) {
return l.selected
}
// 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 [List.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 [List.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 [List.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)
}
// GetItemSelectedFunc returns the function which is called when the user
// selects the item with the given index, if such a function was set. If no
// function was set, nil is returned. Panics if the index is out of range.
func (l *List) GetItemSelectedFunc(index int) func() {
return l.items[index].Selected
}
// 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 && (mainSearch != "" && mainContained || secondarySearch != "" && 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.DrawForSubclass(screen, l)
// Determine the dimensions.
x, y, width, height := l.GetInnerRect()
bottomLimit := y + height
_, totalHeight := screen.Size()
if bottomLimit > totalHeight {
bottomLimit = totalHeight
}
// Adjust offsets to keep the current item in view.
if height == 0 {
return
}
if l.currentItem < l.itemOffset {
l.itemOffset = l.currentItem
} else if l.showSecondaryText {
if 2*(l.currentItem-l.itemOffset) >= height-1 {
l.itemOffset = (2*l.currentItem + 3 - height) / 2
}
} else {
if l.currentItem-l.itemOffset >= height {
l.itemOffset = l.currentItem + 1 - height
}
}
if l.horizontalOffset < 0 {
l.horizontalOffset = 0
}
// Do we show any shortcuts?
var showShortcuts bool
for _, item := range l.items {
if item.Shortcut != 0 {
showShortcuts = true
x += 4
width -= 4
break
}
}
// Draw the list items.
var maxWidth int // The maximum printed item width.
for index, item := range l.items {
if index < l.itemOffset {
continue
}
if y >= bottomLimit {
break
}
// Shortcuts.
if showShortcuts && item.Shortcut != 0 {
printWithStyle(screen, fmt.Sprintf("(%s)", string(item.Shortcut)), x-5, y, 0, 4, AlignRight, l.shortcutStyle, false)
}
// Main text.
selected := index == l.currentItem && (!l.selectedFocusOnly || l.HasFocus())
style := l.mainTextStyle
if selected {
style = l.selectedStyle
}
mainText := item.MainText
if !l.mainStyleTags {
mainText = Escape(mainText)
}
_, _, printedWidth := printWithStyle(screen, mainText, x, y, l.horizontalOffset, width, AlignLeft, style, false)
if printedWidth > maxWidth {
maxWidth = printedWidth
}
// Draw until the end of the line if requested.
if selected && l.highlightFullLine {
for bx := printedWidth; bx < width; bx++ {
screen.SetContent(x+bx, y, ' ', nil, style)
}
}
y++
if y >= bottomLimit {
break
}
// Secondary text.
if l.showSecondaryText {
secondaryText := item.SecondaryText
if !l.secondaryStyleTags {
secondaryText = Escape(secondaryText)
}
_, _, printedWidth := printWithStyle(screen, secondaryText, x, y, l.horizontalOffset, width, AlignLeft, l.secondaryTextStyle, false)
if printedWidth > maxWidth {
maxWidth = printedWidth
}
y++
}
}
// We don't want the item text to get out of view. If the horizontal offset
// is too high, we reset it and redraw. (That should be about as efficient
// as calculating everything up front.)
if l.horizontalOffset > 0 && maxWidth < width {
l.horizontalOffset -= width - maxWidth
l.Draw(screen)
}
}
// 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:
l.currentItem++
case tcell.KeyBacktab, tcell.KeyUp:
l.currentItem--
case tcell.KeyRight:
l.horizontalOffset += 2 // We shift by 2 to account for two-cell characters.
case tcell.KeyLeft:
l.horizontalOffset -= 2
case tcell.KeyHome:
l.currentItem = 0
case tcell.KeyEnd:
l.currentItem = len(l.items) - 1
case tcell.KeyPgDn:
_, _, _, height := l.GetInnerRect()
l.currentItem += height
if l.currentItem >= len(l.items) {
l.currentItem = len(l.items) - 1
}
case tcell.KeyPgUp:
_, _, _, height := l.GetInnerRect()
l.currentItem -= height
if l.currentItem < 0 {
l.currentItem = 0
}
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) {
if 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.itemOffset
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 {
if l.changed != nil {
l.changed(index, item.MainText, item.SecondaryText, item.Shortcut)
}
}
l.currentItem = index
}
consumed = true
case MouseScrollUp:
if l.itemOffset > 0 {
l.itemOffset--
}
consumed = true
case MouseScrollDown:
lines := len(l.items) - l.itemOffset
if l.showSecondaryText {
lines *= 2
}
if _, _, _, height := l.GetInnerRect(); lines > height {
l.itemOffset++
}
consumed = true
case MouseScrollLeft:
l.horizontalOffset--
consumed = true
case MouseScrollRight:
l.horizontalOffset++
consumed = true
}
return
})
}

214
vendor/github.com/rivo/tview/modal.go generated vendored Normal file
View File

@@ -0,0 +1,214 @@
package tview
import (
"github.com/gdamore/tcell/v2"
)
// 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
// [Modal.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().SetBorder(true).SetBackgroundColor(Styles.ContrastBackgroundColor),
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.SetBackgroundColor(Styles.ContrastBackgroundColor).
SetBorderPadding(1, 1, 1, 1)
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
}
// SetButtonStyle sets the style of the buttons when they are not focused.
func (m *Modal) SetButtonStyle(style tcell.Style) *Modal {
m.form.SetButtonStyle(style)
return m
}
// SetButtonActivatedStyle sets the style of the buttons when they are focused.
func (m *Modal) SetButtonActivatedStyle(style tcell.Style) *Modal {
m.form.SetButtonActivatedStyle(style)
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 empty 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 but style tag states will not transfer to following lines. 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.text) + 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.Box.DrawForSubclass(screen, m)
x, y, width, height = m.GetInnerRect()
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 == MouseLeftDown && m.InRect(event.Position()) {
setFocus(m)
consumed = true
}
return
})
}
// InputHandler returns the handler for this primitive.
func (m *Modal) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) {
return m.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p Primitive)) {
if m.frame.HasFocus() {
if handler := m.frame.InputHandler(); handler != nil {
handler(event, setFocus)
return
}
}
})
}

354
vendor/github.com/rivo/tview/pages.go generated vendored Normal file
View File

@@ -0,0 +1,354 @@
package tview
import (
"github.com/gdamore/tcell/v2"
)
// 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 laid out on top of each other,
// overlapping or not. It is 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(),
}
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)
}
// GetPageNames returns all page names ordered from front to back,
// optionally limited to visible pages.
func (p *Pages) GetPageNames(visibleOnly bool) []string {
var names []string
for index := len(p.pages) - 1; index >= 0; index-- {
if !visibleOnly || p.pages[index].Visible {
names = append(names, p.pages[index].Name)
}
}
return names
}
// 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 you should 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
}
// GetPage returns the page with the given name. If no such page exists, nil is
// returned.
func (p *Pages) GetPage(name string) Primitive {
for _, page := range p.pages {
if page.Name == name {
return page.Item
}
}
return nil
}
// HasFocus returns whether or not this primitive has focus.
func (p *Pages) HasFocus() bool {
for _, page := range p.pages {
if page.Item.HasFocus() {
return true
}
}
return p.Box.HasFocus()
}
// 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)
} else {
p.Box.Focus(delegate)
}
}
// Draw draws this primitive onto the screen.
func (p *Pages) Draw(screen tcell.Screen) {
p.Box.DrawForSubclass(screen, p)
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
})
}
// InputHandler returns the handler for this primitive.
func (p *Pages) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) {
return p.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p Primitive)) {
for _, page := range p.pages {
if page.Item.HasFocus() {
if handler := page.Item.InputHandler(); handler != nil {
handler(event, setFocus)
return
}
}
}
})
}
// PasteHandler returns the handler for this primitive.
func (p *Pages) PasteHandler() func(pastedText string, setFocus func(p Primitive)) {
return p.WrapPasteHandler(func(pastedText string, setFocus func(p Primitive)) {
for _, page := range p.pages {
if page.Item.HasFocus() {
if handler := page.Item.PasteHandler(); handler != nil {
handler(pastedText, setFocus)
return
}
}
}
})
}

69
vendor/github.com/rivo/tview/primitive.go generated vendored Normal file
View File

@@ -0,0 +1,69 @@
package tview
import "github.com/gdamore/tcell/v2"
// 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))
// HasFocus determines if the primitive has focus. This function must return
// true also if one of this primitive's child elements has focus.
HasFocus() bool
// Blur is called by the application when the primitive loses focus.
Blur()
// 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)
// PasteHandler returns a handler which receives pasted text.
// It is called by the Application class.
//
// A value of nil may also be returned to stop the downward propagation of
// paste events.
//
// The Box class may provide functionality to intercept paste events in the
// future. If you subclass from Box, it is recommended that you wrap your
// handler using Box.WrapPasteHandler() so you inherit that functionality.
PasteHandler() func(text string, setFocus func(p Primitive))
}

328
vendor/github.com/rivo/tview/semigraphics.go generated vendored Normal file
View File

@@ -0,0 +1,328 @@
package tview
import "github.com/gdamore/tcell/v2"
// 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 Punctuation 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' // ┌
BoxDrawingsDownLightAndRightHeavy rune = '\u250d' // ┍
BoxDrawingsDownHeavyAndRightLight rune = '\u250e' // ┎
BoxDrawingsHeavyDownAndRight rune = '\u250f' // ┏
BoxDrawingsLightDownAndLeft rune = '\u2510' // ┐
BoxDrawingsDownLightAndLeftHeavy 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' // ┞
BoxDrawingsDownHeavyAndRightUpLight rune = '\u251f' // ┟
BoxDrawingsVerticalHeavyAndRightLight rune = '\u2520' // ┠
BoxDrawingsDownLightAndRightUpHeavy 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' // ╛
BoxDrawingsUpDoubleAndLeftSingle 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' // ╿
// Block Elements.
BlockUpperHalfBlock rune = '\u2580' // ▀
BlockLowerOneEighthBlock rune = '\u2581' // ▁
BlockLowerOneQuarterBlock rune = '\u2582' // ▂
BlockLowerThreeEighthsBlock rune = '\u2583' // ▃
BlockLowerHalfBlock rune = '\u2584' // ▄
BlockLowerFiveEighthsBlock rune = '\u2585' // ▅
BlockLowerThreeQuartersBlock rune = '\u2586' // ▆
BlockLowerSevenEighthsBlock rune = '\u2587' // ▇
BlockFullBlock rune = '\u2588' // █
BlockLeftSevenEighthsBlock rune = '\u2589' // ▉
BlockLeftThreeQuartersBlock rune = '\u258A' // ▊
BlockLeftFiveEighthsBlock rune = '\u258B' // ▋
BlockLeftHalfBlock rune = '\u258C' // ▌
BlockLeftThreeEighthsBlock rune = '\u258D' // ▍
BlockLeftOneQuarterBlock rune = '\u258E' // ▎
BlockLeftOneEighthBlock rune = '\u258F' // ▏
BlockRightHalfBlock rune = '\u2590' // ▐
BlockLightShade rune = '\u2591' // ░
BlockMediumShade rune = '\u2592' // ▒
BlockDarkShade rune = '\u2593' // ▓
BlockUpperOneEighthBlock rune = '\u2594' // ▔
BlockRightOneEighthBlock rune = '\u2595' // ▕
BlockQuadrantLowerLeft rune = '\u2596' // ▖
BlockQuadrantLowerRight rune = '\u2597' // ▗
BlockQuadrantUpperLeft rune = '\u2598' // ▘
BlockQuadrantUpperLeftAndLowerLeftAndLowerRight rune = '\u2599' // ▙
BlockQuadrantUpperLeftAndLowerRight rune = '\u259A' // ▚
BlockQuadrantUpperLeftAndUpperRightAndLowerLeft rune = '\u259B' // ▛
BlockQuadrantUpperLeftAndUpperRightAndLowerRight rune = '\u259C' // ▜
BlockQuadrantUpperRight rune = '\u259D' // ▝
BlockQuadrantUpperRightAndLowerLeft rune = '\u259E' // ▞
BlockQuadrantUpperRightAndLowerLeftAndLowerRight rune = '\u259F' // ▟
)
// 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 style, joining it with any existing semigraphics
// rune.At this point, only regular single line borders are supported.
func PrintJoinedSemigraphics(screen tcell.Screen, x, y int, ch rune, style tcell.Style) {
previous, _, _, _ := screen.GetContent(x, y)
// 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)
}

634
vendor/github.com/rivo/tview/strings.go generated vendored Normal file
View File

@@ -0,0 +1,634 @@
package tview
import (
"math/rand"
"regexp"
"strconv"
"strings"
"unicode/utf8"
"github.com/gdamore/tcell/v2"
"github.com/rivo/uniseg"
)
// escapedTagPattern matches an escaped tag, e.g. "[red[]", at the beginning of
// a string.
var escapedTagPattern = regexp.MustCompile(`^\[[^\[\]]+\[+\]`)
// stepOptions is a bit field of options for [step]. A value of 0 results in
// [step] having the same behavior as uniseg.Step, i.e. no tview-related parsing
// is performed.
type stepOptions int
// Bit fields for [stepOptions].
const (
stepOptionsNone stepOptions = 0
stepOptionsStyle stepOptions = 1 << iota // Parse style tags.
stepOptionsRegion // Parse region tags.
)
// stepState represents the current state of the parser implemented in [step].
type stepState struct {
unisegState int // The state of the uniseg parser.
boundaries int // Information about boundaries, as returned by uniseg.Step.
style tcell.Style // The current style.
region string // The current region.
escapedTagState int // States for parsing escaped tags (defined in [step]).
grossLength int // The length of the cluster, including any tags not returned.
// The styles for the initial call to [step].
initialForeground tcell.Color
initialBackground tcell.Color
initialAttributes tcell.AttrMask
}
// IsWordBoundary returns true if the boundary between the returned grapheme
// cluster and the one following it is a word boundary.
func (s *stepState) IsWordBoundary() bool {
return s.boundaries&uniseg.MaskWord != 0
}
// IsSentenceBoundary returns true if the boundary between the returned grapheme
// cluster and the one following it is a sentence boundary.
func (s *stepState) IsSentenceBoundary() bool {
return s.boundaries&uniseg.MaskSentence != 0
}
// LineBreak returns whether the string can be broken into the next line after
// the returned grapheme cluster. If optional is true, the line break is
// optional. If false, the line break is mandatory, e.g. after a newline
// character.
func (s *stepState) LineBreak() (lineBreak, optional bool) {
switch s.boundaries & uniseg.MaskLine {
case uniseg.LineCanBreak:
return true, true
case uniseg.LineMustBreak:
return true, false
}
return false, false // uniseg.LineDontBreak.
}
// Width returns the grapheme cluster's width in cells.
func (s *stepState) Width() int {
return s.boundaries >> uniseg.ShiftWidth
}
// GrossLength returns the grapheme cluster's length in bytes, including any
// tags that were parsed but not explicitly returned.
func (s *stepState) GrossLength() int {
return s.grossLength
}
// Style returns the style for the grapheme cluster.
func (s *stepState) Style() tcell.Style {
return s.style
}
// step uses uniseg.Step to iterate over the grapheme clusters of a string but
// (optionally) also parses the string for style or region tags.
//
// This function can be called consecutively to extract all grapheme clusters
// from str, without returning any contained (parsed) tags. The return values
// are the first grapheme cluster, the remaining string, and the new state. Pass
// the remaining string and the returned state to the next call. If the rest
// string is empty, parsing is complete. Call the returned state's methods for
// boundary and cluster width information.
//
// The returned cluster may be empty if the given string consists of only
// (parsed) tags. The boundary and width information will be meaningless in
// this case but the style will describe the style at the end of the string.
//
// Pass nil for state on the first call. This will assume an initial style with
// [Styles.PrimitiveBackgroundColor] as the background color and
// [Styles.PrimaryTextColor] as the text color, no current region. If you want
// to start with a different style or region, you can set the state accordingly
// but you must then set [state.unisegState] to -1.
//
// There is no need to call uniseg.HasTrailingLineBreakInString on the last
// non-empty cluster as this function will do this for you and adjust the
// returned boundaries accordingly.
func step(str string, state *stepState, opts stepOptions) (cluster, rest string, newState *stepState) {
// Set up initial state.
if state == nil {
state = &stepState{
unisegState: -1,
style: tcell.StyleDefault.Background(Styles.PrimitiveBackgroundColor).Foreground(Styles.PrimaryTextColor),
}
}
if state.unisegState < 0 {
state.initialForeground, state.initialBackground, state.initialAttributes = state.style.Decompose()
}
if len(str) == 0 {
newState = state
return
}
// Get a grapheme cluster.
preState := state.unisegState
cluster, rest, state.boundaries, state.unisegState = uniseg.StepString(str, preState)
state.grossLength = len(cluster)
if rest == "" {
if !uniseg.HasTrailingLineBreakInString(cluster) {
state.boundaries &^= uniseg.MaskLine
}
}
// Parse tags.
if opts != 0 {
const (
etNone int = iota
etStart
etChar
etClosing
)
// Finite state machine for escaped tags.
switch state.escapedTagState {
case etStart:
if cluster[0] == '[' || cluster[0] == ']' { // Invalid escaped tag.
state.escapedTagState = etNone
} else { // Other characters are allowed.
state.escapedTagState = etChar
}
case etChar:
if cluster[0] == ']' { // In theory, this should not happen.
state.escapedTagState = etNone
} else if cluster[0] == '[' { // Starting closing sequence.
// Swallow the first one.
cluster, rest, state.boundaries, state.unisegState = uniseg.StepString(rest, preState)
state.grossLength += len(cluster)
if cluster[0] == ']' {
state.escapedTagState = etNone
} else {
state.escapedTagState = etClosing
}
} // More characters. Remain in etChar.
case etClosing:
if cluster[0] != '[' {
state.escapedTagState = etNone
}
}
// Regular tags.
if state.escapedTagState == etNone {
if cluster[0] == '[' {
// We've already opened a tag. Parse it.
length, style, region := parseTag(str, state)
if length > 0 {
state.style = style
state.region = region
cluster, rest, state.boundaries, state.unisegState = uniseg.StepString(str[length:], preState)
state.grossLength = len(cluster) + length
if rest == "" {
if !uniseg.HasTrailingLineBreakInString(cluster) {
state.boundaries &^= uniseg.MaskLine
}
}
}
// Is this an escaped tag?
if escapedTagPattern.MatchString(str[length:]) {
state.escapedTagState = etStart
}
}
if len(rest) > 0 && rest[0] == '[' {
// A tag might follow the cluster. If so, we need to fix the state
// for the boundaries to be correct.
if length, _, _ := parseTag(rest, state); length > 0 {
if len(rest) > length {
_, l := utf8.DecodeRuneInString(rest[length:])
cluster += rest[length : length+l]
}
var taglessRest string
cluster, taglessRest, state.boundaries, state.unisegState = uniseg.StepString(cluster, preState)
if taglessRest == "" {
if !uniseg.HasTrailingLineBreakInString(cluster) {
state.boundaries &^= uniseg.MaskLine
}
}
}
}
}
}
newState = state
return
}
// parseTag parses str for consecutive style and/or region tags, assuming that
// str starts with the opening bracket for the first tag. It returns the string
// length of all valid tags (0 if the first tag is not valid) and the updated
// style and region for valid tags (based on the provided state).
func parseTag(str string, state *stepState) (length int, style tcell.Style, region string) {
// Automata states for parsing tags.
const (
tagStateNone = iota
tagStateDoneTag
tagStateStart
tagStateRegionStart
tagStateEndForeground
tagStateStartBackground
tagStateNumericForeground
tagStateNameForeground
tagStateEndBackground
tagStateStartAttributes
tagStateNumericBackground
tagStateNameBackground
tagStateAttributes
tagStateRegionEnd
tagStateRegionName
tagStateEndAttributes
tagStateStartURL
tagStateEndURL
tagStateURL
)
// Helper function which checks if the given byte is one of a list of
// characters, including letters and digits.
isOneOf := func(b byte, chars string) bool {
if b >= 'a' && b <= 'z' || b >= 'A' && b <= 'Z' || b >= '0' && b <= '9' {
return true
}
return strings.IndexByte(chars, b) >= 0
}
// Attribute map.
attrs := map[byte]tcell.AttrMask{
'B': tcell.AttrBold,
'I': tcell.AttrItalic,
'L': tcell.AttrBlink,
'D': tcell.AttrDim,
'S': tcell.AttrStrikeThrough,
'R': tcell.AttrReverse,
}
var (
tagState, tagLength int
tempStr strings.Builder
)
tStyle := state.style
tRegion := state.region
// Process state transitions.
for len(str) > 0 {
ch := str[0]
str = str[1:]
tagLength++
// Transition.
switch tagState {
case tagStateNone:
if ch == '[' { // Start of a tag.
tagState = tagStateStart
} else { // Not a tag. We're done.
return
}
case tagStateStart:
switch {
case ch == '"': // Start of a region tag.
tempStr.Reset()
tagState = tagStateRegionStart
case !isOneOf(ch, "#:-"): // Invalid style tag.
return
case ch == '-': // Reset foreground color.
tStyle = tStyle.Foreground(state.initialForeground)
tagState = tagStateEndForeground
case ch == ':': // No foreground color.
tagState = tagStateStartBackground
default:
tempStr.Reset()
tempStr.WriteByte(ch)
if ch == '#' { // Numeric foreground color.
tagState = tagStateNumericForeground
} else { // Letters or numbers.
tagState = tagStateNameForeground
}
}
case tagStateEndForeground:
switch ch {
case ']': // End of tag.
tagState = tagStateDoneTag
case ':':
tagState = tagStateStartBackground
default: // Invalid tag.
return
}
case tagStateNumericForeground:
if ch == ']' || ch == ':' {
if tempStr.Len() != 7 { // Must be #rrggbb.
return
}
tStyle = tStyle.Foreground(tcell.GetColor(tempStr.String()))
}
switch {
case ch == ']': // End of tag.
tagState = tagStateDoneTag
case ch == ':': // Start of background color.
tagState = tagStateStartBackground
case strings.IndexByte("0123456789abcdefABCDEF", ch) >= 0: // Hex digit.
tempStr.WriteByte(ch)
tagState = tagStateNumericForeground
default: // Invalid tag.
return
}
case tagStateNameForeground:
if ch == ']' || ch == ':' {
name := tempStr.String()
if name[0] >= '0' && name[0] <= '9' { // Must not start with a digit.
return
}
tStyle = tStyle.Foreground(tcell.ColorNames[name])
}
switch {
case !isOneOf(ch, "]:"): // Invalid tag.
return
case ch == ']': // End of tag.
tagState = tagStateDoneTag
case ch == ':': // Start of background color.
tagState = tagStateStartBackground
default: // Letters or numbers.
tempStr.WriteByte(ch)
}
case tagStateStartBackground:
switch {
case !isOneOf(ch, "#:-]"): // Invalid style tag.
return
case ch == ']': // End of tag.
tagState = tagStateDoneTag
case ch == '-': // Reset background color.
tStyle = tStyle.Background(state.initialBackground)
tagState = tagStateEndBackground
case ch == ':': // No background color.
tagState = tagStateStartAttributes
default:
tempStr.Reset()
tempStr.WriteByte(ch)
if ch == '#' { // Numeric background color.
tagState = tagStateNumericBackground
} else { // Letters or numbers.
tagState = tagStateNameBackground
}
}
case tagStateEndBackground:
switch ch {
case ']': // End of tag.
tagState = tagStateDoneTag
case ':': // Start of attributes.
tagState = tagStateStartAttributes
default: // Invalid tag.
return
}
case tagStateNumericBackground:
if ch == ']' || ch == ':' {
if tempStr.Len() != 7 { // Must be #rrggbb.
return
}
tStyle = tStyle.Background(tcell.GetColor(tempStr.String()))
}
if ch == ']' { // End of tag.
tagState = tagStateDoneTag
} else if ch == ':' { // Start of attributes.
tagState = tagStateStartAttributes
} else if strings.IndexByte("0123456789abcdefABCDEF", ch) >= 0 { // Hex digit.
tempStr.WriteByte(ch)
tagState = tagStateNumericBackground
} else { // Invalid tag.
return
}
case tagStateNameBackground:
if ch == ']' || ch == ':' {
name := tempStr.String()
if name[0] >= '0' && name[0] <= '9' { // Must not start with a digit.
return
}
tStyle = tStyle.Background(tcell.ColorNames[name])
}
switch {
case !isOneOf(ch, "]:"): // Invalid tag.
return
case ch == ']': // End of tag.
tagState = tagStateDoneTag
case ch == ':': // Start of background color.
tagState = tagStateStartAttributes
default: // Letters or numbers.
tempStr.WriteByte(ch)
}
case tagStateStartAttributes:
switch {
case ch == ']': // End of tag.
tagState = tagStateDoneTag
case ch == '-': // Reset attributes.
tStyle = tStyle.Attributes(state.initialAttributes)
tagState = tagStateEndAttributes
case ch == ':': // Start of URL.
tagState = tagStateStartURL
case strings.IndexByte("buildsrBUILDSR", ch) >= 0: // Attribute tag.
tempStr.Reset()
tempStr.WriteByte(ch)
tagState = tagStateAttributes
default: // Invalid tag.
return
}
case tagStateAttributes:
if ch == ']' || ch == ':' {
flags := tempStr.String()
_, _, a := tStyle.Decompose()
for index := 0; index < len(flags); index++ {
ch := flags[index]
switch {
case ch == 'u':
tStyle = tStyle.Underline(true)
case ch == 'U':
tStyle = tStyle.Underline(false)
case ch >= 'a' && ch <= 'z':
a |= attrs[ch-('a'-'A')]
default:
a &^= attrs[ch]
}
}
tStyle = tStyle.Attributes(a)
}
switch {
case ch == ']': // End of tag.
tagState = tagStateDoneTag
case ch == ':': // Start of URL.
tagState = tagStateStartURL
case strings.IndexByte("buildsrBUILDSR", ch) >= 0: // Attribute tag.
tempStr.WriteByte(ch)
default: // Invalid tag.
return
}
case tagStateEndAttributes:
switch ch {
case ']': // End of tag.
tagState = tagStateDoneTag
case ':': // Start of URL.
tagState = tagStateStartURL
default: // Invalid tag.
return
}
case tagStateStartURL:
switch ch {
case ']': // End of tag.
tagState = tagStateDoneTag
case '-': // Reset URL.
tStyle = tStyle.Url("").UrlId("")
tagState = tagStateEndURL
default: // URL character.
tempStr.Reset()
tempStr.WriteByte(ch)
tStyle = tStyle.UrlId(strconv.Itoa(int(rand.Uint32()))) // Generate a unique ID for this URL.
tagState = tagStateURL
}
case tagStateEndURL:
if ch == ']' { // End of tag.
tagState = tagStateDoneTag
} else { // Invalid tag.
return
}
case tagStateURL:
if ch == ']' { // End of tag.
tStyle = tStyle.Url(tempStr.String())
tagState = tagStateDoneTag
} else { // URL character.
tempStr.WriteByte(ch)
}
case tagStateRegionStart:
switch {
case ch == '"': // End of region tag.
tagState = tagStateRegionEnd
case isOneOf(ch, "_,;: -."): // Region name.
tempStr.WriteByte(ch)
tagState = tagStateRegionName
default: // Invalid tag.
return
}
case tagStateRegionEnd:
if ch == ']' { // End of tag.
tRegion = tempStr.String()
tagState = tagStateDoneTag
} else { // Invalid tag.
return
}
case tagStateRegionName:
switch {
case ch == '"': // End of region tag.
tagState = tagStateRegionEnd
case isOneOf(ch, "_,;: -."): // Region name.
tempStr.WriteByte(ch)
default: // Invalid tag.
return
}
}
// The last transition led to a tag end. Make the tag permanent.
if tagState == tagStateDoneTag {
length, style, region = tagLength, tStyle, tRegion
tagState = tagStateNone // Reset state.
}
}
return
}
// TaggedStringWidth returns the width of the given string needed to print it on
// screen. The text may contain style tags which are not counted.
func TaggedStringWidth(text string) (width int) {
var state *stepState
for len(text) > 0 {
_, text, state = step(text, state, stepOptionsStyle)
width += state.Width()
}
return
}
// WordWrap splits a text such that each resulting line does not exceed the
// given screen width. Split points are determined using the algorithm described
// in [Unicode Standard Annex #14].
//
// This function considers style tags to have no width.
//
// [Unicode Standard Annex #14]: https://www.unicode.org/reports/tr14/
func WordWrap(text string, width int) (lines []string) {
if width <= 0 {
return
}
var (
state *stepState
lineWidth, lineLength, lastOption, lastOptionWidth int
)
str := text
for len(str) > 0 {
// Parse the next character.
_, str, state = step(str, state, stepOptionsStyle)
cWidth := state.Width()
// Would it exceed the line width?
if lineWidth+cWidth > width {
if lastOptionWidth == 0 {
// No split point so far. Just split at the current position.
lines = append(lines, text[:lineLength])
text = text[lineLength:]
lineWidth, lineLength, lastOption, lastOptionWidth = 0, 0, 0, 0
} else {
// Split at the last split point.
lines = append(lines, text[:lastOption])
text = text[lastOption:]
lineWidth -= lastOptionWidth
lineLength -= lastOption
lastOption, lastOptionWidth = 0, 0
}
}
// Move ahead.
lineWidth += cWidth
lineLength += state.GrossLength()
// Check for split points.
if lineBreak, optional := state.LineBreak(); lineBreak {
if optional {
// Remember this split point.
lastOption = lineLength
lastOptionWidth = lineWidth
} else {
// We must split here.
lines = append(lines, strings.TrimRight(text[:lineLength], "\n\r"))
text = text[lineLength:]
lineWidth, lineLength, lastOption, lastOptionWidth = 0, 0, 0, 0
}
}
}
lines = append(lines, text)
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 escapePattern.ReplaceAllString(text, "$1[]")
}
// Unescape unescapes text previously escaped with [Escape].
func Unescape(text string) string {
return unescapePattern.ReplaceAllString(text, "$1]")
}
// stripTags strips style tags from the given string. (Region tags are not
// stripped.)
func stripTags(text string) string {
var (
str strings.Builder
state *stepState
)
for len(text) > 0 {
var c string
c, text, state = step(text, state, stepOptionsStyle)
str.WriteString(c)
}
return str.String()
}

35
vendor/github.com/rivo/tview/styles.go generated vendored Normal file
View File

@@ -0,0 +1,35 @@
package tview
import "github.com/gdamore/tcell/v2"
// 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.ColorNavy,
}

1725
vendor/github.com/rivo/tview/table.go generated vendored Normal file

File diff suppressed because it is too large Load Diff

2447
vendor/github.com/rivo/tview/textarea.go generated vendored Normal file

File diff suppressed because it is too large Load Diff

1446
vendor/github.com/rivo/tview/textview.go generated vendored Normal file

File diff suppressed because it is too large Load Diff

910
vendor/github.com/rivo/tview/treeview.go generated vendored Normal file
View File

@@ -0,0 +1,910 @@
package tview
import (
"github.com/gdamore/tcell/v2"
)
// Tree navigation events.
const (
treeNone int = iota
treeHome
treeEnd
treeMove
treeParent
treeChild
treeScroll // Move without changing the selection, even when off screen.
)
// 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 style.
textStyle tcell.Style
// The style of selected text.
selectedTextStyle tcell.Style
// 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()
// The hierarchy level (0 for the root, 1 for its children, and so on). This
// is only up to date immediately after a call to process() (e.g. via
// Draw()).
level int
// Temporary member variables.
parent *TreeNode // The parent node (nil for the root).
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,
textStyle: tcell.StyleDefault.Foreground(Styles.PrimaryTextColor).Background(Styles.PrimitiveBackgroundColor),
selectedTextStyle: tcell.StyleDefault.Foreground(Styles.PrimitiveBackgroundColor).Background(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
}
// RemoveChild removes a child node from this node. If the child node cannot be
// found, nothing happens.
func (n *TreeNode) RemoveChild(node *TreeNode) *TreeNode {
for index, child := range n.children {
if child == node {
n.children = append(n.children[:index], n.children[index+1:]...)
break
}
}
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 {
node.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 text color.
func (n *TreeNode) GetColor() tcell.Color {
color, _, _ := n.textStyle.Decompose()
return color
}
// SetColor sets the node's text color. For compatibility reasons, this also
// sets the background color of the selected text style. For more control over
// styles, use [TreeNode.SetTextStyle] and [TreeNode.SetSelectedTextStyle].
func (n *TreeNode) SetColor(color tcell.Color) *TreeNode {
n.textStyle = n.textStyle.Foreground(color)
n.selectedTextStyle = n.selectedTextStyle.Background(color)
return n
}
// SetTextStyle sets the text style for this node.
func (n *TreeNode) SetTextStyle(style tcell.Style) *TreeNode {
n.textStyle = style
return n
}
// GetTextStyle returns the text style for this node.
func (n *TreeNode) GetTextStyle() tcell.Style {
return n.textStyle
}
// SetSelectedTextStyle sets the text style for this node when it is selected.
func (n *TreeNode) SetSelectedTextStyle(style tcell.Style) *TreeNode {
n.selectedTextStyle = style
return n
}
// GetSelectedTextStyle returns the text style for this node when it is
// selected.
func (n *TreeNode) GetSelectedTextStyle() tcell.Style {
return n.selectedTextStyle
}
// 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
}
// GetLevel returns the node's level within the hierarchy, where 0 corresponds
// to the root node, 1 corresponds to its children, and so on. This is only
// guaranteed to be up to date immediately after the tree that contains this
// node is drawn.
func (n *TreeNode) GetLevel() int {
return n.level
}
// 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.
// - J: Move (the selection) up one level (if that node is selectable).
// - K: Move (the selection) to the last node one level down (if any).
// - 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 last note that was selected or nil of there is no such node.
lastNode *TreeNode
// The movement to be performed during the call to Draw(), one of the
// constants defined above.
movement int
// The number of nodes to move down or up, when movement is treeMove,
// excluding non-selectable nodes for selection movement, including them for
// scrolling.
step 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
// Temporarily set to true while we know that the tree has not changed and
// therefore does not need to be reprocessed.
stableNodes bool
}
// 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 because the actual node
// that will be selected is not known until the tree is drawn. Triggering the
// "changed" callback is thus deferred until the next call to [TreeView.Draw].
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
}
// GetPath returns all nodes located on the path from the root to the given
// node, including the root and the node itself. If there is no root node, nil
// is returned. If there are multiple paths to the node, a random one is chosen
// and returned.
func (t *TreeView) GetPath(node *TreeNode) []*TreeNode {
if t.root == nil {
return nil
}
var f func(current *TreeNode, path []*TreeNode) []*TreeNode
f = func(current *TreeNode, path []*TreeNode) []*TreeNode {
if current == node {
return path
}
for _, child := range current.children {
newPath := make([]*TreeNode, len(path), len(path)+1)
copy(newPath, path)
if p := f(child, append(newPath, child)); p != nil {
return p
}
}
return nil
}
return f(t.root, []*TreeNode{t.root})
}
// 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 "})
//
// Deeper levels will cycle through the prefixes.
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 currently selected
// node changes, for example 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
}
// GetSelectedFunc returns the function set with [TreeView.SetSelectedFunc]
// or nil if no such function has been set.
func (t *TreeView) GetSelectedFunc() func(node *TreeNode) {
return t.selected
}
// 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)
}
// Move moves the selection (if a node is currently selected) or scrolls the
// tree view (if there is no selection), by the given offset (positive values to
// move/scroll down, negative values to move/scroll up). For selection changes,
// the offset refers to the number selectable, visible nodes. For scrolling, the
// offset refers to the number of visible nodes.
//
// If the offset is 0, nothing happens.
func (t *TreeView) Move(offset int) *TreeView {
if offset == 0 {
return t
}
t.movement = treeMove
t.step = offset
t.process(false)
return t
}
// process builds the visible tree, populates the "nodes" slice, and processes
// pending movement actions. Set "drawingAfter" to true if you know that
// [TreeView.Draw] will be called immediately after this function (to avoid
// having [TreeView.Draw] call it again).
func (t *TreeView) process(drawingAfter bool) {
t.stableNodes = drawingAfter
_, _, _, height := t.GetInnerRect()
// Determine visible nodes and their placement.
t.nodes = nil
if t.root == nil {
return
}
parentSelectedIndex, selectedIndex, topLevelGraphicsX := -1, -1, -1
var graphicsOffset, maxTextX int
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)
// Also find parent node.
for index := len(t.nodes) - 1; index >= 0; index-- {
if t.nodes[index] == parent && t.nodes[index].selectable {
parentSelectedIndex = index
break
}
}
}
// 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.
switch t.movement {
case treeMove:
for t.step < 0 { // Going up.
index := selectedIndex
for index > 0 {
index--
if t.nodes[index].selectable {
selectedIndex = index
break
}
}
t.step++
}
for t.step > 0 { // Going down.
index := selectedIndex
for index < len(t.nodes)-1 {
index++
if t.nodes[index].selectable {
selectedIndex = index
break
}
}
t.step--
}
case treeParent:
if parentSelectedIndex >= 0 {
selectedIndex = parentSelectedIndex
}
case treeChild:
index := selectedIndex
for index < len(t.nodes)-1 {
index++
if t.nodes[index].selectable && t.nodes[index].parent == t.nodes[selectedIndex] {
selectedIndex = index
}
}
}
t.currentNode = t.nodes[selectedIndex]
// Move selection into viewport.
if t.movement != treeScroll {
if selectedIndex-t.offsetY >= height {
t.offsetY = selectedIndex - height + 1
}
if selectedIndex < t.offsetY {
t.offsetY = selectedIndex
}
if t.movement != treeHome && t.movement != treeEnd {
// treeScroll, treeHome, and treeEnd are handled by Draw().
t.movement = treeNone
t.step = 0
}
}
} 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
}
}
// Trigger "changed" callback.
if t.changed != nil && t.currentNode != nil && t.currentNode != t.lastNode {
t.changed(t.currentNode)
}
t.lastNode = t.currentNode
}
// Draw draws this primitive onto the screen.
func (t *TreeView) Draw(screen tcell.Screen) {
t.Box.DrawForSubclass(screen, t)
if t.root == nil {
return
}
_, totalHeight := screen.Size()
if !t.stableNodes {
t.process(false)
} else {
t.stableNodes = false
}
// Scroll the tree, t.movement is treeNone after process() when there is a
// selection, except for treeScroll, treeHome, and treeEnd.
x, y, width, height := t.GetInnerRect()
switch t.movement {
case treeMove, treeScroll:
t.offsetY += t.step
case treeHome:
t.offsetY = 0
case treeEnd:
t.offsetY = len(t.nodes)
}
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, lineStyle)
}
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, lineStyle)
}
// 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 = printWithStyle(screen, t.prefixes[(node.level-t.topLevel)%len(t.prefixes)], x+node.textX, posY, 0, width-node.textX, AlignLeft, node.textStyle, true)
}
// Text.
if node.textX+prefixWidth < width {
style := node.textStyle
if node == t.currentNode {
style = node.selectedTextStyle
}
printWithStyle(screen, node.text, x+node.textX+prefixWidth, posY, 0, width-node.textX-prefixWidth, AlignLeft, style, false)
}
}
// 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 = treeMove
t.step = 1
case tcell.KeyUp, tcell.KeyLeft:
t.movement = treeMove
t.step = -1
case tcell.KeyHome:
t.movement = treeHome
case tcell.KeyEnd:
t.movement = treeEnd
case tcell.KeyPgDn, tcell.KeyCtrlF:
_, _, _, height := t.GetInnerRect()
t.movement = treeMove
t.step = height
case tcell.KeyPgUp, tcell.KeyCtrlB:
_, _, _, height := t.GetInnerRect()
t.movement = treeMove
t.step = -height
case tcell.KeyRune:
switch event.Rune() {
case 'g':
t.movement = treeHome
case 'G':
t.movement = treeEnd
case 'j':
t.movement = treeMove
t.step = 1
case 'J':
t.movement = treeChild
case 'k':
t.movement = treeMove
t.step = -1
case 'K':
t.movement = treeParent
case ' ':
selectNode()
}
case tcell.KeyEnter:
selectNode()
}
t.process(true)
})
}
// 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 MouseLeftDown:
setFocus(t)
consumed = true
case MouseLeftClick:
_, rectY, _, _ := t.GetInnerRect()
y += t.offsetY - 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 = treeScroll
t.step = -1
consumed = true
case MouseScrollDown:
t.movement = treeScroll
t.step = 1
consumed = true
}
return
})
}

BIN
vendor/github.com/rivo/tview/tview.gif generated vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 951 KiB

158
vendor/github.com/rivo/tview/util.go generated vendored Normal file
View File

@@ -0,0 +1,158 @@
package tview
import (
"math"
"os"
"regexp"
"github.com/gdamore/tcell/v2"
)
// Text alignment within a box. Also used to align images.
const (
AlignLeft = iota
AlignCenter
AlignRight
AlignTop = 0
AlignBottom = 2
)
var (
// Regular expression used to escape style/region tags.
escapePattern = regexp.MustCompile(`(\[[a-zA-Z0-9_,;: \-\."#]+\[*)\]`)
// Regular expression used to unescape escaped style/region tags.
unescapePattern = regexp.MustCompile(`(\[[a-zA-Z0-9_,;: \-\."#]+\[*)\[\]`)
// The number of colors available in the terminal.
availableColors = 256
)
// Package initialization.
func init() {
// Determine the number of colors available in the terminal.
info, err := tcell.LookupTerminfo(os.Getenv("TERM"))
if err == nil {
availableColors = info.Colors
}
}
// 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 style tag.
// See the package description for details.
//
// Returns the number of actual bytes of the text printed (including style 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) {
start, end, width := printWithStyle(screen, text, x, y, 0, maxWidth, align, tcell.StyleDefault.Foreground(color), true)
return end - start, width
}
// printWithStyle works like [Print] but it takes a style instead of just a
// foreground color. The skipWidth parameter specifies the number of cells
// skipped at the beginning of the text. It returns the start index, end index
// (exclusively), and screen width of the text actually printed. If
// maintainBackground is "true", the existing screen background is not changed
// (i.e. the style's background color is ignored).
func printWithStyle(screen tcell.Screen, text string, x, y, skipWidth, maxWidth, align int, style tcell.Style, maintainBackground bool) (start, end, printedWidth int) {
totalWidth, totalHeight := screen.Size()
if maxWidth <= 0 || len(text) == 0 || y < 0 || y >= totalHeight {
return 0, 0, 0
}
// If we don't overwrite the background, we use the default color.
if maintainBackground {
style = style.Background(tcell.ColorDefault)
}
// Skip beginning and measure width.
var textWidth int
state := &stepState{
unisegState: -1,
style: style,
}
newState := *state
str := text
for len(str) > 0 {
_, str, state = step(str, state, stepOptionsStyle)
if skipWidth > 0 {
skipWidth -= state.Width()
text = str
newState = *state
start += state.GrossLength()
} else {
textWidth += state.Width()
}
}
state = &newState
// Reduce all alignments to AlignLeft.
if align == AlignRight {
// Chop off characters on the left until it fits.
for len(text) > 0 && textWidth > maxWidth {
_, text, state = step(text, state, stepOptionsStyle)
textWidth -= state.Width()
start += state.GrossLength()
}
x, maxWidth = x+maxWidth-textWidth, textWidth
} else if align == AlignCenter {
// Chop off characters on the left until it fits.
subtracted := (textWidth - maxWidth) / 2
for len(text) > 0 && subtracted > 0 {
_, text, state = step(text, state, stepOptionsStyle)
subtracted -= state.Width()
textWidth -= state.Width()
start += state.GrossLength()
}
if textWidth < maxWidth {
x, maxWidth = x+maxWidth/2-textWidth/2, textWidth
}
}
// Draw left-aligned text.
end = start
rightBorder := x + maxWidth
for len(text) > 0 && x < rightBorder && x < totalWidth {
var c string
c, text, state = step(text, state, stepOptionsStyle)
if c == "" {
break // We don't care about the style at the end.
}
width := state.Width()
if width > 0 {
finalStyle := state.Style()
if maintainBackground {
_, backgroundColor, _ := finalStyle.Decompose()
if backgroundColor == tcell.ColorDefault {
_, _, existingStyle, _ := screen.GetContent(x, y)
_, background, _ := existingStyle.Decompose()
finalStyle = finalStyle.Background(background)
}
}
for offset := width - 1; offset >= 0; offset-- {
// To avoid undesired effects, we populate all cells.
runes := []rune(c)
if offset == 0 {
screen.SetContent(x+offset, y, runes[0], runes[1:], finalStyle)
} else {
screen.SetContent(x+offset, y, ' ', nil, finalStyle)
}
}
}
x += width
end += state.GrossLength()
printedWidth += width
}
return
}
// 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)
}