aboutsummaryrefslogtreecommitdiff
path: root/vendor/maunium.net/go/tview
diff options
context:
space:
mode:
authorTulir Asokan <tulir@maunium.net>2018-11-14 00:00:35 +0200
committerTulir Asokan <tulir@maunium.net>2018-11-14 00:00:35 +0200
commitba387764ca1590625d349e74eb8a8a64d1849b67 (patch)
treebc8f02156a63eac99dcddaed38e45b7c312b40c0 /vendor/maunium.net/go/tview
parentcfb2cc057c32330be0ca0a68cfbd245cb2b8e31b (diff)
Fix things
Diffstat (limited to 'vendor/maunium.net/go/tview')
-rw-r--r--vendor/maunium.net/go/tview/LICENSE.txt2
-rw-r--r--vendor/maunium.net/go/tview/README.md11
-rw-r--r--vendor/maunium.net/go/tview/ansi.go (renamed from vendor/maunium.net/go/tview/ansii.go)64
-rw-r--r--vendor/maunium.net/go/tview/application.go376
-rw-r--r--vendor/maunium.net/go/tview/borders.go45
-rw-r--r--vendor/maunium.net/go/tview/box.go88
-rw-r--r--vendor/maunium.net/go/tview/checkbox.go6
-rw-r--r--vendor/maunium.net/go/tview/doc.go31
-rw-r--r--vendor/maunium.net/go/tview/dropdown.go5
-rw-r--r--vendor/maunium.net/go/tview/flex.go17
-rw-r--r--vendor/maunium.net/go/tview/form.go53
-rw-r--r--vendor/maunium.net/go/tview/grid.go16
-rw-r--r--vendor/maunium.net/go/tview/inputfield.go205
-rw-r--r--vendor/maunium.net/go/tview/list.go15
-rw-r--r--vendor/maunium.net/go/tview/modal.go15
-rw-r--r--vendor/maunium.net/go/tview/semigraphics.go296
-rw-r--r--vendor/maunium.net/go/tview/table.go119
-rw-r--r--vendor/maunium.net/go/tview/textview.go266
-rw-r--r--vendor/maunium.net/go/tview/treeview.go684
-rw-r--r--vendor/maunium.net/go/tview/util.go658
20 files changed, 2229 insertions, 743 deletions
diff --git a/vendor/maunium.net/go/tview/LICENSE.txt b/vendor/maunium.net/go/tview/LICENSE.txt
index 8aa2645..9d69430 100644
--- a/vendor/maunium.net/go/tview/LICENSE.txt
+++ b/vendor/maunium.net/go/tview/LICENSE.txt
@@ -1,6 +1,6 @@
MIT License
-Copyright (c) [year] [fullname]
+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
diff --git a/vendor/maunium.net/go/tview/README.md b/vendor/maunium.net/go/tview/README.md
index 3e5734e..7ce06a2 100644
--- a/vendor/maunium.net/go/tview/README.md
+++ b/vendor/maunium.net/go/tview/README.md
@@ -12,6 +12,7 @@ Among these components are:
- __Input forms__ (include __input/password fields__, __drop-down selections__, __checkboxes__, and __buttons__)
- Navigable multi-color __text views__
- Sophisticated navigable __table views__
+- Flexible __tree views__
- Selectable __lists__
- __Grid__, __Flexbox__ and __page layouts__
- Modal __message windows__
@@ -64,9 +65,17 @@ Add your issue here on GitHub. Feel free to get in touch if you have any questio
(There are no corresponding tags in the project. I only keep such a history in this README.)
+- v0.19 (2018-10-28)
+ - Added `QueueUpdate()` and `QueueEvent()` to `Application` to help with modifications to primitives from goroutines.
+- v0.18 (2018-10-18)
+ - `InputField` elements can now be navigated freely.
+- v0.17 (2018-06-20)
+ - Added `TreeView`.
+- v0.15 (2018-05-02)
+ - `Flex` and `Grid` don't clear their background per default, thus allowing for custom modals. See the [Wiki](https://github.com/rivo/tview/wiki/Modal) for an example.
- v0.14 (2018-04-13)
- Added an `Escape()` function which keep strings like color or region tags from being recognized as such.
- - Added `ANSIIWriter()` and `TranslateANSII()` which convert ANSII escape sequences to `tview` color tags.
+ - Added `ANSIWriter()` and `TranslateANSI()` which convert ANSI escape sequences to `tview` color tags.
- v0.13 (2018-04-01)
- Added background colors and text attributes to color tags.
- v0.12 (2018-03-13)
diff --git a/vendor/maunium.net/go/tview/ansii.go b/vendor/maunium.net/go/tview/ansi.go
index 0ce3d4a..4d14c28 100644
--- a/vendor/maunium.net/go/tview/ansii.go
+++ b/vendor/maunium.net/go/tview/ansi.go
@@ -8,44 +8,44 @@ import (
"strings"
)
-// The states of the ANSII escape code parser.
+// The states of the ANSI escape code parser.
const (
- ansiiText = iota
- ansiiEscape
- ansiiSubstring
- ansiiControlSequence
+ ansiText = iota
+ ansiEscape
+ ansiSubstring
+ ansiControlSequence
)
-// ansii is a io.Writer which translates ANSII escape codes into tview color
+// ansi is a io.Writer which translates ANSI escape codes into tview color
// tags.
-type ansii struct {
+type ansi struct {
io.Writer
// Reusable buffers.
buffer *bytes.Buffer // The entire output text of one Write().
csiParameter, csiIntermediate *bytes.Buffer // Partial CSI strings.
- // The current state of the parser. One of the ansii constants.
+ // The current state of the parser. One of the ansi constants.
state int
}
-// ANSIIWriter returns an io.Writer which translates any ANSII escape codes
+// ANSIWriter returns an io.Writer which translates any ANSI escape codes
// written to it into tview color tags. Other escape codes don't have an effect
// and are simply removed. The translated text is written to the provided
// writer.
-func ANSIIWriter(writer io.Writer) io.Writer {
- return &ansii{
+func ANSIWriter(writer io.Writer) io.Writer {
+ return &ansi{
Writer: writer,
buffer: new(bytes.Buffer),
csiParameter: new(bytes.Buffer),
csiIntermediate: new(bytes.Buffer),
- state: ansiiText,
+ state: ansiText,
}
}
-// Write parses the given text as a string of runes, translates ANSII escape
+// Write parses the given text as a string of runes, translates ANSI escape
// codes to color tags and writes them to the output writer.
-func (a *ansii) Write(text []byte) (int, error) {
+func (a *ansi) Write(text []byte) (int, error) {
defer func() {
a.buffer.Reset()
}()
@@ -54,23 +54,23 @@ func (a *ansii) Write(text []byte) (int, error) {
switch a.state {
// We just entered an escape sequence.
- case ansiiEscape:
+ case ansiEscape:
switch r {
case '[': // Control Sequence Introducer.
a.csiParameter.Reset()
a.csiIntermediate.Reset()
- a.state = ansiiControlSequence
+ a.state = ansiControlSequence
case 'c': // Reset.
fmt.Fprint(a.buffer, "[-:-:-]")
- a.state = ansiiText
+ a.state = ansiText
case 'P', ']', 'X', '^', '_': // Substrings and commands.
- a.state = ansiiSubstring
+ a.state = ansiSubstring
default: // Ignore.
- a.state = ansiiText
+ a.state = ansiText
}
// CSI Sequences.
- case ansiiControlSequence:
+ case ansiControlSequence:
switch {
case r >= 0x30 && r <= 0x3f: // Parameter bytes.
if _, err := a.csiParameter.WriteRune(r); err != nil {
@@ -166,16 +166,16 @@ func (a *ansii) Write(text []byte) (int, error) {
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)
+ 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)
+ 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)
+ color = fmt.Sprintf("#%02x%02x%02x", red, green, blue)
}
}
if len(color) > 0 {
@@ -194,22 +194,22 @@ func (a *ansii) Write(text []byte) (int, error) {
fmt.Fprintf(a.buffer, "[%s:%s%s]", foreground, background, attributes)
}
}
- a.state = ansiiText
+ a.state = ansiText
default: // Undefined byte.
- a.state = ansiiText // Abort CSI.
+ a.state = ansiText // Abort CSI.
}
// We just entered a substring/command sequence.
- case ansiiSubstring:
+ case ansiSubstring:
if r == 27 { // Most likely the end of the substring.
- a.state = ansiiEscape
+ a.state = ansiEscape
} // Ignore all other characters.
- // "ansiiText" and all others.
+ // "ansiText" and all others.
default:
if r == 27 {
// This is the start of an escape sequence.
- a.state = ansiiEscape
+ a.state = ansiEscape
} else {
// Just a regular rune. Send to buffer.
if _, err := a.buffer.WriteRune(r); err != nil {
@@ -227,11 +227,11 @@ func (a *ansii) Write(text []byte) (int, error) {
return len(text), nil
}
-// TranslateANSII replaces ANSII escape sequences found in the provided string
+// TranslateANSI replaces ANSI escape sequences found in the provided string
// with tview's color tags and returns the resulting string.
-func TranslateANSII(text string) string {
+func TranslateANSI(text string) string {
var buffer bytes.Buffer
- writer := ANSIIWriter(&buffer)
+ writer := ANSIWriter(&buffer)
writer.Write([]byte(text))
return buffer.String()
}
diff --git a/vendor/maunium.net/go/tview/application.go b/vendor/maunium.net/go/tview/application.go
index 7e6da3a..ae4d7f8 100644
--- a/vendor/maunium.net/go/tview/application.go
+++ b/vendor/maunium.net/go/tview/application.go
@@ -1,24 +1,36 @@
package tview
import (
- "fmt"
- "os"
"sync"
"maunium.net/go/tcell"
)
+// The size of the event/update/redraw channels.
+const queueSize = 100
+
// 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.
screen tcell.Screen
+ // Indicates whether the application's screen is currently active. This is
+ // false during suspended mode.
+ running bool
+
// The primitive which currently has the keyboard focus.
focus Primitive
@@ -48,13 +60,23 @@ type Application struct {
// was drawn.
afterDraw func(screen tcell.Screen)
- // If this value is true, the application has entered suspended mode.
- suspended bool
+ // 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 func()
+
+ // A channel which signals the end of the suspended mode.
+ suspendToken chan struct{}
}
// NewApplication creates and returns a new application.
func NewApplication() *Application {
- return &Application{}
+ return &Application{
+ events: make(chan tcell.Event, queueSize),
+ updates: make(chan func(), queueSize),
+ suspendToken: make(chan struct{}, 1),
+ }
}
// SetInputCapture sets a function which captures all key events before they are
@@ -97,140 +119,222 @@ func (a *Application) GetScreen() tcell.Screen {
return a.screen
}
+// 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. Run() will call Init() and Fini() on
+// the provided screen object.
+//
+// This function is typically called before calling Run(). Calling it while an
+// application is running will switch the application to the new screen. Fini()
+// will be called on the old screen and Init() on the new screen (errors
+// returned by Init() will lead to a panic).
+//
+// Note that calling Suspend() will invoke Fini() on your screen object and it
+// will not be restored when suspended mode ends. Instead, a new default screen
+// object will be created.
+func (a *Application) SetScreen(screen tcell.Screen) *Application {
+ a.Lock()
+ defer a.Unlock()
+ if a.running {
+ a.screen.Fini()
+ }
+ a.screen = screen
+ if a.running {
+ if err := a.screen.Init(); err != nil {
+ panic(err)
+ }
+ }
+ return a
+}
+
// Run starts the application and thus the event loop. This function returns
// when Stop() was called.
func (a *Application) Run() error {
var err error
a.Lock()
- // Make a screen.
- a.screen, err = tcell.NewScreen()
- if err != nil {
- a.Unlock()
- return err
+ // 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
}
a.screen.EnableMouse()
+ a.running = true
+
+ // 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()
+ }
+ a.running = false
+ panic(p)
+ }
+ }()
// Draw the screen for the first time.
a.Unlock()
- a.Draw()
+ a.draw()
+
+ // Separate loop to wait for screen events.
+ var wg sync.WaitGroup
+ wg.Add(1)
+ a.suspendToken <- struct{}{} // We need this to get started.
+ go func() {
+ defer wg.Done()
+ for range a.suspendToken {
+ for {
+ a.RLock()
+ screen := a.screen
+ a.RUnlock()
+ if screen == nil {
+ // We have no screen. We might need to stop.
+ break
+ }
- // Start event loop.
- for {
- a.Lock()
- screen := a.screen
- if a.suspended {
- a.suspended = false // Clear previous suspended flag.
- }
- a.Unlock()
- if screen == nil {
- break
+ // Wait for next event and queue it.
+ event := screen.PollEvent()
+ if event != nil {
+ // Regular event. Queue.
+ a.QueueEvent(event)
+ continue
+ }
+
+ // A screen was finalized (event is nil).
+ a.RLock()
+ running := a.running
+ a.RUnlock()
+ if running {
+ // The application was stopped. End the event loop.
+ a.QueueEvent(nil)
+ return
+ }
+
+ // We're in suspended mode (running is false). Pause and wait for new
+ // token.
+ break
+ }
}
+ }()
- // Wait for next event.
- event := a.screen.PollEvent()
- if event == nil {
- a.Lock()
- if a.suspended {
- // This screen was renewed due to suspended mode.
- a.suspended = false
- a.Unlock()
- continue // Resume.
+ // Start event loop.
+EventLoop:
+ for {
+ select {
+ case event := <-a.events:
+ if event == nil {
+ break EventLoop
}
- a.Unlock()
- // The screen was finalized. Exit the loop.
- break
- }
+ switch event := event.(type) {
+ case *tcell.EventKey:
+ a.RLock()
+ p := a.focus
+ inputCapture := a.inputCapture
+ a.RUnlock()
+
+ // Intercept keys.
+ if inputCapture != nil {
+ event = inputCapture(event)
+ if event == nil {
+ continue // Don't forward event.
+ }
+ }
- switch event := event.(type) {
- case *tcell.EventKey:
- a.RLock()
- p := a.focus
- a.RUnlock()
-
- // Intercept keys.
- if a.inputCapture != nil {
- event = a.inputCapture(event)
- if event == nil {
- break // Don't forward event.
+ // Ctrl-C closes the application.
+ if event.Key() == tcell.KeyCtrlC {
+ a.Stop()
}
- }
- // Pass other key events to the currently focused primitive.
- if p != nil {
- if handler := p.InputHandler(); handler != nil {
- handler(event, func(p Primitive) {
- a.SetFocus(p)
- })
- a.Draw()
+ // Pass other key events to the currently focused primitive.
+ if p != nil {
+ if handler := p.InputHandler(); handler != nil {
+ handler(event, func(p Primitive) {
+ a.SetFocus(p)
+ })
+ a.draw()
+ }
}
- }
- case *tcell.EventMouse:
- a.RLock()
- p := a.focus
- a.RUnlock()
-
- // Intercept keys.
- if a.mouseCapture != nil {
- event = a.mouseCapture(event)
- if event == nil {
- break // Don't forward event.
+
+ case *tcell.EventMouse:
+ a.RLock()
+ p := a.focus
+ a.RUnlock()
+
+ // Intercept keys.
+ if a.mouseCapture != nil {
+ event = a.mouseCapture(event)
+ if event == nil {
+ break // Don't forward event.
+ }
}
- }
- // Pass other key events to the currently focused primitive.
- if p != nil {
- if handler := p.MouseHandler(); handler != nil {
- handler(event, func(p Primitive) {
- a.SetFocus(p)
- })
- //a.Draw()
+ // Pass other key events to the currently focused primitive.
+ if p != nil {
+ if handler := p.MouseHandler(); handler != nil {
+ handler(event, func(p Primitive) {
+ a.SetFocus(p)
+ })
+ //a.Draw()
+ }
}
- }
- case *tcell.EventPaste:
- a.RLock()
- p := a.focus
- a.RUnlock()
-
- if a.pasteCapture != nil {
- event = a.pasteCapture(event)
- if event == nil {
- break
+ case *tcell.EventPaste:
+ a.RLock()
+ p := a.focus
+ a.RUnlock()
+
+ if a.pasteCapture != nil {
+ event = a.pasteCapture(event)
+ if event == nil {
+ break
+ }
}
- }
- if p != nil {
- if handler := p.PasteHandler(); handler != nil {
- handler(event)
- a.Draw()
+ if p != nil {
+ if handler := p.PasteHandler(); handler != nil {
+ handler(event)
+ a.Draw()
+ }
}
+ case *tcell.EventResize:
+ a.RLock()
+ screen := a.screen
+ a.RUnlock()
+ screen.Clear()
+ a.draw()
}
- case *tcell.EventResize:
- a.Lock()
- screen := a.screen
- a.Unlock()
- screen.Clear()
- a.Draw()
+
+ // If we have updates, now is the time to execute them.
+ case updater := <-a.updates:
+ updater()
}
}
+ a.running = false
+ close(a.suspendToken)
+ wg.Wait()
+
return nil
}
// Stop stops the application, causing Run() to return.
func (a *Application) Stop() {
- a.RLock()
- defer a.RUnlock()
- if a.screen == nil {
+ a.Lock()
+ defer a.Unlock()
+ screen := a.screen
+ if screen == nil {
return
}
- a.screen.Fini()
a.screen = nil
+ screen.Fini()
+ // a.running is still true, the main loop will clean up.
}
// Suspend temporarily suspends the application by exiting terminal UI mode and
@@ -243,29 +347,24 @@ func (a *Application) Stop() {
func (a *Application) Suspend(f func()) bool {
a.Lock()
- if a.suspended || a.screen == nil {
- // Application is already suspended.
+ screen := a.screen
+ if screen == nil {
+ // Screen has not yet been initialized.
a.Unlock()
return false
}
- // Enter suspended mode.
- a.suspended = true
+ // Enter suspended mode. Make a new screen here already so our event loop can
+ // continue.
+ a.screen = nil
+ a.running = false
+ screen.Fini()
a.Unlock()
- a.Stop()
-
- // Deal with panics during suspended mode. Exit the program.
- defer func() {
- if p := recover(); p != nil {
- fmt.Println(p)
- os.Exit(1)
- }
- }()
// Wait for "f" to return.
f()
- // Make a new screen and redraw.
+ // Initialize our new screen and draw the contents.
a.Lock()
var err error
a.screen, err = tcell.NewScreen()
@@ -278,23 +377,36 @@ func (a *Application) Suspend(f func()) bool {
panic(err)
}
a.screen.EnableMouse()
+ a.running = true
a.Unlock()
- a.Draw()
+ a.draw()
+ a.suspendToken <- struct{}{}
+ // One key event will get lost, see https://github.com/gdamore/tcell/issues/194
// Continue application loop.
return true
}
-// Draw refreshes the screen. It calls the Draw() function of the application's
-// root primitive and then syncs the screen buffer.
+// 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.
func (a *Application) Draw() *Application {
- a.RLock()
+ a.QueueUpdate(func() {
+ a.draw()
+ })
+ return a
+}
+
+// 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
- a.RUnlock()
// Maybe we're not ready yet or not anymore.
if screen == nil || root == nil {
@@ -427,3 +539,35 @@ func (a *Application) GetFocus() Primitive {
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.
+func (a *Application) QueueUpdate(f func()) *Application {
+ a.updates <- f
+ 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
+}
diff --git a/vendor/maunium.net/go/tview/borders.go b/vendor/maunium.net/go/tview/borders.go
new file mode 100644
index 0000000..946c878
--- /dev/null
+++ b/vendor/maunium.net/go/tview/borders.go
@@ -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,
+}
diff --git a/vendor/maunium.net/go/tview/box.go b/vendor/maunium.net/go/tview/box.go
index ff3cc1e..85bf1b2 100644
--- a/vendor/maunium.net/go/tview/box.go
+++ b/vendor/maunium.net/go/tview/box.go
@@ -32,6 +32,9 @@ type Box struct {
// The color of the border.
borderColor tcell.Color
+ // The style attributes of the border.
+ borderAttributes tcell.AttrMask
+
// The title. Only visible if there is a border, too.
title string
@@ -48,10 +51,6 @@ type Box struct {
// Whether or not this box has focus.
hasFocus bool
- // If set to true, the inner rect of this box will be within the screen at the
- // last time the box was drawn.
- clampToScreen bool
-
// An optional capture function which receives a key event and returns the
// event to be forwarded to the primitive's default input handler (nil if
// nothing should be forwarded).
@@ -78,7 +77,6 @@ func NewBox() *Box {
borderColor: Styles.BorderColor,
titleColor: Styles.TitleColor,
titleAlign: AlignCenter,
- clampToScreen: true,
}
b.focus = b
return b
@@ -121,6 +119,7 @@ func (b *Box) SetRect(x, y, width, height int) {
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
@@ -273,6 +272,15 @@ func (b *Box) SetBorderColor(color tcell.Color) *Box {
return b
}
+// SetBorderAttributes sets the border's style attributes. You can combine
+// different attributes using bitmask operations:
+//
+// box.SetBorderAttributes(tcell.AttrUnderline | tcell.AttrBold)
+func (b *Box) SetBorderAttributes(attr tcell.AttrMask) *Box {
+ b.borderAttributes = attr
+ return b
+}
+
// SetTitle sets the box's title.
func (b *Box) SetTitle(title string) *Box {
b.title = title
@@ -319,30 +327,30 @@ func (b *Box) Draw(screen tcell.Screen) {
// Draw border.
if b.border && b.width >= 2 && b.height >= 2 {
- border := background.Foreground(b.borderColor)
+ border := background.Foreground(b.borderColor) | tcell.Style(b.borderAttributes)
var vertical, horizontal, topLeft, topRight, bottomLeft, bottomRight rune
if b.focus.HasFocus() {
- vertical = GraphicsDbVertBar
- horizontal = GraphicsDbHorBar
- topLeft = GraphicsDbTopLeftCorner
- topRight = GraphicsDbTopRightCorner
- bottomLeft = GraphicsDbBottomLeftCorner
- bottomRight = GraphicsDbBottomRightCorner
+ horizontal = Borders.HorizontalFocus
+ vertical = Borders.VerticalFocus
+ topLeft = Borders.TopLeftFocus
+ topRight = Borders.TopRightFocus
+ bottomLeft = Borders.BottomLeftFocus
+ bottomRight = Borders.BottomRightFocus
} else {
- vertical = GraphicsHoriBar
- horizontal = GraphicsVertBar
- topLeft = GraphicsTopLeftCorner
- topRight = GraphicsTopRightCorner
- bottomLeft = GraphicsBottomLeftCorner
- bottomRight = GraphicsBottomRightCorner
+ 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, vertical, nil, border)
- screen.SetContent(x, b.y+b.height-1, vertical, nil, border)
+ screen.SetContent(x, b.y, horizontal, nil, border)
+ screen.SetContent(x, b.y+b.height-1, horizontal, nil, border)
}
for y := b.y + 1; y < b.y+b.height-1; y++ {
- screen.SetContent(b.x, y, horizontal, nil, border)
- screen.SetContent(b.x+b.width-1, y, horizontal, nil, border)
+ screen.SetContent(b.x, y, vertical, nil, border)
+ screen.SetContent(b.x+b.width-1, y, vertical, nil, border)
}
screen.SetContent(b.x, b.y, topLeft, nil, border)
screen.SetContent(b.x+b.width-1, b.y, topRight, nil, border)
@@ -351,11 +359,11 @@ func (b *Box) Draw(screen tcell.Screen) {
// Draw title.
if b.title != "" && b.width >= 4 {
- _, printed := Print(screen, b.title, b.x+1, b.y, b.width-2, b.titleAlign, b.titleColor)
- if StringWidth(b.title)-printed > 0 && printed > 0 {
+ printed, _ := Print(screen, b.title, b.x+1, b.y, b.width-2, b.titleAlign, b.titleColor)
+ if len(b.title)-printed > 0 && printed > 0 {
_, _, style, _ := screen.GetContent(b.x+b.width-2, b.y)
fg, _, _ := style.Decompose()
- Print(screen, string(GraphicsEllipsis), b.x+b.width-2, b.y, 1, AlignLeft, fg)
+ Print(screen, string(SemigraphicsHorizontalEllipsis), b.x+b.width-2, b.y, 1, AlignLeft, fg)
}
}
}
@@ -370,22 +378,20 @@ func (b *Box) Draw(screen tcell.Screen) {
}
// Clamp inner rect to screen.
- if b.clampToScreen {
- width, height := screen.Size()
- if b.innerX < 0 {
- b.innerWidth += b.innerX
- b.innerX = 0
- }
- if b.innerX+b.innerWidth >= width {
- b.innerWidth = width - b.innerX
- }
- if b.innerY+b.innerHeight >= height {
- b.innerHeight = height - b.innerY
- }
- if b.innerY < 0 {
- b.innerHeight += b.innerY
- b.innerY = 0
- }
+ width, height := screen.Size()
+ if b.innerX < 0 {
+ b.innerWidth += b.innerX
+ b.innerX = 0
+ }
+ if b.innerX+b.innerWidth >= width {
+ b.innerWidth = width - b.innerX
+ }
+ if b.innerY+b.innerHeight >= height {
+ b.innerHeight = height - b.innerY
+ }
+ if b.innerY < 0 {
+ b.innerHeight += b.innerY
+ b.innerY = 0
}
}
diff --git a/vendor/maunium.net/go/tview/checkbox.go b/vendor/maunium.net/go/tview/checkbox.go
index ae58720..48d4592 100644
--- a/vendor/maunium.net/go/tview/checkbox.go
+++ b/vendor/maunium.net/go/tview/checkbox.go
@@ -124,9 +124,9 @@ func (c *Checkbox) SetChangedFunc(handler func(checked bool)) *Checkbox {
return c
}
-// SetDoneFunc sets a handler which is called when the user is done entering
-// text. The callback function is provided with the key that was pressed, which
-// is one of the following:
+// 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.
diff --git a/vendor/maunium.net/go/tview/doc.go b/vendor/maunium.net/go/tview/doc.go
index ccaaaf1..ddc410f 100644
--- a/vendor/maunium.net/go/tview/doc.go
+++ b/vendor/maunium.net/go/tview/doc.go
@@ -7,10 +7,12 @@ Widgets
The package implements the following widgets:
- - TextView: Scrollable windows that display multi-colored text. Text may also
+ - TextView: A scrollable window that display multi-colored text. Text may also
be highlighted.
- - Table: Scrollable display of tabular data. Table cells, rows, or columns may
- also be highlighted.
+ - 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.
@@ -83,7 +85,7 @@ tag is as follows:
[<foreground>:<background>:<flags>]
-Each of the three fields can be left blank and trailing fields can be ommitted.
+Each of the three fields can be left blank and trailing fields can be omitted.
(Empty square brackets "[]", however, are not considered color tags.) Colors
that are not specified will be left unchanged. A field with just a dash ("-")
means "reset to default".
@@ -135,6 +137,27 @@ Unicode Support
This package supports unicode characters including wide characters.
+Concurrency
+
+Many functions in this package are not thread-safe. For many applications, this
+may not be an issue: If your code makes changes in response to key events, it
+will execute in the main goroutine and thus will not cause any race conditions.
+
+If you access your primitives from other goroutines, however, you will need to
+synchronize execution. The easiest way to do this is to call
+Application.QueueUpdate() or Application.QueueUpdateDraw() (see the function
+documentation for details):
+
+ go func() {
+ app.QueueUpdateDraw(func() {
+ table.SetCellSimple(0, 0, "Foo bar")
+ })
+ }()
+
+One exception to this is the io.Writer interface implemented by TextView. You
+can safely write to a TextView from any goroutine. See the TextView
+documentation for details.
+
Type Hierarchy
All widgets listed above contain the Box type. All of Box's functions are
diff --git a/vendor/maunium.net/go/tview/dropdown.go b/vendor/maunium.net/go/tview/dropdown.go
index 4e4d888..02c93bd 100644
--- a/vendor/maunium.net/go/tview/dropdown.go
+++ b/vendor/maunium.net/go/tview/dropdown.go
@@ -354,6 +354,7 @@ func (d *DropDown) InputHandler() func(event *tcell.EventKey, setFocus func(p Pr
// Hand control over to the list.
d.open = true
+ optionBefore := d.currentOption
d.list.SetSelectedFunc(func(index int, mainText, secondaryText string, shortcut rune) {
// An option was selected. Close the list again.
d.open = false
@@ -374,6 +375,10 @@ func (d *DropDown) InputHandler() func(event *tcell.EventKey, setFocus func(p Pr
d.prefix = string(r[:len(r)-1])
}
evalPrefix()
+ } else if event.Key() == tcell.KeyEscape {
+ d.open = false
+ d.currentOption = optionBefore
+ setFocus(d)
} else {
d.prefix = ""
}
diff --git a/vendor/maunium.net/go/tview/flex.go b/vendor/maunium.net/go/tview/flex.go
index ad42d3a..bc1cfe1 100644
--- a/vendor/maunium.net/go/tview/flex.go
+++ b/vendor/maunium.net/go/tview/flex.go
@@ -28,7 +28,7 @@ type Flex struct {
*Box
// The items to be positioned.
- items []flexItem
+ items []*flexItem
// FlexRow or FlexColumn.
direction int
@@ -79,7 +79,7 @@ func (f *Flex) SetFullScreen(fullScreen bool) *Flex {
// 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})
+ f.items = append(f.items, &flexItem{Item: item, FixedSize: fixedSize, Proportion: proportion, Focus: focus})
return f
}
@@ -94,6 +94,19 @@ func (f *Flex) RemoveItem(p Primitive) *Flex {
return f
}
+// ResizeItem sets a new size for the item(s) with the given primitive. If there
+// are multiple Flex items with the same primitive, they will all receive the
+// same size. For details regarding the size parameters, see AddItem().
+func (f *Flex) ResizeItem(p Primitive, fixedSize, proportion int) *Flex {
+ for _, item := range f.items {
+ if item.Item == p {
+ item.FixedSize = fixedSize
+ item.Proportion = proportion
+ }
+ }
+ return f
+}
+
// Draw draws this primitive onto the screen.
func (f *Flex) Draw(screen tcell.Screen) {
f.Box.Draw(screen)
diff --git a/vendor/maunium.net/go/tview/form.go b/vendor/maunium.net/go/tview/form.go
index fe0e980..e960a52 100644
--- a/vendor/maunium.net/go/tview/form.go
+++ b/vendor/maunium.net/go/tview/form.go
@@ -26,7 +26,7 @@ type FormItem interface {
// required.
GetFieldWidth() int
- // SetEnteredFunc sets the handler function for when the user finished
+ // SetFinishedFunc sets the handler function for when the user finished
// entering data into the item. The handler may receive events for the
// Enter key (we're done), the Escape key (cancel input), the Tab key (move to
// next field), and the Backtab key (move to previous field).
@@ -218,6 +218,37 @@ func (f *Form) AddButton(label string, selected func()) *Form {
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 {
@@ -251,6 +282,14 @@ 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.
@@ -263,6 +302,18 @@ func (f *Form) GetFormItemByLabel(label string) FormItem {
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
+}
+
// SetCancelFunc sets a handler which is called when the user hits the Escape
// key.
func (f *Form) SetCancelFunc(callback func()) *Form {
diff --git a/vendor/maunium.net/go/tview/grid.go b/vendor/maunium.net/go/tview/grid.go
index 77797ba..d8f6c97 100644
--- a/vendor/maunium.net/go/tview/grid.go
+++ b/vendor/maunium.net/go/tview/grid.go
@@ -583,11 +583,11 @@ func (g *Grid) Draw(screen tcell.Screen) {
}
by := item.y - 1
if by >= 0 && by < height {
- PrintJoinedBorder(screen, x+bx, y+by, GraphicsHoriBar, g.bordersColor)
+ PrintJoinedSemigraphics(screen, x+bx, y+by, Borders.Horizontal, g.bordersColor)
}
by = item.y + item.h
if by >= 0 && by < height {
- PrintJoinedBorder(screen, x+bx, y+by, GraphicsHoriBar, g.bordersColor)
+ PrintJoinedSemigraphics(screen, x+bx, y+by, Borders.Horizontal, g.bordersColor)
}
}
for by := item.y; by < item.y+item.h; by++ { // Left/right lines.
@@ -596,28 +596,28 @@ func (g *Grid) Draw(screen tcell.Screen) {
}
bx := item.x - 1
if bx >= 0 && bx < width {
- PrintJoinedBorder(screen, x+bx, y+by, GraphicsVertBar, g.bordersColor)
+ PrintJoinedSemigraphics(screen, x+bx, y+by, Borders.Vertical, g.bordersColor)
}
bx = item.x + item.w
if bx >= 0 && bx < width {
- PrintJoinedBorder(screen, x+bx, y+by, GraphicsVertBar, g.bordersColor)
+ PrintJoinedSemigraphics(screen, x+bx, y+by, Borders.Vertical, g.bordersColor)
}
}
bx, by := item.x-1, item.y-1 // Top-left corner.
if bx >= 0 && bx < width && by >= 0 && by < height {
- PrintJoinedBorder(screen, x+bx, y+by, GraphicsTopLeftCorner, g.bordersColor)
+ PrintJoinedSemigraphics(screen, x+bx, y+by, Borders.TopLeft, g.bordersColor)
}
bx, by = item.x+item.w, item.y-1 // Top-right corner.
if bx >= 0 && bx < width && by >= 0 && by < height {
- PrintJoinedBorder(screen, x+bx, y+by, GraphicsTopRightCorner, g.bordersColor)
+ PrintJoinedSemigraphics(screen, x+bx, y+by, Borders.TopRight, g.bordersColor)
}
bx, by = item.x-1, item.y+item.h // Bottom-left corner.
if bx >= 0 && bx < width && by >= 0 && by < height {
- PrintJoinedBorder(screen, x+bx, y+by, GraphicsBottomLeftCorner, g.bordersColor)
+ PrintJoinedSemigraphics(screen, x+bx, y+by, Borders.BottomLeft, g.bordersColor)
}
bx, by = item.x+item.w, item.y+item.h // Bottom-right corner.
if bx >= 0 && bx < width && by >= 0 && by < height {
- PrintJoinedBorder(screen, x+bx, y+by, GraphicsBottomRightCorner, g.bordersColor)
+ PrintJoinedSemigraphics(screen, x+bx, y+by, Borders.BottomRight, g.bordersColor)
}
}
}
diff --git a/vendor/maunium.net/go/tview/inputfield.go b/vendor/maunium.net/go/tview/inputfield.go
index 73224f2..ccc66e3 100644
--- a/vendor/maunium.net/go/tview/inputfield.go
+++ b/vendor/maunium.net/go/tview/inputfield.go
@@ -11,10 +11,23 @@ import (
)
// InputField is a one-line box (three lines if there is a title) where the
-// user can enter text.
+// user can enter text. Use SetAcceptanceFunc() to accept or reject input,
+// SetChangedFunc() to listen for changes, and SetMaskCharacter() to hide input
+// from onlookers (e.g. for password input).
//
-// Use SetMaskCharacter() to hide input from onlookers (e.g. for password
-// input).
+// The following keys can be used for navigation and editing:
+//
+// - Left arrow: Move left by one character.
+// - Right arrow: Move right by one character.
+// - Home, Ctrl-A, Alt-a: Move to the beginning of the line.
+// - End, Ctrl-E, Alt-e: Move to the end of the line.
+// - Alt-left, Alt-b: Move left by one word.
+// - Alt-right, Alt-f: Move right by one word.
+// - Backspace: Delete the character before the cursor.
+// - Delete: Delete the character after the cursor.
+// - Ctrl-K: Delete from the cursor to the end of the line.
+// - Ctrl-W: Delete the last word before the cursor.
+// - Ctrl-U: Delete the entire line.
//
// See https://github.com/rivo/tview/wiki/InputField for an example.
type InputField struct {
@@ -53,6 +66,12 @@ type InputField struct {
// disables masking.
maskCharacter rune
+ // The cursor position as a byte index into the text string.
+ cursorPos int
+
+ // The number of bytes of the text string skipped ahead while drawing.
+ offset int
+
// An optional function which may reject the last character that was entered.
accept func(text string, ch rune) bool
@@ -83,6 +102,7 @@ func NewInputField() *InputField {
// SetText sets the current text of the input field.
func (i *InputField) SetText(text string) *InputField {
i.text = text
+ i.cursorPos = len(text)
if i.changed != nil {
i.changed(text)
}
@@ -174,7 +194,7 @@ func (i *InputField) SetMaskCharacter(mask rune) *InputField {
// SetAcceptanceFunc sets a handler which may reject the last character that was
// entered (by returning false).
//
-// This package defines a number of variables Prefixed with InputField which may
+// This package defines a number of variables prefixed with InputField which may
// be used for common input (e.g. numbers, maximum text length).
func (i *InputField) SetAcceptanceFunc(handler func(textToCheck string, lastChar rune) bool) *InputField {
i.accept = handler
@@ -244,54 +264,67 @@ func (i *InputField) Draw(screen tcell.Screen) {
screen.SetContent(x+index, y, ' ', nil, fieldStyle)
}
- // Draw placeholder text.
+ // Text.
+ var cursorScreenPos int
text := i.text
if text == "" && i.placeholder != "" {
- Print(screen, i.placeholder, x, y, fieldWidth, AlignLeft, i.placeholderTextColor)
+ // Draw placeholder text.
+ Print(screen, Escape(i.placeholder), x, y, fieldWidth, AlignLeft, i.placeholderTextColor)
+ i.offset = 0
} else {
// Draw entered text.
if i.maskCharacter > 0 {
text = strings.Repeat(string(i.maskCharacter), utf8.RuneCountInString(i.text))
- } else {
- text = Escape(text)
}
- fieldWidth-- // We need one cell for the cursor.
- if fieldWidth < runewidth.StringWidth(text) {
- Print(screen, text, x, y, fieldWidth, AlignRight, i.fieldTextColor)
+ stringWidth := runewidth.StringWidth(text)
+ if fieldWidth >= stringWidth {
+ // We have enough space for the full text.
+ Print(screen, Escape(text), x, y, fieldWidth, AlignLeft, i.fieldTextColor)
+ i.offset = 0
+ iterateString(text, func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool {
+ if textPos >= i.cursorPos {
+ return true
+ }
+ cursorScreenPos += screenWidth
+ return false
+ })
} else {
- Print(screen, text, x, y, fieldWidth, AlignLeft, i.fieldTextColor)
+ // The text doesn't fit. Where is the cursor?
+ if i.cursorPos < 0 {
+ i.cursorPos = 0
+ } else if i.cursorPos > len(text) {
+ i.cursorPos = len(text)
+ }
+ // Shift the text so the cursor is inside the field.
+ var shiftLeft int
+ if i.offset > i.cursorPos {
+ i.offset = i.cursorPos
+ } else if subWidth := runewidth.StringWidth(text[i.offset:i.cursorPos]); subWidth > fieldWidth-1 {
+ shiftLeft = subWidth - fieldWidth + 1
+ }
+ currentOffset := i.offset
+ iterateString(text, func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool {
+ if textPos >= currentOffset {
+ if shiftLeft > 0 {
+ i.offset = textPos + textWidth
+ shiftLeft -= screenWidth
+ } else {
+ if textPos+textWidth > i.cursorPos {
+ return true
+ }
+ cursorScreenPos += screenWidth
+ }
+ }
+ return false
+ })
+ Print(screen, Escape(text[i.offset:]), x, y, fieldWidth, AlignLeft, i.fieldTextColor)
}
}
// Set cursor.
if i.focus.HasFocus() {
- i.setCursor(screen)
- }
-}
-
-// setCursor sets the cursor position.
-func (i *InputField) setCursor(screen tcell.Screen) {
- x := i.x
- y := i.y
- rightLimit := x + i.width
- if i.border {
- x++
- y++
- rightLimit -= 2
- }
- fieldWidth := runewidth.StringWidth(i.text)
- if i.fieldWidth > 0 && fieldWidth > i.fieldWidth-1 {
- fieldWidth = i.fieldWidth - 1
- }
- if i.labelWidth > 0 {
- x += i.labelWidth + fieldWidth
- } else {
- x += StringWidth(i.label) + fieldWidth
- }
- if x >= rightLimit {
- x = rightLimit - 1
+ screen.ShowCursor(x+cursorScreenPos, y)
}
- screen.ShowCursor(x, y)
}
// InputHandler returns the handler for this primitive.
@@ -305,27 +338,101 @@ func (i *InputField) InputHandler() func(event *tcell.EventKey, setFocus func(p
}
}()
+ // Movement functions.
+ home := func() { i.cursorPos = 0 }
+ end := func() { i.cursorPos = len(i.text) }
+ moveLeft := func() {
+ iterateStringReverse(i.text[:i.cursorPos], func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool {
+ i.cursorPos -= textWidth
+ return true
+ })
+ }
+ moveRight := func() {
+ iterateString(i.text[i.cursorPos:], func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool {
+ i.cursorPos += textWidth
+ return true
+ })
+ }
+ moveWordLeft := func() {
+ i.cursorPos = len(regexp.MustCompile(`\S+\s*$`).ReplaceAllString(i.text[:i.cursorPos], ""))
+ }
+ moveWordRight := func() {
+ i.cursorPos = len(i.text) - len(regexp.MustCompile(`^\s*\S+\s*`).ReplaceAllString(i.text[i.cursorPos:], ""))
+ }
+
+ // Add character function. Returns whether or not the rune character is
+ // accepted.
+ add := func(r rune) bool {
+ newText := i.text[:i.cursorPos] + string(r) + i.text[i.cursorPos:]
+ if i.accept != nil {
+ return i.accept(newText, r)
+ }
+ i.text = newText
+ i.cursorPos += len(string(r))
+ return true
+ }
+
// Process key event.
switch key := event.Key(); key {
case tcell.KeyRune: // Regular character.
- newText := i.text + string(event.Rune())
- if i.accept != nil {
- if !i.accept(newText, event.Rune()) {
+ if event.Modifiers()&tcell.ModAlt > 0 {
+ // We accept some Alt- key combinations.
+ switch event.Rune() {
+ case 'a': // Home.
+ home()
+ case 'e': // End.
+ end()
+ case 'b': // Move word left.
+ moveWordLeft()
+ case 'f': // Move word right.
+ moveWordRight()
+ }
+ } else {
+ // Other keys are simply accepted as regular characters.
+ if !add(event.Rune()) {
break
}
}
- i.text = newText
case tcell.KeyCtrlU: // Delete all.
i.text = ""
+ i.cursorPos = 0
+ case tcell.KeyCtrlK: // Delete until the end of the line.
+ i.text = i.text[:i.cursorPos]
case tcell.KeyCtrlW: // Delete last word.
- lastWord := regexp.MustCompile(`\s*\S+\s*$`)
- i.text = lastWord.ReplaceAllString(i.text, "")
- case tcell.KeyBackspace, tcell.KeyBackspace2: // Delete last character.
- if len(i.text) == 0 {
- break
+ lastWord := regexp.MustCompile(`\S+\s*$`)
+ newText := lastWord.ReplaceAllString(i.text[:i.cursorPos], "") + i.text[i.cursorPos:]
+ i.cursorPos -= len(i.text) - len(newText)
+ i.text = newText
+ case tcell.KeyBackspace, tcell.KeyBackspace2: // Delete character before the cursor.
+ iterateStringReverse(i.text[:i.cursorPos], func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool {
+ i.text = i.text[:textPos] + i.text[textPos+textWidth:]
+ i.cursorPos -= textWidth
+ return true
+ })
+ if i.offset >= i.cursorPos {
+ i.offset = 0
+ }
+ case tcell.KeyDelete: // Delete character after the cursor.
+ iterateString(i.text[i.cursorPos:], func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool {
+ i.text = i.text[:i.cursorPos] + i.text[i.cursorPos+textWidth:]
+ return true
+ })
+ case tcell.KeyLeft:
+ if event.Modifiers()&tcell.ModAlt > 0 {
+ moveWordLeft()
+ } else {
+ moveLeft()
+ }
+ case tcell.KeyRight:
+ if event.Modifiers()&tcell.ModAlt > 0 {
+ moveWordRight()
+ } else {
+ moveRight()
}
- runes := []rune(i.text)
- i.text = string(runes[:len(runes)-1])
+ case tcell.KeyHome, tcell.KeyCtrlA:
+ home()
+ case tcell.KeyEnd, tcell.KeyCtrlE:
+ end()
case tcell.KeyEnter, tcell.KeyTab, tcell.KeyBacktab, tcell.KeyEscape: // We're done.
if i.done != nil {
i.done(key)
diff --git a/vendor/maunium.net/go/tview/list.go b/vendor/maunium.net/go/tview/list.go
index bc5be85..e8dc5dd 100644
--- a/vendor/maunium.net/go/tview/list.go
+++ b/vendor/maunium.net/go/tview/list.go
@@ -85,6 +85,19 @@ func (l *List) GetCurrentItem() int {
return l.currentItem
}
+// RemoveItem removes the item with the given index (starting at 0) from the
+// list. Does nothing if the index is out of range.
+func (l *List) RemoveItem(index int) *List {
+ if index < 0 || index >= len(l.items) {
+ return l
+ }
+ l.items = append(l.items[:index], l.items[index+1:]...)
+ if l.currentItem >= len(l.items) {
+ l.currentItem = len(l.items) - 1
+ }
+ return l
+}
+
// SetMainTextColor sets the color of the items' main text.
func (l *List) SetMainTextColor(color tcell.Color) *List {
l.mainTextColor = color
@@ -127,7 +140,7 @@ func (l *List) ShowSecondaryText(show bool) *List {
//
// This function is also called when the first item is added or when
// SetCurrentItem() is called.
-func (l *List) SetChangedFunc(handler func(int, string, string, rune)) *List {
+func (l *List) SetChangedFunc(handler func(index int, mainText string, secondaryText string, shortcut rune)) *List {
l.changed = handler
return l
}
diff --git a/vendor/maunium.net/go/tview/modal.go b/vendor/maunium.net/go/tview/modal.go
index f53a265..f5e92f1 100644
--- a/vendor/maunium.net/go/tview/modal.go
+++ b/vendor/maunium.net/go/tview/modal.go
@@ -40,6 +40,11 @@ func NewModal() *Modal {
SetButtonBackgroundColor(Styles.PrimitiveBackgroundColor).
SetButtonTextColor(Styles.PrimaryTextColor)
m.form.SetBackgroundColor(Styles.ContrastBackgroundColor).SetBorderPadding(0, 0, 0, 0)
+ m.form.SetCancelFunc(func() {
+ if m.done != nil {
+ m.done(-1, "")
+ }
+ })
m.frame = NewFrame(m.form).SetBorders(0, 0, 1, 0, 0, 0)
m.frame.SetBorder(true).
SetBackgroundColor(Styles.ContrastBackgroundColor).
@@ -81,6 +86,16 @@ func (m *Modal) AddButtons(labels []string) *Modal {
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
diff --git a/vendor/maunium.net/go/tview/semigraphics.go b/vendor/maunium.net/go/tview/semigraphics.go
new file mode 100644
index 0000000..2455c87
--- /dev/null
+++ b/vendor/maunium.net/go/tview/semigraphics.go
@@ -0,0 +1,296 @@
+package tview
+
+import "maunium.net/go/tcell"
+
+// Semigraphics provides an easy way to access unicode characters for drawing.
+//
+// Named like the unicode characters, 'Semigraphics'-prefix used if unicode block
+// isn't prefixed itself.
+const (
+ // Block: General Punctation U+2000-U+206F (http://unicode.org/charts/PDF/U2000.pdf)
+ SemigraphicsHorizontalEllipsis rune = '\u2026' // …
+
+ // Block: Box Drawing U+2500-U+257F (http://unicode.org/charts/PDF/U2500.pdf)
+ BoxDrawingsLightHorizontal rune = '\u2500' // ─
+ BoxDrawingsHeavyHorizontal rune = '\u2501' // ━
+ BoxDrawingsLightVertical rune = '\u2502' // │
+ BoxDrawingsHeavyVertical rune = '\u2503' // ┃
+ BoxDrawingsLightTripleDashHorizontal rune = '\u2504' // ┄
+ BoxDrawingsHeavyTripleDashHorizontal rune = '\u2505' // ┅
+ BoxDrawingsLightTripleDashVertical rune = '\u2506' // ┆
+ BoxDrawingsHeavyTripleDashVertical rune = '\u2507' // ┇
+ BoxDrawingsLightQuadrupleDashHorizontal rune = '\u2508' // ┈
+ BoxDrawingsHeavyQuadrupleDashHorizontal rune = '\u2509' // ┉
+ BoxDrawingsLightQuadrupleDashVertical rune = '\u250a' // ┊
+ BoxDrawingsHeavyQuadrupleDashVertical rune = '\u250b' // ┋
+ BoxDrawingsLightDownAndRight rune = '\u250c' // ┌
+ BoxDrawingsDownLighAndRightHeavy rune = '\u250d' // ┍
+ BoxDrawingsDownHeavyAndRightLight rune = '\u250e' // ┎
+ BoxDrawingsHeavyDownAndRight rune = '\u250f' // ┏
+ BoxDrawingsLightDownAndLeft rune = '\u2510' // ┐
+ BoxDrawingsDownLighAndLeftHeavy rune = '\u2511' // ┑
+ BoxDrawingsDownHeavyAndLeftLight rune = '\u2512' // ┒
+ BoxDrawingsHeavyDownAndLeft rune = '\u2513' // ┓
+ BoxDrawingsLightUpAndRight rune = '\u2514' // └
+ BoxDrawingsUpLightAndRightHeavy rune = '\u2515' // ┕
+ BoxDrawingsUpHeavyAndRightLight rune = '\u2516' // ┖
+ BoxDrawingsHeavyUpAndRight rune = '\u2517' // ┗
+ BoxDrawingsLightUpAndLeft rune = '\u2518' // ┘
+ BoxDrawingsUpLightAndLeftHeavy rune = '\u2519' // ┙
+ BoxDrawingsUpHeavyAndLeftLight rune = '\u251a' // ┚
+ BoxDrawingsHeavyUpAndLeft rune = '\u251b' // ┛
+ BoxDrawingsLightVerticalAndRight rune = '\u251c' // ├
+ BoxDrawingsVerticalLightAndRightHeavy rune = '\u251d' // ┝
+ BoxDrawingsUpHeavyAndRightDownLight rune = '\u251e' // ┞
+ BoxDrawingsDownHeacyAndRightUpLight rune = '\u251f' // ┟
+ BoxDrawingsVerticalHeavyAndRightLight rune = '\u2520' // ┠
+ BoxDrawingsDownLightAnbdRightUpHeavy rune = '\u2521' // ┡
+ BoxDrawingsUpLightAndRightDownHeavy rune = '\u2522' // ┢
+ BoxDrawingsHeavyVerticalAndRight rune = '\u2523' // ┣
+ BoxDrawingsLightVerticalAndLeft rune = '\u2524' // ┤
+ BoxDrawingsVerticalLightAndLeftHeavy rune = '\u2525' // ┥
+ BoxDrawingsUpHeavyAndLeftDownLight rune = '\u2526' // ┦
+ BoxDrawingsDownHeavyAndLeftUpLight rune = '\u2527' // ┧
+ BoxDrawingsVerticalheavyAndLeftLight rune = '\u2528' // ┨
+ BoxDrawingsDownLightAndLeftUpHeavy rune = '\u2529' // ┨
+ BoxDrawingsUpLightAndLeftDownHeavy rune = '\u252a' // ┪
+ BoxDrawingsHeavyVerticalAndLeft rune = '\u252b' // ┫
+ BoxDrawingsLightDownAndHorizontal rune = '\u252c' // ┬
+ BoxDrawingsLeftHeavyAndRightDownLight rune = '\u252d' // ┭
+ BoxDrawingsRightHeavyAndLeftDownLight rune = '\u252e' // ┮
+ BoxDrawingsDownLightAndHorizontalHeavy rune = '\u252f' // ┯
+ BoxDrawingsDownHeavyAndHorizontalLight rune = '\u2530' // ┰
+ BoxDrawingsRightLightAndLeftDownHeavy rune = '\u2531' // ┱
+ BoxDrawingsLeftLightAndRightDownHeavy rune = '\u2532' // ┲
+ BoxDrawingsHeavyDownAndHorizontal rune = '\u2533' // ┳
+ BoxDrawingsLightUpAndHorizontal rune = '\u2534' // ┴
+ BoxDrawingsLeftHeavyAndRightUpLight rune = '\u2535' // ┵
+ BoxDrawingsRightHeavyAndLeftUpLight rune = '\u2536' // ┶
+ BoxDrawingsUpLightAndHorizontalHeavy rune = '\u2537' // ┷
+ BoxDrawingsUpHeavyAndHorizontalLight rune = '\u2538' // ┸
+ BoxDrawingsRightLightAndLeftUpHeavy rune = '\u2539' // ┹
+ BoxDrawingsLeftLightAndRightUpHeavy rune = '\u253a' // ┺
+ BoxDrawingsHeavyUpAndHorizontal rune = '\u253b' // ┻
+ BoxDrawingsLightVerticalAndHorizontal rune = '\u253c' // ┼
+ BoxDrawingsLeftHeavyAndRightVerticalLight rune = '\u253d' // ┽
+ BoxDrawingsRightHeavyAndLeftVerticalLight rune = '\u253e' // ┾
+ BoxDrawingsVerticalLightAndHorizontalHeavy rune = '\u253f' // ┿
+ BoxDrawingsUpHeavyAndDownHorizontalLight rune = '\u2540' // ╀
+ BoxDrawingsDownHeavyAndUpHorizontalLight rune = '\u2541' // ╁
+ BoxDrawingsVerticalHeavyAndHorizontalLight rune = '\u2542' // ╂
+ BoxDrawingsLeftUpHeavyAndRightDownLight rune = '\u2543' // ╃
+ BoxDrawingsRightUpHeavyAndLeftDownLight rune = '\u2544' // ╄
+ BoxDrawingsLeftDownHeavyAndRightUpLight rune = '\u2545' // ╅
+ BoxDrawingsRightDownHeavyAndLeftUpLight rune = '\u2546' // ╆
+ BoxDrawingsDownLightAndUpHorizontalHeavy rune = '\u2547' // ╇
+ BoxDrawingsUpLightAndDownHorizontalHeavy rune = '\u2548' // ╈
+ BoxDrawingsRightLightAndLeftVerticalHeavy rune = '\u2549' // ╉
+ BoxDrawingsLeftLightAndRightVerticalHeavy rune = '\u254a' // ╊
+ BoxDrawingsHeavyVerticalAndHorizontal rune = '\u254b' // ╋
+ BoxDrawingsLightDoubleDashHorizontal rune = '\u254c' // ╌
+ BoxDrawingsHeavyDoubleDashHorizontal rune = '\u254d' // ╍
+ BoxDrawingsLightDoubleDashVertical rune = '\u254e' // ╎
+ BoxDrawingsHeavyDoubleDashVertical rune = '\u254f' // ╏
+ BoxDrawingsDoubleHorizontal rune = '\u2550' // ═
+ BoxDrawingsDoubleVertical rune = '\u2551' // ║
+ BoxDrawingsDownSingleAndRightDouble rune = '\u2552' // ╒
+ BoxDrawingsDownDoubleAndRightSingle rune = '\u2553' // ╓
+ BoxDrawingsDoubleDownAndRight rune = '\u2554' // ╔
+ BoxDrawingsDownSingleAndLeftDouble rune = '\u2555' // ╕
+ BoxDrawingsDownDoubleAndLeftSingle rune = '\u2556' // ╖
+ BoxDrawingsDoubleDownAndLeft rune = '\u2557' // ╗
+ BoxDrawingsUpSingleAndRightDouble rune = '\u2558' // ╘
+ BoxDrawingsUpDoubleAndRightSingle rune = '\u2559' // ╙
+ BoxDrawingsDoubleUpAndRight rune = '\u255a' // ╚
+ BoxDrawingsUpSingleAndLeftDouble rune = '\u255b' // ╛
+ BoxDrawingsUpDobuleAndLeftSingle rune = '\u255c' // ╜
+ BoxDrawingsDoubleUpAndLeft rune = '\u255d' // ╝
+ BoxDrawingsVerticalSingleAndRightDouble rune = '\u255e' // ╞
+ BoxDrawingsVerticalDoubleAndRightSingle rune = '\u255f' // ╟
+ BoxDrawingsDoubleVerticalAndRight rune = '\u2560' // ╠
+ BoxDrawingsVerticalSingleAndLeftDouble rune = '\u2561' // ╡
+ BoxDrawingsVerticalDoubleAndLeftSingle rune = '\u2562' // ╢
+ BoxDrawingsDoubleVerticalAndLeft rune = '\u2563' // ╣
+ BoxDrawingsDownSingleAndHorizontalDouble rune = '\u2564' // ╤
+ BoxDrawingsDownDoubleAndHorizontalSingle rune = '\u2565' // ╥
+ BoxDrawingsDoubleDownAndHorizontal rune = '\u2566' // ╦
+ BoxDrawingsUpSingleAndHorizontalDouble rune = '\u2567' // ╧
+ BoxDrawingsUpDoubleAndHorizontalSingle rune = '\u2568' // ╨
+ BoxDrawingsDoubleUpAndHorizontal rune = '\u2569' // ╩
+ BoxDrawingsVerticalSingleAndHorizontalDouble rune = '\u256a' // ╪
+ BoxDrawingsVerticalDoubleAndHorizontalSingle rune = '\u256b' // ╫
+ BoxDrawingsDoubleVerticalAndHorizontal rune = '\u256c' // ╬
+ BoxDrawingsLightArcDownAndRight rune = '\u256d' // ╭
+ BoxDrawingsLightArcDownAndLeft rune = '\u256e' // ╮
+ BoxDrawingsLightArcUpAndLeft rune = '\u256f' // ╯
+ BoxDrawingsLightArcUpAndRight rune = '\u2570' // ╰
+ BoxDrawingsLightDiagonalUpperRightToLowerLeft rune = '\u2571' // ╱
+ BoxDrawingsLightDiagonalUpperLeftToLowerRight rune = '\u2572' // ╲
+ BoxDrawingsLightDiagonalCross rune = '\u2573' // ╳
+ BoxDrawingsLightLeft rune = '\u2574' // ╴
+ BoxDrawingsLightUp rune = '\u2575' // ╵
+ BoxDrawingsLightRight rune = '\u2576' // ╶
+ BoxDrawingsLightDown rune = '\u2577' // ╷
+ BoxDrawingsHeavyLeft rune = '\u2578' // ╸
+ BoxDrawingsHeavyUp rune = '\u2579' // ╹
+ BoxDrawingsHeavyRight rune = '\u257a' // ╺
+ BoxDrawingsHeavyDown rune = '\u257b' // ╻
+ BoxDrawingsLightLeftAndHeavyRight rune = '\u257c' // ╼
+ BoxDrawingsLightUpAndHeavyDown rune = '\u257d' // ╽
+ BoxDrawingsHeavyLeftAndLightRight rune = '\u257e' // ╾
+ BoxDrawingsHeavyUpAndLightDown rune = '\u257f' // ╿
+)
+
+// SemigraphicJoints is a map for joining semigraphic (or otherwise) runes.
+// So far only light lines are supported but if you want to change the border
+// styling you need to provide the joints, too.
+// The matching will be sorted ascending by rune value, so you don't need to
+// provide all rune combinations,
+// e.g. (─) + (│) = (┼) will also match (│) + (─) = (┼)
+var SemigraphicJoints = map[string]rune{
+ // (─) + (│) = (┼)
+ string([]rune{BoxDrawingsLightHorizontal, BoxDrawingsLightVertical}): BoxDrawingsLightVerticalAndHorizontal,
+ // (─) + (┌) = (┬)
+ string([]rune{BoxDrawingsLightHorizontal, BoxDrawingsLightDownAndRight}): BoxDrawingsLightDownAndHorizontal,
+ // (─) + (┐) = (┬)
+ string([]rune{BoxDrawingsLightHorizontal, BoxDrawingsLightDownAndLeft}): BoxDrawingsLightDownAndHorizontal,
+ // (─) + (└) = (┴)
+ string([]rune{BoxDrawingsLightHorizontal, BoxDrawingsLightUpAndRight}): BoxDrawingsLightUpAndHorizontal,
+ // (─) + (┘) = (┴)
+ string([]rune{BoxDrawingsLightHorizontal, BoxDrawingsLightUpAndLeft}): BoxDrawingsLightUpAndHorizontal,
+ // (─) + (├) = (┼)
+ string([]rune{BoxDrawingsLightHorizontal, BoxDrawingsLightVerticalAndRight}): BoxDrawingsLightVerticalAndHorizontal,
+ // (─) + (┤) = (┼)
+ string([]rune{BoxDrawingsLightHorizontal, BoxDrawingsLightVerticalAndLeft}): BoxDrawingsLightVerticalAndHorizontal,
+ // (─) + (┬) = (┬)
+ string([]rune{BoxDrawingsLightHorizontal, BoxDrawingsLightDownAndHorizontal}): BoxDrawingsLightDownAndHorizontal,
+ // (─) + (┴) = (┴)
+ string([]rune{BoxDrawingsLightHorizontal, BoxDrawingsLightUpAndHorizontal}): BoxDrawingsLightUpAndHorizontal,
+ // (─) + (┼) = (┼)
+ string([]rune{BoxDrawingsLightHorizontal, BoxDrawingsLightVerticalAndHorizontal}): BoxDrawingsLightVerticalAndHorizontal,
+
+ // (│) + (┌) = (├)
+ string([]rune{BoxDrawingsLightVertical, BoxDrawingsLightDownAndRight}): BoxDrawingsLightVerticalAndRight,
+ // (│) + (┐) = (┤)
+ string([]rune{BoxDrawingsLightVertical, BoxDrawingsLightDownAndLeft}): BoxDrawingsLightVerticalAndLeft,
+ // (│) + (└) = (├)
+ string([]rune{BoxDrawingsLightVertical, BoxDrawingsLightUpAndRight}): BoxDrawingsLightVerticalAndRight,
+ // (│) + (┘) = (┤)
+ string([]rune{BoxDrawingsLightVertical, BoxDrawingsLightUpAndLeft}): BoxDrawingsLightVerticalAndLeft,
+ // (│) + (├) = (├)
+ string([]rune{BoxDrawingsLightVertical, BoxDrawingsLightVerticalAndRight}): BoxDrawingsLightVerticalAndRight,
+ // (│) + (┤) = (┤)
+ string([]rune{BoxDrawingsLightVertical, BoxDrawingsLightVerticalAndLeft}): BoxDrawingsLightVerticalAndLeft,
+ // (│) + (┬) = (┼)
+ string([]rune{BoxDrawingsLightVertical, BoxDrawingsLightDownAndHorizontal}): BoxDrawingsLightVerticalAndHorizontal,
+ // (│) + (┴) = (┼)
+ string([]rune{BoxDrawingsLightVertical, BoxDrawingsLightUpAndHorizontal}): BoxDrawingsLightVerticalAndHorizontal,
+ // (│) + (┼) = (┼)
+ string([]rune{BoxDrawingsLightVertical, BoxDrawingsLightVerticalAndHorizontal}): BoxDrawingsLightVerticalAndHorizontal,
+
+ // (┌) + (┐) = (┬)
+ string([]rune{BoxDrawingsLightDownAndRight, BoxDrawingsLightDownAndLeft}): BoxDrawingsLightDownAndHorizontal,
+ // (┌) + (└) = (├)
+ string([]rune{BoxDrawingsLightDownAndRight, BoxDrawingsLightUpAndRight}): BoxDrawingsLightVerticalAndRight,
+ // (┌) + (┘) = (┼)
+ string([]rune{BoxDrawingsLightDownAndRight, BoxDrawingsLightUpAndLeft}): BoxDrawingsLightVerticalAndHorizontal,
+ // (┌) + (├) = (├)
+ string([]rune{BoxDrawingsLightDownAndRight, BoxDrawingsLightVerticalAndRight}): BoxDrawingsLightVerticalAndRight,
+ // (┌) + (┤) = (┼)
+ string([]rune{BoxDrawingsLightDownAndRight, BoxDrawingsLightVerticalAndLeft}): BoxDrawingsLightVerticalAndHorizontal,
+ // (┌) + (┬) = (┬)
+ string([]rune{BoxDrawingsLightDownAndRight, BoxDrawingsLightDownAndHorizontal}): BoxDrawingsLightDownAndHorizontal,
+ // (┌) + (┴) = (┼)
+ string([]rune{BoxDrawingsLightDownAndRight, BoxDrawingsLightUpAndHorizontal}): BoxDrawingsLightVerticalAndHorizontal,
+ // (┌) + (┴) = (┼)
+ string([]rune{BoxDrawingsLightDownAndRight, BoxDrawingsLightVerticalAndHorizontal}): BoxDrawingsLightVerticalAndHorizontal,
+
+ // (┐) + (└) = (┼)
+ string([]rune{BoxDrawingsLightDownAndLeft, BoxDrawingsLightUpAndRight}): BoxDrawingsLightVerticalAndHorizontal,
+ // (┐) + (┘) = (┤)
+ string([]rune{BoxDrawingsLightDownAndLeft, BoxDrawingsLightUpAndLeft}): BoxDrawingsLightVerticalAndLeft,
+ // (┐) + (├) = (┼)
+ string([]rune{BoxDrawingsLightDownAndLeft, BoxDrawingsLightVerticalAndRight}): BoxDrawingsLightVerticalAndHorizontal,
+ // (┐) + (┤) = (┤)
+ string([]rune{BoxDrawingsLightDownAndLeft, BoxDrawingsLightVerticalAndLeft}): BoxDrawingsLightVerticalAndLeft,
+ // (┐) + (┬) = (┬)
+ string([]rune{BoxDrawingsLightDownAndLeft, BoxDrawingsLightDownAndHorizontal}): BoxDrawingsLightDownAndHorizontal,
+ // (┐) + (┴) = (┼)
+ string([]rune{BoxDrawingsLightDownAndLeft, BoxDrawingsLightUpAndHorizontal}): BoxDrawingsLightVerticalAndHorizontal,
+ // (┐) + (┼) = (┼)
+ string([]rune{BoxDrawingsLightDownAndLeft, BoxDrawingsLightVerticalAndHorizontal}): BoxDrawingsLightVerticalAndHorizontal,
+
+ // (└) + (┘) = (┴)
+ string([]rune{BoxDrawingsLightUpAndRight, BoxDrawingsLightUpAndLeft}): BoxDrawingsLightUpAndHorizontal,
+ // (└) + (├) = (├)
+ string([]rune{BoxDrawingsLightUpAndRight, BoxDrawingsLightVerticalAndRight}): BoxDrawingsLightVerticalAndRight,
+ // (└) + (┤) = (┼)
+ string([]rune{BoxDrawingsLightUpAndRight, BoxDrawingsLightVerticalAndLeft}): BoxDrawingsLightVerticalAndHorizontal,
+ // (└) + (┬) = (┼)
+ string([]rune{BoxDrawingsLightUpAndRight, BoxDrawingsLightDownAndHorizontal}): BoxDrawingsLightVerticalAndHorizontal,
+ // (└) + (┴) = (┴)
+ string([]rune{BoxDrawingsLightUpAndRight, BoxDrawingsLightUpAndHorizontal}): BoxDrawingsLightUpAndHorizontal,
+ // (└) + (┼) = (┼)
+ string([]rune{BoxDrawingsLightUpAndRight, BoxDrawingsLightVerticalAndHorizontal}): BoxDrawingsLightVerticalAndHorizontal,
+
+ // (┘) + (├) = (┼)
+ string([]rune{BoxDrawingsLightUpAndLeft, BoxDrawingsLightVerticalAndRight}): BoxDrawingsLightVerticalAndHorizontal,
+ // (┘) + (┤) = (┤)
+ string([]rune{BoxDrawingsLightUpAndLeft, BoxDrawingsLightVerticalAndLeft}): BoxDrawingsLightVerticalAndLeft,
+ // (┘) + (┬) = (┼)
+ string([]rune{BoxDrawingsLightUpAndLeft, BoxDrawingsLightDownAndHorizontal}): BoxDrawingsLightVerticalAndHorizontal,
+ // (┘) + (┴) = (┴)
+ string([]rune{BoxDrawingsLightUpAndLeft, BoxDrawingsLightUpAndHorizontal}): BoxDrawingsLightUpAndHorizontal,
+ // (┘) + (┼) = (┼)
+ string([]rune{BoxDrawingsLightUpAndLeft, BoxDrawingsLightVerticalAndHorizontal}): BoxDrawingsLightVerticalAndHorizontal,
+
+ // (├) + (┤) = (┼)
+ string([]rune{BoxDrawingsLightVerticalAndRight, BoxDrawingsLightVerticalAndLeft}): BoxDrawingsLightVerticalAndHorizontal,
+ // (├) + (┬) = (┼)
+ string([]rune{BoxDrawingsLightVerticalAndRight, BoxDrawingsLightDownAndHorizontal}): BoxDrawingsLightVerticalAndHorizontal,
+ // (├) + (┴) = (┼)
+ string([]rune{BoxDrawingsLightVerticalAndRight, BoxDrawingsLightUpAndHorizontal}): BoxDrawingsLightVerticalAndHorizontal,
+ // (├) + (┼) = (┼)
+ string([]rune{BoxDrawingsLightVerticalAndRight, BoxDrawingsLightVerticalAndHorizontal}): BoxDrawingsLightVerticalAndHorizontal,
+
+ // (┤) + (┬) = (┼)
+ string([]rune{BoxDrawingsLightVerticalAndLeft, BoxDrawingsLightDownAndHorizontal}): BoxDrawingsLightVerticalAndHorizontal,
+ // (┤) + (┴) = (┼)
+ string([]rune{BoxDrawingsLightVerticalAndLeft, BoxDrawingsLightUpAndHorizontal}): BoxDrawingsLightVerticalAndHorizontal,
+ // (┤) + (┼) = (┼)
+ string([]rune{BoxDrawingsLightVerticalAndLeft, BoxDrawingsLightVerticalAndHorizontal}): BoxDrawingsLightVerticalAndHorizontal,
+
+ // (┬) + (┴) = (┼)
+ string([]rune{BoxDrawingsLightDownAndHorizontal, BoxDrawingsLightUpAndHorizontal}): BoxDrawingsLightVerticalAndHorizontal,
+ // (┬) + (┼) = (┼)
+ string([]rune{BoxDrawingsLightDownAndHorizontal, BoxDrawingsLightVerticalAndHorizontal}): BoxDrawingsLightVerticalAndHorizontal,
+
+ // (┴) + (┼) = (┼)
+ string([]rune{BoxDrawingsLightUpAndHorizontal, BoxDrawingsLightVerticalAndHorizontal}): BoxDrawingsLightVerticalAndHorizontal,
+}
+
+// PrintJoinedSemigraphics prints a semigraphics rune into the screen at the given
+// position with the given color, joining it with any existing semigraphics
+// rune. Background colors are preserved. At this point, only regular single
+// line borders are supported.
+func PrintJoinedSemigraphics(screen tcell.Screen, x, y int, ch rune, color tcell.Color) {
+ previous, _, style, _ := screen.GetContent(x, y)
+ style = style.Foreground(color)
+
+ // What's the resulting rune?
+ var result rune
+ if ch == previous {
+ result = ch
+ } else {
+ if ch < previous {
+ previous, ch = ch, previous
+ }
+ result = SemigraphicJoints[string([]rune{previous, ch})]
+ }
+ if result == 0 {
+ result = ch
+ }
+
+ // We only print something if we have something.
+ screen.SetContent(x, y, result, nil, style)
+}
diff --git a/vendor/maunium.net/go/tview/table.go b/vendor/maunium.net/go/tview/table.go
index 2491ec7..6c636e7 100644
--- a/vendor/maunium.net/go/tview/table.go
+++ b/vendor/maunium.net/go/tview/table.go
@@ -231,6 +231,10 @@ type Table struct {
// The number of visible rows the last time the table was drawn.
visibleRows int
+ // The style of the selected rows. If this value is 0, selected rows are
+ // simply inverted.
+ selectedStyle tcell.Style
+
// An optional function which gets called when the user presses Enter on a
// selected cell. If entire rows selected, the column value is undefined.
// Likewise for entire columns.
@@ -276,9 +280,21 @@ func (t *Table) SetBordersColor(color tcell.Color) *Table {
return t
}
+// SetSelectedStyle sets a specific style for selected cells. If no such style
+// is set, per default, selected cells are inverted (i.e. their foreground and
+// background colors are swapped).
+//
+// To reset a previous setting to its default, make the following call:
+//
+// table.SetSelectedStyle(tcell.ColorDefault, tcell.ColorDefault, 0)
+func (t *Table) SetSelectedStyle(foregroundColor, backgroundColor tcell.Color, attributes tcell.AttrMask) *Table {
+ t.selectedStyle = tcell.StyleDefault.Foreground(foregroundColor).Background(backgroundColor) | tcell.Style(attributes)
+ return t
+}
+
// SetSeparator sets the character used to fill the space between two
// neighboring cells. This is a space character ' ' per default but you may
-// want to set it to GraphicsVertBar (or any other rune) if the column
+// want to set it to Borders.Vertical (or any other rune) if the column
// separation should be more visible. If cell borders are activated, this is
// ignored.
//
@@ -373,7 +389,7 @@ func (t *Table) SetDoneFunc(handler func(key tcell.Key)) *Table {
}
// SetCell sets the content of a cell the specified position. It is ok to
-// directly instantiate a TableCell object. If the cell has contain, at least
+// directly instantiate a TableCell object. If the cell has content, at least
// the Text and Color fields should be set.
//
// Note that setting cells in previously unknown rows and columns will
@@ -406,7 +422,7 @@ func (t *Table) SetCellSimple(row, column int, text string) *Table {
}
// GetCell returns the contents of the cell at the specified position. A valid
-// TableCell object is always returns but it will be uninitialized if the cell
+// TableCell object is always returned but it will be uninitialized if the cell
// was not previously set.
func (t *Table) GetCell(row, column int) *TableCell {
if row >= len(t.cells) || column >= len(t.cells[row]) {
@@ -415,6 +431,31 @@ func (t *Table) GetCell(row, column int) *TableCell {
return t.cells[row][column]
}
+// RemoveRow removes the row at the given position from the table. If there is
+// no such row, this has no effect.
+func (t *Table) RemoveRow(row int) *Table {
+ if row < 0 || row >= len(t.cells) {
+ return t
+ }
+
+ t.cells = append(t.cells[:row], t.cells[row+1:]...)
+
+ return t
+}
+
+// RemoveColumn removes the column at the given position from the table. If
+// there is no such column, this has no effect.
+func (t *Table) RemoveColumn(column int) *Table {
+ for row := range t.cells {
+ if column < 0 || column >= len(t.cells[row]) {
+ continue
+ }
+ t.cells[row] = append(t.cells[row][:column], t.cells[row][column+1:]...)
+ }
+
+ return t
+}
+
// GetRowCount returns the number of rows in the table.
func (t *Table) GetRowCount() int {
return len(t.cells)
@@ -644,7 +685,6 @@ ColumnLoop:
}
expWidth := toDistribute * expansion / expansionTotal
widths[index] += expWidth
- tableWidth += expWidth
toDistribute -= expWidth
expansionTotal -= expansion
}
@@ -668,24 +708,24 @@ ColumnLoop:
// Draw borders.
rowY *= 2
for pos := 0; pos < columnWidth && columnX+1+pos < width; pos++ {
- drawBorder(columnX+pos+1, rowY, GraphicsHoriBar)
+ drawBorder(columnX+pos+1, rowY, Borders.Horizontal)
}
- ch := GraphicsCross
+ ch := Borders.Cross
if columnIndex == 0 {
if rowY == 0 {
- ch = GraphicsTopLeftCorner
+ ch = Borders.TopLeft
} else {
- ch = GraphicsLeftT
+ ch = Borders.LeftT
}
} else if rowY == 0 {
- ch = GraphicsTopT
+ ch = Borders.TopT
}
drawBorder(columnX, rowY, ch)
rowY++
if rowY >= height {
break // No space for the text anymore.
}
- drawBorder(columnX, rowY, GraphicsVertBar)
+ drawBorder(columnX, rowY, Borders.Vertical)
} else if columnIndex > 0 {
// Draw separator.
drawBorder(columnX, rowY, t.separator)
@@ -706,18 +746,18 @@ ColumnLoop:
_, printed := printWithStyle(screen, cell.Text, x+columnX+1, y+rowY, finalWidth, cell.Align, tcell.StyleDefault.Foreground(cell.Color)|tcell.Style(cell.Attributes))
if StringWidth(cell.Text)-printed > 0 && printed > 0 {
_, _, style, _ := screen.GetContent(x+columnX+1+finalWidth-1, y+rowY)
- printWithStyle(screen, string(GraphicsEllipsis), x+columnX+1+finalWidth-1, y+rowY, 1, AlignLeft, style)
+ printWithStyle(screen, string(SemigraphicsHorizontalEllipsis), x+columnX+1+finalWidth-1, y+rowY, 1, AlignLeft, style)
}
}
// Draw bottom border.
if rowY := 2 * len(rows); t.borders && rowY < height {
for pos := 0; pos < columnWidth && columnX+1+pos < width; pos++ {
- drawBorder(columnX+pos+1, rowY, GraphicsHoriBar)
+ drawBorder(columnX+pos+1, rowY, Borders.Horizontal)
}
- ch := GraphicsBottomT
+ ch := Borders.BottomT
if columnIndex == 0 {
- ch = GraphicsBottomLeftCorner
+ ch = Borders.BottomLeft
}
drawBorder(columnX, rowY, ch)
}
@@ -730,26 +770,31 @@ ColumnLoop:
for rowY := range rows {
rowY *= 2
if rowY+1 < height {
- drawBorder(columnX, rowY+1, GraphicsVertBar)
+ drawBorder(columnX, rowY+1, Borders.Vertical)
}
- ch := GraphicsRightT
+ ch := Borders.RightT
if rowY == 0 {
- ch = GraphicsTopRightCorner
+ ch = Borders.TopRight
}
drawBorder(columnX, rowY, ch)
}
if rowY := 2 * len(rows); rowY < height {
- drawBorder(columnX, rowY, GraphicsBottomRightCorner)
+ drawBorder(columnX, rowY, Borders.BottomRight)
}
}
// Helper function which colors the background of a box.
- colorBackground := func(fromX, fromY, w, h int, backgroundColor, textColor tcell.Color, selected bool) {
+ // backgroundColor == tcell.ColorDefault => Don't color the background.
+ // textColor == tcell.ColorDefault => Don't change the text color.
+ // attr == 0 => Don't change attributes.
+ // invert == true => Ignore attr, set text to backgroundColor or t.backgroundColor;
+ // set background to textColor.
+ colorBackground := func(fromX, fromY, w, h int, backgroundColor, textColor tcell.Color, attr tcell.AttrMask, invert bool) {
for by := 0; by < h && fromY+by < y+height; by++ {
for bx := 0; bx < w && fromX+bx < x+width; bx++ {
m, c, style, _ := screen.GetContent(fromX+bx, fromY+by)
- if selected {
- fg, _, _ := style.Decompose()
+ fg, bg, a := style.Decompose()
+ if invert {
if fg == textColor || fg == t.bordersColor {
fg = backgroundColor
}
@@ -758,10 +803,16 @@ ColumnLoop:
}
style = style.Background(textColor).Foreground(fg)
} else {
- if backgroundColor == tcell.ColorDefault {
- continue
+ if backgroundColor != tcell.ColorDefault {
+ bg = backgroundColor
}
- style = style.Background(backgroundColor)
+ if textColor != tcell.ColorDefault {
+ fg = textColor
+ }
+ if attr != 0 {
+ a = attr
+ }
+ style = style.Background(bg).Foreground(fg) | tcell.Style(a)
}
screen.SetContent(fromX+bx, fromY+by, m, c, style)
}
@@ -770,11 +821,12 @@ ColumnLoop:
// Color the cell backgrounds. To avoid undesirable artefacts, we combine
// the drawing of a cell by background color, selected cells last.
- cellsByBackgroundColor := make(map[tcell.Color][]*struct {
+ type cellInfo struct {
x, y, w, h int
text tcell.Color
selected bool
- })
+ }
+ cellsByBackgroundColor := make(map[tcell.Color][]*cellInfo)
var backgroundColors []tcell.Color
for rowY, row := range rows {
columnX := 0
@@ -794,11 +846,7 @@ ColumnLoop:
columnSelected := t.columnsSelectable && !t.rowsSelectable && column == t.selectedColumn
cellSelected := !cell.NotSelectable && (columnSelected || rowSelected || t.rowsSelectable && t.columnsSelectable && column == t.selectedColumn && row == t.selectedRow)
entries, ok := cellsByBackgroundColor[cell.BackgroundColor]
- cellsByBackgroundColor[cell.BackgroundColor] = append(entries, &struct {
- x, y, w, h int
- text tcell.Color
- selected bool
- }{
+ cellsByBackgroundColor[cell.BackgroundColor] = append(entries, &cellInfo{
x: bx,
y: by,
w: bw,
@@ -822,13 +870,18 @@ ColumnLoop:
_, _, lj := c.Hcl()
return li < lj
})
+ selFg, selBg, selAttr := t.selectedStyle.Decompose()
for _, bgColor := range backgroundColors {
entries := cellsByBackgroundColor[bgColor]
for _, cell := range entries {
if cell.selected {
- defer colorBackground(cell.x, cell.y, cell.w, cell.h, bgColor, cell.text, true)
+ if t.selectedStyle != 0 {
+ defer colorBackground(cell.x, cell.y, cell.w, cell.h, selBg, selFg, selAttr, false)
+ } else {
+ defer colorBackground(cell.x, cell.y, cell.w, cell.h, bgColor, cell.text, 0, true)
+ }
} else {
- colorBackground(cell.x, cell.y, cell.w, cell.h, bgColor, cell.text, false)
+ colorBackground(cell.x, cell.y, cell.w, cell.h, bgColor, tcell.ColorDefault, 0, false)
}
}
}
diff --git a/vendor/maunium.net/go/tview/textview.go b/vendor/maunium.net/go/tview/textview.go
index 44aeb1e..63d9796 100644
--- a/vendor/maunium.net/go/tview/textview.go
+++ b/vendor/maunium.net/go/tview/textview.go
@@ -31,7 +31,7 @@ type textViewIndex struct {
// TextView is a box which displays text. It implements the io.Writer interface
// so you can stream text to it. This does not trigger a redraw automatically
// but if a handler is installed via SetChangedFunc(), you can cause it to be
-// redrawn.
+// redrawn. (See SetChangedFunc() for more details.)
//
// Navigation
//
@@ -103,6 +103,10 @@ type TextView struct {
// during re-indexing. Set to -1 if there is no current highlight.
fromHighlight, toHighlight int
+ // The screen space column of the highlight in its first line. Set to -1 if
+ // there is no current highlight.
+ posHighlight int
+
// A set of region IDs that are currently highlighted.
highlights map[string]struct{}
@@ -170,6 +174,7 @@ func NewTextView() *TextView {
align: AlignLeft,
wrap: true,
textColor: Styles.PrimaryTextColor,
+ regions: false,
dynamicColors: false,
}
}
@@ -255,8 +260,20 @@ func (t *TextView) SetRegions(regions bool) *TextView {
}
// SetChangedFunc sets a handler function which is called when the text of the
-// text view has changed. This is typically used to cause the application to
-// redraw the screen.
+// text view has changed. This is useful when text is written to this io.Writer
+// in a separate goroutine. This does not automatically cause the screen to be
+// refreshed so you may want to use the "changed" handler to redraw the screen.
+//
+// Note that to avoid race conditions or deadlocks, there are a few rules you
+// should follow:
+//
+// - You can call Application.Draw() from this handler.
+// - You can call TextView.HasFocus() from this handler.
+// - During the execution of this handler, access to any other variables from
+// this primitive or any other primitive should be queued using
+// Application.QueueUpdate().
+//
+// See package description for details on dealing with concurrency.
func (t *TextView) SetChangedFunc(handler func()) *TextView {
t.changed = handler
return t
@@ -270,6 +287,16 @@ func (t *TextView) SetDoneFunc(handler func(key tcell.Key)) *TextView {
return t
}
+// ScrollTo scrolls to the specified row and column (both starting with 0).
+func (t *TextView) ScrollTo(row, column int) *TextView {
+ if !t.scrollable {
+ return t
+ }
+ t.lineOffset = row
+ t.columnOffset = column
+ return t
+}
+
// ScrollToBeginning scrolls to the top left corner of the text if the text view
// is scrollable.
func (t *TextView) ScrollToBeginning() *TextView {
@@ -294,6 +321,12 @@ func (t *TextView) ScrollToEnd() *TextView {
return t
}
+// GetScrollOffset returns the number of rows and columns that are skipped at
+// the top left corner when the text view has been scrolled.
+func (t *TextView) GetScrollOffset() (row, column int) {
+ return t.lineOffset, t.columnOffset
+}
+
// Clear removes all text from the buffer.
func (t *TextView) Clear() *TextView {
t.buffer = nil
@@ -420,13 +453,33 @@ func (t *TextView) GetRegionText(regionID string) string {
return escapePattern.ReplaceAllString(buffer.String(), `[$1$2]`)
}
+// Focus is called when this primitive receives focus.
+func (t *TextView) Focus(delegate func(p Primitive)) {
+ // Implemented here with locking because this is used by layout primitives.
+ t.Lock()
+ defer t.Unlock()
+ t.hasFocus = true
+}
+
+// HasFocus returns whether or not this primitive has focus.
+func (t *TextView) HasFocus() bool {
+ // Implemented here with locking because this may be used in the "changed"
+ // callback.
+ t.Lock()
+ defer t.Unlock()
+ return t.hasFocus
+}
+
// Write lets us implement the io.Writer interface. Tab characters will be
// replaced with TabSize space characters. A "\n" or "\r\n" will be interpreted
// as a new line.
func (t *TextView) Write(p []byte) (n int, err error) {
// Notify at the end.
- if t.changed != nil {
- defer t.changed()
+ t.Lock()
+ changed := t.changed
+ t.Unlock()
+ if changed != nil {
+ defer changed() // Deadlocks may occur if we lock here.
}
t.Lock()
@@ -492,7 +545,7 @@ func (t *TextView) reindexBuffer(width int) {
return // Nothing has changed. We can still use the current index.
}
t.index = nil
- t.fromHighlight, t.toHighlight = -1, -1
+ t.fromHighlight, t.toHighlight, t.posHighlight = -1, -1, -1
// If there's no space, there's no index.
if width < 1 {
@@ -511,8 +564,9 @@ func (t *TextView) reindexBuffer(width int) {
colorTags [][]string
escapeIndices [][]int
)
+ strippedStr := str
if t.dynamicColors {
- colorTagIndices, colorTags, escapeIndices, str, _ = decomposeString(str)
+ colorTagIndices, colorTags, escapeIndices, strippedStr, _ = decomposeString(str)
}
// Find all regions in this line. Then remove them.
@@ -523,14 +577,12 @@ func (t *TextView) reindexBuffer(width int) {
if t.regions {
regionIndices = regionPattern.FindAllStringIndex(str, -1)
regions = regionPattern.FindAllStringSubmatch(str, -1)
- str = regionPattern.ReplaceAllString(str, "")
- if !t.dynamicColors {
- // We haven't detected escape tags yet. Do it now.
- escapeIndices = escapePattern.FindAllStringIndex(str, -1)
- str = escapePattern.ReplaceAllString(str, "[$1$2]")
- }
+ strippedStr = regionPattern.ReplaceAllString(strippedStr, "")
}
+ // We don't need the original string anymore for now.
+ str = strippedStr
+
// Split the line if required.
var splitLines []string
if t.wrap && len(str) > 0 {
@@ -574,15 +626,53 @@ func (t *TextView) reindexBuffer(width int) {
// Shift original position with tags.
lineLength := len(splitLine)
+ remainingLength := lineLength
+ tagEnd := originalPos
+ totalTagLength := 0
for {
- if colorPos < len(colorTagIndices) && colorTagIndices[colorPos][0] <= originalPos+lineLength {
+ // Which tag comes next?
+ nextTag := make([][3]int, 0, 3)
+ if colorPos < len(colorTagIndices) {
+ nextTag = append(nextTag, [3]int{colorTagIndices[colorPos][0], colorTagIndices[colorPos][1], 0}) // 0 = color tag.
+ }
+ if regionPos < len(regionIndices) {
+ nextTag = append(nextTag, [3]int{regionIndices[regionPos][0], regionIndices[regionPos][1], 1}) // 1 = region tag.
+ }
+ if escapePos < len(escapeIndices) {
+ nextTag = append(nextTag, [3]int{escapeIndices[escapePos][0], escapeIndices[escapePos][1], 2}) // 2 = escape tag.
+ }
+ minPos := -1
+ tagIndex := -1
+ for index, pair := range nextTag {
+ if minPos < 0 || pair[0] < minPos {
+ minPos = pair[0]
+ tagIndex = index
+ }
+ }
+
+ // Is the next tag in range?
+ if tagIndex < 0 || minPos >= tagEnd+remainingLength {
+ break // No. We're done with this line.
+ }
+
+ // Advance.
+ strippedTagStart := nextTag[tagIndex][0] - originalPos - totalTagLength
+ tagEnd = nextTag[tagIndex][1]
+ tagLength := tagEnd - nextTag[tagIndex][0]
+ if nextTag[tagIndex][2] == 2 {
+ tagLength = 1
+ }
+ totalTagLength += tagLength
+ remainingLength = lineLength - (tagEnd - originalPos - totalTagLength)
+
+ // Process the tag.
+ switch nextTag[tagIndex][2] {
+ case 0:
// Process color tags.
- originalPos += colorTagIndices[colorPos][1] - colorTagIndices[colorPos][0]
foregroundColor, backgroundColor, attributes = styleFromTag(foregroundColor, backgroundColor, attributes, colorTags[colorPos])
colorPos++
- } else if regionPos < len(regionIndices) && regionIndices[regionPos][0] <= originalPos+lineLength {
+ case 1:
// Process region tags.
- originalPos += regionIndices[regionPos][1] - regionIndices[regionPos][0]
regionID = regions[regionPos][1]
_, highlighted = t.highlights[regionID]
@@ -591,23 +681,21 @@ func (t *TextView) reindexBuffer(width int) {
line := len(t.index)
if t.fromHighlight < 0 {
t.fromHighlight, t.toHighlight = line, line
+ t.posHighlight = runewidth.StringWidth(splitLine[:strippedTagStart])
} else if line > t.toHighlight {
t.toHighlight = line
}
}
regionPos++
- } else if escapePos < len(escapeIndices) && escapeIndices[escapePos][0] <= originalPos+lineLength {
+ case 2:
// Process escape tags.
- originalPos++
escapePos++
- } else {
- break
}
}
// Advance to next line.
- originalPos += lineLength
+ originalPos += lineLength + totalTagLength
// Append this line.
line.NextPos = originalPos
@@ -649,7 +737,7 @@ func (t *TextView) Draw(screen tcell.Screen) {
t.pageSize = height
// If the width has changed, we need to reindex.
- if width != t.lastWidth {
+ if width != t.lastWidth && t.wrap {
t.index = nil
}
t.lastWidth = width
@@ -672,6 +760,16 @@ func (t *TextView) Draw(screen tcell.Screen) {
// No, let's move to the start of the highlights.
t.lineOffset = t.fromHighlight
}
+
+ // If the highlight is too far to the right, move it to the middle.
+ if t.posHighlight-t.columnOffset > 3*width/4 {
+ t.columnOffset = t.posHighlight - width/2
+ }
+
+ // If the highlight is off-screen on the left, move it on-screen.
+ if t.posHighlight-t.columnOffset < 0 {
+ t.columnOffset = t.posHighlight - width/4
+ }
}
t.scrollToHighlights = false
@@ -737,8 +835,9 @@ func (t *TextView) Draw(screen tcell.Screen) {
colorTags [][]string
escapeIndices [][]int
)
+ strippedText := text
if t.dynamicColors {
- colorTagIndices, colorTags, escapeIndices, _, _ = decomposeString(text)
+ colorTagIndices, colorTags, escapeIndices, strippedText, _ = decomposeString(text)
}
// Get regions.
@@ -749,8 +848,10 @@ func (t *TextView) Draw(screen tcell.Screen) {
if t.regions {
regionIndices = regionPattern.FindAllStringIndex(text, -1)
regions = regionPattern.FindAllStringSubmatch(text, -1)
+ strippedText = regionPattern.ReplaceAllString(strippedText, "")
if !t.dynamicColors {
escapeIndices = escapePattern.FindAllStringIndex(text, -1)
+ strippedText = string(escapePattern.ReplaceAllString(strippedText, "[$1$2]"))
}
}
@@ -769,11 +870,29 @@ func (t *TextView) Draw(screen tcell.Screen) {
}
// Print the line.
- var currentTag, currentRegion, currentEscapeTag, skipped, runeSeqWidth int
- runeSequence := make([]rune, 0, 10)
- flush := func() {
- if len(runeSequence) == 0 {
- return
+ var colorPos, regionPos, escapePos, tagOffset, skipped int
+ iterateString(strippedText, func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool {
+ // Process tags.
+ for {
+ if colorPos < len(colorTags) && textPos+tagOffset >= colorTagIndices[colorPos][0] && textPos+tagOffset < colorTagIndices[colorPos][1] {
+ // Get the color.
+ foregroundColor, backgroundColor, attributes = styleFromTag(foregroundColor, backgroundColor, attributes, colorTags[colorPos])
+ tagOffset += colorTagIndices[colorPos][1] - colorTagIndices[colorPos][0]
+ colorPos++
+ } else if regionPos < len(regionIndices) && textPos+tagOffset >= regionIndices[regionPos][0] && textPos+tagOffset < regionIndices[regionPos][1] {
+ // Get the region.
+ regionID = regions[regionPos][1]
+ tagOffset += regionIndices[regionPos][1] - regionIndices[regionPos][0]
+ regionPos++
+ } else {
+ break
+ }
+ }
+
+ // Skip the second-to-last character of an escape tag.
+ if escapePos < len(escapeIndices) && textPos+tagOffset == escapeIndices[escapePos][1]-2 {
+ tagOffset++
+ escapePos++
}
// Mix the existing style with the new style.
@@ -803,87 +922,30 @@ func (t *TextView) Draw(screen tcell.Screen) {
style = style.Background(fg).Foreground(bg)
}
- // Draw the character.
- var comb []rune
- if len(runeSequence) > 1 {
- // Allocate space for the combining characters only when necessary.
- comb = make([]rune, len(runeSequence)-1)
- copy(comb, runeSequence[1:])
- }
- for offset := 0; offset < runeSeqWidth; offset++ {
- screen.SetContent(x+posX+offset, y+line-t.lineOffset, runeSequence[0], comb, style)
- }
-
- // Advance.
- posX += runeSeqWidth
- runeSequence = runeSequence[:0]
- runeSeqWidth = 0
- }
- for pos, ch := range text {
- // Get the color.
- if currentTag < len(colorTags) && pos >= colorTagIndices[currentTag][0] && pos < colorTagIndices[currentTag][1] {
- flush()
- if pos == colorTagIndices[currentTag][1]-1 {
- foregroundColor, backgroundColor, attributes = styleFromTag(foregroundColor, backgroundColor, attributes, colorTags[currentTag])
- currentTag++
- }
- continue
- }
-
- // Get the region.
- if currentRegion < len(regionIndices) && pos >= regionIndices[currentRegion][0] && pos < regionIndices[currentRegion][1] {
- flush()
- if pos == regionIndices[currentRegion][1]-1 {
- regionID = regions[currentRegion][1]
- currentRegion++
- }
- continue
- }
-
- // Skip the second-to-last character of an escape tag.
- if currentEscapeTag < len(escapeIndices) && pos >= escapeIndices[currentEscapeTag][0] && pos < escapeIndices[currentEscapeTag][1] {
- flush()
- if pos == escapeIndices[currentEscapeTag][1]-1 {
- currentEscapeTag++
- } else if pos == escapeIndices[currentEscapeTag][1]-2 {
- continue
- }
- }
-
- // Determine the width of this rune.
- chWidth := runewidth.RuneWidth(ch)
- if chWidth == 0 {
- // If this is not a modifier, we treat it as a space character.
- if len(runeSequence) == 0 {
- ch = ' '
- chWidth = 1
- } else {
- runeSequence = append(runeSequence, ch)
- continue
- }
- }
-
// Skip to the right.
if !t.wrap && skipped < skip {
- skipped += chWidth
- continue
+ skipped += screenWidth
+ return false
}
// Stop at the right border.
- if posX+runeSeqWidth+chWidth > width {
- break
+ if posX+screenWidth > width {
+ return true
}
- // Flush the rune sequence.
- flush()
+ // Draw the character.
+ for offset := screenWidth - 1; offset >= 0; offset-- {
+ if offset == 0 {
+ screen.SetContent(x+posX+offset, y+line-t.lineOffset, main, comb, style)
+ } else {
+ screen.SetContent(x+posX+offset, y+line-t.lineOffset, ' ', nil, style)
+ }
+ }
- // Queue this rune.
- runeSequence = append(runeSequence, ch)
- runeSeqWidth += chWidth
- }
- if posX+runeSeqWidth <= width {
- flush()
- }
+ // Advance.
+ posX += screenWidth
+ return false
+ })
}
// If this view is not scrollable, we'll purge the buffer of lines that have
diff --git a/vendor/maunium.net/go/tview/treeview.go b/vendor/maunium.net/go/tview/treeview.go
new file mode 100644
index 0000000..1b0af21
--- /dev/null
+++ b/vendor/maunium.net/go/tview/treeview.go
@@ -0,0 +1,684 @@
+package tview
+
+import (
+ "maunium.net/go/tcell"
+)
+
+// Tree navigation events.
+const (
+ treeNone int = iota
+ treeHome
+ treeEnd
+ treeUp
+ treeDown
+ treePageUp
+ treePageDown
+)
+
+// TreeNode represents one node in a tree view.
+type TreeNode struct {
+ // The reference object.
+ reference interface{}
+
+ // This node's child nodes.
+ children []*TreeNode
+
+ // The item's text.
+ text string
+
+ // The text color.
+ color tcell.Color
+
+ // Whether or not this node can be selected.
+ selectable bool
+
+ // Whether or not this node's children should be displayed.
+ expanded bool
+
+ // The additional horizontal indent of this node's text.
+ indent int
+
+ // An optional function which is called when the user selects this node.
+ selected func()
+
+ // Temporary member variables.
+ parent *TreeNode // The parent node (nil for the root).
+ level int // The hierarchy level (0 for the root, 1 for its children, and so on).
+ graphicsX int // The x-coordinate of the left-most graphics rune.
+ textX int // The x-coordinate of the first rune of the text.
+}
+
+// NewTreeNode returns a new tree node.
+func NewTreeNode(text string) *TreeNode {
+ return &TreeNode{
+ text: text,
+ color: Styles.PrimaryTextColor,
+ indent: 2,
+ expanded: true,
+ selectable: true,
+ }
+}
+
+// Walk traverses this node's subtree in depth-first, pre-order (NLR) order and
+// calls the provided callback function on each traversed node (which includes
+// this node) with the traversed node and its parent node (nil for this node).
+// The callback returns whether traversal should continue with the traversed
+// node's child nodes (true) or not recurse any deeper (false).
+func (n *TreeNode) Walk(callback func(node, parent *TreeNode) bool) *TreeNode {
+ n.parent = nil
+ nodes := []*TreeNode{n}
+ for len(nodes) > 0 {
+ // Pop the top node and process it.
+ node := nodes[len(nodes)-1]
+ nodes = nodes[:len(nodes)-1]
+ if !callback(node, node.parent) {
+ // Don't add any children.
+ continue
+ }
+
+ // Add children in reverse order.
+ for index := len(node.children) - 1; index >= 0; index-- {
+ node.children[index].parent = node
+ nodes = append(nodes, node.children[index])
+ }
+ }
+
+ return n
+}
+
+// SetReference allows you to store a reference of any type in this node. This
+// will allow you to establish a mapping between the TreeView hierarchy and your
+// internal tree structure.
+func (n *TreeNode) SetReference(reference interface{}) *TreeNode {
+ n.reference = reference
+ return n
+}
+
+// GetReference returns this node's reference object.
+func (n *TreeNode) GetReference() interface{} {
+ return n.reference
+}
+
+// SetChildren sets this node's child nodes.
+func (n *TreeNode) SetChildren(childNodes []*TreeNode) *TreeNode {
+ n.children = childNodes
+ return n
+}
+
+// GetChildren returns this node's children.
+func (n *TreeNode) GetChildren() []*TreeNode {
+ return n.children
+}
+
+// ClearChildren removes all child nodes from this node.
+func (n *TreeNode) ClearChildren() *TreeNode {
+ n.children = nil
+ return n
+}
+
+// AddChild adds a new child node to this node.
+func (n *TreeNode) AddChild(node *TreeNode) *TreeNode {
+ n.children = append(n.children, node)
+ return n
+}
+
+// SetSelectable sets a flag indicating whether this node can be selected by
+// the user.
+func (n *TreeNode) SetSelectable(selectable bool) *TreeNode {
+ n.selectable = selectable
+ return n
+}
+
+// SetSelectedFunc sets a function which is called when the user selects this
+// node by hitting Enter when it is selected.
+func (n *TreeNode) SetSelectedFunc(handler func()) *TreeNode {
+ n.selected = handler
+ return n
+}
+
+// SetExpanded sets whether or not this node's child nodes should be displayed.
+func (n *TreeNode) SetExpanded(expanded bool) *TreeNode {
+ n.expanded = expanded
+ return n
+}
+
+// Expand makes the child nodes of this node appear.
+func (n *TreeNode) Expand() *TreeNode {
+ n.expanded = true
+ return n
+}
+
+// Collapse makes the child nodes of this node disappear.
+func (n *TreeNode) Collapse() *TreeNode {
+ n.expanded = false
+ return n
+}
+
+// ExpandAll expands this node and all descendent nodes.
+func (n *TreeNode) ExpandAll() *TreeNode {
+ n.Walk(func(node, parent *TreeNode) bool {
+ node.expanded = true
+ return true
+ })
+ return n
+}
+
+// CollapseAll collapses this node and all descendent nodes.
+func (n *TreeNode) CollapseAll() *TreeNode {
+ n.Walk(func(node, parent *TreeNode) bool {
+ n.expanded = false
+ return true
+ })
+ return n
+}
+
+// IsExpanded returns whether the child nodes of this node are visible.
+func (n *TreeNode) IsExpanded() bool {
+ return n.expanded
+}
+
+// SetText sets the node's text which is displayed.
+func (n *TreeNode) SetText(text string) *TreeNode {
+ n.text = text
+ return n
+}
+
+// SetColor sets the node's text color.
+func (n *TreeNode) SetColor(color tcell.Color) *TreeNode {
+ n.color = color
+ return n
+}
+
+// SetIndent sets an additional indentation for this node's text. A value of 0
+// keeps the text as far left as possible with a minimum of line graphics. Any
+// value greater than that moves the text to the right.
+func (n *TreeNode) SetIndent(indent int) *TreeNode {
+ n.indent = indent
+ return n
+}
+
+// TreeView displays tree structures. A tree consists of nodes (TreeNode
+// objects) where each node has zero or more child nodes and exactly one parent
+// node (except for the root node which has no parent node).
+//
+// The SetRoot() function is used to specify the root of the tree. Other nodes
+// are added locally to the root node or any of its descendents. See the
+// TreeNode documentation for details on node attributes. (You can use
+// SetReference() to store a reference to nodes of your own tree structure.)
+//
+// Nodes can be selected by calling SetCurrentNode(). The user can navigate the
+// selection or the tree by using the following keys:
+//
+// - j, down arrow, right arrow: Move (the selection) down by one node.
+// - k, up arrow, left arrow: Move (the selection) up by one node.
+// - g, home: Move (the selection) to the top.
+// - G, end: Move (the selection) to the bottom.
+// - Ctrl-F, page down: Move (the selection) down by one page.
+// - Ctrl-B, page up: Move (the selection) up by one page.
+//
+// Selected nodes can trigger the "selected" callback when the user hits Enter.
+//
+// The root node corresponds to level 0, its children correspond to level 1,
+// their children to level 2, and so on. Per default, the first level that is
+// displayed is 0, i.e. the root node. You can call SetTopLevel() to hide
+// levels.
+//
+// If graphics are turned on (see SetGraphics()), lines indicate the tree's
+// hierarchy. Alternative (or additionally), you can set different prefixes
+// using SetPrefixes() for different levels, for example to display hierarchical
+// bullet point lists.
+//
+// See https://github.com/rivo/tview/wiki/TreeView for an example.
+type TreeView struct {
+ *Box
+
+ // The root node.
+ root *TreeNode
+
+ // The currently selected node or nil if no node is selected.
+ currentNode *TreeNode
+
+ // The movement to be performed during the call to Draw(), one of the
+ // constants defined above.
+ movement int
+
+ // The top hierarchical level shown. (0 corresponds to the root level.)
+ topLevel int
+
+ // Strings drawn before the nodes, based on their level.
+ prefixes []string
+
+ // Vertical scroll offset.
+ offsetY int
+
+ // If set to true, all node texts will be aligned horizontally.
+ align bool
+
+ // If set to true, the tree structure is drawn using lines.
+ graphics bool
+
+ // The color of the lines.
+ graphicsColor tcell.Color
+
+ // An optional function which is called when the user has navigated to a new
+ // tree node.
+ changed func(node *TreeNode)
+
+ // An optional function which is called when a tree item was selected.
+ selected func(node *TreeNode)
+
+ // The visible nodes, top-down, as set by process().
+ nodes []*TreeNode
+}
+
+// NewTreeView returns a new tree view.
+func NewTreeView() *TreeView {
+ return &TreeView{
+ Box: NewBox(),
+ graphics: true,
+ graphicsColor: Styles.GraphicsColor,
+ }
+}
+
+// SetRoot sets the root node of the tree.
+func (t *TreeView) SetRoot(root *TreeNode) *TreeView {
+ t.root = root
+ return t
+}
+
+// GetRoot returns the root node of the tree. If no such node was previously
+// set, nil is returned.
+func (t *TreeView) GetRoot() *TreeNode {
+ return t.root
+}
+
+// SetCurrentNode sets the currently selected node. Provide nil to clear all
+// selections. Selected nodes must be visible and selectable, or else the
+// selection will be changed to the top-most selectable and visible node.
+//
+// This function does NOT trigger the "changed" callback.
+func (t *TreeView) SetCurrentNode(node *TreeNode) *TreeView {
+ t.currentNode = node
+ return t
+}
+
+// GetCurrentNode returns the currently selected node or nil of no node is
+// currently selected.
+func (t *TreeView) GetCurrentNode() *TreeNode {
+ return t.currentNode
+}
+
+// SetTopLevel sets the first tree level that is visible with 0 referring to the
+// root, 1 to the root's child nodes, and so on. Nodes above the top level are
+// not displayed.
+func (t *TreeView) SetTopLevel(topLevel int) *TreeView {
+ t.topLevel = topLevel
+ return t
+}
+
+// SetPrefixes defines the strings drawn before the nodes' texts. This is a
+// slice of strings where each element corresponds to a node's hierarchy level,
+// i.e. 0 for the root, 1 for the root's children, and so on (levels will
+// cycle).
+//
+// For example, to display a hierarchical list with bullet points:
+//
+// treeView.SetGraphics(false).
+// SetPrefixes([]string{"* ", "- ", "x "})
+func (t *TreeView) SetPrefixes(prefixes []string) *TreeView {
+ t.prefixes = prefixes
+ return t
+}
+
+// SetAlign controls the horizontal alignment of the node texts. If set to true,
+// all texts except that of top-level nodes will be placed in the same column.
+// If set to false, they will indent with the hierarchy.
+func (t *TreeView) SetAlign(align bool) *TreeView {
+ t.align = align
+ return t
+}
+
+// SetGraphics sets a flag which determines whether or not line graphics are
+// drawn to illustrate the tree's hierarchy.
+func (t *TreeView) SetGraphics(showGraphics bool) *TreeView {
+ t.graphics = showGraphics
+ return t
+}
+
+// SetGraphicsColor sets the colors of the lines used to draw the tree structure.
+func (t *TreeView) SetGraphicsColor(color tcell.Color) *TreeView {
+ t.graphicsColor = color
+ return t
+}
+
+// SetChangedFunc sets the function which is called when the user navigates to
+// a new tree node.
+func (t *TreeView) SetChangedFunc(handler func(node *TreeNode)) *TreeView {
+ t.changed = handler
+ return t
+}
+
+// SetSelectedFunc sets the function which is called when the user selects a
+// node by pressing Enter on the current selection.
+func (t *TreeView) SetSelectedFunc(handler func(node *TreeNode)) *TreeView {
+ t.selected = handler
+ return t
+}
+
+// process builds the visible tree, populates the "nodes" slice, and processes
+// pending selection actions.
+func (t *TreeView) process() {
+ _, _, _, height := t.GetInnerRect()
+
+ // Determine visible nodes and their placement.
+ var graphicsOffset, maxTextX int
+ t.nodes = nil
+ selectedIndex := -1
+ topLevelGraphicsX := -1
+ if t.graphics {
+ graphicsOffset = 1
+ }
+ t.root.Walk(func(node, parent *TreeNode) bool {
+ // Set node attributes.
+ node.parent = parent
+ if parent == nil {
+ node.level = 0
+ node.graphicsX = 0
+ node.textX = 0
+ } else {
+ node.level = parent.level + 1
+ node.graphicsX = parent.textX
+ node.textX = node.graphicsX + graphicsOffset + node.indent
+ }
+ if !t.graphics && t.align {
+ // Without graphics, we align nodes on the first column.
+ node.textX = 0
+ }
+ if node.level == t.topLevel {
+ // No graphics for top level nodes.
+ node.graphicsX = 0
+ node.textX = 0
+ }
+ if node.textX > maxTextX {
+ maxTextX = node.textX
+ }
+ if node == t.currentNode && node.selectable {
+ selectedIndex = len(t.nodes)
+ }
+
+ // Maybe we want to skip this level.
+ if t.topLevel == node.level && (topLevelGraphicsX < 0 || node.graphicsX < topLevelGraphicsX) {
+ topLevelGraphicsX = node.graphicsX
+ }
+
+ // Add and recurse (if desired).
+ if node.level >= t.topLevel {
+ t.nodes = append(t.nodes, node)
+ }
+ return node.expanded
+ })
+
+ // Post-process positions.
+ for _, node := range t.nodes {
+ // If text must align, we correct the positions.
+ if t.align && node.level > t.topLevel {
+ node.textX = maxTextX
+ }
+
+ // If we skipped levels, shift to the left.
+ if topLevelGraphicsX > 0 {
+ node.graphicsX -= topLevelGraphicsX
+ node.textX -= topLevelGraphicsX
+ }
+ }
+
+ // Process selection. (Also trigger events if necessary.)
+ if selectedIndex >= 0 {
+ // Move the selection.
+ newSelectedIndex := selectedIndex
+ MovementSwitch:
+ switch t.movement {
+ case treeUp:
+ for newSelectedIndex > 0 {
+ newSelectedIndex--
+ if t.nodes[newSelectedIndex].selectable {
+ break MovementSwitch
+ }
+ }
+ newSelectedIndex = selectedIndex
+ case treeDown:
+ for newSelectedIndex < len(t.nodes)-1 {
+ newSelectedIndex++
+ if t.nodes[newSelectedIndex].selectable {
+ break MovementSwitch
+ }
+ }
+ newSelectedIndex = selectedIndex
+ case treeHome:
+ for newSelectedIndex = 0; newSelectedIndex < len(t.nodes); newSelectedIndex++ {
+ if t.nodes[newSelectedIndex].selectable {
+ break MovementSwitch
+ }
+ }
+ newSelectedIndex = selectedIndex
+ case treeEnd:
+ for newSelectedIndex = len(t.nodes) - 1; newSelectedIndex >= 0; newSelectedIndex-- {
+ if t.nodes[newSelectedIndex].selectable {
+ break MovementSwitch
+ }
+ }
+ newSelectedIndex = selectedIndex
+ case treePageUp:
+ if newSelectedIndex+height < len(t.nodes) {
+ newSelectedIndex += height
+ } else {
+ newSelectedIndex = len(t.nodes) - 1
+ }
+ for ; newSelectedIndex < len(t.nodes); newSelectedIndex++ {
+ if t.nodes[newSelectedIndex].selectable {
+ break MovementSwitch
+ }
+ }
+ newSelectedIndex = selectedIndex
+ case treePageDown:
+ if newSelectedIndex >= height {
+ newSelectedIndex -= height
+ } else {
+ newSelectedIndex = 0
+ }
+ for ; newSelectedIndex >= 0; newSelectedIndex-- {
+ if t.nodes[newSelectedIndex].selectable {
+ break MovementSwitch
+ }
+ }
+ newSelectedIndex = selectedIndex
+ }
+ t.currentNode = t.nodes[newSelectedIndex]
+ if newSelectedIndex != selectedIndex {
+ t.movement = treeNone
+ if t.changed != nil {
+ t.changed(t.currentNode)
+ }
+ }
+ selectedIndex = newSelectedIndex
+
+ // Move selection into viewport.
+ if selectedIndex-t.offsetY >= height {
+ t.offsetY = selectedIndex - height + 1
+ }
+ if selectedIndex < t.offsetY {
+ t.offsetY = selectedIndex
+ }
+ } else {
+ // If selection is not visible or selectable, select the first candidate.
+ if t.currentNode != nil {
+ for index, node := range t.nodes {
+ if node.selectable {
+ selectedIndex = index
+ t.currentNode = node
+ break
+ }
+ }
+ }
+ if selectedIndex < 0 {
+ t.currentNode = nil
+ }
+ }
+}
+
+// Draw draws this primitive onto the screen.
+func (t *TreeView) Draw(screen tcell.Screen) {
+ t.Box.Draw(screen)
+ if t.root == nil {
+ return
+ }
+
+ // Build the tree if necessary.
+ if t.nodes == nil {
+ t.process()
+ }
+ defer func() {
+ t.nodes = nil // Rebuild during next call to Draw()
+ }()
+
+ // Scroll the tree.
+ x, y, width, height := t.GetInnerRect()
+ switch t.movement {
+ case treeUp:
+ t.offsetY--
+ case treeDown:
+ t.offsetY++
+ case treeHome:
+ t.offsetY = 0
+ case treeEnd:
+ t.offsetY = len(t.nodes)
+ case treePageUp:
+ t.offsetY -= height
+ case treePageDown:
+ t.offsetY += height
+ }
+ t.movement = treeNone
+
+ // Fix invalid offsets.
+ if t.offsetY >= len(t.nodes)-height {
+ t.offsetY = len(t.nodes) - height
+ }
+ if t.offsetY < 0 {
+ t.offsetY = 0
+ }
+
+ // Draw the tree.
+ posY := y
+ lineStyle := tcell.StyleDefault.Background(t.backgroundColor).Foreground(t.graphicsColor)
+ for index, node := range t.nodes {
+ // Skip invisible parts.
+ if posY >= y+height+1 {
+ break
+ }
+ if index < t.offsetY {
+ continue
+ }
+
+ // Draw the graphics.
+ if t.graphics {
+ // Draw ancestor branches.
+ ancestor := node.parent
+ for ancestor != nil && ancestor.parent != nil && ancestor.parent.level >= t.topLevel {
+ if ancestor.graphicsX >= width {
+ continue
+ }
+
+ // Draw a branch if this ancestor is not a last child.
+ if ancestor.parent.children[len(ancestor.parent.children)-1] != ancestor {
+ if posY-1 >= y && ancestor.textX > ancestor.graphicsX {
+ PrintJoinedSemigraphics(screen, x+ancestor.graphicsX, posY-1, Borders.Vertical, t.graphicsColor)
+ }
+ if posY < y+height {
+ screen.SetContent(x+ancestor.graphicsX, posY, Borders.Vertical, nil, lineStyle)
+ }
+ }
+ ancestor = ancestor.parent
+ }
+
+ if node.textX > node.graphicsX && node.graphicsX < width {
+ // Connect to the node above.
+ if posY-1 >= y && t.nodes[index-1].graphicsX <= node.graphicsX && t.nodes[index-1].textX > node.graphicsX {
+ PrintJoinedSemigraphics(screen, x+node.graphicsX, posY-1, Borders.TopLeft, t.graphicsColor)
+ }
+
+ // Join this node.
+ if posY < y+height {
+ screen.SetContent(x+node.graphicsX, posY, Borders.BottomLeft, nil, lineStyle)
+ for pos := node.graphicsX + 1; pos < node.textX && pos < width; pos++ {
+ screen.SetContent(x+pos, posY, Borders.Horizontal, nil, lineStyle)
+ }
+ }
+ }
+ }
+
+ // Draw the prefix and the text.
+ if node.textX < width && posY < y+height {
+ // Prefix.
+ var prefixWidth int
+ if len(t.prefixes) > 0 {
+ _, prefixWidth = Print(screen, t.prefixes[(node.level-t.topLevel)%len(t.prefixes)], x+node.textX, posY, width-node.textX, AlignLeft, node.color)
+ }
+
+ // Text.
+ if node.textX+prefixWidth < width {
+ style := tcell.StyleDefault.Foreground(node.color)
+ if node == t.currentNode {
+ style = tcell.StyleDefault.Background(node.color).Foreground(t.backgroundColor)
+ }
+ printWithStyle(screen, node.text, x+node.textX+prefixWidth, posY, width-node.textX-prefixWidth, AlignLeft, style)
+ }
+ }
+
+ // Advance.
+ posY++
+ }
+}
+
+// InputHandler returns the handler for this primitive.
+func (t *TreeView) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) {
+ return t.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p Primitive)) {
+ // 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.KeyDown, tcell.KeyRight:
+ t.movement = treeDown
+ case tcell.KeyBacktab, tcell.KeyUp, tcell.KeyLeft:
+ t.movement = treeUp
+ case tcell.KeyHome:
+ t.movement = treeHome
+ case tcell.KeyEnd:
+ t.movement = treeEnd
+ case tcell.KeyPgDn, tcell.KeyCtrlF:
+ t.movement = treePageDown
+ case tcell.KeyPgUp, tcell.KeyCtrlB:
+ t.movement = treePageUp
+ case tcell.KeyRune:
+ switch event.Rune() {
+ case 'g':
+ t.movement = treeHome
+ case 'G':
+ t.movement = treeEnd
+ case 'j':
+ t.movement = treeDown
+ case 'k':
+ t.movement = treeUp
+ }
+ case tcell.KeyEnter:
+ if t.currentNode != nil {
+ if t.selected != nil {
+ t.selected(t.currentNode)
+ }
+ if t.currentNode.selected != nil {
+ t.currentNode.selected()
+ }
+ }
+ }
+
+ t.process()
+ })
+}
diff --git a/vendor/maunium.net/go/tview/util.go b/vendor/maunium.net/go/tview/util.go
index 41e52dd..e408b18 100644
--- a/vendor/maunium.net/go/tview/util.go
+++ b/vendor/maunium.net/go/tview/util.go
@@ -1,11 +1,9 @@
package tview
import (
- "fmt"
"math"
"regexp"
"strconv"
- "strings"
"unicode"
"maunium.net/go/tcell"
@@ -19,97 +17,13 @@ const (
AlignRight
)
-// Semigraphical runes.
-const (
- GraphicsHoriBar = '\u2500'
- GraphicsVertBar = '\u2502'
- GraphicsTopLeftCorner = '\u250c'
- GraphicsTopRightCorner = '\u2510'
- GraphicsBottomLeftCorner = '\u2514'
- GraphicsBottomRightCorner = '\u2518'
- GraphicsLeftT = '\u251c'
- GraphicsRightT = '\u2524'
- GraphicsTopT = '\u252c'
- GraphicsBottomT = '\u2534'
- GraphicsCross = '\u253c'
- GraphicsDbVertBar = '\u2550'
- GraphicsDbHorBar = '\u2551'
- GraphicsDbTopLeftCorner = '\u2554'
- GraphicsDbTopRightCorner = '\u2557'
- GraphicsDbBottomRightCorner = '\u255d'
- GraphicsDbBottomLeftCorner = '\u255a'
- GraphicsEllipsis = '\u2026'
-)
-
-// joints maps combinations of two graphical runes to the rune that results
-// when joining the two in the same screen cell. The keys of this map are
-// two-rune strings where the value of the first rune is lower than the value
-// of the second rune. Identical runes are not contained.
-var joints = map[string]rune{
- "\u2500\u2502": GraphicsCross,
- "\u2500\u250c": GraphicsTopT,
- "\u2500\u2510": GraphicsTopT,
- "\u2500\u2514": GraphicsBottomT,
- "\u2500\u2518": GraphicsBottomT,
- "\u2500\u251c": GraphicsCross,
- "\u2500\u2524": GraphicsCross,
- "\u2500\u252c": GraphicsTopT,
- "\u2500\u2534": GraphicsBottomT,
- "\u2500\u253c": GraphicsCross,
- "\u2502\u250c": GraphicsLeftT,
- "\u2502\u2510": GraphicsRightT,
- "\u2502\u2514": GraphicsLeftT,
- "\u2502\u2518": GraphicsRightT,
- "\u2502\u251c": GraphicsLeftT,
- "\u2502\u2524": GraphicsRightT,
- "\u2502\u252c": GraphicsCross,
- "\u2502\u2534": GraphicsCross,
- "\u2502\u253c": GraphicsCross,
- "\u250c\u2510": GraphicsTopT,
- "\u250c\u2514": GraphicsLeftT,
- "\u250c\u2518": GraphicsCross,
- "\u250c\u251c": GraphicsLeftT,
- "\u250c\u2524": GraphicsCross,
- "\u250c\u252c": GraphicsTopT,
- "\u250c\u2534": GraphicsCross,
- "\u250c\u253c": GraphicsCross,
- "\u2510\u2514": GraphicsCross,
- "\u2510\u2518": GraphicsRightT,
- "\u2510\u251c": GraphicsCross,
- "\u2510\u2524": GraphicsRightT,
- "\u2510\u252c": GraphicsTopT,
- "\u2510\u2534": GraphicsCross,
- "\u2510\u253c": GraphicsCross,
- "\u2514\u2518": GraphicsBottomT,
- "\u2514\u251c": GraphicsLeftT,
- "\u2514\u2524": GraphicsCross,
- "\u2514\u252c": GraphicsCross,
- "\u2514\u2534": GraphicsBottomT,
- "\u2514\u253c": GraphicsCross,
- "\u2518\u251c": GraphicsCross,
- "\u2518\u2524": GraphicsRightT,
- "\u2518\u252c": GraphicsCross,
- "\u2518\u2534": GraphicsBottomT,
- "\u2518\u253c": GraphicsCross,
- "\u251c\u2524": GraphicsCross,
- "\u251c\u252c": GraphicsCross,
- "\u251c\u2534": GraphicsCross,
- "\u251c\u253c": GraphicsCross,
- "\u2524\u252c": GraphicsCross,
- "\u2524\u2534": GraphicsCross,
- "\u2524\u253c": GraphicsCross,
- "\u252c\u2534": GraphicsCross,
- "\u252c\u253c": GraphicsCross,
- "\u2534\u253c": GraphicsCross,
-}
-
// Common regular expressions.
var (
colorPattern = regexp.MustCompile(`\[([a-zA-Z]+|#[0-9a-zA-Z]{6}|\-)?(:([a-zA-Z]+|#[0-9a-zA-Z]{6}|\-)?(:([lbdru]+|\-)?)?)?\]`)
regionPattern = regexp.MustCompile(`\["([a-zA-Z0-9_,;: \-\.]*)"\]`)
escapePattern = regexp.MustCompile(`\[([a-zA-Z0-9_,;: \-\."#]+)\[(\[*)\]`)
nonEscapePattern = regexp.MustCompile(`(\[[a-zA-Z0-9_,;: \-\."#]+\[*)\]`)
- boundaryPattern = regexp.MustCompile("([[:punct:]]\\s*|\\s+)")
+ boundaryPattern = regexp.MustCompile(`(([[:punct:]]|\n)[ \t\f\r]*|(\s+))`)
spacePattern = regexp.MustCompile(`\s+`)
)
@@ -204,13 +118,12 @@ func overlayStyle(background tcell.Color, defaultStyle tcell.Style, fgColor, bgC
defFg, defBg, defAttr := defaultStyle.Decompose()
style := defaultStyle.Background(background)
- if fgColor == "-" {
- style = style.Foreground(defFg)
- } else if fgColor != "" {
+ style = style.Foreground(defFg)
+ if fgColor != "" {
style = style.Foreground(tcell.GetColor(fgColor))
}
- if bgColor == "-" {
+ if bgColor == "-" || bgColor == "" && defBg != tcell.ColorDefault {
style = style.Background(defBg)
} else if bgColor != "" {
style = style.Background(tcell.GetColor(bgColor))
@@ -288,8 +201,8 @@ func decomposeString(text string) (colorIndices [][]int, colors [][]string, esca
// You can change the colors and text styles mid-text by inserting a color tag.
// See the package description for details.
//
-// Returns the number of actual runes printed (not including color tags) and the
-// actual width used for the printed runes.
+// Returns the number of actual bytes of the text printed (including color tags)
+// and the actual width used for the printed runes.
func Print(screen tcell.Screen, text string, x, y, maxWidth, align int, color tcell.Color) (int, int) {
return printWithStyle(screen, text, x, y, maxWidth, align, tcell.StyleDefault.Foreground(color))
}
@@ -302,186 +215,160 @@ func printWithStyle(screen tcell.Screen, text string, x, y, maxWidth, align int,
}
// Decompose the text.
- colorIndices, colors, escapeIndices, strippedText, _ := decomposeString(text)
+ colorIndices, colors, escapeIndices, strippedText, strippedWidth := decomposeString(text)
- // We deal with runes, not with bytes.
- runes := []rune(strippedText)
-
- // This helper function takes positions for a substring of "runes" and returns
- // a new string corresponding to this substring, making sure printing that
- // substring will observe color tags.
- substring := func(from, to int) string {
+ // We want to reduce all alignments to AlignLeft.
+ if align == AlignRight {
+ if strippedWidth <= maxWidth {
+ // There's enough space for the entire text.
+ return printWithStyle(screen, text, x+maxWidth-strippedWidth, y, maxWidth, AlignLeft, style)
+ }
+ // Trim characters off the beginning.
var (
- colorPos, escapePos, runePos, startPos int
+ bytes, width, colorPos, escapePos, tagOffset int
foregroundColor, backgroundColor, attributes string
)
- if from >= len(runes) {
- return ""
- }
- for pos := range text {
- // Handle color tags.
- if colorPos < len(colorIndices) && pos >= colorIndices[colorPos][0] && pos < colorIndices[colorPos][1] {
- if pos == colorIndices[colorPos][1]-1 {
- if runePos <= from {
- foregroundColor, backgroundColor, attributes = styleFromTag(foregroundColor, backgroundColor, attributes, colors[colorPos])
- }
- colorPos++
- }
- continue
- }
-
- // Handle escape tags.
- if escapePos < len(escapeIndices) && pos >= escapeIndices[escapePos][0] && pos < escapeIndices[escapePos][1] {
- if pos == escapeIndices[escapePos][1]-1 {
- escapePos++
- } else if pos == escapeIndices[escapePos][1]-2 {
- continue
- }
+ _, originalBackground, _ := style.Decompose()
+ iterateString(strippedText, func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool {
+ // Update color/escape tag offset and style.
+ if colorPos < len(colorIndices) && textPos+tagOffset >= colorIndices[colorPos][0] && textPos+tagOffset < colorIndices[colorPos][1] {
+ foregroundColor, backgroundColor, attributes = styleFromTag(foregroundColor, backgroundColor, attributes, colors[colorPos])
+ style = overlayStyle(originalBackground, style, foregroundColor, backgroundColor, attributes)
+ tagOffset += colorIndices[colorPos][1] - colorIndices[colorPos][0]
+ colorPos++
}
-
- // Check boundaries.
- if runePos == from {
- startPos = pos
- } else if runePos >= to {
- return fmt.Sprintf(`[%s:%s:%s]%s`, foregroundColor, backgroundColor, attributes, text[startPos:pos])
+ if escapePos < len(escapeIndices) && textPos+tagOffset >= escapeIndices[escapePos][0] && textPos+tagOffset < escapeIndices[escapePos][1] {
+ tagOffset++
+ escapePos++
}
-
- runePos++
- }
-
- return fmt.Sprintf(`[%s:%s:%s]%s`, foregroundColor, backgroundColor, attributes, text[startPos:])
- }
-
- // We want to reduce everything to AlignLeft.
- if align == AlignRight {
- width := 0
- start := len(runes)
- for index := start - 1; index >= 0; index-- {
- w := runewidth.RuneWidth(runes[index])
- if width+w > maxWidth {
- break
+ if strippedWidth-screenPos < maxWidth {
+ // We chopped off enough.
+ if escapePos > 0 && textPos+tagOffset-1 >= escapeIndices[escapePos-1][0] && textPos+tagOffset-1 < escapeIndices[escapePos-1][1] {
+ // Unescape open escape sequences.
+ escapeCharPos := escapeIndices[escapePos-1][1] - 2
+ text = text[:escapeCharPos] + text[escapeCharPos+1:]
+ }
+ // Print and return.
+ bytes, width = printWithStyle(screen, text[textPos+tagOffset:], x, y, maxWidth, AlignLeft, style)
+ return true
}
- width += w
- start = index
- }
- for start < len(runes) && runewidth.RuneWidth(runes[start]) == 0 {
- start++
- }
- return printWithStyle(screen, substring(start, len(runes)), x+maxWidth-width, y, width, AlignLeft, style)
+ return false
+ })
+ return bytes, width
} else if align == AlignCenter {
- width := runewidth.StringWidth(strippedText)
- if width == maxWidth {
+ if strippedWidth == maxWidth {
// Use the exact space.
return printWithStyle(screen, text, x, y, maxWidth, AlignLeft, style)
- } else if width < maxWidth {
+ } else if strippedWidth < maxWidth {
// We have more space than we need.
- half := (maxWidth - width) / 2
+ half := (maxWidth - strippedWidth) / 2
return printWithStyle(screen, text, x+half, y, maxWidth-half, AlignLeft, style)
} else {
// Chop off runes until we have a perfect fit.
var choppedLeft, choppedRight, leftIndex, rightIndex int
- rightIndex = len(runes) - 1
- for rightIndex > leftIndex && width-choppedLeft-choppedRight > maxWidth {
+ rightIndex = len(strippedText)
+ for rightIndex-1 > leftIndex && strippedWidth-choppedLeft-choppedRight > maxWidth {
if choppedLeft < choppedRight {
- leftWidth := runewidth.RuneWidth(runes[leftIndex])
- choppedLeft += leftWidth
- leftIndex++
- for leftIndex < len(runes) && leftIndex < rightIndex && runewidth.RuneWidth(runes[leftIndex]) == 0 {
- leftIndex++
- }
+ // Iterate on the left by one character.
+ iterateString(strippedText[leftIndex:], func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool {
+ choppedLeft += screenWidth
+ leftIndex += textWidth
+ return true
+ })
} else {
- rightWidth := runewidth.RuneWidth(runes[rightIndex])
- choppedRight += rightWidth
- rightIndex--
+ // Iterate on the right by one character.
+ iterateStringReverse(strippedText[leftIndex:rightIndex], func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool {
+ choppedRight += screenWidth
+ rightIndex -= textWidth
+ return true
+ })
}
}
- return printWithStyle(screen, substring(leftIndex, rightIndex), x, y, maxWidth, AlignLeft, style)
+
+ // Add tag offsets and determine start style.
+ var (
+ colorPos, escapePos, tagOffset int
+ foregroundColor, backgroundColor, attributes string
+ )
+ _, originalBackground, _ := style.Decompose()
+ for index := range strippedText {
+ // We only need the offset of the left index.
+ if index > leftIndex {
+ // We're done.
+ if escapePos > 0 && leftIndex+tagOffset-1 >= escapeIndices[escapePos-1][0] && leftIndex+tagOffset-1 < escapeIndices[escapePos-1][1] {
+ // Unescape open escape sequences.
+ escapeCharPos := escapeIndices[escapePos-1][1] - 2
+ text = text[:escapeCharPos] + text[escapeCharPos+1:]
+ }
+ break
+ }
+
+ // Update color/escape tag offset.
+ if colorPos < len(colorIndices) && index+tagOffset >= colorIndices[colorPos][0] && index+tagOffset < colorIndices[colorPos][1] {
+ if index <= leftIndex {
+ foregroundColor, backgroundColor, attributes = styleFromTag(foregroundColor, backgroundColor, attributes, colors[colorPos])
+ style = overlayStyle(originalBackground, style, foregroundColor, backgroundColor, attributes)
+ }
+ tagOffset += colorIndices[colorPos][1] - colorIndices[colorPos][0]
+ colorPos++
+ }
+ if escapePos < len(escapeIndices) && index+tagOffset >= escapeIndices[escapePos][0] && index+tagOffset < escapeIndices[escapePos][1] {
+ tagOffset++
+ escapePos++
+ }
+ }
+ return printWithStyle(screen, text[leftIndex+tagOffset:], x, y, maxWidth, AlignLeft, style)
}
}
// Draw text.
- drawn := 0
- drawnWidth := 0
var (
- colorPos, escapePos int
- foregroundColor, backgroundColor, attributes string
+ drawn, drawnWidth, colorPos, escapePos, tagOffset int
+ foregroundColor, backgroundColor, attributes string
)
- runeSequence := make([]rune, 0, 10)
- runeSeqWidth := 0
- flush := func() {
- if len(runeSequence) == 0 {
- return // Nothing to flush.
- }
-
- // Print the rune sequence.
- finalX := x + drawnWidth
- _, _, finalStyle, _ := screen.GetContent(finalX, y)
- _, background, _ := finalStyle.Decompose()
- finalStyle = overlayStyle(background, style, foregroundColor, backgroundColor, attributes)
- var comb []rune
- if len(runeSequence) > 1 {
- // Allocate space for the combining characters only when necessary.
- comb = make([]rune, len(runeSequence)-1)
- copy(comb, runeSequence[1:])
- }
- for offset := 0; offset < runeSeqWidth; offset++ {
- // To avoid undesired effects, we place the same character in all cells.
- screen.SetContent(finalX+offset, y, runeSequence[0], comb, finalStyle)
+ iterateString(strippedText, func(main rune, comb []rune, textPos, length, screenPos, screenWidth int) bool {
+ // Only continue if there is still space.
+ if drawnWidth+screenWidth > maxWidth {
+ return true
}
- // Advance and reset.
- drawn += len(runeSequence)
- drawnWidth += runeSeqWidth
- runeSequence = runeSequence[:0]
- runeSeqWidth = 0
- }
- for pos, ch := range text {
// Handle color tags.
- if colorPos < len(colorIndices) && pos >= colorIndices[colorPos][0] && pos < colorIndices[colorPos][1] {
- flush()
- if pos == colorIndices[colorPos][1]-1 {
- foregroundColor, backgroundColor, attributes = styleFromTag(foregroundColor, backgroundColor, attributes, colors[colorPos])
- colorPos++
- }
- continue
+ if colorPos < len(colorIndices) && textPos+tagOffset >= colorIndices[colorPos][0] && textPos+tagOffset < colorIndices[colorPos][1] {
+ foregroundColor, backgroundColor, attributes = styleFromTag(foregroundColor, backgroundColor, attributes, colors[colorPos])
+ tagOffset += colorIndices[colorPos][1] - colorIndices[colorPos][0]
+ colorPos++
}
- // Handle escape tags.
- if escapePos < len(escapeIndices) && pos >= escapeIndices[escapePos][0] && pos < escapeIndices[escapePos][1] {
- flush()
- if pos == escapeIndices[escapePos][1]-1 {
+ // Handle scape tags.
+ if escapePos < len(escapeIndices) && textPos+tagOffset >= escapeIndices[escapePos][0] && textPos+tagOffset < escapeIndices[escapePos][1] {
+ if textPos+tagOffset == escapeIndices[escapePos][1]-2 {
+ tagOffset++
escapePos++
- } else if pos == escapeIndices[escapePos][1]-2 {
- continue
}
}
- // Check if we have enough space for this rune.
- chWidth := runewidth.RuneWidth(ch)
- if drawnWidth+chWidth > maxWidth {
- break // No. We're done then.
- }
-
- // Put this rune in the queue.
- if chWidth == 0 {
- // If this is not a modifier, we treat it as a space character.
- if len(runeSequence) == 0 {
- ch = ' '
- chWidth = 1
+ // Print the rune sequence.
+ finalX := x + drawnWidth
+ _, _, finalStyle, _ := screen.GetContent(finalX, y)
+ _, background, _ := finalStyle.Decompose()
+ finalStyle = overlayStyle(background, style, foregroundColor, backgroundColor, attributes)
+ for offset := screenWidth - 1; offset >= 0; offset-- {
+ // To avoid undesired effects, we populate all cells.
+ if offset == 0 {
+ screen.SetContent(finalX+offset, y, main, comb, finalStyle)
+ } else {
+ screen.SetContent(finalX+offset, y, ' ', nil, finalStyle)
}
- } else {
- // We have a character. Flush all previous runes.
- flush()
}
- runeSequence = append(runeSequence, ch)
- runeSeqWidth += chWidth
- }
- if drawnWidth+runeSeqWidth <= maxWidth {
- flush()
- }
+ // Advance.
+ drawn += length
+ drawnWidth += screenWidth
+
+ return false
+ })
- return drawn, drawnWidth
+ return drawn + tagOffset + len(escapeIndices), drawnWidth
}
// PrintSimple prints white text to the screen at the given position.
@@ -507,131 +394,86 @@ func WordWrap(text string, width int) (lines []string) {
colorTagIndices, _, escapeIndices, strippedText, _ := decomposeString(text)
// Find candidate breakpoints.
- breakPoints := boundaryPattern.FindAllStringIndex(strippedText, -1)
-
- // This helper function adds a new line to the result slice. The provided
- // positions are in stripped index space.
- addLine := func(from, to int) {
- // Shift indices back to original index space.
- var colorTagIndex, escapeIndex int
- for colorTagIndex < len(colorTagIndices) && to >= colorTagIndices[colorTagIndex][0] ||
- escapeIndex < len(escapeIndices) && to >= escapeIndices[escapeIndex][0] {
- past := 0
- if colorTagIndex < len(colorTagIndices) {
- tagWidth := colorTagIndices[colorTagIndex][1] - colorTagIndices[colorTagIndex][0]
- if colorTagIndices[colorTagIndex][0] < from {
- from += tagWidth
- to += tagWidth
- colorTagIndex++
- } else if colorTagIndices[colorTagIndex][0] < to {
- to += tagWidth
- colorTagIndex++
- } else {
- past++
- }
- } else {
- past++
- }
- if escapeIndex < len(escapeIndices) {
- tagWidth := escapeIndices[escapeIndex][1] - escapeIndices[escapeIndex][0]
- if escapeIndices[escapeIndex][0] < from {
- from += tagWidth
- to += tagWidth
- escapeIndex++
- } else if escapeIndices[escapeIndex][0] < to {
- to += tagWidth
- escapeIndex++
- } else {
- past++
- }
- } else {
- past++
- }
- if past == 2 {
- break // All other indices are beyond the requested string.
+ breakpoints := boundaryPattern.FindAllStringSubmatchIndex(strippedText, -1)
+ // Results in one entry for each candidate. Each entry is an array a of
+ // indices into strippedText where a[6] < 0 for newline/punctuation matches
+ // and a[4] < 0 for whitespace matches.
+
+ // Process stripped text one character at a time.
+ var (
+ colorPos, escapePos, breakpointPos, tagOffset int
+ lastBreakpoint, lastContinuation, currentLineStart int
+ lineWidth, continuationWidth int
+ newlineBreakpoint bool
+ )
+ unescape := func(substr string, startIndex int) string {
+ // A helper function to unescape escaped tags.
+ for index := escapePos; index >= 0; index-- {
+ if index < len(escapeIndices) && startIndex > escapeIndices[index][0] && startIndex < escapeIndices[index][1]-1 {
+ pos := escapeIndices[index][1] - 2 - startIndex
+ return substr[:pos] + substr[pos+1:]
}
}
- lines = append(lines, text[from:to])
+ return substr
}
+ iterateString(strippedText, func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool {
+ // Handle colour tags.
+ if colorPos < len(colorTagIndices) && textPos+tagOffset >= colorTagIndices[colorPos][0] && textPos+tagOffset < colorTagIndices[colorPos][1] {
+ tagOffset += colorTagIndices[colorPos][1] - colorTagIndices[colorPos][0]
+ colorPos++
+ }
- // Determine final breakpoints.
- var start, lastEnd, newStart, breakPoint int
- for {
- // What's our candidate string?
- var candidate string
- if breakPoint < len(breakPoints) {
- candidate = text[start:breakPoints[breakPoint][1]]
- } else {
- candidate = text[start:]
+ // Handle escape tags.
+ if escapePos < len(escapeIndices) && textPos+tagOffset == escapeIndices[escapePos][1]-2 {
+ tagOffset++
+ escapePos++
}
- candidate = strings.TrimRightFunc(candidate, unicode.IsSpace)
-
- if runewidth.StringWidth(candidate) >= width {
- // We're past the available width.
- if lastEnd > start {
- // Use the previous candidate.
- addLine(start, lastEnd)
- start = newStart
- } else {
- // We have no previous candidate. Make a hard break.
- var lineWidth int
- for index, ch := range text {
- if index < start {
- continue
- }
- chWidth := runewidth.RuneWidth(ch)
- if lineWidth > 0 && lineWidth+chWidth >= width {
- addLine(start, index)
- start = index
- break
- }
- lineWidth += chWidth
- }
- }
- } else {
- // We haven't hit the right border yet.
- if breakPoint >= len(breakPoints) {
- // It's the last line. We're done.
- if len(candidate) > 0 {
- addLine(start, len(strippedText))
- }
- break
- } else {
- // We have a new candidate.
- lastEnd = start + len(candidate)
- newStart = breakPoints[breakPoint][1]
- breakPoint++
- }
+
+ // Check if a break is warranted.
+ afterContinuation := lastContinuation > 0 && textPos+tagOffset >= lastContinuation
+ noBreakpoint := lastContinuation == 0
+ beyondWidth := lineWidth > 0 && lineWidth > width
+ if beyondWidth && noBreakpoint {
+ // We need a hard break without a breakpoint.
+ lines = append(lines, unescape(text[currentLineStart:textPos+tagOffset], currentLineStart))
+ currentLineStart = textPos + tagOffset
+ lineWidth = continuationWidth
+ } else if afterContinuation && (beyondWidth || newlineBreakpoint) {
+ // Break at last breakpoint or at newline.
+ lines = append(lines, unescape(text[currentLineStart:lastBreakpoint], currentLineStart))
+ currentLineStart = lastContinuation
+ lineWidth = continuationWidth
+ lastBreakpoint, lastContinuation, newlineBreakpoint = 0, 0, false
}
- }
- return
-}
+ // Is this a breakpoint?
+ if breakpointPos < len(breakpoints) && textPos == breakpoints[breakpointPos][0] {
+ // Yes, it is. Set up breakpoint infos depending on its type.
+ lastBreakpoint = breakpoints[breakpointPos][0] + tagOffset
+ lastContinuation = breakpoints[breakpointPos][1] + tagOffset
+ newlineBreakpoint = main == '\n'
+ if breakpoints[breakpointPos][6] < 0 && !newlineBreakpoint {
+ lastBreakpoint++ // Don't skip punctuation.
+ }
+ breakpointPos++
+ }
-// PrintJoinedBorder prints a border graphics rune into the screen at the given
-// position with the given color, joining it with any existing border graphics
-// rune. Background colors are preserved. At this point, only regular single
-// line borders are supported.
-func PrintJoinedBorder(screen tcell.Screen, x, y int, ch rune, color tcell.Color) {
- previous, _, style, _ := screen.GetContent(x, y)
- style = style.Foreground(color)
-
- // What's the resulting rune?
- var result rune
- if ch == previous {
- result = ch
- } else {
- if ch < previous {
- previous, ch = ch, previous
+ // Once we hit the continuation point, we start buffering widths.
+ if textPos+tagOffset < lastContinuation {
+ continuationWidth = 0
}
- result = joints[string(previous)+string(ch)]
- }
- if result == 0 {
- result = ch
+
+ lineWidth += screenWidth
+ continuationWidth += screenWidth
+ return false
+ })
+
+ // Flush the rest.
+ if currentLineStart < len(text) {
+ lines = append(lines, unescape(text[currentLineStart:], currentLineStart))
}
- // We only print something if we have something.
- screen.SetContent(x, y, result, nil, style)
+ return
}
// Escape escapes the given text such that color and/or region tags are not
@@ -643,3 +485,121 @@ func PrintJoinedBorder(screen tcell.Screen, x, y int, ch rune, color tcell.Color
func Escape(text string) string {
return nonEscapePattern.ReplaceAllString(text, "$1[]")
}
+
+// iterateString iterates through the given string one printed character at a
+// time. For each such character, the callback function is called with the
+// Unicode code points of the character (the first rune and any combining runes
+// which may be nil if there aren't any), the starting position (in bytes)
+// within the original string, its length in bytes, the screen position of the
+// character, and the screen width of it. The iteration stops if the callback
+// returns true. This function returns true if the iteration was stopped before
+// the last character.
+func iterateString(text string, callback func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool) bool {
+ var (
+ runes []rune
+ lastZeroWidthJoiner bool
+ startIndex int
+ startPos int
+ pos int
+ )
+
+ // Helper function which invokes the callback.
+ flush := func(index int) bool {
+ var comb []rune
+ if len(runes) > 1 {
+ comb = runes[1:]
+ }
+ return callback(runes[0], comb, startIndex, index-startIndex, startPos, pos-startPos)
+ }
+
+ for index, r := range text {
+ if unicode.In(r, unicode.Lm, unicode.M) || r == '\u200d' {
+ lastZeroWidthJoiner = r == '\u200d'
+ } else {
+ // We have a rune that's not a modifier. It could be the beginning of a
+ // new character.
+ if !lastZeroWidthJoiner {
+ if len(runes) > 0 {
+ // It is. Invoke callback.
+ if flush(index) {
+ return true // We're done.
+ }
+ // Reset rune store.
+ runes = runes[:0]
+ startIndex = index
+ startPos = pos
+ }
+ pos += runewidth.RuneWidth(r)
+ } else {
+ lastZeroWidthJoiner = false
+ }
+ }
+ runes = append(runes, r)
+ }
+
+ // Flush any remaining runes.
+ if len(runes) > 0 {
+ flush(len(text))
+ }
+
+ return false
+}
+
+// iterateStringReverse iterates through the given string in reverse, starting
+// from the end of the string, one printed character at a time. For each such
+// character, the callback function is called with the Unicode code points of
+// the character (the first rune and any combining runes which may be nil if
+// there aren't any), the starting position (in bytes) within the original
+// string, its length in bytes, the screen position of the character, and the
+// screen width of it. The iteration stops if the callback returns true. This
+// function returns true if the iteration was stopped before the last character.
+func iterateStringReverse(text string, callback func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool) bool {
+ type runePos struct {
+ r rune
+ pos int // The byte position of the rune in the original string.
+ width int // The screen width of the rune.
+ mod bool // Modifier or zero-width-joiner.
+ }
+
+ // We use the following:
+ // len(text) >= number of runes in text.
+
+ // Put all runes into a runePos slice in reverse.
+ runesReverse := make([]runePos, len(text))
+ index := len(text) - 1
+ for pos, ch := range text {
+ runesReverse[index].r = ch
+ runesReverse[index].pos = pos
+ runesReverse[index].width = runewidth.RuneWidth(ch)
+ runesReverse[index].mod = unicode.In(ch, unicode.Lm, unicode.M) || ch == '\u200d'
+ index--
+ }
+ runesReverse = runesReverse[index+1:]
+
+ // Parse reverse runes.
+ var screenWidth int
+ buffer := make([]rune, len(text)) // We fill this up from the back so it's forward again.
+ bufferPos := len(text)
+ stringWidth := runewidth.StringWidth(text)
+ for index, r := range runesReverse {
+ // Put this rune into the buffer.
+ bufferPos--
+ buffer[bufferPos] = r.r
+
+ // Do we need to flush the buffer?
+ if r.pos == 0 || !r.mod && runesReverse[index+1].r != '\u200d' {
+ // Yes, invoke callback.
+ var comb []rune
+ if len(text)-bufferPos > 1 {
+ comb = buffer[bufferPos+1:]
+ }
+ if callback(r.r, comb, r.pos, len(text)-r.pos, stringWidth-screenWidth, r.width) {
+ return true
+ }
+ screenWidth += r.width
+ bufferPos = len(text)
+ }
+ }
+
+ return false
+}