diff options
author | Tulir Asokan <tulir@maunium.net> | 2018-11-14 00:00:35 +0200 |
---|---|---|
committer | Tulir Asokan <tulir@maunium.net> | 2018-11-14 00:00:35 +0200 |
commit | ba387764ca1590625d349e74eb8a8a64d1849b67 (patch) | |
tree | bc8f02156a63eac99dcddaed38e45b7c312b40c0 /vendor/maunium.net/go/tview | |
parent | cfb2cc057c32330be0ca0a68cfbd245cb2b8e31b (diff) |
Fix things
Diffstat (limited to 'vendor/maunium.net/go/tview')
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 +} |