1
0
mirror of https://github.com/rivo/tview.git synced 2025-04-28 13:48:53 +08:00

Added basic text entering.

This commit is contained in:
Oliver 2022-08-08 22:58:05 +02:00
parent 17194d2e94
commit 286f73a109
2 changed files with 138 additions and 113 deletions

158
doc.go
View File

@ -3,77 +3,77 @@ Package tview implements rich widgets for terminal based user interfaces. The
widgets provided with this package are useful for data exploration and data widgets provided with this package are useful for data exploration and data
entry. entry.
Widgets # Widgets
The package implements the following widgets: The package implements the following widgets:
- TextView: A scrollable window that display multi-colored text. Text may also - [TextView]: A scrollable window that display multi-colored text. Text may
be highlighted. also be highlighted.
- Table: A scrollable display of tabular data. Table cells, rows, or columns - [Table]: A scrollable display of tabular data. Table cells, rows, or columns
may also be highlighted. may also be highlighted.
- TreeView: A scrollable display for hierarchical data. Tree nodes can be - [TreeView]: A scrollable display for hierarchical data. Tree nodes can be
highlighted, collapsed, expanded, and more. highlighted, collapsed, expanded, and more.
- List: A navigable text list with optional keyboard shortcuts. - [List]: A navigable text list with optional keyboard shortcuts.
- InputField: One-line input fields to enter text. - [InputField]: One-line input fields to enter text.
- DropDown: Drop-down selection fields. - [DropDown]: Drop-down selection fields.
- Checkbox: Selectable checkbox for boolean values. - [Checkbox]: Selectable checkbox for boolean values.
- Button: Buttons which get activated when the user selects them. - [Button]: Buttons which get activated when the user selects them.
- Form: Forms composed of input fields, drop down selections, checkboxes, and - Form: Forms composed of input fields, drop down selections, checkboxes, and
buttons. buttons.
- Modal: A centered window with a text message and one or more buttons. - [Modal]: A centered window with a text message and one or more buttons.
- Grid: A grid based layout manager. - [Grid]: A grid based layout manager.
- Flex: A Flexbox based layout manager. - [Flex]: A Flexbox based layout manager.
- Pages: A page based layout manager. - [Pages]: A page based layout manager.
The package also provides Application which is used to poll the event queue and The package also provides Application which is used to poll the event queue and
draw widgets on screen. draw widgets on screen.
Hello World # Hello World
The following is a very basic example showing a box with the title "Hello, The following is a very basic example showing a box with the title "Hello,
world!": world!":
package main package main
import ( import (
"github.com/rivo/tview" "github.com/rivo/tview"
) )
func main() { func main() {
box := tview.NewBox().SetBorder(true).SetTitle("Hello, world!") box := tview.NewBox().SetBorder(true).SetTitle("Hello, world!")
if err := tview.NewApplication().SetRoot(box, true).Run(); err != nil { if err := tview.NewApplication().SetRoot(box, true).Run(); err != nil {
panic(err) panic(err)
} }
} }
First, we create a box primitive with a border and a title. Then we create an First, we create a box primitive with a border and a title. Then we create an
application, set the box as its root primitive, and run the event loop. The application, set the box as its root primitive, and run the event loop. The
application exits when the application's Stop() function is called or when application exits when the application's [Application.Stop] function is called
Ctrl-C is pressed. or when Ctrl-C is pressed.
If we have a primitive which consumes key presses, we call the application's If we have a primitive which consumes key presses, we call the application's
SetFocus() function to redirect all key presses to that primitive. Most [Application.SetFocus] function to redirect all key presses to that primitive.
primitives then offer ways to install handlers that allow you to react to any Most primitives then offer ways to install handlers that allow you to react to
actions performed on them. any actions performed on them.
More Demos # More Demos
You will find more demos in the "demos" subdirectory. It also contains a You will find more demos in the "demos" subdirectory. It also contains a
presentation (written using tview) which gives an overview of the different presentation (written using tview) which gives an overview of the different
widgets and how they can be used. widgets and how they can be used.
Colors # Colors
Throughout this package, colors are specified using the tcell.Color type. Throughout this package, colors are specified using the [tcell.Color] type.
Functions such as tcell.GetColor(), tcell.NewHexColor(), and tcell.NewRGBColor() Functions such as [tcell.GetColor], [tcell.NewHexColor], and [tcell.NewRGBColor]
can be used to create colors from W3C color names or RGB values. can be used to create colors from W3C color names or RGB values.
Almost all strings which are displayed can contain color tags. Color tags are Almost all strings which are displayed can contain color tags. Color tags are
W3C color names or six hexadecimal digits following a hash tag, wrapped in W3C color names or six hexadecimal digits following a hash tag, wrapped in
square brackets. Examples: square brackets. Examples:
This is a [red]warning[white]! This is a [red]warning[white]!
The sky is [#8080ff]blue[#ffffff]. The sky is [#8080ff]blue[#ffffff].
A color tag changes the color of the characters following that color tag. This A color tag changes the color of the characters following that color tag. This
applies to almost everything from box titles, list text, form item labels, to applies to almost everything from box titles, list text, form item labels, to
@ -84,7 +84,7 @@ Color tags may contain not just the foreground (text) color but also the
background color and additional flags. In fact, the full definition of a color background color and additional flags. In fact, the full definition of a color
tag is as follows: tag is as follows:
[<foreground>:<background>:<flags>] [<foreground>:<background>:<flags>]
Each of the three fields can be left blank and trailing fields can be omitted. 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 (Empty square brackets "[]", however, are not considered color tags.) Colors
@ -94,26 +94,26 @@ means "reset to default".
You can specify the following flags (some flags may not be supported by your You can specify the following flags (some flags may not be supported by your
terminal): terminal):
l: blink l: blink
b: bold b: bold
i: italic i: italic
d: dim d: dim
r: reverse (switch foreground and background color) r: reverse (switch foreground and background color)
u: underline u: underline
s: strike-through s: strike-through
Examples: Examples:
[yellow]Yellow text [yellow]Yellow text
[yellow:red]Yellow text on red background [yellow:red]Yellow text on red background
[:red]Red background, text color unchanged [:red]Red background, text color unchanged
[yellow::u]Yellow text underlined [yellow::u]Yellow text underlined
[::bl]Bold, blinking text [::bl]Bold, blinking text
[::-]Colors unchanged, flags reset [::-]Colors unchanged, flags reset
[-]Reset foreground color [-]Reset foreground color
[-:-:-]Reset everything [-:-:-]Reset everything
[:]No effect [:]No effect
[]Not a valid color tag, will print square brackets as they are []Not a valid color tag, will print square brackets as they are
In the rare event that you want to display a string such as "[red]" or In the rare event that you want to display a string such as "[red]" or
"[#00ff1a]" without applying its effect, you need to put an opening square "[#00ff1a]" without applying its effect, you need to put an opening square
@ -121,26 +121,26 @@ bracket before the closing square bracket. Note that the text inside the
brackets will be matched less strictly than region or colors tags. I.e. any brackets will be matched less strictly than region or colors tags. I.e. any
character that may be used in color or region tags will be recognized. Examples: character that may be used in color or region tags will be recognized. Examples:
[red[] will be output as [red] [red[] will be output as [red]
["123"[] will be output as ["123"] ["123"[] will be output as ["123"]
[#6aff00[[] will be output as [#6aff00[] [#6aff00[[] will be output as [#6aff00[]
[a#"[[[] will be output as [a#"[[] [a#"[[[] will be output as [a#"[[]
[] will be output as [] (see color tags above) [] will be output as [] (see color tags above)
[[] will be output as [[] (not an escaped tag) [[] will be output as [[] (not an escaped tag)
You can use the Escape() function to insert brackets automatically where needed. You can use the Escape() function to insert brackets automatically where needed.
Styles # Styles
When primitives are instantiated, they are initialized with colors taken from When primitives are instantiated, they are initialized with colors taken from
the global Styles variable. You may change this variable to adapt the look and the global Styles variable. You may change this variable to adapt the look and
feel of the primitives to your preferred style. feel of the primitives to your preferred style.
Unicode Support # Unicode Support
This package supports unicode characters including wide characters. This package supports unicode characters including wide characters.
Concurrency # Concurrency
Many functions in this package are not thread-safe. For many applications, this 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 may not be an issue: If your code makes changes in response to key events, it
@ -148,34 +148,32 @@ 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 If you access your primitives from other goroutines, however, you will need to
synchronize execution. The easiest way to do this is to call synchronize execution. The easiest way to do this is to call
Application.QueueUpdate() or Application.QueueUpdateDraw() (see the function [Application.QueueUpdate] or [Application.QueueUpdateDraw] (see the function
documentation for details): documentation for details):
go func() { go func() {
app.QueueUpdateDraw(func() { app.QueueUpdateDraw(func() {
table.SetCellSimple(0, 0, "Foo bar") table.SetCellSimple(0, 0, "Foo bar")
}) })
}() }()
One exception to this is the io.Writer interface implemented by TextView. You 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 can safely write to a [TextView] from any goroutine. See the [TextView]
documentation for details. documentation for details.
You can also call Application.Draw() from any goroutine without having to wrap You can also call [Application.Draw] from any goroutine without having to wrap
it in QueueUpdate(). And, as mentioned above, key event callbacks are executed it in [Application.QueueUpdate]. And, as mentioned above, key event callbacks
in the main goroutine and thus should not use QueueUpdate() as that may lead to are executed in the main goroutine and thus should not use
deadlocks. [Application.QueueUpdate] as that may lead to deadlocks.
Type Hierarchy # Type Hierarchy
All widgets listed above contain the Box type. All of Box's functions are All widgets listed above contain the [Box] type. All of [Box]'s functions are
therefore available for all widgets, too. therefore available for all widgets, too.
All widgets also implement the Primitive interface. All widgets also implement the [Primitive] interface.
The tview package is based on https://github.com/gdamore/tcell. It uses types The tview package is based on https://github.com/gdamore/tcell. It uses types
and constants from that package (e.g. colors and keyboard values). and constants from that package (e.g. colors and keyboard values).
This package does not process mouse input (yet).
*/ */
package tview package tview

View File

@ -41,17 +41,16 @@ var (
// text in the editor as part of a doubly-linked list. // text in the editor as part of a doubly-linked list.
// //
// In most places where we reference a position in the text, we use a // In most places where we reference a position in the text, we use a
// two-element int array. The first element is the index of the referenced span // three-element int array. The first element is the index of the referenced
// in the piece chain. The second element is the offset into the span's // span in the piece chain. The second element is the offset into the span's
// referenced text (relative to the span's start), its value is always >= 0 and // referenced text (relative to the span's start), its value is always >= 0 and
// < span.length. Sometimes, we may use a three-element int array which also // < span.length. The third elements is the corresponding text parser's state.
// contains the corresponding text parser's state in the third position.
// //
// A range of text is represented by a span range which is a starting position // A range of text is represented by a span range which is a starting position
// (int array) and an ending position (int array). The starting position // (int array) and an ending position (int array). The starting position
// references the first character of the range, the ending position references // references the first character of the range, the ending position references
// the position after the last character of the range. The end of the text is // the position after the last character of the range. The end of the text is
// therefore always [2]int{1, 0}, position 0 of the ending sentinel. // therefore always [3]int{1, 0, 0}, position 0 of the ending sentinel.
type textAreaSpan struct { type textAreaSpan struct {
// Links to the previous and next textAreaSpan objects as indices into the // Links to the previous and next textAreaSpan objects as indices into the
// TextArea.spans slice. The sentinel spans (index 0 and 1) have -1 as their // TextArea.spans slice. The sentinel spans (index 0 and 1) have -1 as their
@ -128,12 +127,12 @@ type textAreaSpan struct {
// clipboard is not used. The Ctrl-Q key was chosen for the "copy" function // clipboard is not used. The Ctrl-Q key was chosen for the "copy" function
// because the Ctrl-C key is the default key to stop the application. If your // because the Ctrl-C key is the default key to stop the application. If your
// application frees up the global Ctrl-C key and you want to bind it to the // application frees up the global Ctrl-C key and you want to bind it to the
// "copy to clipboard" function, you may use SetInputCapture() to override the // "copy to clipboard" function, you may use [Box.SetInputCapture] to override
// Ctrl-Q key to implement copying to the clipboard. // the Ctrl-Q key to implement copying to the clipboard.
// //
// Similarly, if you want to implement your own clipboard (or make use of your // Similarly, if you want to implement your own clipboard (or make use of your
// operating system's clipboard), you can also use SetInputCapture() to override // operating system's clipboard), you can also use [Box.SetInputCapture] to
// the key binds for copy, cut, and paste. The GetSelection(), ReplaceText(), // override the key binds for copy, cut, and paste. The GetSelection(), ReplaceText(),
// and SetSelection() provide all the functionality needed for your own // and SetSelection() provide all the functionality needed for your own
// clipboard. TODO: This will need to be reviewed. // clipboard. TODO: This will need to be reviewed.
// //
@ -334,12 +333,17 @@ func (t *TextArea) SetOffset(row, column int) *TextArea {
// replace deletes a range of text and inserts the given text at that position. // replace deletes a range of text and inserts the given text at that position.
// If the resulting text would exceed the maximum length, the function does not // If the resulting text would exceed the maximum length, the function does not
// do anything. See textAreaSpan for information about text positions and span // do anything. The function returns the new position of the deleted/inserted
// ranges. // range (with an undefined state).
//
// The function can hang if "deleteStart" is located after "deleteEnd".
// //
// This function does not generate Undo events. Undo events are generated // This function does not generate Undo events. Undo events are generated
// elsewhere, when the user changes their type of edit. // elsewhere, when the user changes their type of edit. It also does not modify
func (t *TextArea) replace(deleteStart, deleteEnd [2]int, insert string) { // [TextArea.lineStarts].
func (t *TextArea) replace(deleteStart, deleteEnd [3]int, insert string) (end [3]int) {
end = deleteEnd
// Check max length. // Check max length.
if t.maxLength > 0 && t.length+len(insert) > t.maxLength { if t.maxLength > 0 && t.length+len(insert) > t.maxLength {
return return
@ -367,22 +371,24 @@ func (t *TextArea) replace(deleteStart, deleteEnd [2]int, insert string) {
} }
} // At this point, deleteStart[0] == deleteEnd[0]. } // At this point, deleteStart[0] == deleteEnd[0].
if deleteEnd[1] > deleteStart[1] { if deleteEnd[1] > deleteStart[1] {
if deleteStart[1] == 0 { if deleteStart[1] != 0 {
// Delete a partial span at the beginning.
t.length -= deleteEnd[1]
if t.spans[deleteEnd[0]].length < 0 {
// Initial text span. Has negative length.
t.spans[deleteEnd[0]].length += deleteEnd[1]
} else {
// Edit buffer span. Has positive length.
t.spans[deleteEnd[0]].length -= deleteEnd[1]
}
t.spans[deleteEnd[0]].offset += deleteEnd[1]
} else {
// Delete in the middle by splitting the span. // Delete in the middle by splitting the span.
deleteEnd[1] -= deleteStart[1]
deleteStart[0] = t.splitSpan(deleteStart[0], deleteStart[1]) deleteStart[0] = t.splitSpan(deleteStart[0], deleteStart[1])
deleteStart[1] = 0 deleteStart[1] = 0
} }
// Delete a partial span at the beginning.
t.length -= deleteEnd[1]
if t.spans[deleteEnd[0]].length < 0 {
// Initial text span. Has negative length.
t.spans[deleteEnd[0]].length += deleteEnd[1]
} else {
// Edit buffer span. Has positive length.
t.spans[deleteEnd[0]].length -= deleteEnd[1]
}
t.spans[deleteEnd[0]].offset += deleteEnd[1]
deleteEnd[1] = 0
end[1] = 0
} }
// Insert. // Insert.
@ -395,7 +401,7 @@ func (t *TextArea) replace(deleteStart, deleteEnd [2]int, insert string) {
if previousSpan.length > 0 && previousSpan.offset+previousSpan.length == t.editText.Len() { if previousSpan.length > 0 && previousSpan.offset+previousSpan.length == t.editText.Len() {
// We can simply append to the edit buffer. // We can simply append to the edit buffer.
length, _ := t.editText.WriteString(insert) length, _ := t.editText.WriteString(insert)
previousSpan.length += length t.spans[span.previous].length += length
t.length += length t.length += length
} else { } else {
// Insert a new span. // Insert a new span.
@ -405,15 +411,18 @@ func (t *TextArea) replace(deleteStart, deleteEnd [2]int, insert string) {
// Split and insert. // Split and insert.
spanIndex = t.splitSpan(spanIndex, offset) spanIndex = t.splitSpan(spanIndex, offset)
t.insertSpan(insert, spanIndex) t.insertSpan(insert, spanIndex)
end = [3]int{spanIndex, 0, 0}
} }
} }
return
} }
// deleteSpan removes the span with the given index from the piece chain. It // deleteSpan removes the span with the given index from the piece chain. It
// returns the index of the span after the deleted span (or the provided index // returns the index of the span after the deleted span (or the provided index
// if no span was deleted due to an invalid span index). // if no span was deleted due to an invalid span index).
// //
// This function also adjusts TextArea.length. // This function also adjusts [TextArea.length].
func (t *TextArea) deleteSpan(index int) int { func (t *TextArea) deleteSpan(index int) int {
if index < 2 || index >= len(t.spans) { if index < 2 || index >= len(t.spans) {
return index return index
@ -440,7 +449,9 @@ func (t *TextArea) deleteSpan(index int) int {
// index if no span was split due to an invalid span index or an invalid // index if no span was split due to an invalid span index or an invalid
// offset. // offset.
func (t *TextArea) splitSpan(index, offset int) int { func (t *TextArea) splitSpan(index, offset int) int {
if index < 2 || index >= len(t.spans) || offset <= 0 || offset >= t.spans[index].length { if index < 2 || index >= len(t.spans) || offset <= 0 ||
(t.spans[index].length < 0 && offset >= -t.spans[index].length) ||
(t.spans[index].length >= 0 && offset >= t.spans[index].length) {
return index return index
} }
@ -456,18 +467,18 @@ func (t *TextArea) splitSpan(index, offset int) int {
if span.length < 0 { if span.length < 0 {
// Initial text span. Has negative length. // Initial text span. Has negative length.
newSpan.length = span.length + offset newSpan.length = span.length + offset
span.length = -offset t.spans[index].length = -offset
} else { } else {
// Edit buffer span. Has positive length. // Edit buffer span. Has positive length.
newSpan.length = span.length - offset newSpan.length = span.length - offset
span.length = offset t.spans[index].length = offset
} }
// Insert it. // Insert the modified and new spans.
newIndex := len(t.spans) newIndex := len(t.spans)
t.spans = append(t.spans, newSpan) t.spans = append(t.spans, newSpan)
t.spans[span.next].previous = newIndex t.spans[span.next].previous = newIndex
span.next = newIndex t.spans[index].next = newIndex
return newIndex return newIndex
} }
@ -493,7 +504,7 @@ func (t *TextArea) insertSpan(text string, index int) int {
// Insert into piece chain. // Insert into piece chain.
newIndex := len(t.spans) newIndex := len(t.spans)
t.spans[nextSpan.previous].next = newIndex t.spans[nextSpan.previous].next = newIndex
nextSpan.previous = newIndex t.spans[index].previous = newIndex
t.spans = append(t.spans, span) t.spans = append(t.spans, span)
// Adjust text area length. // Adjust text area length.
@ -1153,6 +1164,15 @@ func (t *TextArea) InputHandler() func(event *tcell.EventKey, setFocus func(p Pr
t.moveCursor(t.cursor.row+t.lastHeight, t.cursor.column) t.moveCursor(t.cursor.row+t.lastHeight, t.cursor.column)
case tcell.KeyPgUp, tcell.KeyCtrlB: // Move one page up. case tcell.KeyPgUp, tcell.KeyCtrlB: // Move one page up.
t.moveCursor(t.cursor.row-t.lastHeight, t.cursor.column) t.moveCursor(t.cursor.row-t.lastHeight, t.cursor.column)
case tcell.KeyEnter: // Insert a newline.
t.cursor.pos = t.replace(t.cursor.pos, t.cursor.pos, NewLine)
row := t.cursor.row
t.cursor.row++
t.cursor.column, t.cursor.actualColumn = 0, 0
if row < len(t.lineStarts)-1 {
t.lineStarts = t.lineStarts[:row]
}
t.clampToCursor(row)
case tcell.KeyRune: case tcell.KeyRune:
if event.Modifiers()&tcell.ModAlt > 0 { if event.Modifiers()&tcell.ModAlt > 0 {
// We accept some Alt- key combinations. // We accept some Alt- key combinations.
@ -1164,6 +1184,13 @@ func (t *TextArea) InputHandler() func(event *tcell.EventKey, setFocus func(p Pr
} }
} else { } else {
// Other keys are simply accepted as regular characters. // Other keys are simply accepted as regular characters.
t.cursor.pos = t.replace(t.cursor.pos, t.cursor.pos, string(event.Rune()))
row := t.cursor.row
t.cursor.row = -1
if row < len(t.lineStarts)-1 {
t.lineStarts = t.lineStarts[:row]
}
t.clampToCursor(row)
} }
} }
}) })