mirror of
https://github.com/rivo/tview.git
synced 2025-04-24 13:48:56 +08:00
Added basic text entering.
This commit is contained in:
parent
17194d2e94
commit
286f73a109
158
doc.go
158
doc.go
@ -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
|
||||
entry.
|
||||
|
||||
Widgets
|
||||
# Widgets
|
||||
|
||||
The package implements the following widgets:
|
||||
|
||||
- TextView: A scrollable window that display multi-colored text. Text may also
|
||||
be highlighted.
|
||||
- Table: A scrollable display of tabular data. Table cells, rows, or columns
|
||||
- [TextView]: A scrollable window that display multi-colored text. Text may
|
||||
also be highlighted.
|
||||
- [Table]: A scrollable display of tabular data. Table cells, rows, or columns
|
||||
may also be highlighted.
|
||||
- TreeView: A scrollable display for hierarchical data. Tree nodes can be
|
||||
- [TreeView]: A scrollable display for hierarchical data. Tree nodes can be
|
||||
highlighted, collapsed, expanded, and more.
|
||||
- List: A navigable text list with optional keyboard shortcuts.
|
||||
- InputField: One-line input fields to enter text.
|
||||
- DropDown: Drop-down selection fields.
|
||||
- Checkbox: Selectable checkbox for boolean values.
|
||||
- Button: Buttons which get activated when the user selects them.
|
||||
- [List]: A navigable text list with optional keyboard shortcuts.
|
||||
- [InputField]: One-line input fields to enter text.
|
||||
- [DropDown]: Drop-down selection fields.
|
||||
- [Checkbox]: Selectable checkbox for boolean values.
|
||||
- [Button]: Buttons which get activated when the user selects them.
|
||||
- Form: Forms composed of input fields, drop down selections, checkboxes, and
|
||||
buttons.
|
||||
- Modal: A centered window with a text message and one or more buttons.
|
||||
- Grid: A grid based layout manager.
|
||||
- Flex: A Flexbox based layout manager.
|
||||
- Pages: A page based layout manager.
|
||||
- [Modal]: A centered window with a text message and one or more buttons.
|
||||
- [Grid]: A grid based layout manager.
|
||||
- [Flex]: A Flexbox based layout manager.
|
||||
- [Pages]: A page based layout manager.
|
||||
|
||||
The package also provides Application which is used to poll the event queue and
|
||||
draw widgets on screen.
|
||||
|
||||
Hello World
|
||||
# Hello World
|
||||
|
||||
The following is a very basic example showing a box with the title "Hello,
|
||||
world!":
|
||||
|
||||
package main
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/rivo/tview"
|
||||
)
|
||||
import (
|
||||
"github.com/rivo/tview"
|
||||
)
|
||||
|
||||
func main() {
|
||||
box := tview.NewBox().SetBorder(true).SetTitle("Hello, world!")
|
||||
if err := tview.NewApplication().SetRoot(box, true).Run(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
func main() {
|
||||
box := tview.NewBox().SetBorder(true).SetTitle("Hello, world!")
|
||||
if err := tview.NewApplication().SetRoot(box, true).Run(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
First, we create a box primitive with a border and a title. Then we create an
|
||||
application, set the box as its root primitive, and run the event loop. The
|
||||
application exits when the application's Stop() function is called or when
|
||||
Ctrl-C is pressed.
|
||||
application exits when the application's [Application.Stop] function is called
|
||||
or when Ctrl-C is pressed.
|
||||
|
||||
If we have a primitive which consumes key presses, we call the application's
|
||||
SetFocus() function to redirect all key presses to that primitive. Most
|
||||
primitives then offer ways to install handlers that allow you to react to any
|
||||
actions performed on them.
|
||||
[Application.SetFocus] function to redirect all key presses to that primitive.
|
||||
Most primitives then offer ways to install handlers that allow you to react to
|
||||
any actions performed on them.
|
||||
|
||||
More Demos
|
||||
# More Demos
|
||||
|
||||
You will find more demos in the "demos" subdirectory. It also contains a
|
||||
presentation (written using tview) which gives an overview of the different
|
||||
widgets and how they can be used.
|
||||
|
||||
Colors
|
||||
# Colors
|
||||
|
||||
Throughout this package, colors are specified using the tcell.Color type.
|
||||
Functions such as tcell.GetColor(), tcell.NewHexColor(), and tcell.NewRGBColor()
|
||||
Throughout this package, colors are specified using the [tcell.Color] type.
|
||||
Functions such as [tcell.GetColor], [tcell.NewHexColor], and [tcell.NewRGBColor]
|
||||
can be used to create colors from W3C color names or RGB values.
|
||||
|
||||
Almost all strings which are displayed can contain color tags. Color tags are
|
||||
W3C color names or six hexadecimal digits following a hash tag, wrapped in
|
||||
square brackets. Examples:
|
||||
|
||||
This is a [red]warning[white]!
|
||||
The sky is [#8080ff]blue[#ffffff].
|
||||
This is a [red]warning[white]!
|
||||
The sky is [#8080ff]blue[#ffffff].
|
||||
|
||||
A color tag changes the color of the characters following that color tag. This
|
||||
applies to almost everything from box titles, list text, form item labels, to
|
||||
@ -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
|
||||
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.
|
||||
(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
|
||||
terminal):
|
||||
|
||||
l: blink
|
||||
b: bold
|
||||
i: italic
|
||||
d: dim
|
||||
r: reverse (switch foreground and background color)
|
||||
u: underline
|
||||
s: strike-through
|
||||
l: blink
|
||||
b: bold
|
||||
i: italic
|
||||
d: dim
|
||||
r: reverse (switch foreground and background color)
|
||||
u: underline
|
||||
s: strike-through
|
||||
|
||||
Examples:
|
||||
|
||||
[yellow]Yellow text
|
||||
[yellow:red]Yellow text on red background
|
||||
[:red]Red background, text color unchanged
|
||||
[yellow::u]Yellow text underlined
|
||||
[::bl]Bold, blinking text
|
||||
[::-]Colors unchanged, flags reset
|
||||
[-]Reset foreground color
|
||||
[-:-:-]Reset everything
|
||||
[:]No effect
|
||||
[]Not a valid color tag, will print square brackets as they are
|
||||
[yellow]Yellow text
|
||||
[yellow:red]Yellow text on red background
|
||||
[:red]Red background, text color unchanged
|
||||
[yellow::u]Yellow text underlined
|
||||
[::bl]Bold, blinking text
|
||||
[::-]Colors unchanged, flags reset
|
||||
[-]Reset foreground color
|
||||
[-:-:-]Reset everything
|
||||
[:]No effect
|
||||
[]Not a valid color tag, will print square brackets as they are
|
||||
|
||||
In the rare event that you want to display a string such as "[red]" or
|
||||
"[#00ff1a]" without applying its effect, you need to put an opening square
|
||||
@ -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
|
||||
character that may be used in color or region tags will be recognized. Examples:
|
||||
|
||||
[red[] will be output as [red]
|
||||
["123"[] will be output as ["123"]
|
||||
[#6aff00[[] will be output as [#6aff00[]
|
||||
[a#"[[[] will be output as [a#"[[]
|
||||
[] will be output as [] (see color tags above)
|
||||
[[] will be output as [[] (not an escaped tag)
|
||||
[red[] will be output as [red]
|
||||
["123"[] will be output as ["123"]
|
||||
[#6aff00[[] will be output as [#6aff00[]
|
||||
[a#"[[[] will be output as [a#"[[]
|
||||
[] will be output as [] (see color tags above)
|
||||
[[] will be output as [[] (not an escaped tag)
|
||||
|
||||
You can use the Escape() function to insert brackets automatically where needed.
|
||||
|
||||
Styles
|
||||
# Styles
|
||||
|
||||
When primitives are instantiated, they are initialized with colors taken from
|
||||
the global Styles variable. You may change this variable to adapt the look and
|
||||
feel of the primitives to your preferred style.
|
||||
|
||||
Unicode Support
|
||||
# Unicode Support
|
||||
|
||||
This package supports unicode characters including wide characters.
|
||||
|
||||
Concurrency
|
||||
# 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
|
||||
@ -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
|
||||
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):
|
||||
|
||||
go func() {
|
||||
app.QueueUpdateDraw(func() {
|
||||
table.SetCellSimple(0, 0, "Foo bar")
|
||||
})
|
||||
}()
|
||||
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
|
||||
One exception to this is the io.Writer interface implemented by [TextView]. You
|
||||
can safely write to a [TextView] from any goroutine. See the [TextView]
|
||||
documentation for details.
|
||||
|
||||
You can also call Application.Draw() from any goroutine without having to wrap
|
||||
it in QueueUpdate(). And, as mentioned above, key event callbacks are executed
|
||||
in the main goroutine and thus should not use QueueUpdate() as that may lead to
|
||||
deadlocks.
|
||||
You can also call [Application.Draw] from any goroutine without having to wrap
|
||||
it in [Application.QueueUpdate]. And, as mentioned above, key event callbacks
|
||||
are executed in the main goroutine and thus should not use
|
||||
[Application.QueueUpdate] as that may lead to deadlocks.
|
||||
|
||||
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.
|
||||
|
||||
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
|
||||
and constants from that package (e.g. colors and keyboard values).
|
||||
|
||||
This package does not process mouse input (yet).
|
||||
*/
|
||||
package tview
|
||||
|
93
textarea.go
93
textarea.go
@ -41,17 +41,16 @@ var (
|
||||
// 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
|
||||
// two-element int array. The first element is the index of the referenced span
|
||||
// in the piece chain. The second element is the offset into the span's
|
||||
// three-element int array. The first element is the index of the referenced
|
||||
// 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
|
||||
// < span.length. Sometimes, we may use a three-element int array which also
|
||||
// contains the corresponding text parser's state in the third position.
|
||||
// < span.length. The third elements is the corresponding text parser's state.
|
||||
//
|
||||
// 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
|
||||
// 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
|
||||
// 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 {
|
||||
// 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
|
||||
@ -128,12 +127,12 @@ type textAreaSpan struct {
|
||||
// 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
|
||||
// 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
|
||||
// Ctrl-Q key to implement copying to the clipboard.
|
||||
// "copy to clipboard" function, you may use [Box.SetInputCapture] to override
|
||||
// the Ctrl-Q key to implement copying to the clipboard.
|
||||
//
|
||||
// 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
|
||||
// the key binds for copy, cut, and paste. The GetSelection(), ReplaceText(),
|
||||
// operating system's clipboard), you can also use [Box.SetInputCapture] to
|
||||
// override the key binds for copy, cut, and paste. The GetSelection(), ReplaceText(),
|
||||
// and SetSelection() provide all the functionality needed for your own
|
||||
// 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.
|
||||
// If the resulting text would exceed the maximum length, the function does not
|
||||
// do anything. See textAreaSpan for information about text positions and span
|
||||
// ranges.
|
||||
// do anything. The function returns the new position of the deleted/inserted
|
||||
// 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
|
||||
// elsewhere, when the user changes their type of edit.
|
||||
func (t *TextArea) replace(deleteStart, deleteEnd [2]int, insert string) {
|
||||
// elsewhere, when the user changes their type of edit. It also does not modify
|
||||
// [TextArea.lineStarts].
|
||||
func (t *TextArea) replace(deleteStart, deleteEnd [3]int, insert string) (end [3]int) {
|
||||
end = deleteEnd
|
||||
|
||||
// Check max length.
|
||||
if t.maxLength > 0 && t.length+len(insert) > t.maxLength {
|
||||
return
|
||||
@ -367,22 +371,24 @@ func (t *TextArea) replace(deleteStart, deleteEnd [2]int, insert string) {
|
||||
}
|
||||
} // At this point, deleteStart[0] == deleteEnd[0].
|
||||
if deleteEnd[1] > deleteStart[1] {
|
||||
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 {
|
||||
if deleteStart[1] != 0 {
|
||||
// Delete in the middle by splitting the span.
|
||||
deleteEnd[1] -= deleteStart[1]
|
||||
deleteStart[0] = t.splitSpan(deleteStart[0], deleteStart[1])
|
||||
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.
|
||||
@ -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() {
|
||||
// We can simply append to the edit buffer.
|
||||
length, _ := t.editText.WriteString(insert)
|
||||
previousSpan.length += length
|
||||
t.spans[span.previous].length += length
|
||||
t.length += length
|
||||
} else {
|
||||
// Insert a new span.
|
||||
@ -405,15 +411,18 @@ func (t *TextArea) replace(deleteStart, deleteEnd [2]int, insert string) {
|
||||
// Split and insert.
|
||||
spanIndex = t.splitSpan(spanIndex, offset)
|
||||
t.insertSpan(insert, spanIndex)
|
||||
end = [3]int{spanIndex, 0, 0}
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// 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
|
||||
// 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 {
|
||||
if index < 2 || index >= len(t.spans) {
|
||||
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
|
||||
// offset.
|
||||
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
|
||||
}
|
||||
|
||||
@ -456,18 +467,18 @@ func (t *TextArea) splitSpan(index, offset int) int {
|
||||
if span.length < 0 {
|
||||
// Initial text span. Has negative length.
|
||||
newSpan.length = span.length + offset
|
||||
span.length = -offset
|
||||
t.spans[index].length = -offset
|
||||
} else {
|
||||
// Edit buffer span. Has positive length.
|
||||
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)
|
||||
t.spans = append(t.spans, newSpan)
|
||||
t.spans[span.next].previous = newIndex
|
||||
span.next = newIndex
|
||||
t.spans[index].next = newIndex
|
||||
|
||||
return newIndex
|
||||
}
|
||||
@ -493,7 +504,7 @@ func (t *TextArea) insertSpan(text string, index int) int {
|
||||
// Insert into piece chain.
|
||||
newIndex := len(t.spans)
|
||||
t.spans[nextSpan.previous].next = newIndex
|
||||
nextSpan.previous = newIndex
|
||||
t.spans[index].previous = newIndex
|
||||
t.spans = append(t.spans, span)
|
||||
|
||||
// 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)
|
||||
case tcell.KeyPgUp, tcell.KeyCtrlB: // Move one page up.
|
||||
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:
|
||||
if event.Modifiers()&tcell.ModAlt > 0 {
|
||||
// We accept some Alt- key combinations.
|
||||
@ -1164,6 +1184,13 @@ func (t *TextArea) InputHandler() func(event *tcell.EventKey, setFocus func(p Pr
|
||||
}
|
||||
} else {
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
Loading…
x
Reference in New Issue
Block a user