// Package cpick is an interactive color picker in the terminal using cview
package cpick
import (
"encoding/json"
"fmt"
"io/ioutil"
"log"
"math"
"os"
"os/user"
"regexp"
"strconv"
"strings"
color "github.com/ethanbaker/colors"
"github.com/ethanbaker/cpick/cview"
"github.com/gdamore/tcell/v2"
)
// Breakpoint boundaries
const BREAKPOINT_HEIGHT = 30
const BREAKPOINT_WIDTH = 110
// Global configuration variables
var testingMode = false
var smallWidth = false
var smallHeight = false
// jsonColorInfo type used to hold imported colors
type jsonColorInfo struct {
name string
length int
colors []jsonColor
table *cview.Table
}
// ColorValues type used to hold color values and optional name
type ColorValues struct {
RGB color.RGB
HSV color.HSV
HSL color.HSL
CMYK color.CMYK
Hex color.Hex
Decimal color.Decimal
Ansi color.Ansi
Name string
}
var colorBlockWide string = `
███████████████████
███████████████████
███████████████████
███████████████████
`
var colorBlockSmall string = `
████████████
`
var colorTextWide string = `
RGB: %v, %v, %v
HSV: %v°, %v%%, %v%%
HSL: %v°, %v%%, %v%%
CMYK: %v%%, %v%%, %v%%, %v%%
Hex: #%v
Decimal: %v
Ansi: "\033%v"
`
var colorTextSmall string = `
RGB: %v,%v,%v
HSV: %v°,%v%%,%v%%
HSL: %v°,%v%%,%v%%
CMYK: %v%%,%v%%
%v%%,%v%%
Hex: #%v
Decimal: %v
Ansi:
"\033%v"
`
var colorPageText string = "██████████ %v %v "
var helpString string = `
Movement: vim keys (h,j,k,l) or arrow keys
Quitting the application: escape or q
While on the hue table:
- Press enter to create a new saturation-value table
- Press space to select the preset color table
- Press tab to switch to the saturation-value table
While on the preset color table:
- Press enter to create a new saturation-value table
- Press space to select the hue table
- Press tab to switch to the saturation-value table
- Press C to go to the next color page
- Press c to go to the previous color page
- Press ? to enter a search menu for colors
- Press N to go to the next search instance
- Press n to go to the previous search instance
While on the saturation-value table:
- Press enter to select the final color
- Press tab to switch to the hue table
`
var searchHelpString string = `
To search for a color name, type the name of the color into the search bar. Related colors will appear below.
Once a color (or phrase) is desired, press enter. You can press N (forward) and n (reverse) to swap between instances.
Each value type you want to select will have instructions below:
- Hexadecimal: type the hex value starting with "#" (EX: #ffffff)
- RGB: type "rgb:" and three RGB values separated by a space (EX: rgb: 255 255 255)
- HSV: type "hsv:" and three HSV values separated by a space (EX: hsv: 0 100 0)
- HSL: type "hsl:" and three HSL values separated by a space (EX: hsl: 0 100 50)
- CMYK: type "cmyk:" and four CMYK values separated by a space (EX: cmyk: 0 0 0 0)
- Decimal: type "decimal:" and then the decimal value (EX: 16777215)
Once a color is selected, you will be taken to the Saturation-Value table with the specified color selected.
Any errors that you make will appear in red below the search bar.
`
// Global variables to make up elements on screen
var app *cview.Application = cview.NewApplication()
var pages *cview.Pages = cview.NewPages()
var hFlex *cview.Flex = cview.NewFlex()
var svFlex *cview.Flex = cview.NewFlex()
var hTable *cview.Table = cview.NewTable()
var svTable *cview.Table = cview.NewTable()
var darkHBlock *cview.TextView = cview.NewTextView()
var darkHText *cview.TextView = cview.NewTextView()
var lightHBlock *cview.TextView = cview.NewTextView()
var lightHText *cview.TextView = cview.NewTextView()
var darkSVBlock *cview.TextView = cview.NewTextView()
var darkSVText *cview.TextView = cview.NewTextView()
var lightSVBlock *cview.TextView = cview.NewTextView()
var lightSVText *cview.TextView = cview.NewTextView()
var colorPageTitle *cview.TextView = cview.NewTextView()
var jsonColors *cview.Flex = cview.NewFlex()
var colorPages *cview.Pages = cview.NewPages()
var colorPageIndex int
var colorInfo []jsonColorInfo
var helpFlex *cview.Flex = cview.NewFlex()
var helpModal *cview.Modal = cview.NewModal()
var helpFocus cview.Primitive = hTable
var searchFlex *cview.Flex = cview.NewFlex()
var searchStatus *cview.TextView = cview.NewTextView()
var searchInput *cview.InputField = cview.NewInputField()
var searchNames []string
var searchIndexes [][]int
var searchIndex int
var hFocus cview.Primitive = hTable
var hue int
var returnColor ColorValues
// Input Handlers ---------------------------------------------------------
func inputCaptureHandler(event *tcell.EventKey) *tcell.EventKey {
switch {
case event.Rune() == 'q':
if !searchFlex.HasFocus() {
app.Stop()
}
case event.Rune() == '`':
showHelp()
case event.Key() == tcell.KeyCtrlF || event.Rune() == '?':
showSearch()
return nil
}
if svTable.HasFocus() {
event = svCaptureHandler(event)
} else if hTable.HasFocus() {
event = hCaptureHandler(event)
} else if colorPages.HasFocus() {
event = colorPageCaptureHandler(event)
}
return event
}
func svCaptureHandler(event *tcell.EventKey) *tcell.EventKey {
return event
}
func hCaptureHandler(event *tcell.EventKey) *tcell.EventKey {
switch {
case event.Rune() == ' ':
hFocus = colorPages
app.SetFocus(colorPages)
row, col := colorInfo[colorPageIndex].table.GetSelection()
text := colorInfo[colorPageIndex].table.GetCell(row, col).Text
raw := strings.Split(string(text[:]), "#")
hsv := color.HextoHSV(color.Hex(raw[1]))
darkHSV := hsv
lightHSV := hsv
if hsv.V%2 == 0 {
darkHSV.V -= 1
} else {
lightHSV.V += 1
}
setColorValues(darkHSV, darkHBlock, darkHText, lightHSV, lightHBlock, lightHText)
colorInfo[colorPageIndex].table.SetSelectedStyle(tcell.ColorDefault, tcell.ColorGray, tcell.AttrNone)
}
return event
}
func colorPageCaptureHandler(event *tcell.EventKey) *tcell.EventKey {
switch {
// Change pages of color tables
case event.Rune() == 'C':
if colorPageIndex < len(colorInfo)-1 {
colorPageIndex++
colorInfo[colorPageIndex].table.SetSelectedStyle(tcell.ColorDefault, tcell.ColorGray, tcell.AttrNone)
name := colorInfo[colorPageIndex].name
colorPageTitle.SetText(name)
pageId := fmt.Sprintf("page-%d", colorPageIndex)
colorPages.SwitchToPage(pageId)
row, col := colorInfo[colorPageIndex].table.GetSelection()
text := string(colorInfo[colorPageIndex].table.GetCell(row, col).Text[:])
raw := strings.Split(text, "#")
hsv := color.HextoHSV(color.Hex(raw[1]))
darkHSV := hsv
lightHSV := hsv
if hsv.V%2 == 0 {
darkHSV.V -= 1
} else {
lightHSV.V += 1
}
setColorValues(darkHSV, darkHBlock, darkHText, lightHSV, lightHBlock, lightHText)
}
case event.Rune() == 'c':
if colorPageIndex > 0 {
colorPageIndex--
colorInfo[colorPageIndex].table.SetSelectedStyle(tcell.ColorDefault, tcell.ColorGray, tcell.AttrNone)
name := colorInfo[colorPageIndex].name
colorPageTitle.SetText(name)
pageId := fmt.Sprintf("page-%d", colorPageIndex)
colorPages.SwitchToPage(pageId)
row, col := colorInfo[colorPageIndex].table.GetSelection()
text := string(colorInfo[colorPageIndex].table.GetCell(row, col).Text[:])
raw := strings.Split(text, "#")
hsv := color.HextoHSV(color.Hex(raw[1]))
darkHSV := hsv
lightHSV := hsv
if hsv.V%2 == 0 {
darkHSV.V -= 1
} else {
lightHSV.V += 1
}
setColorValues(darkHSV, darkHBlock, darkHText, lightHSV, lightHBlock, lightHText)
}
// Switch to hTable
case event.Rune() == ' ':
hFocus = hTable
app.SetFocus(hTable)
_, col := hTable.GetSelection()
darkHSV := color.HSV{H: col * 2, S: 100, V: 100}
lightHSV := color.HSV{H: col*2 + 1, S: 100, V: 100}
setColorValues(darkHSV, darkHBlock, darkHText, lightHSV, lightHBlock, lightHText)
colorInfo[colorPageIndex].table.SetSelectedStyle(tcell.ColorDefault, tcell.ColorDefault, tcell.AttrBold)
}
searchInputCaptureHandler(event)
return colorPageMovementHandler(event)
}
// Handle any movement events by preventing the user from selecting
// a blank filler cell
func colorPageMovementHandler(event *tcell.EventKey) *tcell.EventKey {
switch {
case event.Rune() == 'l' || event.Key() == tcell.KeyRight:
row, col := colorInfo[colorPageIndex].table.GetSelection()
if col < colorInfo[colorPageIndex].table.GetColumnCount()-1 {
cell := colorInfo[colorPageIndex].table.GetCell(row, col+1)
if len(cell.Text) == 0 {
return nil
}
}
case event.Rune() == 'j' || event.Key() == tcell.KeyDown:
row, col := colorInfo[colorPageIndex].table.GetSelection()
if row < colorInfo[colorPageIndex].table.GetRowCount()-1 {
cell := colorInfo[colorPageIndex].table.GetCell(row+1, col)
if len(cell.Text) == 0 {
return nil
}
}
case event.Rune() == 'G':
row := colorInfo[colorPageIndex].table.GetRowCount() - 1
col := colorInfo[colorPageIndex].table.GetColumnCount() - 1
for true {
cell := colorInfo[colorPageIndex].table.GetCell(row, col)
if len(cell.Text) != 0 {
break
} else {
row--
}
}
colorInfo[colorPageIndex].table.Select(row, col)
return nil
}
return event
}
func searchInputCaptureHandler(event *tcell.EventKey) *tcell.EventKey {
if len(searchIndexes) > 1 {
switch event.Rune() {
// Go back a selection
case 'n':
if searchIndex == 0 {
searchIndex = len(searchIndexes)
}
searchIndex--
colorPages.SwitchToPage(fmt.Sprintf("page-%v", searchIndexes[searchIndex][0]))
colorPageIndex = searchIndexes[searchIndex][0]
colorInfo[colorPageIndex].table.Select(searchIndexes[searchIndex][2], searchIndexes[searchIndex][1])
colorInfo[colorPageIndex].table.SetSelectedStyle(tcell.ColorDefault, tcell.ColorGray, tcell.AttrNone)
// Go forward a selection
case 'N':
if searchIndex == len(searchIndexes)-1 {
searchIndex = -1
}
searchIndex++
colorPages.SwitchToPage(fmt.Sprintf("page-%v", searchIndexes[searchIndex][0]))
colorPageIndex = searchIndexes[searchIndex][0]
colorInfo[colorPageIndex].table.Select(searchIndexes[searchIndex][2], searchIndexes[searchIndex][1])
colorInfo[colorPageIndex].table.SetSelectedStyle(tcell.ColorDefault, tcell.ColorGray, tcell.AttrNone)
}
}
return event
}
// Screen setup -----------------------------------------------------------
func hScreenSetup() {
// Dark color value setup
darkText := cview.NewTextView()
darkText.SetScrollBarVisibility(cview.ScrollBarNever)
if !smallWidth && !smallHeight {
darkText.SetText("Dark Tint Color")
darkHBlock.SetText(colorBlockWide)
} else {
darkText.SetText("Color")
darkHBlock.SetText(colorBlockSmall)
}
darkHBlock.SetScrollBarVisibility(cview.ScrollBarNever)
darkHText.SetScrollBarVisibility(cview.ScrollBarNever)
darkColorFlex := cview.NewFlex()
darkColorFlex.SetDirection(cview.FlexRow)
darkColorFlex.AddItem(darkText, 0, 1, false)
darkColorFlex.AddItem(darkHBlock, 0, 2, false)
darkColorFlex.AddItem(darkHText, 0, 9, false)
// Light color value setup
lightText := cview.NewTextView()
lightColorFlex := cview.NewFlex()
if !smallHeight && !smallWidth {
lightText.SetScrollBarVisibility(cview.ScrollBarNever)
lightText.SetText(" Light Tint Color")
lightHBlock.SetText(colorBlockWide)
lightHBlock.SetScrollBarVisibility(cview.ScrollBarNever)
lightHText.SetScrollBarVisibility(cview.ScrollBarNever)
lightColorFlex.SetDirection(cview.FlexRow)
lightColorFlex.AddItem(lightText, 0, 1, false)
lightColorFlex.AddItem(lightHBlock, 0, 2, false)
lightColorFlex.AddItem(lightHText, 0, 9, false)
}
colorFlex := cview.NewFlex()
colorFlex.SetDirection(cview.FlexRow)
colorFlex.AddItem(darkColorFlex, 0, 1, false)
if !smallHeight && !smallWidth {
colorFlex.AddItem(lightColorFlex, 0, 1, false)
}
// Everything except hTable setup
lowerFlex := cview.NewFlex()
lowerFlex.SetDirection(cview.FlexColumn)
lowerFlex.AddItem(colorFlex, 0, 3, false)
lowerFlex.AddItem(jsonColors, 0, 9, false)
help := cview.NewTextView()
help.SetTextAlign(cview.AlignRight)
help.SetText("Press ` to see help")
hFlex.SetDirection(cview.FlexRow)
hFlex.AddItem(hTable, 0, 1, true)
hFlex.AddItem(help, 0, 1, false)
hFlex.AddItem(lowerFlex, 0, 20, false)
darkHSV := color.HSV{H: 0, S: 100, V: 100}
lightHSV := color.HSV{H: 0, S: 100, V: 100}
setColorValues(darkHSV, darkHBlock, darkHText, lightHSV, lightHBlock, lightHText)
}
func svScreenSetup() {
// Fill the text with the default values
darkSVBlock.SetScrollBarVisibility(cview.ScrollBarNever)
lightSVBlock.SetScrollBarVisibility(cview.ScrollBarNever)
if !smallWidth && !smallHeight {
darkSVBlock.SetText(colorBlockWide)
lightSVBlock.SetText(colorBlockWide)
} else {
darkSVBlock.SetText(colorBlockSmall)
}
darkHSV := color.HSV{H: 0, S: 100, V: 99}
lightHSV := color.HSV{H: 0, S: 100, V: 100}
setColorValues(darkHSV, darkSVBlock, darkSVText, lightHSV, lightSVBlock, lightSVText)
// Setup the screen
darkTitle := cview.NewTextView()
if !smallWidth && !smallHeight {
darkTitle.SetText(" Dark Tint Color")
} else {
darkTitle.SetText(" Color")
}
darkSVText.SetScrollBarVisibility(cview.ScrollBarNever)
darkSVFlex := cview.NewFlex()
darkSVFlex.SetDirection(cview.FlexRow)
darkSVFlex.AddItem(darkTitle, 0, 1, false)
darkSVFlex.AddItem(darkSVBlock, 0, 2, false)
darkSVFlex.AddItem(darkSVText, 0, 9, false)
lightTitle := cview.NewTextView()
lightSVFlex := cview.NewFlex()
if !smallHeight && !smallWidth {
lightTitle.SetText(" Light Tint Color")
lightSVText.SetScrollBarVisibility(cview.ScrollBarNever)
lightSVFlex.SetDirection(cview.FlexRow)
lightSVFlex.AddItem(lightTitle, 0, 1, false)
lightSVFlex.AddItem(lightSVBlock, 0, 2, false)
lightSVFlex.AddItem(lightSVText, 0, 9, false)
}
colorFlex := cview.NewFlex()
colorFlex.SetDirection(cview.FlexRow)
colorFlex.AddItem(darkSVFlex, 0, 1, false)
if !smallHeight && !smallWidth {
colorFlex.AddItem(lightSVFlex, 0, 1, false)
}
svFlex.AddItem(svTable, 0, 4, false)
svFlex.AddItem(colorFlex, 0, 1, false)
}
// Help page setup --------------------------------------------------------
func helpPageSetup() {
helpModal.SetText(helpString)
helpModal.AddButtons([]string{"Exit help"})
helpModal.SetDoneFunc(helpModalDoneFunc)
helpFlex.AddItem(helpModal, 0, 1, false)
}
func helpModalDoneFunc(buttonIndex int, buttonLabel string) {
if buttonLabel == "Exit help" {
if helpFocus == hTable || helpFocus == colorPages {
hFlex.RemoveItem(helpFlex)
} else if helpFocus == svTable {
svFlex.RemoveItem(helpFlex)
svFlex.SetDirection(cview.FlexColumn)
}
app.SetFocus(helpFocus)
}
}
// Search page setup ------------------------------------------------------
func searchInputSetup() {
for i := 0; i < len(colorInfo); i++ {
for _, c := range colorInfo[i].colors {
searchNames = append(searchNames, strings.ToLower(c.NAME))
}
}
searchInput.SetLabel("Enter a color name or value to search for: ")
searchInput.SetFieldWidth(60)
searchInput.SetDoneFunc(searchInputDoneFunc)
searchInput.SetAutocompleteFunc(searchInputAutocompleteFunc)
searchStatus.SetTextColor(tcell.ColorRed)
searchHelp := cview.NewTextView()
searchHelp.SetText(searchHelpString)
searchFlex.SetDirection(cview.FlexRow)
searchFlex.AddItem(searchInput, 0, 1, false)
searchFlex.AddItem(searchStatus, 0, 1, false)
searchFlex.AddItem(searchHelp, 0, 4, false)
}
func searchInputDoneFunc(key tcell.Key) {
switch key {
// Go back to the main application
case tcell.KeyEscape:
pages.SwitchToPage("Hue page")
colorInfo[colorPageIndex].table.Select(0, 0)
app.SetFocus(colorPages)
// Select a value on the color tables
case tcell.KeyEnter:
text := strings.ToLower(strings.TrimSpace(searchInput.GetText()))
if len(text) > 0 {
parseSearchText(text)
}
}
}
func parseSearchText(text string) {
raw := strings.Split(strings.TrimSpace(strings.Join(strings.Split(text, ":")[1:], "")), " ")
var ints []int
safe := true
for _, v := range raw {
num, err := strconv.ParseInt(v, 10, 64)
if err != nil {
safe = false
break
}
ints = append(ints, int(num))
}
if len(text) > 5 && text[0:5] == "ansi:" {
searchStatus.SetText("Please enter the RGB values inside of the ansi escape sequence")
return
} else if !safe && text[0] != '#' && strings.Contains(text, ":") {
searchStatus.SetText("Please enter valid numbers")
return
}
var hsv color.HSV
var statusMessage string
switch {
case strings.HasPrefix(strings.ToLower(text), strings.ToLower("#")):
matched, err := regexp.MatchString(`[0-9a-fA-F]+`, text)
testErr(err)
if matched && len(text) == 7 {
hsv = color.HextoHSV(color.Hex(text))
} else {
statusMessage = "Please enter a valid hexadecimal value"
}
case strings.HasPrefix(strings.ToLower(text), strings.ToLower("rgb:")):
for _, v := range ints {
if v > 255 || v < 0 {
statusMessage = "Please enter valid RGB values (0 < x < 255)"
}
}
if len(ints) == 3 {
rgb := color.RGB{R: ints[0], G: ints[1], B: ints[2]}
hsv = color.RGBtoHSV(rgb)
} else {
statusMessage = "Please enter 3 RGB values"
}
case strings.HasPrefix(strings.ToLower(text), strings.ToLower("hsv:")):
if ints[0] < 0 || ints[0] > 359 {
statusMessage = "Please enter a valid hue value (0 < x < 359)"
}
for _, v := range ints[1:] {
if v > 100 || v < 0 {
statusMessage = "Please enter valid Saturation and Value values (0 < x < 100)"
}
}
if len(ints) == 3 {
hsv = color.HSV{H: ints[0], S: ints[1], V: ints[2]}
} else {
statusMessage = "Please enter 3 HSV values"
}
case strings.HasPrefix(strings.ToLower(text), strings.ToLower("hsl:")):
if ints[0] < 0 || ints[0] > 359 {
statusMessage = "Please enter a valid hue value (0 < x < 359)"
}
for _, v := range ints[1:] {
if v > 100 || v < 0 {
statusMessage = "Please enter valid Saturation and Length values (0 < x < 100)"
}
}
if len(ints) == 3 {
hsl := color.HSL{H: ints[0], S: ints[1], L: ints[2]}
hsv = color.HSLtoHSV(hsl)
} else {
statusMessage = "Please enter 3 HSL values"
}
case strings.HasPrefix(strings.ToLower(text), strings.ToLower("cmyk:")):
for _, v := range ints[1:] {
if v > 100 || v < 0 {
statusMessage = "Please enter valid CMYK values (0 < x < 100)"
}
}
if len(ints) == 4 {
cmyk := color.CMYK{C: ints[0], M: ints[1], Y: ints[2], K: ints[3]}
hsv = color.CMYKtoHSV(cmyk)
} else {
statusMessage = "Please enter 4 CMYK values"
}
case strings.HasPrefix(strings.ToLower(text), strings.ToLower("decimal:")):
if ints[0] < 0 || ints[0] > 16777215 {
statusMessage = "Please enter a valid decimal value (0 < x < 16777215)"
}
if len(ints) == 1 {
decimal := ints[0]
hsv = color.DecimaltoHSV(color.Decimal(decimal))
} else {
statusMessage = "Please enter 1 decimal value"
}
default:
locations := getColorLocations(text)
searchIndexes = locations
pages.SwitchToPage("Hue page")
app.SetFocus(colorPages)
if len(locations) > 0 {
colorPages.SwitchToPage(fmt.Sprintf("page-%v", locations[0][0]))
colorPageIndex = locations[0][0]
colorInfo[colorPageIndex].table.Select(locations[0][2], locations[0][1])
colorInfo[colorPageIndex].table.SetSelectedStyle(tcell.ColorDefault, tcell.ColorGray, tcell.AttrNone)
}
searchInput.SetText("")
return
}
if statusMessage != "" {
searchStatus.SetText(statusMessage)
return
}
hue = hsv.H
drawSVTable()
svTable.Select(int(math.Round(50-float64(hsv.V/2))), hsv.S)
pages.SwitchToPage("Saturation-Value page")
app.SetFocus(svTable)
searchInput.SetText("")
searchStatus.SetText("")
return
}
func searchInputAutocompleteFunc(currentText string) []*cview.ListItem {
if len(currentText) == 0 {
return nil
}
// Create a list of possible selections
var entries []string
for _, word := range searchNames {
if strings.HasPrefix(strings.ToLower(word), strings.ToLower(currentText)) {
entries = append(entries, word)
}
}
// If the list is 0 or there is only one option that the user already
// typed
if len(entries) < 1 {
entries = nil
} else if len(entries) == 1 && currentText == entries[0] {
entries = nil
}
// Convert strings to list items
var items []*cview.ListItem
for _, v := range entries {
items = append(items, cview.NewListItem(v))
}
return items
}
// Color pages setup ------------------------------------------------------
func colorPageSetup() {
colorInfo = make([]jsonColorInfo, 0)
path, err := getPath()
testErr(err)
var data jsonData
data = getCustomColors(path)
// Get the lists of all of the imported colors
for i := 0; i < len(data.COLORLIST); i++ {
c := jsonColorInfo{}
colorInfo = append(colorInfo, c)
colorInfo[i].name = strings.Title(data.COLORLIST[i].NAME + " pages")
colorInfo[i].length = len(data.COLORLIST[i].COLORS)
colorInfo[i].colors = data.COLORLIST[i].COLORS
for j := 0; j < 8; j++ {
colorInfo[i].colors = append(colorInfo[i].colors, jsonColor{"", "000000"})
}
colorInfo[i].table = cview.NewTable()
colorInfo[i].table.SetCellPadding(3, 0)
colorInfo[i].table.SetScrollBarVisibility(cview.ScrollBarNever)
colorInfo[i].table.SetSelectable(true, true)
colorInfo[i].table.SetSelectedStyle(tcell.ColorDefault, tcell.ColorDefault, tcell.AttrBold)
colorInfo[i].table.SetDoneFunc(colorPageDoneFunc)
colorInfo[i].table.SetSelectedFunc(colorPageSelectedFunc)
colorInfo[i].table.SetSelectionChangedFunc(colorPageSelectionChangedFunc)
}
// Make pages to hold the tables for all of the colors
for colorIndex := 0; colorIndex < len(colorInfo); colorIndex++ {
for x := 0; x < int(math.Ceil(float64(colorInfo[colorIndex].length/9)))+1; x++ {
for y := 0; y < 9; y++ {
rgb := color.HextoRGB(color.Hex(colorInfo[colorIndex].colors[x*9+y].VALUE))
name := strings.ToLower(colorInfo[colorIndex].colors[x*9+y].NAME)
val := strings.ToLower(colorInfo[colorIndex].colors[x*9+y].VALUE)
// Draw the color if it can actually be seen
if colorInfo[colorIndex].colors[x*9+y].NAME == "" {
cell := cview.NewTableCell("")
cell.SetTextColor(0)
colorInfo[colorIndex].table.SetCell(y, x, cell)
} else if rgb.R+rgb.G+rgb.B > 84 {
text := fmt.Sprintf(colorPageText, name, val)
c := tcell.NewHexColor(int32(color.HextoDecimal(color.Hex(val))))
cell := cview.NewTableCell(text)
cell.SetTextColor(c)
colorInfo[colorIndex].table.SetCell(y, x, cell)
} else {
text := fmt.Sprintf("██████████ [white]%v %v ", name, val)
c := tcell.NewHexColor(int32(color.HextoDecimal(color.Hex(val))))
cell := cview.NewTableCell(text)
cell.SetTextColor(c)
colorInfo[colorIndex].table.SetCell(y, x, cell)
}
}
}
pageId := fmt.Sprintf("page-%d", colorIndex)
colorPages.AddPage(pageId, colorInfo[colorIndex].table, true, false)
}
colorPages.SwitchToPage("page-0")
colorPageTitle.SetTextAlign(cview.AlignCenter)
colorPageTitle.SetText(strings.Title(colorInfo[0].name))
// Setup the color page
jsonColors.SetDirection(cview.FlexRow)
jsonColors.AddItem(colorPageTitle, 0, 1, false)
jsonColors.AddItem(colorPages, 0, 10, false)
}
func colorPageDoneFunc(key tcell.Key) {
switch {
case key == tcell.KeyEscape:
app.Stop()
case key == tcell.KeyTab:
pages.SwitchToPage("Saturation-Value page")
app.SetFocus(svTable)
}
}
func colorPageSelectedFunc(row int, column int) {
// Switch to blank saturation-value page
svTable.ScrollToBeginning()
svTable.Clear()
pages.SwitchToPage("Saturation-Value page")
app.SetFocus(svTable)
// Get the color displayed in the table
text := colorInfo[colorPageIndex].table.GetCell(row, column).Text
raw := strings.Split(string(text[:]), "#")
hsv := color.HextoHSV(color.Hex(raw[1]))
cursor := color.HSVtoRGB(color.HSV{H: (hsv.H + 180) % 360, S: 100, V: 100})
c := tcell.NewRGBColor(int32(cursor.R), int32(cursor.G), int32(cursor.B))
svTable.SetSelectedStyle(c, c, tcell.AttrNone)
hue = hsv.H
drawSVTable()
// Move the user to the selected color
x := hsv.S
y := 50 - hsv.V/2
if hsv.V%2 == 1 {
y--
}
svTable.Select(y, x)
}
func colorPageSelectionChangedFunc(row int, column int) {
// Get the color from the table
text := colorInfo[colorPageIndex].table.GetCell(row, column).Text
raw := strings.Split(string(text[:]), "#")
hsv := color.HextoHSV(color.Hex(raw[1]))
// Fill the color format string with the correct values for the selected
// color
darkHSV := hsv
lightHSV := hsv
if hsv.V%2 == 0 {
darkHSV.V -= 1
} else {
lightHSV.V += 1
}
setColorValues(darkHSV, darkHBlock, darkHText, lightHSV, lightHBlock, lightHText)
}
// hTable setup ----------------------------------------------------------
func hTableSetup() {
// Set the hue table with its necessary properties
hTable.SetSelectable(true, true)
hTable.Select(0, 0)
hTable.SetSelectedStyle(tcell.ColorWhite, tcell.ColorWhite, tcell.AttrNone)
hTable.SetCellPadding(0, 0)
// Color the hue table
for h := 0; h < 360; h += 2 {
bg := color.HSVtoRGB(color.HSV{H: h + 1, S: 100, V: 100})
fg := color.HSVtoRGB(color.HSV{H: h, S: 100, V: 100})
bc := tcell.NewRGBColor(int32(bg.R), int32(bg.G), int32(bg.B))
c := tcell.NewRGBColor(int32(fg.R), int32(fg.G), int32(fg.B))
cell := cview.NewTableCell("▐")
cell.SetBackgroundColor(bc)
cell.SetTextColor(c)
hTable.SetCell(0, h/2, cell)
}
hTable.SetDoneFunc(hTableDoneFunc)
hTable.SetSelectedFunc(hTableSelectedFunc)
hTable.SetSelectionChangedFunc(hTableSelectionChangedFunc)
}
func hTableDoneFunc(key tcell.Key) {
switch {
case key == tcell.KeyEscape:
app.Stop()
case key == tcell.KeyTab:
pages.SwitchToPage("Saturation-Value page")
app.SetFocus(svTable)
}
}
func hTableSelectedFunc(row int, column int) {
hue = column * 2
// Switch to saturation-value page with the correct setup
svTable.Clear()
pages.SwitchToPage("Saturation-Value page")
app.SetFocus(svTable)
svTable.Select(0, 100)
cursor := color.HSVtoRGB(color.HSV{H: (hue + 180) % 360, S: 100, V: 100})
c := tcell.NewRGBColor(int32(cursor.R), int32(cursor.G), int32(cursor.B))
svTable.SetSelectedStyle(c, c, tcell.AttrNone)
drawSVTable()
darkHSV := color.HSV{H: hue, S: 100, V: 99}
lightHSV := color.HSV{H: hue, S: 100, V: 100}
setColorValues(darkHSV, darkHBlock, darkHText, lightHSV, lightHBlock, lightHText)
}
func hTableSelectionChangedFunc(row int, column int) {
darkHSV := color.HSV{H: column * 2, S: 100, V: 100}
lightHSV := color.HSV{H: column*2 + 1, S: 100, V: 100}
setColorValues(darkHSV, darkHBlock, darkHText, lightHSV, lightHBlock, lightHText)
}
// svTable setup ---------------------------------------------------------
func svTableSetup() {
drawSVTable()
// 16842751 is cyan which makes the cursor stand out on red table
svTable.SetSelectedStyle(16842751, 16842751, tcell.AttrNone)
svTable.SetSelectable(true, true)
svTable.SetCellPadding(0, 0)
svTable.Select(0, 100)
svTable.SetDoneFunc(svTableDoneFunc)
svTable.SetSelectedFunc(svTableSelectedFunc)
svTable.SetSelectionChangedFunc(svTableSelectionChangedFunc)
}
func svTableDoneFunc(key tcell.Key) {
switch {
case key == tcell.KeyEscape:
app.Stop()
case key == tcell.KeyTab:
pages.SwitchToPage("Hue page")
app.SetFocus(hFocus)
}
}
func svTableSelectedFunc(row int, column int) {
hsv := color.HSV{H: hue, S: column, V: 100 - row*2}
rgb := color.HSVtoRGB(hsv)
hsl := color.HSVtoHSL(hsv)
cmyk := color.HSVtoCMYK(hsv)
hex := color.HSVtoHex(hsv)
decimal := color.HSVtoDecimal(hsv)
ansi := color.HSVtoAnsi(hsv)
altHsv := color.HSV{H: hue, S: column, V: 99 - row*2}
name := getColorName(hsv, altHsv)
returnColor = ColorValues{rgb, hsv, hsl, cmyk, hex, decimal, ansi, name}
app.Stop()
}
func svTableSelectionChangedFunc(row int, column int) {
// Set the dark saturation-value block to the correct color and the
// saturation-value text to contain the right values
var darkHSV color.HSV
if row*2+1 > 100 {
darkHSV = color.HSV{H: hue, S: column, V: 0}
} else {
darkHSV = color.HSV{H: hue, S: column, V: 100 - (row*2 + 1)}
}
lightHSV := color.HSV{H: hue, S: column, V: 100 - (row * 2)}
setColorValues(darkHSV, darkSVBlock, darkSVText, lightHSV, lightSVBlock, lightSVText)
}
// Helper functions ---------------------------------------------------
func drawSVTable() {
// Draw the table with the correct hue
for s := 0; s <= 100; s++ {
for v := 0; v < 50; v++ {
bg := color.HSVtoRGB(color.HSV{H: hue, S: s, V: 100 - v*2})
fg := color.HSVtoRGB(color.HSV{H: hue, S: s, V: 100 - (v*2 + 1)})
bc := tcell.NewRGBColor(int32(bg.R), int32(bg.G), int32(bg.B))
c := tcell.NewRGBColor(int32(fg.R), int32(fg.G), int32(fg.B))
cell := cview.NewTableCell("▄")
cell.SetBackgroundColor(bc)
cell.SetTextColor(c)
svTable.SetCell(v, s, cell)
}
}
for i := 0; i <= 100; i++ {
cell := cview.NewTableCell(" ")
cell.SetBackgroundColor(0)
svTable.SetCell(50, i, cell)
}
}
func setColorValues(darkHSV color.HSV, darkBlock *cview.TextView, darkText *cview.TextView, lightHSV color.HSV, lightBlock *cview.TextView, lightText *cview.TextView) {
if darkHSV.S > 100 {
darkHSV.S = 100
} else if darkHSV.S < 0 {
darkHSV.S = 0
}
if darkHSV.V > 100 {
darkHSV.V = 100
} else if darkHSV.V < 0 {
darkHSV.V = 0
}
if lightHSV.S > 100 {
lightHSV.S = 100
} else if lightHSV.S < 0 {
lightHSV.S = 0
}
if lightHSV.V > 100 {
lightHSV.V = 100
} else if lightHSV.V < 0 {
lightHSV.V = 0
}
// Fill in the color blocks with the color info
darkRGB := color.HSVtoRGB(darkHSV)
darkHSL := color.HSVtoHSL(darkHSV)
darkCMYK := color.HSVtoCMYK(darkHSV)
darkHex := color.HSVtoHex(darkHSV)
darkDecimal := color.HSVtoDecimal(darkHSV)
darkAnsi := color.HSVtoAnsi(darkHSV)
lightRGB := color.HSVtoRGB(lightHSV)
lightHSL := color.HSVtoHSL(lightHSV)
lightCMYK := color.HSVtoCMYK(lightHSV)
lightHex := color.HSVtoHex(lightHSV)
lightDecimal := color.HSVtoDecimal(lightHSV)
lightAnsi := color.HSVtoAnsi(lightHSV)
if !smallWidth && !smallHeight {
dc := tcell.NewRGBColor(int32(darkRGB.R), int32(darkRGB.G), int32(darkRGB.B))
darkBlock.SetTextColor(dc)
dText := fmt.Sprintf(colorTextWide, darkRGB.R, darkRGB.G, darkRGB.B, darkHSV.H, darkHSV.S, darkHSV.V, darkHSL.H, darkHSL.S, darkHSL.L, darkCMYK.C, darkCMYK.M, darkCMYK.Y, darkCMYK.K, darkHex, darkDecimal, darkAnsi)
darkText.SetText(dText)
lc := tcell.NewRGBColor(int32(lightRGB.R), int32(lightRGB.G), int32(lightRGB.B))
lightBlock.SetTextColor(lc)
lText := fmt.Sprintf(colorTextWide, lightRGB.R, lightRGB.G, lightRGB.B, lightHSV.H, lightHSV.S, lightHSV.V, lightHSL.H, lightHSL.S, lightHSL.L, lightCMYK.C, lightCMYK.M, lightCMYK.Y, lightCMYK.K, lightHex, lightDecimal, lightAnsi)
lightText.SetText(lText)
} else {
dc := tcell.NewRGBColor(int32(darkRGB.R), int32(darkRGB.G), int32(darkRGB.B))
darkBlock.SetTextColor(dc)
dText := fmt.Sprintf(colorTextSmall, darkRGB.R, darkRGB.G, darkRGB.B, darkHSV.H, darkHSV.S, darkHSV.V, darkHSL.H, darkHSL.S, darkHSL.L, darkCMYK.C, darkCMYK.M, darkCMYK.Y, darkCMYK.K, darkHex, darkDecimal, darkAnsi)
darkText.SetText(dText)
}
}
func getColorName(hsv color.HSV, altHSV color.HSV) string {
// If one of the preset colors is equal to the selected hsv, return the name
var h color.HSV
for i := 0; i < len(colorInfo); i++ {
for _, c := range colorInfo[i].colors {
h = color.HextoHSV(color.Hex(c.VALUE))
if h == hsv || h == altHSV {
return c.NAME
}
}
}
return "custom color"
}
// Get the location of a searched color
func getColorLocations(name string) [][]int {
var locations [][]int
for i := 0; i < len(colorInfo); i++ {
for x := 0; x < int(math.Ceil(float64(colorInfo[i].length/9)))+1; x++ {
for y := 0; y < 9; y++ {
if strings.Contains(strings.ToLower(colorInfo[i].colors[x*9+y].NAME), name) {
var location = []int{i, x, y}
locations = append(locations, location)
}
}
}
}
return locations
}
func getPath() (string, error) {
usr, err := user.Current()
testErr(err)
homeDir := usr.HomeDir
paths := [...]string{"./colors.json", homeDir + "/.config/cpick/colors.json", homeDir + "/.cpick/colors.json"}
for i := 0; i < 3; i++ {
if _, err := os.Stat(paths[i]); err == nil { // Path exists
return paths[i], nil
} else if os.IsNotExist(err) { // Path does not exist
continue
}
}
// Else: an error occurred
return "", err
}
func getCustomColors(path string) jsonData {
var data jsonData
if path != "" {
raw, err := ioutil.ReadFile(path)
if testingMode {
err = nil
}
testErr(err)
if testingMode {
raw = []byte(presetData)
}
err = json.Unmarshal([]byte(raw), &data)
testErr(err)
} else {
err := json.Unmarshal([]byte(presetData), &data)
testErr(err)
}
return data
}
func showHelp() {
if searchFlex.HasFocus() {
return
}
if hTable.HasFocus() {
helpFocus = hTable
hFlex.AddItem(helpFlex, 100, 1, false)
} else if colorPages.HasFocus() {
helpFocus = colorPages
hFlex.AddItem(helpFlex, 100, 1, false)
} else if svTable.HasFocus() {
helpFocus = svTable
svFlex.SetDirection(cview.FlexRow)
svFlex.AddItem(helpFlex, 100, 1, false)
}
app.SetFocus(helpModal)
}
func showSearch() {
pages.SwitchToPage("Search page")
app.SetFocus(searchInput)
}
func testErr(err error) {
if err != nil {
log.Fatal(err)
app.Stop()
}
}
// Start function starts the cpick application.
// Testing (bool) is used to test all of the functions to make sure they
// can run properly without a need for user input (testing = true).
func Start(testing bool) (ColorValues, error) {
if testing {
// If being run in testing mode, run the tester function
testingMode = true
tester()
} else if !testingMode {
// Find the width and height of the application
app.Init()
width, height := app.GetScreenSize()
smallWidth = width < BREAKPOINT_WIDTH
smallHeight = height < BREAKPOINT_HEIGHT
}
app.SetInputCapture(inputCaptureHandler)
pages.AddPage("Hue page", hFlex, true, true)
pages.AddPage("Saturation-Value page", svFlex, true, false)
pages.AddPage("Search page", searchFlex, true, false)
hTableSetup()
svTableSetup()
colorPageSetup()
helpPageSetup()
searchInputSetup()
hScreenSetup()
svScreenSetup()
if !testingMode {
app.SetRoot(pages, true)
if err := app.Run(); err != nil {
log.Fatal(err)
panic(err)
}
}
return returnColor, nil
}
package cview
import (
"bytes"
"fmt"
"io"
"strconv"
"strings"
)
// The states of the ANSI escape code parser.
const (
ansiText = iota
ansiEscape
ansiSubstring
ansiControlSequence
)
// ansi is a io.Writer which translates ANSI escape codes into cview color
// tags.
type ansi struct {
io.Writer
// Reusable buffers.
buffer *bytes.Buffer // The entire output text of one Write().
csiParameter, csiIntermediate *bytes.Buffer // Partial CSI strings.
attributes string // The buffer's current text attributes (a tview attribute string).
// The current state of the parser. One of the ansi constants.
state int
}
// ANSIWriter returns an io.Writer which translates any ANSI escape codes
// written to it into cview color tags. Other escape codes don't have an effect
// and are simply removed. The translated text is written to the provided
// writer.
func ANSIWriter(writer io.Writer) io.Writer {
return &ansi{
Writer: writer,
buffer: new(bytes.Buffer),
csiParameter: new(bytes.Buffer),
csiIntermediate: new(bytes.Buffer),
state: ansiText,
}
}
// Write parses the given text as a string of runes, translates ANSI escape
// codes to color tags and writes them to the output writer.
func (a *ansi) Write(text []byte) (int, error) {
defer func() {
a.buffer.Reset()
}()
for _, r := range string(text) {
switch a.state {
// We just entered an escape sequence.
case ansiEscape:
switch r {
case '[': // Control Sequence Introducer.
a.csiParameter.Reset()
a.csiIntermediate.Reset()
a.state = ansiControlSequence
case 'c': // Reset.
fmt.Fprint(a.buffer, "[-:-:-]")
a.state = ansiText
case 'P', ']', 'X', '^', '_': // Substrings and commands.
a.state = ansiSubstring
default: // Ignore.
a.state = ansiText
}
// CSI Sequences.
case ansiControlSequence:
switch {
case r >= 0x30 && r <= 0x3f: // Parameter bytes.
if _, err := a.csiParameter.WriteRune(r); err != nil {
return 0, err
}
case r >= 0x20 && r <= 0x2f: // Intermediate bytes.
if _, err := a.csiIntermediate.WriteRune(r); err != nil {
return 0, err
}
case r >= 0x40 && r <= 0x7e: // Final byte.
switch r {
case 'E': // Next line.
count, _ := strconv.Atoi(a.csiParameter.String())
if count == 0 {
count = 1
}
fmt.Fprint(a.buffer, strings.Repeat("\n", count))
case 'm': // Select Graphic Rendition.
var background, foreground string
params := a.csiParameter.String()
fields := strings.Split(params, ";")
if len(params) == 0 || len(fields) == 1 && fields[0] == "0" {
// Reset.
a.attributes = ""
if _, err := a.buffer.WriteString("[-:-:-]"); err != nil {
return 0, err
}
break
}
lookupColor := func(colorNumber int) string {
if colorNumber < 0 || colorNumber > 15 {
return "black"
}
return []string{
"black",
"maroon",
"green",
"olive",
"navy",
"purple",
"teal",
"silver",
"gray",
"red",
"lime",
"yellow",
"blue",
"fuchsia",
"aqua",
"white",
}[colorNumber]
}
FieldLoop:
for index, field := range fields {
switch field {
case "1", "01":
if strings.IndexRune(a.attributes, 'b') < 0 {
a.attributes += "b"
}
case "2", "02":
if strings.IndexRune(a.attributes, 'd') < 0 {
a.attributes += "d"
}
case "4", "04":
if strings.IndexRune(a.attributes, 'u') < 0 {
a.attributes += "u"
}
case "5", "05":
if strings.IndexRune(a.attributes, 'l') < 0 {
a.attributes += "l"
}
case "22":
if i := strings.IndexRune(a.attributes, 'b'); i >= 0 {
a.attributes = a.attributes[:i] + a.attributes[i+1:]
}
if i := strings.IndexRune(a.attributes, 'd'); i >= 0 {
a.attributes = a.attributes[:i] + a.attributes[i+1:]
}
case "24":
if i := strings.IndexRune(a.attributes, 'u'); i >= 0 {
a.attributes = a.attributes[:i] + a.attributes[i+1:]
}
case "25":
if i := strings.IndexRune(a.attributes, 'l'); i >= 0 {
a.attributes = a.attributes[:i] + a.attributes[i+1:]
}
case "30", "31", "32", "33", "34", "35", "36", "37":
colorNumber, _ := strconv.Atoi(field)
foreground = lookupColor(colorNumber - 30)
case "39":
foreground = "-"
case "40", "41", "42", "43", "44", "45", "46", "47":
colorNumber, _ := strconv.Atoi(field)
background = lookupColor(colorNumber - 40)
case "49":
background = "-"
case "90", "91", "92", "93", "94", "95", "96", "97":
colorNumber, _ := strconv.Atoi(field)
foreground = lookupColor(colorNumber - 82)
case "100", "101", "102", "103", "104", "105", "106", "107":
colorNumber, _ := strconv.Atoi(field)
background = lookupColor(colorNumber - 92)
case "38", "48":
var color string
if len(fields) > index+1 {
if fields[index+1] == "5" && len(fields) > index+2 { // 8-bit colors.
colorNumber, _ := strconv.Atoi(fields[index+2])
if colorNumber <= 15 {
color = lookupColor(colorNumber)
} else if colorNumber <= 231 {
red := (colorNumber - 16) / 36
green := ((colorNumber - 16) / 6) % 6
blue := (colorNumber - 16) % 6
color = fmt.Sprintf("#%02x%02x%02x", 255*red/5, 255*green/5, 255*blue/5)
} else if colorNumber <= 255 {
grey := 255 * (colorNumber - 232) / 23
color = fmt.Sprintf("#%02x%02x%02x", grey, grey, grey)
}
} else if fields[index+1] == "2" && len(fields) > index+4 { // 24-bit colors.
red, _ := strconv.Atoi(fields[index+2])
green, _ := strconv.Atoi(fields[index+3])
blue, _ := strconv.Atoi(fields[index+4])
color = fmt.Sprintf("#%02x%02x%02x", red, green, blue)
}
}
if len(color) > 0 {
if field == "38" {
foreground = color
} else {
background = color
}
}
break FieldLoop
}
}
var colon string
if len(a.attributes) > 0 {
colon = ":"
}
if len(foreground) > 0 || len(background) > 0 || len(a.attributes) > 0 {
fmt.Fprintf(a.buffer, "[%s:%s%s%s]", foreground, background, colon, a.attributes)
}
}
a.state = ansiText
default: // Undefined byte.
a.state = ansiText // Abort CSI.
}
// We just entered a substring/command sequence.
case ansiSubstring:
if r == 27 { // Most likely the end of the substring.
a.state = ansiEscape
} // Ignore all other characters.
// "ansiText" and all others.
default:
if r == 27 {
// This is the start of an escape sequence.
a.state = ansiEscape
} else {
// Just a regular rune. Send to buffer.
if _, err := a.buffer.WriteRune(r); err != nil {
return 0, err
}
}
}
}
// Write buffer to target writer.
n, err := a.buffer.WriteTo(a.Writer)
if err != nil {
return int(n), err
}
return len(text), nil
}
// TranslateANSI replaces ANSI escape sequences found in the provided string
// with cview's color tags and returns the resulting string.
func TranslateANSI(text string) string {
var buffer bytes.Buffer
writer := ANSIWriter(&buffer)
writer.Write([]byte(text))
return buffer.String()
}
package cview
import (
"fmt"
"sync"
"time"
"github.com/gdamore/tcell/v2"
)
const (
// The size of the event/update/redraw channels.
queueSize = 100
// The minimum duration between resize event callbacks.
resizeEventThrottle = 50 * time.Millisecond
)
// 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 := cview.NewApplication().SetRoot(p, true).Run(); err != nil {
// panic(err)
// }
type Application struct {
// The application's screen. Apart from Run(), this variable should never be
// set directly. Always use the screenReplacement channel after calling
// Fini(), to set a new screen (or nil to stop the application).
screen tcell.Screen
// The size of the application's screen.
width, height int
// The primitive which currently has the keyboard focus.
focus Primitive
// The root primitive to be seen on the screen.
root Primitive
// Whether or not the application resizes the root primitive.
rootFullscreen bool
// Whether or not to enable bracketed paste mode.
enableBracketedPaste bool
// Whether or not to enable mouse events.
enableMouse bool
// An optional capture function which receives a key event and returns the
// event to be forwarded to the default input handler (nil if nothing should
// be forwarded).
inputCapture func(event *tcell.EventKey) *tcell.EventKey
// Time a resize event was last processed.
lastResize time.Time
// Timer limiting how quickly resize events are processed.
throttleResize *time.Timer
// An optional callback function which is invoked when the application's
// window is initialized, and when the application's window size changes.
// After invoking this callback the screen is cleared and the application
// is drawn.
afterResize func(width int, height int)
// An optional callback function which is invoked before the application's
// focus changes.
beforeFocus func(p Primitive) bool
// An optional callback function which is invoked after the application's
// focus changes.
afterFocus func(p Primitive)
// An optional callback function which is invoked just before the root
// primitive is drawn.
beforeDraw func(screen tcell.Screen) bool
// An optional callback function which is invoked after the root primitive
// was drawn.
afterDraw func(screen tcell.Screen)
// Used to send screen events from separate goroutine to main event loop
events chan tcell.Event
// Functions queued from goroutines, used to serialize updates to primitives.
updates chan func()
// An object that the screen variable will be set to after Fini() was called.
// Use this channel to set a new screen object for the application
// (screen.Init() and draw() will be called implicitly). A value of nil will
// stop the application.
screenReplacement chan tcell.Screen
// An optional capture function which receives a mouse event and returns the
// event to be forwarded to the default mouse handler (nil if nothing should
// be forwarded).
mouseCapture func(event *tcell.EventMouse, action MouseAction) (*tcell.EventMouse, MouseAction)
// doubleClickInterval specifies the maximum time between clicks to register a
// double click rather than a single click.
doubleClickInterval time.Duration
mouseCapturingPrimitive Primitive // A Primitive returned by a MouseHandler which will capture future mouse events.
lastMouseX, lastMouseY int // The last position of the mouse.
mouseDownX, mouseDownY int // The position of the mouse when its button was last pressed.
lastMouseClick time.Time // The time when a mouse button was last clicked.
lastMouseButtons tcell.ButtonMask // The last mouse button state.
sync.RWMutex
}
// NewApplication creates and returns a new application.
func NewApplication() *Application {
return &Application{
enableBracketedPaste: true,
events: make(chan tcell.Event, queueSize),
updates: make(chan func(), queueSize),
screenReplacement: make(chan tcell.Screen, 1),
}
}
// SetInputCapture sets a function which captures all key events before they are
// forwarded to the key event handler of the primitive which currently has
// focus. This function can then choose to forward that key event (or a
// different one) by returning it or stop the key event processing by returning
// nil.
//
// Note that this also affects the default event handling of the application
// itself: Such a handler can intercept the Ctrl-C event which closes the
// application.
func (a *Application) SetInputCapture(capture func(event *tcell.EventKey) *tcell.EventKey) {
a.Lock()
defer a.Unlock()
a.inputCapture = capture
}
// GetInputCapture returns the function installed with SetInputCapture() or nil
// if no such function has been installed.
func (a *Application) GetInputCapture() func(event *tcell.EventKey) *tcell.EventKey {
a.RLock()
defer a.RUnlock()
return a.inputCapture
}
// SetMouseCapture sets a function which captures mouse events (consisting of
// the original tcell mouse event and the semantic mouse action) before they are
// forwarded to the appropriate mouse event handler. This function can then
// choose to forward that event (or a different one) by returning it or stop
// the event processing by returning a nil mouse event.
func (a *Application) SetMouseCapture(capture func(event *tcell.EventMouse, action MouseAction) (*tcell.EventMouse, MouseAction)) {
a.mouseCapture = capture
}
// GetMouseCapture returns the function installed with SetMouseCapture() or nil
// if no such function has been installed.
func (a *Application) GetMouseCapture() func(event *tcell.EventMouse, action MouseAction) (*tcell.EventMouse, MouseAction) {
return a.mouseCapture
}
// SetDoubleClickInterval sets the maximum time between clicks to register a
// double click rather than a single click. A standard duration is provided as
// StandardDoubleClick. No interval is set by default, disabling double clicks.
func (a *Application) SetDoubleClickInterval(interval time.Duration) {
a.doubleClickInterval = interval
}
// 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.
//
// This function is typically called before the first call to Run(). Init() need
// not be called on the screen.
func (a *Application) SetScreen(screen tcell.Screen) {
if screen == nil {
return // Invalid input. Do nothing.
}
a.Lock()
if a.screen == nil {
// Run() has not been called yet.
a.screen = screen
a.Unlock()
return
}
// Run() is already in progress. Exchange screen.
oldScreen := a.screen
a.Unlock()
oldScreen.Fini()
a.screenReplacement <- screen
}
// GetScreen returns the current tcell.Screen of the application. Lock the
// application when manipulating the screen to prevent race conditions. This
// value is only available after calling Init or Run.
func (a *Application) GetScreen() tcell.Screen {
a.RLock()
defer a.RUnlock()
return a.screen
}
// GetScreenSize returns the size of the application's screen. These values are
// only available after calling Init or Run.
func (a *Application) GetScreenSize() (width, height int) {
a.RLock()
defer a.RUnlock()
return a.width, a.height
}
// Init initializes the application screen. Calling Init before running is not
// required. Its primary use is to populate screen dimensions before running an
// application.
func (a *Application) Init() error {
a.Lock()
defer a.Unlock()
return a.init()
}
func (a *Application) init() error {
if a.screen != nil {
return nil
}
var err error
a.screen, err = tcell.NewScreen()
if err != nil {
a.Unlock()
return err
}
if err = a.screen.Init(); err != nil {
a.Unlock()
return err
}
a.width, a.height = a.screen.Size()
if a.enableBracketedPaste {
a.screen.EnablePaste()
}
if a.enableMouse {
a.screen.EnableMouse()
}
return nil
}
// EnableBracketedPaste enables bracketed paste mode, which is enabled by default.
func (a *Application) EnableBracketedPaste(enable bool) {
a.Lock()
defer a.Unlock()
if enable != a.enableBracketedPaste && a.screen != nil {
if enable {
a.screen.EnablePaste()
} else {
a.screen.DisablePaste()
}
}
a.enableBracketedPaste = enable
}
// EnableMouse enables mouse events.
func (a *Application) EnableMouse(enable bool) {
a.Lock()
defer a.Unlock()
if enable != a.enableMouse && a.screen != nil {
if enable {
a.screen.EnableMouse()
} else {
a.screen.DisableMouse()
}
}
a.enableMouse = enable
}
// Run starts the application and thus the event loop. This function returns
// when Stop() was called.
func (a *Application) Run() error {
a.Lock()
// Initialize screen
err := a.init()
if err != nil {
return err
}
// We catch panics to clean up because they mess up the terminal.
defer func() {
if p := recover(); p != nil {
if a.screen != nil {
a.screen.Fini()
}
panic(p)
}
}()
// Draw the screen for the first time.
a.Unlock()
a.draw()
// Separate loop to wait for screen events.
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
for {
a.RLock()
screen := a.screen
a.RUnlock()
if screen == nil {
// We have no screen. Let's stop.
a.QueueEvent(nil)
break
}
// Wait for next event and queue it.
event := screen.PollEvent()
if event != nil {
// Regular event. Queue.
a.QueueEvent(event)
continue
}
// A screen was finalized (event is nil). Wait for a new screen.
screen = <-a.screenReplacement
if screen == nil {
// No new screen. We're done.
a.QueueEvent(nil)
return
}
// We have a new screen. Keep going.
a.Lock()
a.screen = screen
a.Unlock()
// Initialize and draw this screen.
if err := screen.Init(); err != nil {
panic(err)
}
if a.enableBracketedPaste {
screen.EnablePaste()
}
if a.enableMouse {
screen.EnableMouse()
}
a.draw()
}
}()
handle := func(event interface{}) {
a.RLock()
p := a.focus
inputCapture := a.inputCapture
screen := a.screen
a.RUnlock()
switch event := event.(type) {
case *tcell.EventKey:
// Intercept keys.
if inputCapture != nil {
event = inputCapture(event)
if event == nil {
a.draw()
return // Don't forward event.
}
}
// Ctrl-C closes the application.
if event.Key() == tcell.KeyCtrlC {
a.Stop()
return
}
// 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.EventResize:
// Throttle resize events.
if time.Since(a.lastResize) < resizeEventThrottle {
// Stop timer
if a.throttleResize != nil && !a.throttleResize.Stop() {
select {
case <-a.throttleResize.C:
default:
}
}
event := event // Capture
// Start timer
a.throttleResize = time.AfterFunc(resizeEventThrottle, func() {
a.events <- event
})
return
}
a.lastResize = time.Now()
if screen == nil {
return
}
screen.Clear()
a.width, a.height = event.Size()
// Call afterResize handler if there is one.
if a.afterResize != nil {
a.afterResize(a.width, a.height)
}
a.draw()
case *tcell.EventMouse:
consumed, isMouseDownAction := a.fireMouseActions(event)
if consumed {
a.draw()
}
a.lastMouseButtons = event.Buttons()
if isMouseDownAction {
a.mouseDownX, a.mouseDownY = event.Position()
}
}
}
// Start event loop.
EventLoop:
for {
// Handle events before executing updates
select {
case event := <-a.events:
if event == nil {
break EventLoop
}
handle(event)
continue
default:
}
select {
case event := <-a.events:
if event == nil {
break EventLoop
}
handle(event)
case update := <-a.updates:
update()
}
}
// Wait for the event loop to finish.
wg.Wait()
a.screen = nil
return nil
}
// fireMouseActions analyzes the provided mouse event, derives mouse actions
// from it and then forwards them to the corresponding primitives.
func (a *Application) fireMouseActions(event *tcell.EventMouse) (consumed, isMouseDownAction bool) {
// We want to relay follow-up events to the same target primitive.
var targetPrimitive Primitive
// Helper function to fire a mouse action.
fire := func(action MouseAction) {
switch action {
case MouseLeftDown, MouseMiddleDown, MouseRightDown:
isMouseDownAction = true
}
// Intercept event.
if a.mouseCapture != nil {
event, action = a.mouseCapture(event, action)
if event == nil {
consumed = true
return // Don't forward event.
}
}
// Determine the target primitive.
var primitive, capturingPrimitive Primitive
if a.mouseCapturingPrimitive != nil {
primitive = a.mouseCapturingPrimitive
targetPrimitive = a.mouseCapturingPrimitive
} else if targetPrimitive != nil {
primitive = targetPrimitive
} else {
primitive = a.root
}
if primitive != nil {
if handler := primitive.MouseHandler(); handler != nil {
var wasConsumed bool
wasConsumed, capturingPrimitive = handler(action, event, func(p Primitive) {
a.SetFocus(p)
})
if wasConsumed {
consumed = true
}
}
}
a.mouseCapturingPrimitive = capturingPrimitive
}
x, y := event.Position()
buttons := event.Buttons()
clickMoved := x != a.mouseDownX || y != a.mouseDownY
buttonChanges := buttons ^ a.lastMouseButtons
if x != a.lastMouseX || y != a.lastMouseY {
fire(MouseMove)
a.lastMouseX = x
a.lastMouseY = y
}
for _, buttonEvent := range []struct {
button tcell.ButtonMask
down, up, click, dclick MouseAction
}{
{tcell.ButtonPrimary, MouseLeftDown, MouseLeftUp, MouseLeftClick, MouseLeftDoubleClick},
{tcell.ButtonMiddle, MouseMiddleDown, MouseMiddleUp, MouseMiddleClick, MouseMiddleDoubleClick},
{tcell.ButtonSecondary, MouseRightDown, MouseRightUp, MouseRightClick, MouseRightDoubleClick},
} {
if buttonChanges&buttonEvent.button != 0 {
if buttons&buttonEvent.button != 0 {
fire(buttonEvent.down)
} else {
fire(buttonEvent.up)
if !clickMoved {
if a.doubleClickInterval == 0 || a.lastMouseClick.Add(a.doubleClickInterval).Before(time.Now()) {
fire(buttonEvent.click)
a.lastMouseClick = time.Now()
} else {
fire(buttonEvent.dclick)
a.lastMouseClick = time.Time{} // reset
}
}
}
}
}
for _, wheelEvent := range []struct {
button tcell.ButtonMask
action MouseAction
}{
{tcell.WheelUp, MouseScrollUp},
{tcell.WheelDown, MouseScrollDown},
{tcell.WheelLeft, MouseScrollLeft},
{tcell.WheelRight, MouseScrollRight}} {
if buttons&wheelEvent.button != 0 {
fire(wheelEvent.action)
}
}
return consumed, isMouseDownAction
}
// Stop stops the application, causing Run() to return.
func (a *Application) Stop() {
a.Lock()
defer a.Unlock()
screen := a.screen
if screen == nil {
return
}
a.screen = nil
screen.Fini()
a.screenReplacement <- nil
}
// Suspend temporarily suspends the application by exiting terminal UI mode and
// invoking the provided function "f". When "f" returns, terminal UI mode is
// entered again and the application resumes.
//
// A return value of true indicates that the application was suspended and "f"
// was called. If false is returned, the application was already suspended,
// terminal UI mode was not exited, and "f" was not called.
func (a *Application) Suspend(f func()) bool {
a.RLock()
screen := a.screen
a.RUnlock()
if screen == nil {
return false // Screen has not yet been initialized.
}
// Enter suspended mode.
screen.Fini()
// Wait for "f" to return.
f()
// Make a new screen.
var err error
screen, err = tcell.NewScreen()
if err != nil {
panic(err)
}
a.screenReplacement <- screen
// Continue application loop.
return true
}
// Draw refreshes the screen (during the next update cycle). It calls the Draw()
// function of the application's root primitive and then syncs the screen
// buffer.
func (a *Application) Draw() {
a.QueueUpdate(func() {
a.draw()
})
}
// draw actually does what Draw() promises to do.
func (a *Application) draw() {
a.Lock()
screen := a.screen
root := a.root
fullscreen := a.rootFullscreen
before := a.beforeDraw
after := a.afterDraw
// Maybe we're not ready yet or not anymore.
if screen == nil || root == nil {
a.Unlock()
return
}
// Resize if requested.
if fullscreen {
root.SetRect(0, 0, a.width, a.height)
}
// Call before handler if there is one.
if before != nil {
a.Unlock()
if before(screen) {
screen.Show()
return
}
} else {
a.Unlock()
}
// Draw all primitives.
root.Draw(screen)
// Call after handler if there is one.
if after != nil {
after(screen)
}
// Sync screen.
screen.Show()
}
// SetBeforeDrawFunc installs a callback function which is invoked just before
// the root primitive is drawn during screen updates. If the function returns
// true, drawing will not continue, i.e. the root primitive will not be drawn
// (and an after-draw-handler will not be called).
//
// Note that the screen is not cleared by the application. To clear the screen,
// you may call screen.Clear().
//
// Provide nil to uninstall the callback function.
func (a *Application) SetBeforeDrawFunc(handler func(screen tcell.Screen) bool) {
a.Lock()
defer a.Unlock()
a.beforeDraw = handler
}
// GetBeforeDrawFunc returns the callback function installed with
// SetBeforeDrawFunc() or nil if none has been installed.
func (a *Application) GetBeforeDrawFunc() func(screen tcell.Screen) bool {
a.RLock()
defer a.RUnlock()
return a.beforeDraw
}
// SetAfterDrawFunc installs a callback function which is invoked after the root
// primitive was drawn during screen updates.
//
// Provide nil to uninstall the callback function.
func (a *Application) SetAfterDrawFunc(handler func(screen tcell.Screen)) {
a.Lock()
defer a.Unlock()
a.afterDraw = handler
}
// GetAfterDrawFunc returns the callback function installed with
// SetAfterDrawFunc() or nil if none has been installed.
func (a *Application) GetAfterDrawFunc() func(screen tcell.Screen) {
a.RLock()
defer a.RUnlock()
return a.afterDraw
}
// SetRoot sets the root primitive for this application. If "fullscreen" is set
// to true, the root primitive's position will be changed to fill the screen.
//
// This function must be called at least once or nothing will be displayed when
// the application starts.
//
// It also calls SetFocus() on the primitive.
func (a *Application) SetRoot(root Primitive, fullscreen bool) {
a.Lock()
a.root = root
a.rootFullscreen = fullscreen
if a.screen != nil {
a.screen.Clear()
}
a.Unlock()
a.SetFocus(root)
}
// ResizeToFullScreen resizes the given primitive such that it fills the entire
// screen.
func (a *Application) ResizeToFullScreen(p Primitive) {
a.RLock()
width, height := a.width, a.height
a.RUnlock()
p.SetRect(0, 0, width, height)
}
// SetAfterResizeFunc installs a callback function which is invoked when the
// application's window is initialized, and when the application's window size
// changes. After invoking this callback the screen is cleared and the
// application is drawn.
//
// Provide nil to uninstall the callback function.
func (a *Application) SetAfterResizeFunc(handler func(width int, height int)) {
a.Lock()
defer a.Unlock()
a.afterResize = handler
}
// GetAfterResizeFunc returns the callback function installed with
// SetAfterResizeFunc() or nil if none has been installed.
func (a *Application) GetAfterResizeFunc() func(width int, height int) {
a.RLock()
defer a.RUnlock()
return a.afterResize
}
// SetFocus sets the focus on a new primitive. All key events will be redirected
// to that primitive. Callers must ensure that the primitive will handle key
// events.
//
// Blur() will be called on the previously focused primitive. Focus() will be
// called on the new primitive.
func (a *Application) SetFocus(p Primitive) {
a.Lock()
if a.beforeFocus != nil {
a.Unlock()
ok := a.beforeFocus(p)
if !ok {
return
}
a.Lock()
}
if a.focus != nil {
a.focus.Blur()
}
a.focus = p
if a.screen != nil {
a.screen.HideCursor()
}
if a.afterFocus != nil {
a.Unlock()
a.afterFocus(p)
} else {
a.Unlock()
}
if p != nil {
p.Focus(func(p Primitive) {
a.SetFocus(p)
})
}
}
// GetFocus returns the primitive which has the current focus. If none has it,
// nil is returned.
func (a *Application) GetFocus() Primitive {
a.RLock()
defer a.RUnlock()
return a.focus
}
// SetBeforeFocusFunc installs a callback function which is invoked before the
// application's focus changes. Return false to maintain the current focus.
//
// Provide nil to uninstall the callback function.
func (a *Application) SetBeforeFocusFunc(handler func(p Primitive) bool) {
a.Lock()
defer a.Unlock()
a.beforeFocus = handler
}
// SetAfterFocusFunc installs a callback function which is invoked after the
// application's focus changes.
//
// Provide nil to uninstall the callback function.
func (a *Application) SetAfterFocusFunc(handler func(p Primitive)) {
a.Lock()
defer a.Unlock()
a.afterFocus = handler
}
// QueueUpdate queues a function to be executed as part of the event loop.
//
// 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()) {
a.updates <- f
}
// QueueUpdateDraw works like QueueUpdate() except it refreshes the screen
// immediately after executing f.
func (a *Application) QueueUpdateDraw(f func()) {
a.QueueUpdate(func() {
f()
a.draw()
})
}
// 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) {
a.events <- event
}
// RingBell sends a bell code to the terminal.
func (a *Application) RingBell() {
a.QueueUpdate(func() {
fmt.Print(string(byte(7)))
})
}
package cview
import (
"sync"
"github.com/gdamore/tcell/v2"
)
// Box is the base Primitive for all widgets. It has a background color and
// optional surrounding elements such as a border and a title. It does not have
// inner text. Widgets embed Box and draw their text over it.
type Box struct {
// The position of the rect.
x, y, width, height int
// Padding.
paddingTop, paddingBottom, paddingLeft, paddingRight int
// The inner rect reserved for the box's content.
innerX, innerY, innerWidth, innerHeight int
// Whether or not the box is visible.
visible bool
// The border color when the box has focus.
borderColorFocused tcell.Color
// The box's background color.
backgroundColor tcell.Color
// Whether or not the box's background is transparent.
backgroundTransparent bool
// Whether or not a border is drawn, reducing the box's space for content by
// two in width and height.
border bool
// 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 []byte
// The color of the title.
titleColor tcell.Color
// The alignment of the title.
titleAlign int
// Provides a way to find out if this box has focus. We always go through
// this interface because it may be overridden by implementing classes.
focus Focusable
// Whether or not this box has focus.
hasFocus bool
// Whether or not this box shows its focus.
showFocus 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).
inputCapture func(event *tcell.EventKey) *tcell.EventKey
// An optional function which is called before the box is drawn.
draw func(screen tcell.Screen, x, y, width, height int) (int, int, int, int)
// An optional capture function which receives a mouse event and returns the
// event to be forwarded to the primitive's default mouse event handler (at
// least one nil if nothing should be forwarded).
mouseCapture func(action MouseAction, event *tcell.EventMouse) (MouseAction, *tcell.EventMouse)
l sync.RWMutex
}
// NewBox returns a Box without a border.
func NewBox() *Box {
b := &Box{
width: 15,
height: 10,
visible: true,
backgroundColor: Styles.PrimitiveBackgroundColor,
borderColor: Styles.BorderColor,
titleColor: Styles.TitleColor,
borderColorFocused: ColorUnset,
titleAlign: AlignCenter,
showFocus: true,
}
b.focus = b
b.updateInnerRect()
return b
}
func (b *Box) updateInnerRect() {
x, y, width, height := b.x, b.y, b.width, b.height
// Subtract border space
if b.border {
x++
y++
width -= 2
height -= 2
}
// Subtract padding
x, y, width, height =
x+b.paddingLeft,
y+b.paddingTop,
width-b.paddingLeft-b.paddingRight,
height-b.paddingTop-b.paddingBottom
if width < 0 {
width = 0
}
if height < 0 {
height = 0
}
b.innerX, b.innerY, b.innerWidth, b.innerHeight = x, y, width, height
}
// GetPadding returns the size of the padding around the box content.
func (b *Box) GetPadding() (top, bottom, left, right int) {
b.l.RLock()
defer b.l.RUnlock()
return b.paddingTop, b.paddingBottom, b.paddingLeft, b.paddingRight
}
// SetPadding sets the size of the padding around the box content.
func (b *Box) SetPadding(top, bottom, left, right int) {
b.l.Lock()
defer b.l.Unlock()
b.paddingTop, b.paddingBottom, b.paddingLeft, b.paddingRight = top, bottom, left, right
b.updateInnerRect()
}
// GetRect returns the current position of the rectangle, x, y, width, and
// height.
func (b *Box) GetRect() (int, int, int, int) {
b.l.RLock()
defer b.l.RUnlock()
return b.x, b.y, b.width, b.height
}
// GetInnerRect returns the position of the inner rectangle (x, y, width,
// height), without the border and without any padding. Width and height values
// will clamp to 0 and thus never be negative.
func (b *Box) GetInnerRect() (int, int, int, int) {
b.l.RLock()
defer b.l.RUnlock()
return b.innerX, b.innerY, b.innerWidth, b.innerHeight
}
// SetRect sets a new position of the primitive. Note that this has no effect
// if this primitive is part of a layout (e.g. Flex, Grid) or if it was added
// like this:
//
// application.SetRoot(b, true)
func (b *Box) SetRect(x, y, width, height int) {
b.l.Lock()
defer b.l.Unlock()
b.x, b.y, b.width, b.height = x, y, width, height
b.updateInnerRect()
}
// SetVisible sets the flag indicating whether or not the box is visible.
func (b *Box) SetVisible(v bool) {
b.l.Lock()
defer b.l.Unlock()
b.visible = v
}
// GetVisible returns a value indicating whether or not the box is visible.
func (b *Box) GetVisible() bool {
b.l.RLock()
defer b.l.RUnlock()
return b.visible
}
// SetDrawFunc sets a callback function which is invoked after the box primitive
// has been drawn. This allows you to add a more individual style to the box
// (and all primitives which extend it).
//
// The function is provided with the box's dimensions (set via SetRect()). It
// must return the box's inner dimensions (x, y, width, height) which will be
// returned by GetInnerRect(), used by descendent primitives to draw their own
// content.
func (b *Box) SetDrawFunc(handler func(screen tcell.Screen, x, y, width, height int) (int, int, int, int)) {
b.l.Lock()
defer b.l.Unlock()
b.draw = handler
}
// GetDrawFunc returns the callback function which was installed with
// SetDrawFunc() or nil if no such function has been installed.
func (b *Box) GetDrawFunc() func(screen tcell.Screen, x, y, width, height int) (int, int, int, int) {
b.l.RLock()
defer b.l.RUnlock()
return b.draw
}
// WrapInputHandler wraps an input handler (see InputHandler()) with the
// functionality to capture input (see SetInputCapture()) before passing it
// on to the provided (default) input handler.
//
// This is only meant to be used by subclassing primitives.
func (b *Box) WrapInputHandler(inputHandler func(*tcell.EventKey, func(p Primitive))) func(*tcell.EventKey, func(p Primitive)) {
return func(event *tcell.EventKey, setFocus func(p Primitive)) {
if b.inputCapture != nil {
event = b.inputCapture(event)
}
if event != nil && inputHandler != nil {
inputHandler(event, setFocus)
}
}
}
// InputHandler returns nil.
func (b *Box) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) {
b.l.RLock()
defer b.l.RUnlock()
return b.WrapInputHandler(nil)
}
// SetInputCapture installs a function which captures key events before they are
// forwarded to the primitive's default key event handler. This function can
// then choose to forward that key event (or a different one) to the default
// handler by returning it. If nil is returned, the default handler will not
// be called.
//
// Providing a nil handler will remove a previously existing handler.
//
// Note that this function will not have an effect on primitives composed of
// other primitives, such as Form, Flex, or Grid. Key events are only captured
// by the primitives that have focus (e.g. InputField) and only one primitive
// can have focus at a time. Composing primitives such as Form pass the focus on
// to their contained primitives and thus never receive any key events
// themselves. Therefore, they cannot intercept key events.
func (b *Box) SetInputCapture(capture func(event *tcell.EventKey) *tcell.EventKey) {
b.l.Lock()
defer b.l.Unlock()
b.inputCapture = capture
}
// GetInputCapture returns the function installed with SetInputCapture() or nil
// if no such function has been installed.
func (b *Box) GetInputCapture() func(event *tcell.EventKey) *tcell.EventKey {
b.l.RLock()
defer b.l.RUnlock()
return b.inputCapture
}
// WrapMouseHandler wraps a mouse event handler (see MouseHandler()) with the
// functionality to capture mouse events (see SetMouseCapture()) before passing
// them on to the provided (default) event handler.
//
// This is only meant to be used by subclassing primitives.
func (b *Box) WrapMouseHandler(mouseHandler func(MouseAction, *tcell.EventMouse, func(p Primitive)) (bool, Primitive)) func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
return func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
if b.mouseCapture != nil {
action, event = b.mouseCapture(action, event)
}
if event != nil && mouseHandler != nil {
consumed, capture = mouseHandler(action, event, setFocus)
}
return
}
}
// MouseHandler returns nil.
func (b *Box) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
return b.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
if action == MouseLeftClick && b.InRect(event.Position()) {
setFocus(b)
consumed = true
}
return
})
}
// SetMouseCapture sets a function which captures mouse events (consisting of
// the original tcell mouse event and the semantic mouse action) before they are
// forwarded to the primitive's default mouse event handler. This function can
// then choose to forward that event (or a different one) by returning it or
// returning a nil mouse event, in which case the default handler will not be
// called.
//
// Providing a nil handler will remove a previously existing handler.
func (b *Box) SetMouseCapture(capture func(action MouseAction, event *tcell.EventMouse) (MouseAction, *tcell.EventMouse)) {
b.mouseCapture = capture
}
// InRect returns true if the given coordinate is within the bounds of the box's
// rectangle.
func (b *Box) InRect(x, y int) bool {
rectX, rectY, width, height := b.GetRect()
return x >= rectX && x < rectX+width && y >= rectY && y < rectY+height
}
// GetMouseCapture returns the function installed with SetMouseCapture() or nil
// if no such function has been installed.
func (b *Box) GetMouseCapture() func(action MouseAction, event *tcell.EventMouse) (MouseAction, *tcell.EventMouse) {
return b.mouseCapture
}
// SetBackgroundColor sets the box's background color.
func (b *Box) SetBackgroundColor(color tcell.Color) {
b.l.Lock()
defer b.l.Unlock()
b.backgroundColor = color
}
// GetBackgroundColor returns the box's background color.
func (b *Box) GetBackgroundColor() tcell.Color {
b.l.RLock()
defer b.l.RUnlock()
return b.backgroundColor
}
// SetBackgroundTransparent sets the flag indicating whether or not the box's
// background is transparent. The screen is not cleared before drawing the
// application. Overlaying transparent widgets directly onto the screen may
// result in artifacts. To resolve this, add a blank, non-transparent Box to
// the bottom layer of the interface via Panels, or set a handler via
// SetBeforeDrawFunc which clears the screen.
func (b *Box) SetBackgroundTransparent(transparent bool) {
b.l.Lock()
defer b.l.Unlock()
b.backgroundTransparent = transparent
}
// GetBorder returns a value indicating whether the box have a border
// or not.
func (b *Box) GetBorder() bool {
b.l.RLock()
defer b.l.RUnlock()
return b.border
}
// SetBorder sets the flag indicating whether or not the box should have a
// border.
func (b *Box) SetBorder(show bool) {
b.l.Lock()
defer b.l.Unlock()
b.border = show
b.updateInnerRect()
}
// SetBorderColor sets the box's border color.
func (b *Box) SetBorderColor(color tcell.Color) {
b.l.Lock()
defer b.l.Unlock()
b.borderColor = color
}
// SetBorderColorFocused sets the box's border color when the box is focused.
func (b *Box) SetBorderColorFocused(color tcell.Color) {
b.l.Lock()
defer b.l.Unlock()
b.borderColorFocused = color
}
// 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) {
b.l.Lock()
defer b.l.Unlock()
b.borderAttributes = attr
}
// SetTitle sets the box's title.
func (b *Box) SetTitle(title string) {
b.l.Lock()
defer b.l.Unlock()
b.title = []byte(title)
}
// GetTitle returns the box's current title.
func (b *Box) GetTitle() string {
b.l.RLock()
defer b.l.RUnlock()
return string(b.title)
}
// SetTitleColor sets the box's title color.
func (b *Box) SetTitleColor(color tcell.Color) {
b.l.Lock()
defer b.l.Unlock()
b.titleColor = color
}
// SetTitleAlign sets the alignment of the title, one of AlignLeft, AlignCenter,
// or AlignRight.
func (b *Box) SetTitleAlign(align int) {
b.l.Lock()
defer b.l.Unlock()
b.titleAlign = align
}
// Draw draws this primitive onto the screen.
func (b *Box) Draw(screen tcell.Screen) {
b.l.Lock()
defer b.l.Unlock()
// Don't draw anything if the box is hidden
if !b.visible {
return
}
// Don't draw anything if there is no space.
if b.width <= 0 || b.height <= 0 {
return
}
def := tcell.StyleDefault
// Fill background.
background := def.Background(b.backgroundColor)
if !b.backgroundTransparent {
for y := b.y; y < b.y+b.height; y++ {
for x := b.x; x < b.x+b.width; x++ {
screen.SetContent(x, y, ' ', nil, background)
}
}
}
// Draw border.
if b.border && b.width >= 2 && b.height >= 2 {
border := SetAttributes(background.Foreground(b.borderColor), b.borderAttributes)
var vertical, horizontal, topLeft, topRight, bottomLeft, bottomRight rune
var hasFocus bool
if b.focus == b {
hasFocus = b.hasFocus
} else {
hasFocus = b.focus.HasFocus()
}
if hasFocus && b.borderColorFocused != ColorUnset {
border = SetAttributes(background.Foreground(b.borderColorFocused), b.borderAttributes)
}
if hasFocus && b.showFocus {
horizontal = Borders.HorizontalFocus
vertical = Borders.VerticalFocus
topLeft = Borders.TopLeftFocus
topRight = Borders.TopRightFocus
bottomLeft = Borders.BottomLeftFocus
bottomRight = Borders.BottomRightFocus
} else {
horizontal = Borders.Horizontal
vertical = Borders.Vertical
topLeft = Borders.TopLeft
topRight = Borders.TopRight
bottomLeft = Borders.BottomLeft
bottomRight = Borders.BottomRight
}
for x := b.x + 1; x < b.x+b.width-1; x++ {
screen.SetContent(x, b.y, horizontal, nil, 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, 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)
screen.SetContent(b.x, b.y+b.height-1, bottomLeft, nil, border)
screen.SetContent(b.x+b.width-1, b.y+b.height-1, bottomRight, nil, border)
// Draw title.
if len(b.title) > 0 && b.width >= 4 {
printed, _ := Print(screen, b.title, b.x+1, b.y, b.width-2, b.titleAlign, b.titleColor)
if len(b.title)-printed > 0 && printed > 0 {
_, _, style, _ := screen.GetContent(b.x+b.width-2, b.y)
fg, _, _ := style.Decompose()
Print(screen, []byte(string(SemigraphicsHorizontalEllipsis)), b.x+b.width-2, b.y, 1, AlignLeft, fg)
}
}
}
// Call custom draw function.
if b.draw != nil {
b.innerX, b.innerY, b.innerWidth, b.innerHeight = b.draw(screen, b.x, b.y, b.width, b.height)
}
}
// ShowFocus sets the flag indicating whether or not the borders of this
// primitive should change thickness when focused.
func (b *Box) ShowFocus(showFocus bool) {
b.l.Lock()
defer b.l.Unlock()
b.showFocus = showFocus
}
// Focus is called when this primitive receives focus.
func (b *Box) Focus(delegate func(p Primitive)) {
b.l.Lock()
defer b.l.Unlock()
b.hasFocus = true
}
// Blur is called when this primitive loses focus.
func (b *Box) Blur() {
b.l.Lock()
defer b.l.Unlock()
b.hasFocus = false
}
// HasFocus returns whether or not this primitive has focus.
func (b *Box) HasFocus() bool {
b.l.RLock()
defer b.l.RUnlock()
return b.hasFocus
}
// GetFocusable returns the item's Focusable.
func (b *Box) GetFocusable() Focusable {
b.l.RLock()
defer b.l.RUnlock()
return b.focus
}
// GetBorderPadding returns the size of the padding around the box content.
//
// Deprecated: This function is provided for backwards compatibility.
// Developers should use GetPadding instead.
func (b *Box) GetBorderPadding() (top, bottom, left, right int) {
return b.GetPadding()
}
// SetBorderPadding sets the size of the padding around the box content.
//
// Deprecated: This function is provided for backwards compatibility.
// Developers should use SetPadding instead.
func (b *Box) SetBorderPadding(top, bottom, left, right int) {
b.SetPadding(top, bottom, left, right)
}
package cview
import (
"sync"
"github.com/gdamore/tcell/v2"
)
// Button is labeled box that triggers an action when selected.
type Button struct {
*Box
// The text to be displayed before the input area.
label []byte
// The label color.
labelColor tcell.Color
// The label color when the button is in focus.
labelColorFocused tcell.Color
// The background color when the button is in focus.
backgroundColorFocused tcell.Color
// An optional function which is called when the button was selected.
selected func()
// An optional function which is called when the user leaves the button. A
// key is provided indicating which key was pressed to leave (tab or backtab).
blur func(tcell.Key)
sync.RWMutex
}
// NewButton returns a new input field.
func NewButton(label string) *Button {
box := NewBox()
box.SetBackgroundColor(Styles.ContrastBackgroundColor)
box.SetRect(0, 0, TaggedStringWidth(label)+4, 1)
return &Button{
Box: box,
label: []byte(label),
labelColor: Styles.PrimaryTextColor,
labelColorFocused: Styles.InverseTextColor,
backgroundColorFocused: Styles.PrimaryTextColor,
}
}
// SetLabel sets the button text.
func (b *Button) SetLabel(label string) {
b.Lock()
defer b.Unlock()
b.label = []byte(label)
}
// GetLabel returns the button text.
func (b *Button) GetLabel() string {
b.RLock()
defer b.RUnlock()
return string(b.label)
}
// SetLabelColor sets the color of the button text.
func (b *Button) SetLabelColor(color tcell.Color) {
b.Lock()
defer b.Unlock()
b.labelColor = color
}
// SetLabelColorFocused sets the color of the button text when the button is
// in focus.
func (b *Button) SetLabelColorFocused(color tcell.Color) {
b.Lock()
defer b.Unlock()
b.labelColorFocused = color
}
// SetBackgroundColorFocused sets the background color of the button text when
// the button is in focus.
func (b *Button) SetBackgroundColorFocused(color tcell.Color) {
b.Lock()
defer b.Unlock()
b.backgroundColorFocused = color
}
// SetSelectedFunc sets a handler which is called when the button was selected.
func (b *Button) SetSelectedFunc(handler func()) {
b.Lock()
defer b.Unlock()
b.selected = handler
}
// SetBlurFunc sets a handler which is called when the user leaves the button.
// The callback function is provided with the key that was pressed, which is one
// of the following:
//
// - KeyEscape: Leaving the button with no specific direction.
// - KeyTab: Move to the next field.
// - KeyBacktab: Move to the previous field.
func (b *Button) SetBlurFunc(handler func(key tcell.Key)) {
b.Lock()
defer b.Unlock()
b.blur = handler
}
// Draw draws this primitive onto the screen.
func (b *Button) Draw(screen tcell.Screen) {
if !b.GetVisible() {
return
}
b.Lock()
defer b.Unlock()
// Draw the box.
borderColor := b.borderColor
backgroundColor := b.backgroundColor
if b.focus.HasFocus() {
b.backgroundColor = b.backgroundColorFocused
b.borderColor = b.labelColorFocused
defer func() {
b.borderColor = borderColor
}()
}
b.Unlock()
b.Box.Draw(screen)
b.Lock()
b.backgroundColor = backgroundColor
// Draw label.
x, y, width, height := b.GetInnerRect()
if width > 0 && height > 0 {
y = y + height/2
labelColor := b.labelColor
if b.focus.HasFocus() {
labelColor = b.labelColorFocused
}
Print(screen, b.label, x, y, width, AlignCenter, labelColor)
}
}
// InputHandler returns the handler for this primitive.
func (b *Button) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) {
return b.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p Primitive)) {
// Process key event.
if HitShortcut(event, Keys.Select, Keys.Select2) {
if b.selected != nil {
b.selected()
}
} else if HitShortcut(event, Keys.Cancel, Keys.MovePreviousField, Keys.MoveNextField) {
if b.blur != nil {
b.blur(event.Key())
}
}
})
}
// MouseHandler returns the mouse handler for this primitive.
func (b *Button) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
return b.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
if !b.InRect(event.Position()) {
return false, nil
}
// Process mouse event.
if action == MouseLeftClick {
setFocus(b)
if b.selected != nil {
b.selected()
}
consumed = true
}
return
})
}
package cview
import (
"sync"
"github.com/gdamore/tcell/v2"
)
// CheckBox implements a simple box for boolean values which can be checked and
// unchecked.
type CheckBox struct {
*Box
// Whether or not this box is checked.
checked bool
// The text to be displayed before the checkbox.
label []byte
// The text to be displayed after the checkbox.
message []byte
// The screen width of the label area. A value of 0 means use the width of
// the label text.
labelWidth int
// The label color.
labelColor tcell.Color
// The label color when focused.
labelColorFocused tcell.Color
// The background color of the input area.
fieldBackgroundColor tcell.Color
// The background color of the input area when focused.
fieldBackgroundColorFocused tcell.Color
// The text color of the input area.
fieldTextColor tcell.Color
// The text color of the input area when focused.
fieldTextColorFocused tcell.Color
// An optional function which is called when the user changes the checked
// state of this checkbox.
changed func(checked bool)
// An optional function which is called when the user indicated that they
// are done entering text. The key which was pressed is provided (tab,
// shift-tab, or escape).
done func(tcell.Key)
// A callback function set by the Form class and called when the user leaves
// this form item.
finished func(tcell.Key)
// The rune to show when the checkbox is checked
checkedRune rune
sync.RWMutex
}
// NewCheckBox returns a new input field.
func NewCheckBox() *CheckBox {
return &CheckBox{
Box: NewBox(),
labelColor: Styles.SecondaryTextColor,
fieldBackgroundColor: Styles.ContrastBackgroundColor,
fieldTextColor: Styles.PrimaryTextColor,
checkedRune: Styles.CheckBoxCheckedRune,
labelColorFocused: ColorUnset,
fieldBackgroundColorFocused: ColorUnset,
fieldTextColorFocused: ColorUnset,
}
}
// SetChecked sets the state of the checkbox.
func (c *CheckBox) SetChecked(checked bool) {
c.Lock()
defer c.Unlock()
c.checked = checked
}
// SetCheckedRune sets the rune to show when the checkbox is checked.
func (c *CheckBox) SetCheckedRune(rune rune) {
c.Lock()
defer c.Unlock()
c.checkedRune = rune
}
// IsChecked returns whether or not the box is checked.
func (c *CheckBox) IsChecked() bool {
c.RLock()
defer c.RUnlock()
return c.checked
}
// SetLabel sets the text to be displayed before the input area.
func (c *CheckBox) SetLabel(label string) {
c.Lock()
defer c.Unlock()
c.label = []byte(label)
}
// GetLabel returns the text to be displayed before the input area.
func (c *CheckBox) GetLabel() string {
c.RLock()
defer c.RUnlock()
return string(c.label)
}
// SetMessage sets the text to be displayed after the checkbox
func (c *CheckBox) SetMessage(message string) {
c.Lock()
defer c.Unlock()
c.message = []byte(message)
}
// GetMessage returns the text to be displayed after the checkbox
func (c *CheckBox) GetMessage() string {
c.RLock()
defer c.RUnlock()
return string(c.message)
}
// SetLabelWidth sets the screen width of the label. A value of 0 will cause the
// primitive to use the width of the label string.
func (c *CheckBox) SetLabelWidth(width int) {
c.Lock()
defer c.Unlock()
c.labelWidth = width
}
// SetLabelColor sets the color of the label.
func (c *CheckBox) SetLabelColor(color tcell.Color) {
c.Lock()
defer c.Unlock()
c.labelColor = color
}
// SetLabelColorFocused sets the color of the label when focused.
func (c *CheckBox) SetLabelColorFocused(color tcell.Color) {
c.Lock()
defer c.Unlock()
c.labelColorFocused = color
}
// SetFieldBackgroundColor sets the background color of the input area.
func (c *CheckBox) SetFieldBackgroundColor(color tcell.Color) {
c.Lock()
defer c.Unlock()
c.fieldBackgroundColor = color
}
// SetFieldBackgroundColorFocused sets the background color of the input area when focused.
func (c *CheckBox) SetFieldBackgroundColorFocused(color tcell.Color) {
c.Lock()
defer c.Unlock()
c.fieldBackgroundColorFocused = color
}
// SetFieldTextColor sets the text color of the input area.
func (c *CheckBox) SetFieldTextColor(color tcell.Color) {
c.Lock()
defer c.Unlock()
c.fieldTextColor = color
}
// SetFieldTextColorFocused sets the text color of the input area when focused.
func (c *CheckBox) SetFieldTextColorFocused(color tcell.Color) {
c.Lock()
defer c.Unlock()
c.fieldTextColorFocused = color
}
// GetFieldHeight returns the height of the field.
func (c *CheckBox) GetFieldHeight() int {
return 1
}
// GetFieldWidth returns this primitive's field width.
func (c *CheckBox) GetFieldWidth() int {
c.RLock()
defer c.RUnlock()
if len(c.message) == 0 {
return 1
}
return 2 + len(c.message)
}
// SetChangedFunc sets a handler which is called when the checked state of this
// checkbox was changed by the user. The handler function receives the new
// state.
func (c *CheckBox) SetChangedFunc(handler func(checked bool)) {
c.Lock()
defer c.Unlock()
c.changed = handler
}
// SetDoneFunc sets a handler which is called when the user is done using the
// checkbox. The callback function is provided with the key that was pressed,
// which is one of the following:
//
// - KeyEscape: Abort text input.
// - KeyTab: Move to the next field.
// - KeyBacktab: Move to the previous field.
func (c *CheckBox) SetDoneFunc(handler func(key tcell.Key)) {
c.Lock()
defer c.Unlock()
c.done = handler
}
// SetFinishedFunc sets a callback invoked when the user leaves this form item.
func (c *CheckBox) SetFinishedFunc(handler func(key tcell.Key)) {
c.Lock()
defer c.Unlock()
c.finished = handler
}
// Draw draws this primitive onto the screen.
func (c *CheckBox) Draw(screen tcell.Screen) {
if !c.GetVisible() {
return
}
c.Box.Draw(screen)
c.Lock()
defer c.Unlock()
// Select colors
labelColor := c.labelColor
fieldBackgroundColor := c.fieldBackgroundColor
fieldTextColor := c.fieldTextColor
if c.GetFocusable().HasFocus() {
if c.labelColorFocused != ColorUnset {
labelColor = c.labelColorFocused
}
if c.fieldBackgroundColorFocused != ColorUnset {
fieldBackgroundColor = c.fieldBackgroundColorFocused
}
if c.fieldTextColorFocused != ColorUnset {
fieldTextColor = c.fieldTextColorFocused
}
}
// Prepare
x, y, width, height := c.GetInnerRect()
rightLimit := x + width
if height < 1 || rightLimit <= x {
return
}
// Draw label.
if c.labelWidth > 0 {
labelWidth := c.labelWidth
if labelWidth > rightLimit-x {
labelWidth = rightLimit - x
}
Print(screen, c.label, x, y, labelWidth, AlignLeft, labelColor)
x += labelWidth
} else {
_, drawnWidth := Print(screen, c.label, x, y, rightLimit-x, AlignLeft, labelColor)
x += drawnWidth
}
// Draw checkbox.
fieldStyle := tcell.StyleDefault.Background(fieldBackgroundColor).Foreground(fieldTextColor)
checkedRune := c.checkedRune
if !c.checked {
checkedRune = ' '
}
screen.SetContent(x, y, ' ', nil, fieldStyle)
screen.SetContent(x+1, y, checkedRune, nil, fieldStyle)
screen.SetContent(x+2, y, ' ', nil, fieldStyle)
if len(c.message) > 0 {
Print(screen, c.message, x+4, y, len(c.message), AlignLeft, labelColor)
}
}
// InputHandler returns the handler for this primitive.
func (c *CheckBox) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) {
return c.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p Primitive)) {
if HitShortcut(event, Keys.Select, Keys.Select2) {
c.Lock()
c.checked = !c.checked
c.Unlock()
if c.changed != nil {
c.changed(c.checked)
}
} else if HitShortcut(event, Keys.Cancel, Keys.MovePreviousField, Keys.MoveNextField) {
if c.done != nil {
c.done(event.Key())
}
if c.finished != nil {
c.finished(event.Key())
}
}
})
}
// MouseHandler returns the mouse handler for this primitive.
func (c *CheckBox) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
return c.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
x, y := event.Position()
_, rectY, _, _ := c.GetInnerRect()
if !c.InRect(x, y) {
return false, nil
}
// Process mouse event.
if action == MouseLeftClick && y == rectY {
setFocus(c)
c.checked = !c.checked
if c.changed != nil {
c.changed(c.checked)
}
consumed = true
}
return
})
}
package cview
import "sync"
// ContextMenu is a menu that appears upon user interaction, such as right
// clicking or pressing Alt+Enter.
type ContextMenu struct {
parent Primitive
item int
open bool
drag bool
list *List
x, y int
selected func(int, string, rune)
l sync.RWMutex
}
// NewContextMenu returns a new context menu.
func NewContextMenu(parent Primitive) *ContextMenu {
return &ContextMenu{
parent: parent,
}
}
func (c *ContextMenu) initializeList() {
if c.list != nil {
return
}
c.list = NewList()
c.list.ShowSecondaryText(false)
c.list.SetHover(true)
c.list.SetWrapAround(true)
c.list.ShowFocus(false)
c.list.SetBorder(true)
c.list.SetPadding(
Styles.ContextMenuPaddingTop,
Styles.ContextMenuPaddingBottom,
Styles.ContextMenuPaddingLeft,
Styles.ContextMenuPaddingRight)
}
// ContextMenuList returns the underlying List of the context menu.
func (c *ContextMenu) ContextMenuList() *List {
c.l.Lock()
defer c.l.Unlock()
c.initializeList()
return c.list
}
// AddContextItem adds an item to the context menu. Adding an item with no text
// or shortcut will add a divider.
func (c *ContextMenu) AddContextItem(text string, shortcut rune, selected func(index int)) {
c.l.Lock()
defer c.l.Unlock()
c.initializeList()
item := NewListItem(text)
item.SetShortcut(shortcut)
item.SetSelectedFunc(c.wrap(selected))
c.list.AddItem(item)
if text == "" && shortcut == 0 {
c.list.Lock()
index := len(c.list.items) - 1
c.list.items[index].disabled = true
c.list.Unlock()
}
}
func (c *ContextMenu) wrap(f func(index int)) func() {
return func() {
f(c.item)
}
}
// ClearContextMenu removes all items from the context menu.
func (c *ContextMenu) ClearContextMenu() {
c.l.Lock()
defer c.l.Unlock()
c.initializeList()
c.list.Clear()
}
// SetContextSelectedFunc sets the function which is called when the user
// selects a context menu item. The function receives the item's index in the
// menu (starting with 0), its text and its shortcut rune. SetSelectedFunc must
// be called before the context menu is shown.
func (c *ContextMenu) SetContextSelectedFunc(handler func(index int, text string, shortcut rune)) {
c.l.Lock()
defer c.l.Unlock()
c.selected = handler
}
// ShowContextMenu shows the context menu. Provide -1 for both to position on
// the selected item, or specify a position.
func (c *ContextMenu) ShowContextMenu(item int, x int, y int, setFocus func(Primitive)) {
c.l.Lock()
defer c.l.Unlock()
c.show(item, x, y, setFocus)
}
// HideContextMenu hides the context menu.
func (c *ContextMenu) HideContextMenu(setFocus func(Primitive)) {
c.l.Lock()
defer c.l.Unlock()
c.hide(setFocus)
}
// ContextMenuVisible returns whether or not the context menu is visible.
func (c *ContextMenu) ContextMenuVisible() bool {
c.l.Lock()
defer c.l.Unlock()
return c.open
}
func (c *ContextMenu) show(item int, x int, y int, setFocus func(Primitive)) {
c.initializeList()
if len(c.list.items) == 0 {
return
}
c.open = true
c.item = item
c.x, c.y = x, y
c.list.Lock()
for i, item := range c.list.items {
if !item.disabled {
c.list.currentItem = i
break
}
}
c.list.Unlock()
c.list.SetSelectedFunc(func(index int, item *ListItem) {
c.l.Lock()
// A context item was selected. Close the menu.
c.hide(setFocus)
if c.selected != nil {
c.l.Unlock()
c.selected(index, string(item.mainText), item.shortcut)
} else {
c.l.Unlock()
}
})
c.list.SetDoneFunc(func() {
c.l.Lock()
defer c.l.Unlock()
c.hide(setFocus)
})
setFocus(c.list)
}
func (c *ContextMenu) hide(setFocus func(Primitive)) {
c.initializeList()
c.open = false
if c.list.HasFocus() {
setFocus(c.parent)
}
}
package cview
import (
"strings"
"sync"
"github.com/gdamore/tcell/v2"
"github.com/mattn/go-runewidth"
)
// DropDownOption is one option that can be selected in a drop-down primitive.
type DropDownOption struct {
text string // The text to be displayed in the drop-down.
selected func(index int, option *DropDownOption) // The (optional) callback for when this option was selected.
reference interface{} // An optional reference object.
sync.RWMutex
}
// NewDropDownOption returns a new option for a dropdown.
func NewDropDownOption(text string) *DropDownOption {
return &DropDownOption{text: text}
}
// GetText returns the text of this dropdown option.
func (d *DropDownOption) GetText() string {
d.RLock()
defer d.RUnlock()
return d.text
}
// SetText returns the text of this dropdown option.
func (d *DropDownOption) SetText(text string) {
d.text = text
}
// SetSelectedFunc sets the handler to be called when this option is selected.
func (d *DropDownOption) SetSelectedFunc(handler func(index int, option *DropDownOption)) {
d.selected = handler
}
// GetReference returns the reference object of this dropdown option.
func (d *DropDownOption) GetReference() interface{} {
d.RLock()
defer d.RUnlock()
return d.reference
}
// SetReference allows you to store a reference of any type in this option.
func (d *DropDownOption) SetReference(reference interface{}) {
d.reference = reference
}
// DropDown implements a selection widget whose options become visible in a
// drop-down list when activated.
type DropDown struct {
*Box
// The options from which the user can choose.
options []*DropDownOption
// Strings to be placed before and after each drop-down option.
optionPrefix, optionSuffix string
// The index of the currently selected option. Negative if no option is
// currently selected.
currentOption int
// Strings to be placed before and after the current option.
currentOptionPrefix, currentOptionSuffix string
// The text to be displayed when no option has yet been selected.
noSelection string
// Set to true if the options are visible and selectable.
open bool
// The runes typed so far to directly access one of the list items.
prefix string
// The list element for the options.
list *List
// The text to be displayed before the input area.
label string
// The label color.
labelColor tcell.Color
// The label color when focused.
labelColorFocused tcell.Color
// The background color of the input area.
fieldBackgroundColor tcell.Color
// The background color of the input area when focused.
fieldBackgroundColorFocused tcell.Color
// The text color of the input area.
fieldTextColor tcell.Color
// The text color of the input area when focused.
fieldTextColorFocused tcell.Color
// The color for prefixes.
prefixTextColor tcell.Color
// The screen width of the label area. A value of 0 means use the width of
// the label text.
labelWidth int
// The screen width of the input area. A value of 0 means extend as much as
// possible.
fieldWidth int
// An optional function which is called when the user indicated that they
// are done selecting options. The key which was pressed is provided (tab,
// shift-tab, or escape).
done func(tcell.Key)
// A callback function set by the Form class and called when the user leaves
// this form item.
finished func(tcell.Key)
// A callback function which is called when the user changes the drop-down's
// selection.
selected func(index int, option *DropDownOption)
// Set to true when mouse dragging is in progress.
dragging bool
// The chars to show when the option's text gets shortened.
abbreviationChars string
// The symbol to draw at the end of the field.
dropDownSymbol rune
sync.RWMutex
}
// NewDropDown returns a new drop-down.
func NewDropDown() *DropDown {
list := NewList()
list.ShowSecondaryText(false)
list.SetMainTextColor(Styles.PrimitiveBackgroundColor)
list.SetSelectedTextColor(Styles.PrimitiveBackgroundColor)
list.SetSelectedBackgroundColor(Styles.PrimaryTextColor)
list.SetHighlightFullLine(true)
list.SetBackgroundColor(Styles.MoreContrastBackgroundColor)
d := &DropDown{
Box: NewBox(),
currentOption: -1,
list: list,
labelColor: Styles.SecondaryTextColor,
fieldBackgroundColor: Styles.ContrastBackgroundColor,
fieldTextColor: Styles.PrimaryTextColor,
prefixTextColor: Styles.ContrastSecondaryTextColor,
dropDownSymbol: Styles.DropDownSymbol,
abbreviationChars: Styles.DropDownAbbreviationChars,
labelColorFocused: ColorUnset,
fieldBackgroundColorFocused: ColorUnset,
fieldTextColorFocused: ColorUnset,
}
d.focus = d
return d
}
// SetDropDownSymbolRune sets the rune to be drawn at the end of the dropdown field
// to indicate that this field is a dropdown.
func (d *DropDown) SetDropDownSymbolRune(symbol rune) {
d.Lock()
defer d.Unlock()
d.dropDownSymbol = symbol
}
// SetCurrentOption sets the index of the currently selected option. This may
// be a negative value to indicate that no option is currently selected. Calling
// this function will also trigger the "selected" callback (if there is one).
func (d *DropDown) SetCurrentOption(index int) {
d.Lock()
defer d.Unlock()
if index >= 0 && index < len(d.options) {
d.currentOption = index
d.list.SetCurrentItem(index)
if d.selected != nil {
d.Unlock()
d.selected(index, d.options[index])
d.Lock()
}
if d.options[index].selected != nil {
d.Unlock()
d.options[index].selected(index, d.options[index])
d.Lock()
}
} else {
d.currentOption = -1
d.list.SetCurrentItem(0) // Set to 0 because -1 means "last item".
if d.selected != nil {
d.Unlock()
d.selected(-1, nil)
d.Lock()
}
}
}
// GetCurrentOption returns the index of the currently selected option as well
// as the option itself. If no option was selected, -1 and nil is returned.
func (d *DropDown) GetCurrentOption() (int, *DropDownOption) {
d.RLock()
defer d.RUnlock()
var option *DropDownOption
if d.currentOption >= 0 && d.currentOption < len(d.options) {
option = d.options[d.currentOption]
}
return d.currentOption, option
}
// SetTextOptions sets the text to be placed before and after each drop-down
// option (prefix/suffix), the text placed before and after the currently
// selected option (currentPrefix/currentSuffix) as well as the text to be
// displayed when no option is currently selected. Per default, all of these
// strings are empty.
func (d *DropDown) SetTextOptions(prefix, suffix, currentPrefix, currentSuffix, noSelection string) {
d.Lock()
defer d.Unlock()
d.currentOptionPrefix = currentPrefix
d.currentOptionSuffix = currentSuffix
d.noSelection = noSelection
d.optionPrefix = prefix
d.optionSuffix = suffix
for index := 0; index < d.list.GetItemCount(); index++ {
d.list.SetItemText(index, prefix+d.options[index].text+suffix, "")
}
}
// SetLabel sets the text to be displayed before the input area.
func (d *DropDown) SetLabel(label string) {
d.Lock()
defer d.Unlock()
d.label = label
}
// GetLabel returns the text to be displayed before the input area.
func (d *DropDown) GetLabel() string {
d.RLock()
defer d.RUnlock()
return d.label
}
// SetLabelWidth sets the screen width of the label. A value of 0 will cause the
// primitive to use the width of the label string.
func (d *DropDown) SetLabelWidth(width int) {
d.Lock()
defer d.Unlock()
d.labelWidth = width
}
// SetLabelColor sets the color of the label.
func (d *DropDown) SetLabelColor(color tcell.Color) {
d.Lock()
defer d.Unlock()
d.labelColor = color
}
// SetLabelColorFocused sets the color of the label when focused.
func (d *DropDown) SetLabelColorFocused(color tcell.Color) {
d.Lock()
defer d.Unlock()
d.labelColorFocused = color
}
// SetFieldBackgroundColor sets the background color of the options area.
func (d *DropDown) SetFieldBackgroundColor(color tcell.Color) {
d.Lock()
defer d.Unlock()
d.fieldBackgroundColor = color
}
// SetFieldBackgroundColorFocused sets the background color of the options area when focused.
func (d *DropDown) SetFieldBackgroundColorFocused(color tcell.Color) {
d.Lock()
defer d.Unlock()
d.fieldBackgroundColorFocused = color
}
// SetFieldTextColor sets the text color of the options area.
func (d *DropDown) SetFieldTextColor(color tcell.Color) {
d.Lock()
defer d.Unlock()
d.fieldTextColor = color
}
// SetFieldTextColorFocused sets the text color of the options area when focused.
func (d *DropDown) SetFieldTextColorFocused(color tcell.Color) {
d.Lock()
defer d.Unlock()
d.fieldTextColorFocused = color
}
// SetDropDownTextColor sets text color of the drop-down list.
func (d *DropDown) SetDropDownTextColor(color tcell.Color) {
d.Lock()
defer d.Unlock()
d.list.SetMainTextColor(color)
}
// SetDropDownBackgroundColor sets the background color of the drop-down list.
func (d *DropDown) SetDropDownBackgroundColor(color tcell.Color) {
d.Lock()
defer d.Unlock()
d.list.SetBackgroundColor(color)
}
// SetDropDownSelectedTextColor sets the text color of the selected option in
// the drop-down list.
func (d *DropDown) SetDropDownSelectedTextColor(color tcell.Color) {
d.Lock()
defer d.Unlock()
d.list.SetSelectedTextColor(color)
}
// SetDropDownSelectedBackgroundColor sets the background color of the selected
// option in the drop-down list.
func (d *DropDown) SetDropDownSelectedBackgroundColor(color tcell.Color) {
d.Lock()
defer d.Unlock()
d.list.SetSelectedBackgroundColor(color)
}
// SetPrefixTextColor sets the color of the prefix string. The prefix string is
// shown when the user starts typing text, which directly selects the first
// option that starts with the typed string.
func (d *DropDown) SetPrefixTextColor(color tcell.Color) {
d.Lock()
defer d.Unlock()
d.prefixTextColor = color
}
// SetFieldWidth sets the screen width of the options area. A value of 0 means
// extend to as long as the longest option text.
func (d *DropDown) SetFieldWidth(width int) {
d.Lock()
defer d.Unlock()
d.fieldWidth = width
}
// GetFieldHeight returns the height of the field.
func (d *DropDown) GetFieldHeight() int {
return 1
}
// GetFieldWidth returns this primitive's field screen width.
func (d *DropDown) GetFieldWidth() int {
d.RLock()
defer d.RUnlock()
return d.getFieldWidth()
}
func (d *DropDown) getFieldWidth() int {
if d.fieldWidth > 0 {
return d.fieldWidth
}
fieldWidth := 0
for _, option := range d.options {
width := TaggedStringWidth(option.text)
if width > fieldWidth {
fieldWidth = width
}
}
fieldWidth += len(d.optionPrefix) + len(d.optionSuffix)
fieldWidth += len(d.currentOptionPrefix) + len(d.currentOptionSuffix)
fieldWidth += 3 // space + dropDownSymbol + space
return fieldWidth
}
// AddOptionsSimple adds new selectable options to this drop-down.
func (d *DropDown) AddOptionsSimple(options ...string) {
optionsToAdd := make([]*DropDownOption, len(options))
for i, option := range options {
optionsToAdd[i] = NewDropDownOption(option)
}
d.AddOptions(optionsToAdd...)
}
// AddOptions adds new selectable options to this drop-down.
func (d *DropDown) AddOptions(options ...*DropDownOption) {
d.Lock()
defer d.Unlock()
d.addOptions(options...)
}
func (d *DropDown) addOptions(options ...*DropDownOption) {
d.options = append(d.options, options...)
for _, option := range options {
d.list.AddItem(NewListItem(d.optionPrefix + option.text + d.optionSuffix))
}
}
// SetOptionsSimple replaces all current options with the ones provided and installs
// one callback function which is called when one of the options is selected.
// It will be called with the option's index and the option itself
// The "selected" parameter may be nil.
func (d *DropDown) SetOptionsSimple(selected func(index int, option *DropDownOption), options ...string) {
optionsToSet := make([]*DropDownOption, len(options))
for i, option := range options {
optionsToSet[i] = NewDropDownOption(option)
}
d.SetOptions(selected, optionsToSet...)
}
// SetOptions replaces all current options with the ones provided and installs
// one callback function which is called when one of the options is selected.
// It will be called with the option's index and the option itself.
// The "selected" parameter may be nil.
func (d *DropDown) SetOptions(selected func(index int, option *DropDownOption), options ...*DropDownOption) {
d.Lock()
defer d.Unlock()
d.list.Clear()
d.options = nil
d.addOptions(options...)
d.selected = selected
}
// SetChangedFunc sets a handler which is called when the user changes the
// focused drop-down option. The handler is provided with the selected option's
// index and the option itself. If "no option" was selected, these values are
// -1 and nil.
func (d *DropDown) SetChangedFunc(handler func(index int, option *DropDownOption)) {
d.list.SetChangedFunc(func(index int, item *ListItem) {
handler(index, d.options[index])
})
}
// SetSelectedFunc sets a handler which is called when the user selects a
// drop-down's option. This handler will be called in addition and prior to
// an option's optional individual handler. The handler is provided with the
// selected option's index and the option itself. If "no option" was selected, these values
// are -1 and nil.
func (d *DropDown) SetSelectedFunc(handler func(index int, option *DropDownOption)) {
d.Lock()
defer d.Unlock()
d.selected = handler
}
// SetDoneFunc sets a handler which is called when the user is done selecting
// options. The callback function is provided with the key that was pressed,
// which is one of the following:
//
// - KeyEscape: Abort selection.
// - KeyTab: Move to the next field.
// - KeyBacktab: Move to the previous field.
func (d *DropDown) SetDoneFunc(handler func(key tcell.Key)) {
d.Lock()
defer d.Unlock()
d.done = handler
}
// SetFinishedFunc sets a callback invoked when the user leaves this form item.
func (d *DropDown) SetFinishedFunc(handler func(key tcell.Key)) {
d.Lock()
defer d.Unlock()
d.finished = handler
}
// Draw draws this primitive onto the screen.
func (d *DropDown) Draw(screen tcell.Screen) {
d.Box.Draw(screen)
hasFocus := d.GetFocusable().HasFocus()
d.Lock()
defer d.Unlock()
// Select colors
labelColor := d.labelColor
fieldBackgroundColor := d.fieldBackgroundColor
fieldTextColor := d.fieldTextColor
if hasFocus {
if d.labelColorFocused != ColorUnset {
labelColor = d.labelColorFocused
}
if d.fieldBackgroundColorFocused != ColorUnset {
fieldBackgroundColor = d.fieldBackgroundColorFocused
}
if d.fieldTextColorFocused != ColorUnset {
fieldTextColor = d.fieldTextColorFocused
}
}
// Prepare.
x, y, width, height := d.GetInnerRect()
rightLimit := x + width
if height < 1 || rightLimit <= x {
return
}
// Draw label.
if d.labelWidth > 0 {
labelWidth := d.labelWidth
if labelWidth > rightLimit-x {
labelWidth = rightLimit - x
}
Print(screen, []byte(d.label), x, y, labelWidth, AlignLeft, labelColor)
x += labelWidth
} else {
_, drawnWidth := Print(screen, []byte(d.label), x, y, rightLimit-x, AlignLeft, labelColor)
x += drawnWidth
}
// What's the longest option text?
maxWidth := 0
optionWrapWidth := TaggedStringWidth(d.optionPrefix + d.optionSuffix)
for _, option := range d.options {
strWidth := TaggedStringWidth(option.text) + optionWrapWidth
if strWidth > maxWidth {
maxWidth = strWidth
}
}
// Draw selection area.
fieldWidth := d.getFieldWidth()
if fieldWidth == 0 {
fieldWidth = maxWidth
if d.currentOption < 0 {
noSelectionWidth := TaggedStringWidth(d.noSelection)
if noSelectionWidth > fieldWidth {
fieldWidth = noSelectionWidth
}
} else if d.currentOption < len(d.options) {
currentOptionWidth := TaggedStringWidth(d.currentOptionPrefix + d.options[d.currentOption].text + d.currentOptionSuffix)
if currentOptionWidth > fieldWidth {
fieldWidth = currentOptionWidth
}
}
}
if rightLimit-x < fieldWidth {
fieldWidth = rightLimit - x
}
fieldStyle := tcell.StyleDefault.Background(fieldBackgroundColor)
for index := 0; index < fieldWidth; index++ {
screen.SetContent(x+index, y, ' ', nil, fieldStyle)
}
// Draw selected text.
if d.open && len(d.prefix) > 0 {
// Show the prefix.
currentOptionPrefixWidth := TaggedStringWidth(d.currentOptionPrefix)
prefixWidth := runewidth.StringWidth(d.prefix)
listItemText := d.options[d.list.GetCurrentItemIndex()].text
Print(screen, []byte(d.currentOptionPrefix), x, y, fieldWidth, AlignLeft, fieldTextColor)
Print(screen, []byte(d.prefix), x+currentOptionPrefixWidth, y, fieldWidth-currentOptionPrefixWidth, AlignLeft, d.prefixTextColor)
if len(d.prefix) < len(listItemText) {
Print(screen, []byte(listItemText[len(d.prefix):]+d.currentOptionSuffix), x+prefixWidth+currentOptionPrefixWidth, y, fieldWidth-prefixWidth-currentOptionPrefixWidth, AlignLeft, fieldTextColor)
}
} else {
color := fieldTextColor
text := d.noSelection
if d.currentOption >= 0 && d.currentOption < len(d.options) {
text = d.currentOptionPrefix + d.options[d.currentOption].text + d.currentOptionSuffix
}
// Abbreviate text when not fitting
if fieldWidth > len(d.abbreviationChars)+3 && len(text) > fieldWidth {
text = text[0:fieldWidth-3-len(d.abbreviationChars)] + d.abbreviationChars
}
// Just show the current selection.
Print(screen, []byte(text), x, y, fieldWidth, AlignLeft, color)
}
// Draw drop-down symbol
screen.SetContent(x+fieldWidth-2, y, d.dropDownSymbol, nil, new(tcell.Style).Foreground(fieldTextColor).Background(fieldBackgroundColor))
// Draw options list.
if hasFocus && d.open {
// We prefer to drop-down but if there is no space, maybe drop up?
lx := x
ly := y + 1
lheight := len(d.options)
_, sheight := screen.Size()
if ly+lheight >= sheight && ly-2 > lheight-ly {
ly = y - lheight
if ly < 0 {
ly = 0
}
}
if ly+lheight >= sheight {
lheight = sheight - ly
}
lwidth := maxWidth
if d.list.scrollBarVisibility == ScrollBarAlways || (d.list.scrollBarVisibility == ScrollBarAuto && len(d.options) > lheight) {
lwidth++ // Add space for scroll bar
}
if lwidth < fieldWidth {
lwidth = fieldWidth
}
d.list.SetRect(lx, ly, lwidth, lheight)
d.list.Draw(screen)
}
}
// InputHandler returns the handler for this primitive.
func (d *DropDown) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) {
return d.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p Primitive)) {
// Process key event.
switch key := event.Key(); key {
case tcell.KeyEnter, tcell.KeyRune, tcell.KeyDown:
d.Lock()
defer d.Unlock()
d.prefix = ""
// If the first key was a letter already, it becomes part of the prefix.
if r := event.Rune(); key == tcell.KeyRune && r != ' ' {
d.prefix += string(r)
d.evalPrefix()
}
d.openList(setFocus)
case tcell.KeyEscape, tcell.KeyTab, tcell.KeyBacktab:
if d.done != nil {
d.done(key)
}
if d.finished != nil {
d.finished(key)
}
}
})
}
// evalPrefix selects an item in the drop-down list based on the current prefix.
func (d *DropDown) evalPrefix() {
if len(d.prefix) > 0 {
for index, option := range d.options {
if strings.HasPrefix(strings.ToLower(option.text), d.prefix) {
d.list.SetCurrentItem(index)
return
}
}
// Prefix does not match any item. Remove last rune.
r := []rune(d.prefix)
d.prefix = string(r[:len(r)-1])
}
}
// openList hands control over to the embedded List primitive.
func (d *DropDown) openList(setFocus func(Primitive)) {
d.open = true
optionBefore := d.currentOption
d.list.SetSelectedFunc(func(index int, item *ListItem) {
if d.dragging {
return // If we're dragging the mouse, we don't want to trigger any events.
}
// An option was selected. Close the list again.
d.currentOption = index
d.closeList(setFocus)
// Trigger "selected" event.
if d.selected != nil {
d.selected(d.currentOption, d.options[d.currentOption])
}
if d.options[d.currentOption].selected != nil {
d.options[d.currentOption].selected(d.currentOption, d.options[d.currentOption])
}
})
d.list.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
if event.Key() == tcell.KeyRune {
d.prefix += string(event.Rune())
d.evalPrefix()
} else if event.Key() == tcell.KeyBackspace || event.Key() == tcell.KeyBackspace2 {
if len(d.prefix) > 0 {
r := []rune(d.prefix)
d.prefix = string(r[:len(r)-1])
}
d.evalPrefix()
} else if event.Key() == tcell.KeyEscape {
d.currentOption = optionBefore
d.list.SetCurrentItem(d.currentOption)
d.closeList(setFocus)
if d.selected != nil {
if d.currentOption > -1 {
d.selected(d.currentOption, d.options[d.currentOption])
}
}
} else {
d.prefix = ""
}
return event
})
setFocus(d.list)
}
// closeList closes the embedded List element by hiding it and removing focus
// from it.
func (d *DropDown) closeList(setFocus func(Primitive)) {
d.open = false
if d.list.HasFocus() {
setFocus(d)
}
}
// Focus is called by the application when the primitive receives focus.
func (d *DropDown) Focus(delegate func(p Primitive)) {
d.Box.Focus(delegate)
if d.open {
delegate(d.list)
}
}
// HasFocus returns whether or not this primitive has focus.
func (d *DropDown) HasFocus() bool {
d.RLock()
defer d.RUnlock()
if d.open {
return d.list.HasFocus()
}
return d.hasFocus
}
// MouseHandler returns the mouse handler for this primitive.
func (d *DropDown) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
return d.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
// Was the mouse event in the drop-down box itself (or on its label)?
x, y := event.Position()
_, rectY, _, _ := d.GetInnerRect()
inRect := y == rectY
if !d.open && !inRect {
return d.InRect(x, y), nil // No, and it's not expanded either. Ignore.
}
// Handle dragging. Clicks are implicitly handled by this logic.
switch action {
case MouseLeftDown:
consumed = d.open || inRect
capture = d
if !d.open {
d.openList(setFocus)
d.dragging = true
} else if consumed, _ := d.list.MouseHandler()(MouseLeftClick, event, setFocus); !consumed {
d.closeList(setFocus) // Close drop-down if clicked outside of it.
}
case MouseMove:
if d.dragging {
// We pretend it's a left click so we can see the selection during
// dragging. Because we don't act upon it, it's not a problem.
d.list.MouseHandler()(MouseLeftClick, event, setFocus)
consumed = true
capture = d
}
case MouseLeftUp:
if d.dragging {
d.dragging = false
d.list.MouseHandler()(MouseLeftClick, event, setFocus)
consumed = true
}
}
return
})
}
package cview
import (
"sync"
"github.com/gdamore/tcell/v2"
)
// Configuration values.
const (
FlexRow = iota
FlexColumn
)
// flexItem holds layout options for one item.
type flexItem struct {
Item Primitive // The item to be positioned. May be nil for an empty item.
FixedSize int // The item's fixed size which may not be changed, 0 if it has no fixed size.
Proportion int // The item's proportion.
Focus bool // Whether or not this item attracts the layout's focus.
}
// Flex is a basic implementation of the Flexbox layout. The contained
// primitives are arranged horizontally or vertically. The way they are
// distributed along that dimension depends on their layout settings, which is
// either a fixed length or a proportional length. See AddItem() for details.
type Flex struct {
*Box
// The items to be positioned.
items []*flexItem
// FlexRow or FlexColumn.
direction int
// If set to true, Flex will use the entire screen as its available space
// instead its box dimensions.
fullScreen bool
sync.RWMutex
}
// NewFlex returns a new flexbox layout container with no primitives and its
// direction set to FlexColumn. To add primitives to this layout, see AddItem().
// To change the direction, see SetDirection().
//
// Note that Flex will have a transparent background by default so that any nil
// flex items will show primitives behind the Flex.
// To disable this transparency:
//
// flex.SetBackgroundTransparent(false)
func NewFlex() *Flex {
f := &Flex{
Box: NewBox(),
direction: FlexColumn,
}
f.SetBackgroundTransparent(true)
f.focus = f
return f
}
// GetDirection returns the direction in which the contained primitives are
// distributed. This can be either FlexColumn (default) or FlexRow.
func (f *Flex) GetDirection() int {
f.RLock()
defer f.RUnlock()
return f.direction
}
// SetDirection sets the direction in which the contained primitives are
// distributed. This can be either FlexColumn (default) or FlexRow.
func (f *Flex) SetDirection(direction int) {
f.Lock()
defer f.Unlock()
f.direction = direction
}
// SetFullScreen sets the flag which, when true, causes the flex layout to use
// the entire screen space instead of whatever size it is currently assigned to.
func (f *Flex) SetFullScreen(fullScreen bool) {
f.Lock()
defer f.Unlock()
f.fullScreen = fullScreen
}
// AddItem adds a new item to the container. The "fixedSize" argument is a width
// or height that may not be changed by the layout algorithm. A value of 0 means
// that its size is flexible and may be changed. The "proportion" argument
// defines the relative size of the item compared to other flexible-size items.
// For example, items with a proportion of 2 will be twice as large as items
// with a proportion of 1. The proportion must be at least 1 if fixedSize == 0
// (ignored otherwise).
//
// If "focus" is set to true, the item will receive focus when the Flex
// primitive receives focus. If multiple items have the "focus" flag set to
// true, the first one will receive focus.
//
// You can provide a nil value for the primitive. This will fill the empty
// screen space with the default background color. To show content behind the
// space, add a Box with a transparent background instead.
func (f *Flex) AddItem(item Primitive, fixedSize, proportion int, focus bool) {
f.Lock()
defer f.Unlock()
if item == nil {
item = NewBox()
}
f.items = append(f.items, &flexItem{Item: item, FixedSize: fixedSize, Proportion: proportion, Focus: focus})
}
// AddItemAtIndex adds an item to the flex at a given index.
// For more information see AddItem.
func (f *Flex) AddItemAtIndex(index int, item Primitive, fixedSize, proportion int, focus bool) {
f.Lock()
defer f.Unlock()
newItem := &flexItem{Item: item, FixedSize: fixedSize, Proportion: proportion, Focus: focus}
if index == 0 {
f.items = append([]*flexItem{newItem}, f.items...)
} else {
f.items = append(f.items[:index], append([]*flexItem{newItem}, f.items[index:]...)...)
}
}
// RemoveItem removes all items for the given primitive from the container,
// keeping the order of the remaining items intact.
func (f *Flex) RemoveItem(p Primitive) {
f.Lock()
defer f.Unlock()
for index := len(f.items) - 1; index >= 0; index-- {
if f.items[index].Item == p {
f.items = append(f.items[:index], f.items[index+1:]...)
}
}
}
// 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) {
f.Lock()
defer f.Unlock()
for _, item := range f.items {
if item.Item == p {
item.FixedSize = fixedSize
item.Proportion = proportion
}
}
}
// Draw draws this primitive onto the screen.
func (f *Flex) Draw(screen tcell.Screen) {
if !f.GetVisible() {
return
}
f.Box.Draw(screen)
f.Lock()
defer f.Unlock()
// Calculate size and position of the items.
// Do we use the entire screen?
if f.fullScreen {
width, height := screen.Size()
f.SetRect(0, 0, width, height)
}
// How much space can we distribute?
x, y, width, height := f.GetInnerRect()
var proportionSum int
distSize := width
if f.direction == FlexRow {
distSize = height
}
for _, item := range f.items {
if item.FixedSize > 0 {
distSize -= item.FixedSize
} else {
proportionSum += item.Proportion
}
}
// Calculate positions and draw items.
pos := x
if f.direction == FlexRow {
pos = y
}
for _, item := range f.items {
size := item.FixedSize
if size <= 0 {
if proportionSum > 0 {
size = distSize * item.Proportion / proportionSum
distSize -= size
proportionSum -= item.Proportion
} else {
size = 0
}
}
if item.Item != nil {
if f.direction == FlexColumn {
item.Item.SetRect(pos, y, size, height)
} else {
item.Item.SetRect(x, pos, width, size)
}
}
pos += size
if item.Item != nil {
if item.Item.GetFocusable().HasFocus() {
defer item.Item.Draw(screen)
} else {
item.Item.Draw(screen)
}
}
}
}
// Focus is called when this primitive receives focus.
func (f *Flex) Focus(delegate func(p Primitive)) {
f.Lock()
for _, item := range f.items {
if item.Item != nil && item.Focus {
f.Unlock()
delegate(item.Item)
return
}
}
f.Unlock()
}
// HasFocus returns whether or not this primitive has focus.
func (f *Flex) HasFocus() bool {
f.RLock()
defer f.RUnlock()
for _, item := range f.items {
if item.Item != nil && item.Item.GetFocusable().HasFocus() {
return true
}
}
return false
}
// MouseHandler returns the mouse handler for this primitive.
func (f *Flex) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
return f.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
if !f.InRect(event.Position()) {
return false, nil
}
// Pass mouse events along to the first child item that takes it.
for _, item := range f.items {
if item.Item == nil {
continue
}
consumed, capture = item.Item.MouseHandler()(action, event, setFocus)
if consumed {
return
}
}
return
})
}
package cview
import "sync"
// Focusable provides a method which determines if a primitive has focus.
// Composed primitives may be focused based on the focused state of their
// contained primitives.
type Focusable interface {
HasFocus() bool
}
type focusElement struct {
primitive Primitive
disabled bool
}
// FocusManager manages application focus.
type FocusManager struct {
elements []*focusElement
focused int
wrapAround bool
setFocus func(p Primitive)
sync.RWMutex
}
// NewFocusManager returns a new FocusManager object.
func NewFocusManager(setFocus func(p Primitive)) *FocusManager {
return &FocusManager{setFocus: setFocus}
}
// SetWrapAround sets the flag that determines whether navigation will wrap
// around. That is, navigating forwards on the last field will move the
// selection to the first field (similarly in the other direction). If set to
// false, the focus won't change when navigating forwards on the last element
// or navigating backwards on the first element.
func (f *FocusManager) SetWrapAround(wrapAround bool) {
f.Lock()
defer f.Unlock()
f.wrapAround = wrapAround
}
// Add adds an element to the focus handler.
func (f *FocusManager) Add(p ...Primitive) {
f.Lock()
defer f.Unlock()
for _, primitive := range p {
f.elements = append(f.elements, &focusElement{primitive: primitive})
}
}
// AddAt adds an element to the focus handler at the specified index.
func (f *FocusManager) AddAt(index int, p Primitive) {
f.Lock()
defer f.Unlock()
if index < 0 || index > len(f.elements) {
panic("index out of range")
}
element := &focusElement{primitive: p}
if index == len(f.elements) {
f.elements = append(f.elements, element)
return
}
f.elements = append(f.elements[:index+1], f.elements[index:]...)
f.elements[index] = element
}
// Focus focuses the provided element.
func (f *FocusManager) Focus(p Primitive) {
f.Lock()
defer f.Unlock()
for i, element := range f.elements {
if p == element.primitive && !element.disabled {
f.focused = i
break
}
}
f.setFocus(f.elements[f.focused].primitive)
}
// FocusPrevious focuses the previous element.
func (f *FocusManager) FocusPrevious() {
f.Lock()
defer f.Unlock()
f.focused--
f.updateFocusIndex(true)
f.setFocus(f.elements[f.focused].primitive)
}
// FocusNext focuses the next element.
func (f *FocusManager) FocusNext() {
f.Lock()
defer f.Unlock()
f.focused++
f.updateFocusIndex(false)
f.setFocus(f.elements[f.focused].primitive)
}
// FocusAt focuses the element at the provided index.
func (f *FocusManager) FocusAt(index int) {
f.Lock()
defer f.Unlock()
f.focused = index
f.setFocus(f.elements[f.focused].primitive)
}
// GetFocusIndex returns the index of the currently focused element.
func (f *FocusManager) GetFocusIndex() int {
f.Lock()
defer f.Unlock()
return f.focused
}
// GetFocusedPrimitive returns the currently focused primitive.
func (f *FocusManager) GetFocusedPrimitive() Primitive {
f.Lock()
defer f.Unlock()
return f.elements[f.focused].primitive
}
func (f *FocusManager) updateFocusIndex(decreasing bool) {
for i := 0; i < len(f.elements); i++ {
if f.focused < 0 {
if f.wrapAround {
f.focused = len(f.elements) - 1
} else {
f.focused = 0
}
} else if f.focused >= len(f.elements) {
if f.wrapAround {
f.focused = 0
} else {
f.focused = len(f.elements) - 1
}
}
item := f.elements[f.focused]
if !item.disabled {
break
}
if decreasing {
f.focused--
} else {
f.focused++
}
}
}
// Transform modifies the current focus.
func (f *FocusManager) Transform(tr Transformation) {
var decreasing bool
switch tr {
case TransformFirstItem:
f.focused = 0
decreasing = true
case TransformLastItem:
f.focused = len(f.elements) - 1
case TransformPreviousItem:
f.focused--
decreasing = true
case TransformNextItem:
f.focused++
}
f.updateFocusIndex(decreasing)
}
package cview
import (
"reflect"
"sync"
"github.com/gdamore/tcell/v2"
)
// DefaultFormFieldWidth is the default field screen width of form elements
// whose field width is flexible (0). This is used in the Form class for
// horizontal layouts.
var DefaultFormFieldWidth = 10
// FormItemAttributes is a set of attributes to be applied.
type FormItemAttributes struct {
// The screen width of the label. A value of 0 will cause the primitive to
// use the width of the label string.
LabelWidth int
BackgroundColor tcell.Color
LabelColor tcell.Color
LabelColorFocused tcell.Color
FieldBackgroundColor tcell.Color
FieldBackgroundColorFocused tcell.Color
FieldTextColor tcell.Color
FieldTextColorFocused tcell.Color
FinishedFunc func(key tcell.Key)
}
// FormItem is the interface all form items must implement to be able to be
// included in a form.
type FormItem interface {
Primitive
// GetLabel returns the item's label text.
GetLabel() string
// SetLabelWidth sets the screen width of the label. A value of 0 will cause the
// primitive to use the width of the label string.
SetLabelWidth(int)
// SetLabelColor sets the color of the label.
SetLabelColor(tcell.Color)
// SetLabelColor sets the color of the label when focused.
SetLabelColorFocused(tcell.Color)
// GetFieldWidth returns the width of the form item's field (the area which
// is manipulated by the user) in number of screen cells. A value of 0
// indicates the the field width is flexible and may use as much space as
// required.
GetFieldWidth() int
// GetFieldHeight returns the height of the form item.
GetFieldHeight() int
// SetFieldTextColor sets the text color of the input area.
SetFieldTextColor(tcell.Color)
// SetFieldTextColorFocused sets the text color of the input area when focused.
SetFieldTextColorFocused(tcell.Color)
// SetFieldBackgroundColor sets the background color of the input area.
SetFieldBackgroundColor(tcell.Color)
// SetFieldBackgroundColor sets the background color of the input area when focused.
SetFieldBackgroundColorFocused(tcell.Color)
// SetBackgroundColor sets the background color of the form item.
SetBackgroundColor(tcell.Color)
// SetFinishedFunc sets a callback invoked when the user leaves the form item.
SetFinishedFunc(func(key tcell.Key))
}
// Form allows you to combine multiple one-line form elements into a vertical
// or horizontal layout. Form elements include types such as InputField or
// CheckBox. These elements can be optionally followed by one or more buttons
// for which you can define form-wide actions (e.g. Save, Clear, Cancel).
type Form struct {
*Box
// The items of the form (one row per item).
items []FormItem
// The buttons of the form.
buttons []*Button
// If set to true, instead of position items and buttons from top to bottom,
// they are positioned from left to right.
horizontal bool
// The alignment of the buttons.
buttonsAlign int
// The number of empty rows between items.
itemPadding int
// The index of the item or button which has focus. (Items are counted first,
// buttons are counted last.) This is only used when the form itself receives
// focus so that the last element that had focus keeps it.
focusedElement int
// Whether or not navigating the form will wrap around.
wrapAround bool
// The label color.
labelColor tcell.Color
// The label color when focused.
labelColorFocused tcell.Color
// The background color of the input area.
fieldBackgroundColor tcell.Color
// The background color of the input area when focused.
fieldBackgroundColorFocused tcell.Color
// The text color of the input area.
fieldTextColor tcell.Color
// The text color of the input area when focused.
fieldTextColorFocused tcell.Color
// The background color of the buttons.
buttonBackgroundColor tcell.Color
// The background color of the buttons when focused.
buttonBackgroundColorFocused tcell.Color
// The color of the button text.
buttonTextColor tcell.Color
// The color of the button text when focused.
buttonTextColorFocused tcell.Color
// An optional function which is called when the user hits Escape.
cancel func()
sync.RWMutex
}
// NewForm returns a new form.
func NewForm() *Form {
box := NewBox()
box.SetPadding(1, 1, 1, 1)
f := &Form{
Box: box,
itemPadding: 1,
labelColor: Styles.SecondaryTextColor,
fieldBackgroundColor: Styles.ContrastBackgroundColor,
fieldTextColor: Styles.PrimaryTextColor,
buttonBackgroundColor: Styles.ContrastBackgroundColor,
buttonBackgroundColorFocused: Styles.PrimaryTextColor,
buttonTextColor: Styles.PrimaryTextColor,
buttonTextColorFocused: Styles.InverseTextColor,
labelColorFocused: ColorUnset,
fieldTextColorFocused: ColorUnset,
fieldBackgroundColorFocused: ColorUnset,
}
f.focus = f
return f
}
// SetItemPadding sets the number of empty rows between form items for vertical
// layouts and the number of empty cells between form items for horizontal
// layouts.
func (f *Form) SetItemPadding(padding int) {
f.Lock()
defer f.Unlock()
f.itemPadding = padding
}
// SetHorizontal sets the direction the form elements are laid out. If set to
// true, instead of positioning them from top to bottom (the default), they are
// positioned from left to right, moving into the next row if there is not
// enough space.
func (f *Form) SetHorizontal(horizontal bool) {
f.Lock()
defer f.Unlock()
f.horizontal = horizontal
}
// SetLabelColor sets the color of the labels.
func (f *Form) SetLabelColor(color tcell.Color) {
f.Lock()
defer f.Unlock()
f.labelColor = color
}
// SetLabelColorFocused sets the color of the labels when focused.
func (f *Form) SetLabelColorFocused(color tcell.Color) {
f.Lock()
defer f.Unlock()
f.labelColorFocused = color
}
// SetFieldBackgroundColor sets the background color of the input areas.
func (f *Form) SetFieldBackgroundColor(color tcell.Color) {
f.Lock()
defer f.Unlock()
f.fieldBackgroundColor = color
}
// SetFieldBackgroundColorFocused sets the background color of the input areas when focused.
func (f *Form) SetFieldBackgroundColorFocused(color tcell.Color) {
f.Lock()
defer f.Unlock()
f.fieldBackgroundColorFocused = color
}
// SetFieldTextColor sets the text color of the input areas.
func (f *Form) SetFieldTextColor(color tcell.Color) {
f.Lock()
defer f.Unlock()
f.fieldTextColor = color
}
// SetFieldTextColorFocused sets the text color of the input areas when focused.
func (f *Form) SetFieldTextColorFocused(color tcell.Color) {
f.Lock()
defer f.Unlock()
f.fieldTextColorFocused = color
}
// SetButtonsAlign sets how the buttons align horizontally, one of AlignLeft
// (the default), AlignCenter, and AlignRight. This is only
func (f *Form) SetButtonsAlign(align int) {
f.Lock()
defer f.Unlock()
f.buttonsAlign = align
}
// SetButtonBackgroundColor sets the background color of the buttons.
func (f *Form) SetButtonBackgroundColor(color tcell.Color) {
f.Lock()
defer f.Unlock()
f.buttonBackgroundColor = color
}
// SetButtonBackgroundColorFocused sets the background color of the buttons when focused.
func (f *Form) SetButtonBackgroundColorFocused(color tcell.Color) {
f.Lock()
defer f.Unlock()
f.buttonBackgroundColorFocused = color
}
// SetButtonTextColor sets the color of the button texts.
func (f *Form) SetButtonTextColor(color tcell.Color) {
f.Lock()
defer f.Unlock()
f.buttonTextColor = color
}
// SetButtonTextColorFocused sets the color of the button texts when focused.
func (f *Form) SetButtonTextColorFocused(color tcell.Color) {
f.Lock()
defer f.Unlock()
f.buttonTextColorFocused = color
}
// SetFocus shifts the focus to the form element with the given index, counting
// non-button items first and buttons last. Note that this index is only used
// when the form itself receives focus.
func (f *Form) SetFocus(index int) {
f.Lock()
defer f.Unlock()
if index < 0 {
f.focusedElement = 0
} else if index >= len(f.items)+len(f.buttons) {
f.focusedElement = len(f.items) + len(f.buttons)
} else {
f.focusedElement = index
}
}
// AddInputField adds an input field to the form. It has a label, an optional
// initial value, a field width (a value of 0 extends it as far as possible),
// an optional accept function to validate the item's value (set to nil to
// accept any text), and an (optional) callback function which is invoked when
// the input field's text has changed.
func (f *Form) AddInputField(label, value string, fieldWidth int, accept func(textToCheck string, lastChar rune) bool, changed func(text string)) {
f.Lock()
defer f.Unlock()
inputField := NewInputField()
inputField.SetLabel(label)
inputField.SetText(value)
inputField.SetFieldWidth(fieldWidth)
inputField.SetAcceptanceFunc(accept)
inputField.SetChangedFunc(changed)
f.items = append(f.items, inputField)
}
// AddPasswordField adds a password field to the form. This is similar to an
// input field except that the user's input not shown. Instead, a "mask"
// character is displayed. The password field has a label, an optional initial
// value, a field width (a value of 0 extends it as far as possible), and an
// (optional) callback function which is invoked when the input field's text has
// changed.
func (f *Form) AddPasswordField(label, value string, fieldWidth int, mask rune, changed func(text string)) {
f.Lock()
defer f.Unlock()
if mask == 0 {
mask = '*'
}
passwordField := NewInputField()
passwordField.SetLabel(label)
passwordField.SetText(value)
passwordField.SetFieldWidth(fieldWidth)
passwordField.SetMaskCharacter(mask)
passwordField.SetChangedFunc(changed)
f.items = append(f.items, passwordField)
}
// AddDropDownSimple adds a drop-down element to the form. It has a label, options,
// and an (optional) callback function which is invoked when an option was
// selected. The initial option may be a negative value to indicate that no
// option is currently selected.
func (f *Form) AddDropDownSimple(label string, initialOption int, selected func(index int, option *DropDownOption), options ...string) {
f.Lock()
defer f.Unlock()
dd := NewDropDown()
dd.SetLabel(label)
dd.SetOptionsSimple(selected, options...)
dd.SetCurrentOption(initialOption)
f.items = append(f.items, dd)
}
// AddDropDown adds a drop-down element to the form. It has a label, options,
// and an (optional) callback function which is invoked when an option was
// selected. The initial option may be a negative value to indicate that no
// option is currently selected.
func (f *Form) AddDropDown(label string, initialOption int, selected func(index int, option *DropDownOption), options []*DropDownOption) {
f.Lock()
defer f.Unlock()
dd := NewDropDown()
dd.SetLabel(label)
dd.SetOptions(selected, options...)
dd.SetCurrentOption(initialOption)
f.items = append(f.items, dd)
}
// AddCheckBox adds a checkbox to the form. It has a label, a message, an
// initial state, and an (optional) callback function which is invoked when the
// state of the checkbox was changed by the user.
func (f *Form) AddCheckBox(label string, message string, checked bool, changed func(checked bool)) {
f.Lock()
defer f.Unlock()
c := NewCheckBox()
c.SetLabel(label)
c.SetMessage(message)
c.SetChecked(checked)
c.SetChangedFunc(changed)
f.items = append(f.items, c)
}
// AddSlider adds a slider to the form. It has a label, an initial value, a
// maximum value, an amount to increment by when modified via keyboard, and an
// (optional) callback function which is invoked when the state of the slider
// was changed by the user.
func (f *Form) AddSlider(label string, current, max, increment int, changed func(value int)) {
f.Lock()
defer f.Unlock()
s := NewSlider()
s.SetLabel(label)
s.SetMax(max)
s.SetProgress(current)
s.SetIncrement(increment)
s.SetChangedFunc(changed)
f.items = append(f.items, s)
}
// AddButton adds a new button to the form. The "selected" function is called
// when the user selects this button. It may be nil.
func (f *Form) AddButton(label string, selected func()) {
f.Lock()
defer f.Unlock()
button := NewButton(label)
button.SetSelectedFunc(selected)
f.buttons = append(f.buttons, button)
}
// 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 {
f.RLock()
defer f.RUnlock()
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) {
f.Lock()
defer f.Unlock()
f.buttons = append(f.buttons[:index], f.buttons[index+1:]...)
}
// GetButtonCount returns the number of buttons in this form.
func (f *Form) GetButtonCount() int {
f.RLock()
defer f.RUnlock()
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 {
f.RLock()
defer f.RUnlock()
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) {
f.Lock()
defer f.Unlock()
f.items = nil
if includeButtons {
f.buttons = nil
}
f.focusedElement = 0
}
// ClearButtons removes all buttons from the form.
func (f *Form) ClearButtons() {
f.Lock()
defer f.Unlock()
f.buttons = nil
}
// AddFormItem adds a new item to the form. This can be used to add your own
// objects to the form. Note, however, that the Form class will override some
// of its attributes to make it work in the form context. Specifically, these
// are:
//
// - The label width
// - The label color
// - The background color
// - The field text color
// - The field background color
func (f *Form) AddFormItem(item FormItem) {
f.Lock()
defer f.Unlock()
if reflect.ValueOf(item).IsNil() {
panic("Invalid FormItem")
}
f.items = append(f.items, item)
}
// GetFormItemCount returns the number of items in the form (not including the
// buttons).
func (f *Form) GetFormItemCount() int {
f.RLock()
defer f.RUnlock()
return len(f.items)
}
// IndexOfFormItem returns the index of the given FormItem.
func (f *Form) IndexOfFormItem(item FormItem) int {
f.l.RLock()
defer f.l.RUnlock()
for index, formItem := range f.items {
if item == formItem {
return index
}
}
return -1
}
// GetFormItem returns the form item at the given position, starting with index
// 0. Elements are referenced in the order they were added. Buttons are not included.
// If index is out of bounds it returns nil.
func (f *Form) GetFormItem(index int) FormItem {
f.RLock()
defer f.RUnlock()
if index > len(f.items)-1 || index < 0 {
return nil
}
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) {
f.Lock()
defer f.Unlock()
f.items = append(f.items[:index], f.items[index+1:]...)
}
// GetFormItemByLabel returns the first form element with the given label. If
// no such element is found, nil is returned. Buttons are not searched and will
// therefore not be returned.
func (f *Form) GetFormItemByLabel(label string) FormItem {
f.RLock()
defer f.RUnlock()
for _, item := range f.items {
if item.GetLabel() == label {
return item
}
}
return nil
}
// GetFormItemIndex returns the index of the first form element with the given
// label. If no such element is found, -1 is returned. Buttons are not searched
// and will therefore not be returned.
func (f *Form) GetFormItemIndex(label string) int {
f.RLock()
defer f.RUnlock()
for index, item := range f.items {
if item.GetLabel() == label {
return index
}
}
return -1
}
// GetFocusedItemIndex returns the indices of the form element or button which
// currently has focus. If they don't, -1 is returned resepectively.
func (f *Form) GetFocusedItemIndex() (formItem, button int) {
f.RLock()
defer f.RUnlock()
index := f.focusIndex()
if index < 0 {
return -1, -1
}
if index < len(f.items) {
return index, -1
}
return -1, index - len(f.items)
}
// SetWrapAround sets the flag that determines whether navigating the form will
// wrap around. That is, navigating downwards on the last item will move the
// selection to the first item (similarly in the other direction). If set to
// false, the selection won't change when navigating downwards on the last item
// or navigating upwards on the first item.
func (f *Form) SetWrapAround(wrapAround bool) {
f.Lock()
defer f.Unlock()
f.wrapAround = wrapAround
}
// SetCancelFunc sets a handler which is called when the user hits the Escape
// key.
func (f *Form) SetCancelFunc(callback func()) {
f.Lock()
defer f.Unlock()
f.cancel = callback
}
// GetAttributes returns the current attribute settings of a form.
func (f *Form) GetAttributes() *FormItemAttributes {
f.Lock()
defer f.Unlock()
return f.getAttributes()
}
func (f *Form) getAttributes() *FormItemAttributes {
attrs := &FormItemAttributes{
BackgroundColor: f.backgroundColor,
LabelColor: f.labelColor,
FieldBackgroundColor: f.fieldBackgroundColor,
FieldTextColor: f.fieldTextColor,
}
if f.labelColorFocused == ColorUnset {
attrs.LabelColorFocused = f.labelColor
} else {
attrs.LabelColorFocused = f.labelColorFocused
}
if f.fieldBackgroundColorFocused == ColorUnset {
attrs.FieldBackgroundColorFocused = f.fieldTextColor
} else {
attrs.FieldBackgroundColorFocused = f.fieldBackgroundColorFocused
}
if f.fieldTextColorFocused == ColorUnset {
attrs.FieldTextColorFocused = f.fieldBackgroundColor
} else {
attrs.FieldTextColorFocused = f.fieldTextColorFocused
}
return attrs
}
// Draw draws this primitive onto the screen.
func (f *Form) Draw(screen tcell.Screen) {
if !f.GetVisible() {
return
}
f.Box.Draw(screen)
f.Lock()
defer f.Unlock()
// Determine the actual item that has focus.
if index := f.focusIndex(); index >= 0 {
f.focusedElement = index
}
// Determine the dimensions.
x, y, width, height := f.GetInnerRect()
topLimit := y
bottomLimit := y + height
rightLimit := x + width
startX := x
// Find the longest label.
var maxLabelWidth int
for _, item := range f.items {
labelWidth := TaggedStringWidth(item.GetLabel())
if labelWidth > maxLabelWidth {
maxLabelWidth = labelWidth
}
}
maxLabelWidth++ // Add one space.
// Calculate positions of form items.
positions := make([]struct{ x, y, width, height int }, len(f.items)+len(f.buttons))
var focusedPosition struct{ x, y, width, height int }
for index, item := range f.items {
if !item.GetVisible() {
continue
}
// Calculate the space needed.
labelWidth := TaggedStringWidth(item.GetLabel())
var itemWidth int
if f.horizontal {
fieldWidth := item.GetFieldWidth()
if fieldWidth == 0 {
fieldWidth = DefaultFormFieldWidth
}
labelWidth++
itemWidth = labelWidth + fieldWidth
} else {
// We want all fields to align vertically.
labelWidth = maxLabelWidth
itemWidth = width
}
// Advance to next line if there is no space.
if f.horizontal && x+labelWidth+1 >= rightLimit {
x = startX
y += 2
}
// Adjust the item's attributes.
if x+itemWidth >= rightLimit {
itemWidth = rightLimit - x
}
attributes := f.getAttributes()
attributes.LabelWidth = labelWidth
setFormItemAttributes(item, attributes)
// Save position.
positions[index].x = x
positions[index].y = y
positions[index].width = itemWidth
positions[index].height = 1
if item.GetFocusable().HasFocus() {
focusedPosition = positions[index]
}
// Advance to next item.
if f.horizontal {
x += itemWidth + f.itemPadding
} else {
y += item.GetFieldHeight() + f.itemPadding
}
}
// How wide are the buttons?
buttonWidths := make([]int, len(f.buttons))
buttonsWidth := 0
for index, button := range f.buttons {
w := TaggedStringWidth(button.GetLabel()) + 4
buttonWidths[index] = w
buttonsWidth += w + 1
}
buttonsWidth--
// Where do we place them?
if !f.horizontal && x+buttonsWidth < rightLimit {
if f.buttonsAlign == AlignRight {
x = rightLimit - buttonsWidth
} else if f.buttonsAlign == AlignCenter {
x = (x + rightLimit - buttonsWidth) / 2
}
// In vertical layouts, buttons always appear after an empty line.
if f.itemPadding == 0 {
y++
}
}
// Calculate positions of buttons.
for index, button := range f.buttons {
if !button.GetVisible() {
continue
}
space := rightLimit - x
buttonWidth := buttonWidths[index]
if f.horizontal {
if space < buttonWidth-4 {
x = startX
y += 2
space = width
}
} else {
if space < 1 {
break // No space for this button anymore.
}
}
if buttonWidth > space {
buttonWidth = space
}
button.SetLabelColor(f.buttonTextColor)
button.SetLabelColorFocused(f.buttonTextColorFocused)
button.SetBackgroundColorFocused(f.buttonBackgroundColorFocused)
button.SetBackgroundColor(f.buttonBackgroundColor)
buttonIndex := index + len(f.items)
positions[buttonIndex].x = x
positions[buttonIndex].y = y
positions[buttonIndex].width = buttonWidth
positions[buttonIndex].height = 1
if button.HasFocus() {
focusedPosition = positions[buttonIndex]
}
x += buttonWidth + 1
}
// Determine vertical offset based on the position of the focused item.
var offset int
if focusedPosition.y+focusedPosition.height > bottomLimit {
offset = focusedPosition.y + focusedPosition.height - bottomLimit
if focusedPosition.y-offset < topLimit {
offset = focusedPosition.y - topLimit
}
}
// Draw items.
for index, item := range f.items {
if !item.GetVisible() {
continue
}
// Set position.
y := positions[index].y - offset
height := positions[index].height
item.SetRect(positions[index].x, y, positions[index].width, height)
// Is this item visible?
if y+height <= topLimit || y >= bottomLimit {
continue
}
// Draw items with focus last (in case of overlaps).
if item.GetFocusable().HasFocus() {
defer item.Draw(screen)
} else {
item.Draw(screen)
}
}
// Draw buttons.
for index, button := range f.buttons {
if !button.GetVisible() {
continue
}
// Set position.
buttonIndex := index + len(f.items)
y := positions[buttonIndex].y - offset
height := positions[buttonIndex].height
button.SetRect(positions[buttonIndex].x, y, positions[buttonIndex].width, height)
// Is this button visible?
if y+height <= topLimit || y >= bottomLimit {
continue
}
// Draw button.
button.Draw(screen)
}
}
func (f *Form) updateFocusedElement(decreasing bool) {
li := len(f.items)
l := len(f.items) + len(f.buttons)
for i := 0; i < l; i++ {
if f.focusedElement < 0 {
if f.wrapAround {
f.focusedElement = l - 1
} else {
f.focusedElement = 0
}
} else if f.focusedElement >= l {
if f.wrapAround {
f.focusedElement = 0
} else {
f.focusedElement = l - 1
}
}
if f.focusedElement < li {
item := f.items[f.focusedElement]
if item.GetVisible() {
break
}
} else {
button := f.buttons[f.focusedElement-li]
if button.GetVisible() {
break
}
}
if decreasing {
f.focusedElement--
} else {
f.focusedElement++
}
}
}
func (f *Form) formItemInputHandler(delegate func(p Primitive)) func(key tcell.Key) {
return func(key tcell.Key) {
f.Lock()
switch key {
case tcell.KeyTab, tcell.KeyEnter:
f.focusedElement++
f.updateFocusedElement(false)
f.Unlock()
f.Focus(delegate)
f.Lock()
case tcell.KeyBacktab:
f.focusedElement--
f.updateFocusedElement(true)
f.Unlock()
f.Focus(delegate)
f.Lock()
case tcell.KeyEscape:
if f.cancel != nil {
f.Unlock()
f.cancel()
f.Lock()
} else {
f.focusedElement = 0
f.updateFocusedElement(true)
f.Unlock()
f.Focus(delegate)
f.Lock()
}
}
f.Unlock()
}
}
// Focus is called by the application when the primitive receives focus.
func (f *Form) Focus(delegate func(p Primitive)) {
f.Lock()
if len(f.items)+len(f.buttons) == 0 {
f.hasFocus = true
f.Unlock()
return
}
f.hasFocus = false
// Hand on the focus to one of our child elements.
if f.focusedElement < 0 || f.focusedElement >= len(f.items)+len(f.buttons) {
f.focusedElement = 0
}
if f.focusedElement < len(f.items) {
// We're selecting an item.
item := f.items[f.focusedElement]
attributes := f.getAttributes()
attributes.FinishedFunc = f.formItemInputHandler(delegate)
f.Unlock()
setFormItemAttributes(item, attributes)
delegate(item)
} else {
// We're selecting a button.
button := f.buttons[f.focusedElement-len(f.items)]
button.SetBlurFunc(f.formItemInputHandler(delegate))
f.Unlock()
delegate(button)
}
}
// HasFocus returns whether or not this primitive has focus.
func (f *Form) HasFocus() bool {
f.Lock()
defer f.Unlock()
if f.hasFocus {
return true
}
return f.focusIndex() >= 0
}
// focusIndex returns the index of the currently focused item, counting form
// items first, then buttons. A negative value indicates that no containeed item
// has focus.
func (f *Form) focusIndex() int {
for index, item := range f.items {
if item.GetVisible() && item.GetFocusable().HasFocus() {
return index
}
}
for index, button := range f.buttons {
if button.GetVisible() && button.focus.HasFocus() {
return len(f.items) + index
}
}
return -1
}
// MouseHandler returns the mouse handler for this primitive.
func (f *Form) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
return f.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
if !f.InRect(event.Position()) {
return false, nil
}
// Determine items to pass mouse events to.
for _, item := range f.items {
consumed, capture = item.MouseHandler()(action, event, setFocus)
if consumed {
return
}
}
for _, button := range f.buttons {
consumed, capture = button.MouseHandler()(action, event, setFocus)
if consumed {
return
}
}
// A mouse click anywhere else will return the focus to the last selected
// element.
if action == MouseLeftClick {
if f.focusedElement < len(f.items) {
setFocus(f.items[f.focusedElement])
} else if f.focusedElement < len(f.items)+len(f.buttons) {
setFocus(f.buttons[f.focusedElement-len(f.items)])
}
consumed = true
}
return
})
}
func setFormItemAttributes(item FormItem, attrs *FormItemAttributes) {
item.SetLabelWidth(attrs.LabelWidth)
item.SetBackgroundColor(attrs.BackgroundColor)
item.SetLabelColor(attrs.LabelColor)
item.SetLabelColorFocused(attrs.LabelColorFocused)
item.SetFieldTextColor(attrs.FieldTextColor)
item.SetFieldTextColorFocused(attrs.FieldTextColorFocused)
item.SetFieldBackgroundColor(attrs.FieldBackgroundColor)
item.SetFieldBackgroundColorFocused(attrs.FieldBackgroundColorFocused)
if attrs.FinishedFunc != nil {
item.SetFinishedFunc(attrs.FinishedFunc)
}
}
package cview
import (
"sync"
"github.com/gdamore/tcell/v2"
)
// frameText holds information about a line of text shown in the frame.
type frameText struct {
Text string // The text to be displayed.
Header bool // true = place in header, false = place in footer.
Align int // One of the Align constants.
Color tcell.Color // The text color.
}
// Frame is a wrapper which adds space around another primitive. In addition,
// the top area (header) and the bottom area (footer) may also contain text.
type Frame struct {
*Box
// The contained primitive.
primitive Primitive
// The lines of text to be displayed.
text []*frameText
// Border spacing.
top, bottom, header, footer, left, right int
sync.RWMutex
}
// NewFrame returns a new frame around the given primitive. The primitive's
// size will be changed to fit within this frame.
func NewFrame(primitive Primitive) *Frame {
box := NewBox()
f := &Frame{
Box: box,
primitive: primitive,
top: 1,
bottom: 1,
header: 1,
footer: 1,
left: 1,
right: 1,
}
f.focus = f
return f
}
// AddText adds text to the frame. Set "header" to true if the text is to appear
// in the header, above the contained primitive. Set it to false for it to
// appear in the footer, below the contained primitive. "align" must be one of
// the Align constants. Rows in the header are printed top to bottom, rows in
// the footer are printed bottom to top. Note that long text can overlap as
// different alignments will be placed on the same row.
func (f *Frame) AddText(text string, header bool, align int, color tcell.Color) {
f.Lock()
defer f.Unlock()
f.text = append(f.text, &frameText{
Text: text,
Header: header,
Align: align,
Color: color,
})
}
// Clear removes all text from the frame.
func (f *Frame) Clear() {
f.Lock()
defer f.Unlock()
f.text = nil
}
// SetBorders sets the width of the frame borders as well as "header" and
// "footer", the vertical space between the header and footer text and the
// contained primitive (does not apply if there is no text).
func (f *Frame) SetBorders(top, bottom, header, footer, left, right int) {
f.Lock()
defer f.Unlock()
f.top, f.bottom, f.header, f.footer, f.left, f.right = top, bottom, header, footer, left, right
}
// Draw draws this primitive onto the screen.
func (f *Frame) Draw(screen tcell.Screen) {
if !f.GetVisible() {
return
}
f.Box.Draw(screen)
f.Lock()
defer f.Unlock()
// Calculate start positions.
x, top, width, height := f.GetInnerRect()
bottom := top + height - 1
x += f.left
top += f.top
bottom -= f.bottom
width -= f.left + f.right
if width <= 0 || top >= bottom {
return // No space left.
}
// Draw text.
var rows [6]int // top-left, top-center, top-right, bottom-left, bottom-center, bottom-right.
topMax := top
bottomMin := bottom
for _, text := range f.text {
// Where do we place this text?
var y int
if text.Header {
y = top + rows[text.Align]
rows[text.Align]++
if y >= bottomMin {
continue
}
if y+1 > topMax {
topMax = y + 1
}
} else {
y = bottom - rows[3+text.Align]
rows[3+text.Align]++
if y <= topMax {
continue
}
if y-1 < bottomMin {
bottomMin = y - 1
}
}
// Draw text.
Print(screen, []byte(text.Text), x, y, width, text.Align, text.Color)
}
// Set the size of the contained primitive.
if topMax > top {
top = topMax + f.header
}
if bottomMin < bottom {
bottom = bottomMin - f.footer
}
if top > bottom {
return // No space for the primitive.
}
f.primitive.SetRect(x, top, width, bottom+1-top)
// Finally, draw the contained primitive.
f.primitive.Draw(screen)
}
// Focus is called when this primitive receives focus.
func (f *Frame) Focus(delegate func(p Primitive)) {
f.Lock()
primitive := f.primitive
defer f.Unlock()
delegate(primitive)
}
// HasFocus returns whether or not this primitive has focus.
func (f *Frame) HasFocus() bool {
f.RLock()
defer f.RUnlock()
focusable, ok := f.primitive.(Focusable)
if ok {
return focusable.HasFocus()
}
return false
}
// MouseHandler returns the mouse handler for this primitive.
func (f *Frame) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
return f.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
if !f.InRect(event.Position()) {
return false, nil
}
// Pass mouse events on to contained primitive.
return f.primitive.MouseHandler()(action, event, setFocus)
})
}
package cview
import (
"math"
"sync"
"github.com/gdamore/tcell/v2"
)
// gridItem represents one primitive and its possible position on a grid.
type gridItem struct {
Item Primitive // The item to be positioned. May be nil for an empty item.
Row, Column int // The top-left grid cell where the item is placed.
Width, Height int // The number of rows and columns the item occupies.
MinGridWidth, MinGridHeight int // The minimum grid width/height for which this item is visible.
Focus bool // Whether or not this item attracts the layout's focus.
visible bool // Whether or not this item was visible the last time the grid was drawn.
x, y, w, h int // The last position of the item relative to the top-left corner of the grid. Undefined if visible is false.
}
// Grid is an implementation of a grid-based layout. It works by defining the
// size of the rows and columns, then placing primitives into the grid.
//
// Some settings can lead to the grid exceeding its available space. SetOffset()
// can then be used to scroll in steps of rows and columns. These offset values
// can also be controlled with the arrow keys (or the "g","G", "j", "k", "h",
// and "l" keys) while the grid has focus and none of its contained primitives
// do.
type Grid struct {
*Box
// The items to be positioned.
items []*gridItem
// The definition of the rows and columns of the grid. See
// SetRows()/SetColumns() for details.
rows, columns []int
// The minimum sizes for rows and columns.
minWidth, minHeight int
// The size of the gaps between neighboring primitives. This is automatically
// set to 1 if borders is true.
gapRows, gapColumns int
// The number of rows and columns skipped before drawing the top-left corner
// of the grid.
rowOffset, columnOffset int
// Whether or not borders are drawn around grid items. If this is set to true,
// a gap size of 1 is automatically assumed (which is filled with the border
// graphics).
borders bool
// The color of the borders around grid items.
bordersColor tcell.Color
sync.RWMutex
}
// NewGrid returns a new grid-based layout container with no initial primitives.
//
// Note that Grid will have a transparent background by default so that any
// areas not covered by any primitives will show primitives behind the Grid.
// To disable this transparency:
//
// grid.SetBackgroundTransparent(false)
func NewGrid() *Grid {
g := &Grid{
Box: NewBox(),
bordersColor: Styles.GraphicsColor,
}
g.SetBackgroundTransparent(true)
g.focus = g
return g
}
// SetColumns defines how the columns of the grid are distributed. Each value
// defines the size of one column, starting with the leftmost column. Values
// greater 0 represent absolute column widths (gaps not included). Values less
// or equal 0 represent proportional column widths or fractions of the remaining
// free space, where 0 is treated the same as -1. That is, a column with a value
// of -3 will have three times the width of a column with a value of -1 (or 0).
// The minimum width set with SetMinSize() is always observed.
//
// Primitives may extend beyond the columns defined explicitly with this
// function. A value of 0 is assumed for any undefined column. In fact, if you
// never call this function, all columns occupied by primitives will have the
// same width. On the other hand, unoccupied columns defined with this function
// will always take their place.
//
// Assuming a total width of the grid of 100 cells and a minimum width of 0, the
// following call will result in columns with widths of 30, 10, 15, 15, and 30
// cells:
//
// grid.SetColumns(30, 10, -1, -1, -2)
//
// If a primitive were then placed in the 6th and 7th column, the resulting
// widths would be: 30, 10, 10, 10, 20, 10, and 10 cells.
//
// If you then called SetMinSize() as follows:
//
// grid.SetMinSize(15, 20)
//
// The resulting widths would be: 30, 15, 15, 15, 20, 15, and 15 cells, a total
// of 125 cells, 25 cells wider than the available grid width.
func (g *Grid) SetColumns(columns ...int) {
g.Lock()
defer g.Unlock()
g.columns = columns
}
// SetRows defines how the rows of the grid are distributed. These values behave
// the same as the column values provided with SetColumns(), see there for a
// definition and examples.
//
// The provided values correspond to row heights, the first value defining
// the height of the topmost row.
func (g *Grid) SetRows(rows ...int) {
g.Lock()
defer g.Unlock()
g.rows = rows
}
// SetSize is a shortcut for SetRows() and SetColumns() where all row and column
// values are set to the given size values. See SetColumns() for details on sizes.
func (g *Grid) SetSize(numRows, numColumns, rowSize, columnSize int) {
g.Lock()
defer g.Unlock()
g.rows = make([]int, numRows)
for index := range g.rows {
g.rows[index] = rowSize
}
g.columns = make([]int, numColumns)
for index := range g.columns {
g.columns[index] = columnSize
}
}
// SetMinSize sets an absolute minimum width for rows and an absolute minimum
// height for columns. Panics if negative values are provided.
func (g *Grid) SetMinSize(row, column int) {
g.Lock()
defer g.Unlock()
if row < 0 || column < 0 {
panic("Invalid minimum row/column size")
}
g.minHeight, g.minWidth = row, column
}
// SetGap sets the size of the gaps between neighboring primitives on the grid.
// If borders are drawn (see SetBorders()), these values are ignored and a gap
// of 1 is assumed. Panics if negative values are provided.
func (g *Grid) SetGap(row, column int) {
g.Lock()
defer g.Unlock()
if row < 0 || column < 0 {
panic("Invalid gap size")
}
g.gapRows, g.gapColumns = row, column
}
// SetBorders sets whether or not borders are drawn around grid items. Setting
// this value to true will cause the gap values (see SetGap()) to be ignored and
// automatically assumed to be 1 where the border graphics are drawn.
func (g *Grid) SetBorders(borders bool) {
g.Lock()
defer g.Unlock()
g.borders = borders
}
// SetBordersColor sets the color of the item borders.
func (g *Grid) SetBordersColor(color tcell.Color) {
g.Lock()
defer g.Unlock()
g.bordersColor = color
}
// AddItem adds a primitive and its position to the grid. The top-left corner
// of the primitive will be located in the top-left corner of the grid cell at
// the given row and column and will span "rowSpan" rows and "colSpan" columns.
// For example, for a primitive to occupy rows 2, 3, and 4 and columns 5 and 6:
//
// grid.AddItem(p, 2, 5, 3, 2, 0, 0, true)
//
// If rowSpan or colSpan is 0, the primitive will not be drawn.
//
// You can add the same primitive multiple times with different grid positions.
// The minGridWidth and minGridHeight values will then determine which of those
// positions will be used. This is similar to CSS media queries. These minimum
// values refer to the overall size of the grid. If multiple items for the same
// primitive apply, the one that has at least one highest minimum value will be
// used, or the primitive added last if those values are the same. Example:
//
// grid.AddItem(p, 0, 0, 0, 0, 0, 0, true). // Hide in small grids.
// AddItem(p, 0, 0, 1, 2, 100, 0, true). // One-column layout for medium grids.
// AddItem(p, 1, 1, 3, 2, 300, 0, true) // Multi-column layout for large grids.
//
// To use the same grid layout for all sizes, simply set minGridWidth and
// minGridHeight to 0.
//
// If the item's focus is set to true, it will receive focus when the grid
// receives focus. If there are multiple items with a true focus flag, the last
// visible one that was added will receive focus.
func (g *Grid) AddItem(p Primitive, row, column, rowSpan, colSpan, minGridHeight, minGridWidth int, focus bool) {
g.Lock()
defer g.Unlock()
g.items = append(g.items, &gridItem{
Item: p,
Row: row,
Column: column,
Height: rowSpan,
Width: colSpan,
MinGridHeight: minGridHeight,
MinGridWidth: minGridWidth,
Focus: focus,
})
}
// RemoveItem removes all items for the given primitive from the grid, keeping
// the order of the remaining items intact.
func (g *Grid) RemoveItem(p Primitive) {
g.Lock()
defer g.Unlock()
for index := len(g.items) - 1; index >= 0; index-- {
if g.items[index].Item == p {
g.items = append(g.items[:index], g.items[index+1:]...)
}
}
}
// Clear removes all items from the grid.
func (g *Grid) Clear() {
g.Lock()
defer g.Unlock()
g.items = nil
}
// SetOffset sets the number of rows and columns which are skipped before
// drawing the first grid cell in the top-left corner. As the grid will never
// completely move off the screen, these values may be adjusted the next time
// the grid is drawn. The actual position of the grid may also be adjusted such
// that contained primitives that have focus remain visible.
func (g *Grid) SetOffset(rows, columns int) {
g.Lock()
defer g.Unlock()
g.rowOffset, g.columnOffset = rows, columns
}
// GetOffset returns the current row and column offset (see SetOffset() for
// details).
func (g *Grid) GetOffset() (rows, columns int) {
g.RLock()
defer g.RUnlock()
return g.rowOffset, g.columnOffset
}
// Focus is called when this primitive receives focus.
func (g *Grid) Focus(delegate func(p Primitive)) {
g.Lock()
items := g.items
g.Unlock()
for _, item := range items {
if item.Focus {
delegate(item.Item)
return
}
}
g.Lock()
g.hasFocus = true
g.Unlock()
}
// Blur is called when this primitive loses focus.
func (g *Grid) Blur() {
g.Lock()
defer g.Unlock()
g.hasFocus = false
}
// HasFocus returns whether or not this primitive has focus.
func (g *Grid) HasFocus() bool {
g.RLock()
defer g.RUnlock()
for _, item := range g.items {
if item.visible && item.Item.GetFocusable().HasFocus() {
return true
}
}
return g.hasFocus
}
// InputHandler returns the handler for this primitive.
func (g *Grid) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) {
return g.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p Primitive)) {
g.Lock()
defer g.Unlock()
if HitShortcut(event, Keys.MoveFirst, Keys.MoveFirst2) {
g.rowOffset, g.columnOffset = 0, 0
} else if HitShortcut(event, Keys.MoveLast, Keys.MoveLast2) {
g.rowOffset = math.MaxInt32
} else if HitShortcut(event, Keys.MoveUp, Keys.MoveUp2, Keys.MovePreviousField) {
g.rowOffset--
} else if HitShortcut(event, Keys.MoveDown, Keys.MoveDown2, Keys.MoveNextField) {
g.rowOffset++
} else if HitShortcut(event, Keys.MoveLeft, Keys.MoveLeft2) {
g.columnOffset--
} else if HitShortcut(event, Keys.MoveRight, Keys.MoveRight2) {
g.columnOffset++
}
})
}
// Draw draws this primitive onto the screen.
func (g *Grid) Draw(screen tcell.Screen) {
if !g.GetVisible() {
return
}
g.Box.Draw(screen)
g.Lock()
defer g.Unlock()
x, y, width, height := g.GetInnerRect()
screenWidth, screenHeight := screen.Size()
// Make a list of items which apply.
items := make(map[Primitive]*gridItem)
for _, item := range g.items {
item.visible = false
if item.Width <= 0 || item.Height <= 0 || width < item.MinGridWidth || height < item.MinGridHeight {
continue
}
previousItem, ok := items[item.Item]
if ok && item.MinGridWidth < previousItem.MinGridWidth && item.MinGridHeight < previousItem.MinGridHeight {
continue
}
items[item.Item] = item
}
// How many rows and columns do we have?
rows := len(g.rows)
columns := len(g.columns)
for _, item := range items {
rowEnd := item.Row + item.Height
if rowEnd > rows {
rows = rowEnd
}
columnEnd := item.Column + item.Width
if columnEnd > columns {
columns = columnEnd
}
}
if rows == 0 || columns == 0 {
return // No content.
}
// Where are they located?
rowPos := make([]int, rows)
rowHeight := make([]int, rows)
columnPos := make([]int, columns)
columnWidth := make([]int, columns)
// How much space do we distribute?
remainingWidth := width
remainingHeight := height
proportionalWidth := 0
proportionalHeight := 0
for index, row := range g.rows {
if row > 0 {
if row < g.minHeight {
row = g.minHeight
}
remainingHeight -= row
rowHeight[index] = row
} else if row == 0 {
proportionalHeight++
} else {
proportionalHeight += -row
}
}
for index, column := range g.columns {
if column > 0 {
if column < g.minWidth {
column = g.minWidth
}
remainingWidth -= column
columnWidth[index] = column
} else if column == 0 {
proportionalWidth++
} else {
proportionalWidth += -column
}
}
if g.borders {
remainingHeight -= rows + 1
remainingWidth -= columns + 1
} else {
remainingHeight -= (rows - 1) * g.gapRows
remainingWidth -= (columns - 1) * g.gapColumns
}
if rows > len(g.rows) {
proportionalHeight += rows - len(g.rows)
}
if columns > len(g.columns) {
proportionalWidth += columns - len(g.columns)
}
// Distribute proportional rows/columns.
for index := 0; index < rows; index++ {
row := 0
if index < len(g.rows) {
row = g.rows[index]
}
if row > 0 {
if row < g.minHeight {
row = g.minHeight
}
continue // Not proportional. We already know the width.
} else if row == 0 {
row = 1
} else {
row = -row
}
rowAbs := row * remainingHeight / proportionalHeight
remainingHeight -= rowAbs
proportionalHeight -= row
if rowAbs < g.minHeight {
rowAbs = g.minHeight
}
rowHeight[index] = rowAbs
}
for index := 0; index < columns; index++ {
column := 0
if index < len(g.columns) {
column = g.columns[index]
}
if column > 0 {
if column < g.minWidth {
column = g.minWidth
}
continue // Not proportional. We already know the height.
} else if column == 0 {
column = 1
} else {
column = -column
}
columnAbs := column * remainingWidth / proportionalWidth
remainingWidth -= columnAbs
proportionalWidth -= column
if columnAbs < g.minWidth {
columnAbs = g.minWidth
}
columnWidth[index] = columnAbs
}
// Calculate row/column positions.
var columnX, rowY int
if g.borders {
columnX++
rowY++
}
for index, row := range rowHeight {
rowPos[index] = rowY
gap := g.gapRows
if g.borders {
gap = 1
}
rowY += row + gap
}
for index, column := range columnWidth {
columnPos[index] = columnX
gap := g.gapColumns
if g.borders {
gap = 1
}
columnX += column + gap
}
// Calculate primitive positions.
var focus *gridItem // The item which has focus.
for primitive, item := range items {
px := columnPos[item.Column]
py := rowPos[item.Row]
var pw, ph int
for index := 0; index < item.Height; index++ {
ph += rowHeight[item.Row+index]
}
for index := 0; index < item.Width; index++ {
pw += columnWidth[item.Column+index]
}
if g.borders {
pw += item.Width - 1
ph += item.Height - 1
} else {
pw += (item.Width - 1) * g.gapColumns
ph += (item.Height - 1) * g.gapRows
}
item.x, item.y, item.w, item.h = px, py, pw, ph
item.visible = true
if primitive.GetFocusable().HasFocus() {
focus = item
}
}
// Calculate screen offsets.
var offsetX, offsetY int
add := 1
if !g.borders {
add = g.gapRows
}
for index, height := range rowHeight {
if index >= g.rowOffset {
break
}
offsetY += height + add
}
if !g.borders {
add = g.gapColumns
}
for index, width := range columnWidth {
if index >= g.columnOffset {
break
}
offsetX += width + add
}
// Line up the last row/column with the end of the available area.
var border int
if g.borders {
border = 1
}
last := len(rowPos) - 1
if rowPos[last]+rowHeight[last]+border-offsetY < height {
offsetY = rowPos[last] - height + rowHeight[last] + border
}
last = len(columnPos) - 1
if columnPos[last]+columnWidth[last]+border-offsetX < width {
offsetX = columnPos[last] - width + columnWidth[last] + border
}
// The focused item must be within the visible area.
if focus != nil {
if focus.y+focus.h-offsetY >= height {
offsetY = focus.y - height + focus.h
}
if focus.y-offsetY < 0 {
offsetY = focus.y
}
if focus.x+focus.w-offsetX >= width {
offsetX = focus.x - width + focus.w
}
if focus.x-offsetX < 0 {
offsetX = focus.x
}
}
// Adjust row/column offsets based on this value.
var from, to int
for index, pos := range rowPos {
if pos-offsetY < 0 {
from = index + 1
}
if pos-offsetY < height {
to = index
}
}
if g.rowOffset < from {
g.rowOffset = from
}
if g.rowOffset > to {
g.rowOffset = to
}
from, to = 0, 0
for index, pos := range columnPos {
if pos-offsetX < 0 {
from = index + 1
}
if pos-offsetX < width {
to = index
}
}
if g.columnOffset < from {
g.columnOffset = from
}
if g.columnOffset > to {
g.columnOffset = to
}
// Draw primitives and borders.
for primitive, item := range items {
// Final primitive position.
if !item.visible {
continue
}
item.x -= offsetX
item.y -= offsetY
if item.x >= width || item.x+item.w <= 0 || item.y >= height || item.y+item.h <= 0 {
item.visible = false
continue
}
if item.x+item.w > width {
item.w = width - item.x
}
if item.y+item.h > height {
item.h = height - item.y
}
if item.x < 0 {
item.w += item.x
item.x = 0
}
if item.y < 0 {
item.h += item.y
item.y = 0
}
if item.w <= 0 || item.h <= 0 {
item.visible = false
continue
}
item.x += x
item.y += y
primitive.SetRect(item.x, item.y, item.w, item.h)
// Draw primitive.
if item == focus {
defer primitive.Draw(screen)
} else {
primitive.Draw(screen)
}
// Draw border around primitive.
if g.borders {
for bx := item.x; bx < item.x+item.w; bx++ { // Top/bottom lines.
if bx < 0 || bx >= screenWidth {
continue
}
by := item.y - 1
if by >= 0 && by < screenHeight {
PrintJoinedSemigraphics(screen, bx, by, Borders.Horizontal, g.bordersColor)
}
by = item.y + item.h
if by >= 0 && by < screenHeight {
PrintJoinedSemigraphics(screen, bx, by, Borders.Horizontal, g.bordersColor)
}
}
for by := item.y; by < item.y+item.h; by++ { // Left/right lines.
if by < 0 || by >= screenHeight {
continue
}
bx := item.x - 1
if bx >= 0 && bx < screenWidth {
PrintJoinedSemigraphics(screen, bx, by, Borders.Vertical, g.bordersColor)
}
bx = item.x + item.w
if bx >= 0 && bx < screenWidth {
PrintJoinedSemigraphics(screen, bx, by, Borders.Vertical, g.bordersColor)
}
}
bx, by := item.x-1, item.y-1 // Top-left corner.
if bx >= 0 && bx < screenWidth && by >= 0 && by < screenHeight {
PrintJoinedSemigraphics(screen, bx, by, Borders.TopLeft, g.bordersColor)
}
bx, by = item.x+item.w, item.y-1 // Top-right corner.
if bx >= 0 && bx < screenWidth && by >= 0 && by < screenHeight {
PrintJoinedSemigraphics(screen, bx, by, Borders.TopRight, g.bordersColor)
}
bx, by = item.x-1, item.y+item.h // Bottom-left corner.
if bx >= 0 && bx < screenWidth && by >= 0 && by < screenHeight {
PrintJoinedSemigraphics(screen, bx, by, Borders.BottomLeft, g.bordersColor)
}
bx, by = item.x+item.w, item.y+item.h // Bottom-right corner.
if bx >= 0 && bx < screenWidth && by >= 0 && by < screenHeight {
PrintJoinedSemigraphics(screen, bx, by, Borders.BottomRight, g.bordersColor)
}
}
}
}
// MouseHandler returns the mouse handler for this primitive.
func (g *Grid) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
return g.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
if !g.InRect(event.Position()) {
return false, nil
}
// Pass mouse events along to the first child item that takes it.
for _, item := range g.items {
consumed, capture = item.Item.MouseHandler()(action, event, setFocus)
if consumed {
return
}
}
return
})
}
package cview
import (
"bytes"
"math"
"regexp"
"sync"
"unicode/utf8"
"github.com/gdamore/tcell/v2"
"github.com/mattn/go-runewidth"
)
// InputField is a one-line box (three lines if there is a title) where the
// 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).
//
// 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.
type InputField struct {
*Box
// The text that was entered.
text []byte
// The text to be displayed before the input area.
label []byte
// The text to be displayed in the input area when "text" is empty.
placeholder []byte
// The label color.
labelColor tcell.Color
// The label color when focused.
labelColorFocused tcell.Color
// The background color of the input area.
fieldBackgroundColor tcell.Color
// The background color of the input area when focused.
fieldBackgroundColorFocused tcell.Color
// The text color of the input area.
fieldTextColor tcell.Color
// The text color of the input area when focused.
fieldTextColorFocused tcell.Color
// The text color of the placeholder.
placeholderTextColor tcell.Color
// The text color of the placeholder when focused.
placeholderTextColorFocused tcell.Color
// The text color of the list items.
autocompleteListTextColor tcell.Color
// The background color of the autocomplete list.
autocompleteListBackgroundColor tcell.Color
// The text color of the selected ListItem.
autocompleteListSelectedTextColor tcell.Color
// The background color of the selected ListItem.
autocompleteListSelectedBackgroundColor tcell.Color
// The text color of the suggestion.
autocompleteSuggestionTextColor tcell.Color
// The text color of the note below the input field.
fieldNoteTextColor tcell.Color
// The note to show below the input field.
fieldNote []byte
// The screen width of the label area. A value of 0 means use the width of
// the label text.
labelWidth int
// The screen width of the input area. A value of 0 means extend as much as
// possible.
fieldWidth int
// A character to mask entered text (useful for password fields). A value of 0
// disables masking.
maskCharacter rune
// The cursor position as a byte index into the text string.
cursorPos int
// An optional autocomplete function which receives the current text of the
// input field and returns a slice of ListItems to be displayed in a drop-down
// selection. Items' main text is displayed in the autocomplete list. When
// set, items' secondary text is used as the selection value. Otherwise,
// the main text is used.
autocomplete func(text string) []*ListItem
// The List object which shows the selectable autocomplete entries. If not
// nil, the list's main texts represent the current autocomplete entries.
autocompleteList *List
// The suggested completion of the current autocomplete ListItem.
autocompleteListSuggestion []byte
// An optional function which may reject the last character that was entered.
accept func(text string, ch rune) bool
// An optional function which is called when the input has changed.
changed func(text string)
// An optional function which is called when the user indicated that they
// are done entering text. The key which was pressed is provided (tab,
// shift-tab, enter, or escape).
done func(tcell.Key)
// A callback function set by the Form class and called when the user leaves
// this form item.
finished func(tcell.Key)
// The x-coordinate of the input field as determined during the last call to Draw().
fieldX int
// The number of bytes of the text string skipped ahead while drawing.
offset int
sync.RWMutex
}
// NewInputField returns a new input field.
func NewInputField() *InputField {
return &InputField{
Box: NewBox(),
labelColor: Styles.SecondaryTextColor,
fieldBackgroundColor: Styles.ContrastBackgroundColor,
fieldTextColor: Styles.PrimaryTextColor,
placeholderTextColor: Styles.ContrastSecondaryTextColor,
autocompleteListTextColor: Styles.PrimitiveBackgroundColor,
autocompleteListBackgroundColor: Styles.MoreContrastBackgroundColor,
autocompleteListSelectedTextColor: Styles.PrimitiveBackgroundColor,
autocompleteListSelectedBackgroundColor: Styles.PrimaryTextColor,
autocompleteSuggestionTextColor: Styles.ContrastPrimaryTextColor,
fieldNoteTextColor: Styles.SecondaryTextColor,
labelColorFocused: ColorUnset,
fieldBackgroundColorFocused: ColorUnset,
fieldTextColorFocused: ColorUnset,
placeholderTextColorFocused: ColorUnset,
}
}
// SetText sets the current text of the input field.
func (i *InputField) SetText(text string) {
i.Lock()
i.text = []byte(text)
i.cursorPos = len(text)
if i.changed != nil {
i.Unlock()
i.changed(text)
} else {
i.Unlock()
}
}
// GetText returns the current text of the input field.
func (i *InputField) GetText() string {
i.RLock()
defer i.RUnlock()
return string(i.text)
}
// SetLabel sets the text to be displayed before the input area.
func (i *InputField) SetLabel(label string) {
i.Lock()
defer i.Unlock()
i.label = []byte(label)
}
// GetLabel returns the text to be displayed before the input area.
func (i *InputField) GetLabel() string {
i.RLock()
defer i.RUnlock()
return string(i.label)
}
// SetLabelWidth sets the screen width of the label. A value of 0 will cause the
// primitive to use the width of the label string.
func (i *InputField) SetLabelWidth(width int) {
i.Lock()
defer i.Unlock()
i.labelWidth = width
}
// SetPlaceholder sets the text to be displayed when the input text is empty.
func (i *InputField) SetPlaceholder(text string) {
i.Lock()
defer i.Unlock()
i.placeholder = []byte(text)
}
// SetLabelColor sets the color of the label.
func (i *InputField) SetLabelColor(color tcell.Color) {
i.Lock()
defer i.Unlock()
i.labelColor = color
}
// SetLabelColorFocused sets the color of the label when focused.
func (i *InputField) SetLabelColorFocused(color tcell.Color) {
i.Lock()
defer i.Unlock()
i.labelColorFocused = color
}
// SetFieldBackgroundColor sets the background color of the input area.
func (i *InputField) SetFieldBackgroundColor(color tcell.Color) {
i.Lock()
defer i.Unlock()
i.fieldBackgroundColor = color
}
// SetFieldBackgroundColorFocused sets the background color of the input area
// when focused.
func (i *InputField) SetFieldBackgroundColorFocused(color tcell.Color) {
i.Lock()
defer i.Unlock()
i.fieldBackgroundColorFocused = color
}
// SetFieldTextColor sets the text color of the input area.
func (i *InputField) SetFieldTextColor(color tcell.Color) {
i.Lock()
defer i.Unlock()
i.fieldTextColor = color
}
// SetFieldTextColorFocused sets the text color of the input area when focused.
func (i *InputField) SetFieldTextColorFocused(color tcell.Color) {
i.Lock()
defer i.Unlock()
i.fieldTextColorFocused = color
}
// SetPlaceholderTextColor sets the text color of placeholder text.
func (i *InputField) SetPlaceholderTextColor(color tcell.Color) {
i.Lock()
defer i.Unlock()
i.placeholderTextColor = color
}
// SetPlaceholderTextColorFocused sets the text color of placeholder text when
// focused.
func (i *InputField) SetPlaceholderTextColorFocused(color tcell.Color) {
i.Lock()
defer i.Unlock()
i.placeholderTextColorFocused = color
}
// SetAutocompleteListTextColor sets the text color of the ListItems.
func (i *InputField) SetAutocompleteListTextColor(color tcell.Color) {
i.Lock()
defer i.Unlock()
i.autocompleteListTextColor = color
}
// SetAutocompleteListBackgroundColor sets the background color of the
// autocomplete list.
func (i *InputField) SetAutocompleteListBackgroundColor(color tcell.Color) {
i.Lock()
defer i.Unlock()
i.autocompleteListBackgroundColor = color
}
// SetAutocompleteListSelectedTextColor sets the text color of the selected
// ListItem.
func (i *InputField) SetAutocompleteListSelectedTextColor(color tcell.Color) {
i.Lock()
defer i.Unlock()
i.autocompleteListSelectedTextColor = color
}
// SetAutocompleteListSelectedBackgroundColor sets the background of the
// selected ListItem.
func (i *InputField) SetAutocompleteListSelectedBackgroundColor(color tcell.Color) {
i.Lock()
defer i.Unlock()
i.autocompleteListSelectedBackgroundColor = color
}
// SetAutocompleteSuggestionTextColor sets the text color of the autocomplete
// suggestion in the input field.
func (i *InputField) SetAutocompleteSuggestionTextColor(color tcell.Color) {
i.Lock()
defer i.Unlock()
i.autocompleteSuggestionTextColor = color
}
// SetFieldNoteTextColor sets the text color of the note.
func (i *InputField) SetFieldNoteTextColor(color tcell.Color) {
i.Lock()
defer i.Unlock()
i.fieldNoteTextColor = color
}
// SetFieldNote sets the text to show below the input field, e.g. when the
// input is invalid.
func (i *InputField) SetFieldNote(note string) {
i.Lock()
defer i.Unlock()
i.fieldNote = []byte(note)
}
// ResetFieldNote sets the note to an empty string.
func (i *InputField) ResetFieldNote() {
i.Lock()
defer i.Unlock()
i.fieldNote = nil
}
// SetFieldWidth sets the screen width of the input area. A value of 0 means
// extend as much as possible.
func (i *InputField) SetFieldWidth(width int) {
i.Lock()
defer i.Unlock()
i.fieldWidth = width
}
// GetFieldWidth returns this primitive's field width.
func (i *InputField) GetFieldWidth() int {
i.RLock()
defer i.RUnlock()
return i.fieldWidth
}
// GetFieldHeight returns the height of the field.
func (i *InputField) GetFieldHeight() int {
i.RLock()
defer i.RUnlock()
if len(i.fieldNote) == 0 {
return 1
}
return 2
}
// GetCursorPosition returns the cursor position.
func (i *InputField) GetCursorPosition() int {
i.RLock()
defer i.RUnlock()
return i.cursorPos
}
// SetCursorPosition sets the cursor position.
func (i *InputField) SetCursorPosition(cursorPos int) {
i.Lock()
defer i.Unlock()
i.cursorPos = cursorPos
}
// SetMaskCharacter sets a character that masks user input on a screen. A value
// of 0 disables masking.
func (i *InputField) SetMaskCharacter(mask rune) {
i.Lock()
defer i.Unlock()
i.maskCharacter = mask
}
// SetAutocompleteFunc sets an autocomplete callback function which may return
// ListItems to be selected from a drop-down based on the current text of the
// input field. The drop-down appears only if len(entries) > 0. The callback is
// invoked in this function and whenever the current text changes or when
// Autocomplete() is called. Entries are cleared when the user selects an entry
// or presses Escape.
func (i *InputField) SetAutocompleteFunc(callback func(currentText string) (entries []*ListItem)) {
i.Lock()
i.autocomplete = callback
i.Unlock()
i.Autocomplete()
}
// Autocomplete invokes the autocomplete callback (if there is one). If the
// length of the returned autocomplete entries slice is greater than 0, the
// input field will present the user with a corresponding drop-down list the
// next time the input field is drawn.
//
// It is safe to call this function from any goroutine. Note that the input
// field is not redrawn automatically unless called from the main goroutine
// (e.g. in response to events).
func (i *InputField) Autocomplete() {
i.Lock()
if i.autocomplete == nil {
i.Unlock()
return
}
i.Unlock()
// Do we have any autocomplete entries?
entries := i.autocomplete(string(i.text))
if len(entries) == 0 {
// No entries, no list.
i.Lock()
i.autocompleteList = nil
i.autocompleteListSuggestion = nil
i.Unlock()
return
}
i.Lock()
// Make a list if we have none.
if i.autocompleteList == nil {
l := NewList()
l.SetChangedFunc(i.autocompleteChanged)
l.ShowSecondaryText(false)
l.SetMainTextColor(i.autocompleteListTextColor)
l.SetSelectedTextColor(i.autocompleteListSelectedTextColor)
l.SetSelectedBackgroundColor(i.autocompleteListSelectedBackgroundColor)
l.SetHighlightFullLine(true)
l.SetBackgroundColor(i.autocompleteListBackgroundColor)
i.autocompleteList = l
}
// Fill it with the entries.
currentEntry := -1
i.autocompleteList.Clear()
for index, entry := range entries {
i.autocompleteList.AddItem(entry)
if currentEntry < 0 && entry.GetMainText() == string(i.text) {
currentEntry = index
}
}
// Set the selection if we have one.
if currentEntry >= 0 {
i.autocompleteList.SetCurrentItem(currentEntry)
}
i.Unlock()
}
// autocompleteChanged gets called when another item in the
// autocomplete list has been selected.
func (i *InputField) autocompleteChanged(_ int, item *ListItem) {
mainText := item.GetMainBytes()
secondaryText := item.GetSecondaryBytes()
if len(i.text) < len(secondaryText) {
i.autocompleteListSuggestion = secondaryText[len(i.text):]
} else if len(i.text) < len(mainText) {
i.autocompleteListSuggestion = mainText[len(i.text):]
} else {
i.autocompleteListSuggestion = nil
}
}
// 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
// be used for common input (e.g. numbers, maximum text length).
func (i *InputField) SetAcceptanceFunc(handler func(textToCheck string, lastChar rune) bool) {
i.Lock()
defer i.Unlock()
i.accept = handler
}
// SetChangedFunc sets a handler which is called whenever the text of the input
// field has changed. It receives the current text (after the change).
func (i *InputField) SetChangedFunc(handler func(text string)) {
i.Lock()
defer i.Unlock()
i.changed = handler
}
// SetDoneFunc sets a handler which is called when the user is done entering
// text. The callback function is provided with the key that was pressed, which
// is one of the following:
//
// - KeyEnter: Done entering text.
// - KeyEscape: Abort text input.
// - KeyTab: Move to the next field.
// - KeyBacktab: Move to the previous field.
func (i *InputField) SetDoneFunc(handler func(key tcell.Key)) {
i.Lock()
defer i.Unlock()
i.done = handler
}
// SetFinishedFunc sets a callback invoked when the user leaves this form item.
func (i *InputField) SetFinishedFunc(handler func(key tcell.Key)) {
i.Lock()
defer i.Unlock()
i.finished = handler
}
// Draw draws this primitive onto the screen.
func (i *InputField) Draw(screen tcell.Screen) {
if !i.GetVisible() {
return
}
i.Box.Draw(screen)
i.Lock()
defer i.Unlock()
// Select colors
labelColor := i.labelColor
fieldBackgroundColor := i.fieldBackgroundColor
fieldTextColor := i.fieldTextColor
if i.GetFocusable().HasFocus() {
if i.labelColorFocused != ColorUnset {
labelColor = i.labelColorFocused
}
if i.fieldBackgroundColorFocused != ColorUnset {
fieldBackgroundColor = i.fieldBackgroundColorFocused
}
if i.fieldTextColorFocused != ColorUnset {
fieldTextColor = i.fieldTextColorFocused
}
}
// Prepare
x, y, width, height := i.GetInnerRect()
rightLimit := x + width
if height < 1 || rightLimit <= x {
return
}
// Draw label.
if i.labelWidth > 0 {
labelWidth := i.labelWidth
if labelWidth > rightLimit-x {
labelWidth = rightLimit - x
}
Print(screen, i.label, x, y, labelWidth, AlignLeft, labelColor)
x += labelWidth
} else {
_, drawnWidth := Print(screen, i.label, x, y, rightLimit-x, AlignLeft, labelColor)
x += drawnWidth
}
// Draw input area.
i.fieldX = x
fieldWidth := i.fieldWidth
if fieldWidth == 0 {
fieldWidth = math.MaxInt32
}
if rightLimit-x < fieldWidth {
fieldWidth = rightLimit - x
}
fieldStyle := tcell.StyleDefault.Background(fieldBackgroundColor)
for index := 0; index < fieldWidth; index++ {
screen.SetContent(x+index, y, ' ', nil, fieldStyle)
}
// Text.
var cursorScreenPos int
text := i.text
if len(text) == 0 && len(i.placeholder) > 0 {
// Draw placeholder text.
placeholderTextColor := i.placeholderTextColor
if i.GetFocusable().HasFocus() && i.placeholderTextColorFocused != ColorUnset {
placeholderTextColor = i.placeholderTextColorFocused
}
Print(screen, EscapeBytes(i.placeholder), x, y, fieldWidth, AlignLeft, placeholderTextColor)
i.offset = 0
} else {
// Draw entered text.
if i.maskCharacter > 0 {
text = bytes.Repeat([]byte(string(i.maskCharacter)), utf8.RuneCount(i.text))
}
var drawnText []byte
if fieldWidth >= runewidth.StringWidth(string(text)) {
// We have enough space for the full text.
drawnText = EscapeBytes(text)
Print(screen, drawnText, x, y, fieldWidth, AlignLeft, fieldTextColor)
i.offset = 0
iterateString(string(text), func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool {
if textPos >= i.cursorPos {
return true
}
cursorScreenPos += screenWidth
return false
})
} else {
// 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(string(text[i.offset:i.cursorPos])); subWidth > fieldWidth-1 {
shiftLeft = subWidth - fieldWidth + 1
}
currentOffset := i.offset
iterateString(string(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
})
drawnText = EscapeBytes(text[i.offset:])
Print(screen, drawnText, x, y, fieldWidth, AlignLeft, fieldTextColor)
}
// Draw suggestion
if i.maskCharacter == 0 && len(i.autocompleteListSuggestion) > 0 {
Print(screen, i.autocompleteListSuggestion, x+runewidth.StringWidth(string(drawnText)), y, fieldWidth-runewidth.StringWidth(string(drawnText)), AlignLeft, i.autocompleteSuggestionTextColor)
}
}
// Draw field note
if len(i.fieldNote) > 0 {
Print(screen, i.fieldNote, x, y+1, fieldWidth, AlignLeft, i.fieldNoteTextColor)
}
// Draw autocomplete list.
if i.autocompleteList != nil {
// How much space do we need?
lheight := i.autocompleteList.GetItemCount()
lwidth := 0
for index := 0; index < lheight; index++ {
entry, _ := i.autocompleteList.GetItemText(index)
width := TaggedStringWidth(entry)
if width > lwidth {
lwidth = width
}
}
// We prefer to drop down but if there is no space, maybe drop up?
lx := x
ly := y + 1
_, sheight := screen.Size()
if ly+lheight >= sheight && ly-2 > lheight-ly {
ly = y - lheight
if ly < 0 {
ly = 0
}
}
if ly+lheight >= sheight {
lheight = sheight - ly
}
if i.autocompleteList.scrollBarVisibility == ScrollBarAlways || (i.autocompleteList.scrollBarVisibility == ScrollBarAuto && i.autocompleteList.GetItemCount() > lheight) {
lwidth++ // Add space for scroll bar
}
i.autocompleteList.SetRect(lx, ly, lwidth, lheight)
i.autocompleteList.Draw(screen)
}
// Set cursor.
if i.focus.HasFocus() {
screen.ShowCursor(x+cursorScreenPos, y)
}
}
// InputHandler returns the handler for this primitive.
func (i *InputField) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) {
return i.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p Primitive)) {
i.Lock()
// Trigger changed events.
currentText := i.text
defer func() {
i.Lock()
newText := i.text
i.Unlock()
if !bytes.Equal(newText, currentText) {
i.Autocomplete()
if i.changed != nil {
i.changed(string(i.text))
}
}
}()
// Movement functions.
home := func() { i.cursorPos = 0 }
end := func() { i.cursorPos = len(i.text) }
moveLeft := func() {
iterateStringReverse(string(i.text[:i.cursorPos]), func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool {
i.cursorPos -= textWidth
return true
})
}
moveRight := func() {
iterateString(string(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(regexRightWord.ReplaceAll(i.text[:i.cursorPos], nil))
}
moveWordRight := func() {
i.cursorPos = len(i.text) - len(regexLeftWord.ReplaceAll(i.text[i.cursorPos:], nil))
}
// Add character function. Returns whether or not the rune character is
// accepted.
add := func(r rune) bool {
newText := append(append(i.text[:i.cursorPos], []byte(string(r))...), i.text[i.cursorPos:]...)
if i.accept != nil && !i.accept(string(newText), r) {
return false
}
i.text = newText
i.cursorPos += len(string(r))
return true
}
// Finish up.
finish := func(key tcell.Key) {
if i.done != nil {
i.done(key)
}
if i.finished != nil {
i.finished(key)
}
}
// Process key event.
switch key := event.Key(); key {
case tcell.KeyRune: // Regular character.
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()
default:
if !add(event.Rune()) {
i.Unlock()
return
}
}
} else {
// Other keys are simply accepted as regular characters.
if !add(event.Rune()) {
i.Unlock()
return
}
}
case tcell.KeyCtrlU: // Delete all.
i.text = nil
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.
newText := append(regexRightWord.ReplaceAll(i.text[:i.cursorPos], nil), 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(string(i.text[:i.cursorPos]), func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool {
i.text = append(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(string(i.text[i.cursorPos:]), func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool {
i.text = append(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()
}
case tcell.KeyHome, tcell.KeyCtrlA:
home()
case tcell.KeyEnd, tcell.KeyCtrlE:
end()
case tcell.KeyEnter: // We might be done.
if i.autocompleteList != nil {
currentItem := i.autocompleteList.GetCurrentItem()
selectionText := currentItem.GetMainText()
if currentItem.GetSecondaryText() != "" {
selectionText = currentItem.GetSecondaryText()
}
i.Unlock()
i.SetText(selectionText)
i.Lock()
i.autocompleteList = nil
i.autocompleteListSuggestion = nil
i.Unlock()
} else {
i.Unlock()
finish(key)
}
return
case tcell.KeyEscape:
if i.autocompleteList != nil {
i.autocompleteList = nil
i.autocompleteListSuggestion = nil
i.Unlock()
} else {
i.Unlock()
finish(key)
}
return
case tcell.KeyDown, tcell.KeyTab: // Autocomplete selection.
if i.autocompleteList != nil {
count := i.autocompleteList.GetItemCount()
newEntry := i.autocompleteList.GetCurrentItemIndex() + 1
if newEntry >= count {
newEntry = 0
}
i.autocompleteList.SetCurrentItem(newEntry)
i.Unlock()
} else {
i.Unlock()
finish(key)
}
return
case tcell.KeyUp, tcell.KeyBacktab: // Autocomplete selection.
if i.autocompleteList != nil {
newEntry := i.autocompleteList.GetCurrentItemIndex() - 1
if newEntry < 0 {
newEntry = i.autocompleteList.GetItemCount() - 1
}
i.autocompleteList.SetCurrentItem(newEntry)
i.Unlock()
} else {
i.Unlock()
finish(key)
}
return
}
i.Unlock()
})
}
// MouseHandler returns the mouse handler for this primitive.
func (i *InputField) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
return i.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
x, y := event.Position()
_, rectY, _, _ := i.GetInnerRect()
if !i.InRect(x, y) {
return false, nil
}
// Process mouse event.
if action == MouseLeftClick && y == rectY {
// Determine where to place the cursor.
if x >= i.fieldX {
if !iterateString(string(i.text), func(main rune, comb []rune, textPos int, textWidth int, screenPos int, screenWidth int) bool {
if x-i.fieldX < screenPos+screenWidth {
i.cursorPos = textPos
return true
}
return false
}) {
i.cursorPos = len(i.text)
}
}
setFocus(i)
consumed = true
}
return
})
}
var (
regexRightWord = regexp.MustCompile(`(\w*|\W)$`)
regexLeftWord = regexp.MustCompile(`^(\W|\w*)`)
)
package cview
import (
"code.rocketnine.space/tslocum/cbind"
"github.com/gdamore/tcell/v2"
)
// Key defines the keyboard shortcuts of an application.
// Secondary shortcuts apply when not focusing a text input.
type Key struct {
Cancel []string
Select []string
Select2 []string
MoveUp []string
MoveUp2 []string
MoveDown []string
MoveDown2 []string
MoveLeft []string
MoveLeft2 []string
MoveRight []string
MoveRight2 []string
MoveFirst []string
MoveFirst2 []string
MoveLast []string
MoveLast2 []string
MovePreviousField []string
MoveNextField []string
MovePreviousPage []string
MoveNextPage []string
ShowContextMenu []string
}
// Keys defines the keyboard shortcuts of an application.
// Secondary shortcuts apply when not focusing a text input.
var Keys = Key{
Cancel: []string{"Escape"},
Select: []string{"Enter", "Ctrl+J"}, // Ctrl+J = keypad enter
Select2: []string{"Space"},
MoveUp: []string{"Up"},
MoveUp2: []string{"k"},
MoveDown: []string{"Down"},
MoveDown2: []string{"j"},
MoveLeft: []string{"Left"},
MoveLeft2: []string{"h"},
MoveRight: []string{"Right"},
MoveRight2: []string{"l"},
MoveFirst: []string{"Home", "Ctrl+A"},
MoveFirst2: []string{"g"},
MoveLast: []string{"End", "Ctrl+E"},
MoveLast2: []string{"G"},
MovePreviousField: []string{"Backtab"},
MoveNextField: []string{"Tab"},
MovePreviousPage: []string{"PageUp", "Ctrl+B"},
MoveNextPage: []string{"PageDown", "Ctrl+F"},
ShowContextMenu: []string{"Alt+Enter"},
}
// HitShortcut returns whether the EventKey provided is present in one or more
// sets of keybindings.
func HitShortcut(event *tcell.EventKey, keybindings ...[]string) bool {
enc, err := cbind.Encode(event.Modifiers(), event.Key(), event.Rune())
if err != nil {
return false
}
for _, binds := range keybindings {
for _, key := range binds {
if key == enc {
return true
}
}
}
return false
}
package cview
import (
"bytes"
"fmt"
"strings"
"sync"
"github.com/gdamore/tcell/v2"
)
// ListItem represents an item in a List.
type ListItem struct {
disabled bool // Whether or not the list item is selectable.
mainText []byte // The main text of the list item.
secondaryText []byte // A secondary text to be shown underneath the main text.
shortcut rune // The key to select the list item directly, 0 if there is no shortcut.
selected func() // The optional function which is called when the item is selected.
reference interface{} // An optional reference object.
sync.RWMutex
}
// NewListItem returns a new item for a list.
func NewListItem(mainText string) *ListItem {
return &ListItem{
mainText: []byte(mainText),
}
}
// SetMainBytes sets the main text of the list item.
func (l *ListItem) SetMainBytes(val []byte) {
l.Lock()
defer l.Unlock()
l.mainText = val
}
// SetMainText sets the main text of the list item.
func (l *ListItem) SetMainText(val string) {
l.SetMainBytes([]byte(val))
}
// GetMainBytes returns the item's main text.
func (l *ListItem) GetMainBytes() []byte {
l.RLock()
defer l.RUnlock()
return l.mainText
}
// GetMainText returns the item's main text.
func (l *ListItem) GetMainText() string {
return string(l.GetMainBytes())
}
// SetSecondaryBytes sets a secondary text to be shown underneath the main text.
func (l *ListItem) SetSecondaryBytes(val []byte) {
l.Lock()
defer l.Unlock()
l.secondaryText = val
}
// SetSecondaryText sets a secondary text to be shown underneath the main text.
func (l *ListItem) SetSecondaryText(val string) {
l.SetSecondaryBytes([]byte(val))
}
// GetSecondaryBytes returns the item's secondary text.
func (l *ListItem) GetSecondaryBytes() []byte {
l.RLock()
defer l.RUnlock()
return l.secondaryText
}
// GetSecondaryText returns the item's secondary text.
func (l *ListItem) GetSecondaryText() string {
return string(l.GetSecondaryBytes())
}
// SetShortcut sets the key to select the ListItem directly, 0 if there is no shortcut.
func (l *ListItem) SetShortcut(val rune) {
l.Lock()
defer l.Unlock()
l.shortcut = val
}
// GetShortcut returns the ListItem's shortcut.
func (l *ListItem) GetShortcut() rune {
l.RLock()
defer l.RUnlock()
return l.shortcut
}
// SetSelectedFunc sets a function which is called when the ListItem is selected.
func (l *ListItem) SetSelectedFunc(handler func()) {
l.Lock()
defer l.Unlock()
l.selected = handler
}
// SetReference allows you to store a reference of any type in the item
func (l *ListItem) SetReference(val interface{}) {
l.Lock()
defer l.Unlock()
l.reference = val
}
// GetReference returns the item's reference object.
func (l *ListItem) GetReference() interface{} {
l.RLock()
defer l.RUnlock()
return l.reference
}
// List displays rows of items, each of which can be selected.
type List struct {
*Box
*ContextMenu
// The items of the list.
items []*ListItem
// The index of the currently selected item.
currentItem int
// Whether or not to show the secondary item texts.
showSecondaryText bool
// The item main text color.
mainTextColor tcell.Color
// The item secondary text color.
secondaryTextColor tcell.Color
// The item shortcut text color.
shortcutColor tcell.Color
// The text color for selected items.
selectedTextColor tcell.Color
// The style attributes for selected items.
selectedTextAttributes tcell.AttrMask
// Visibility of the scroll bar.
scrollBarVisibility ScrollBarVisibility
// The scroll bar color.
scrollBarColor tcell.Color
// The background color for selected items.
selectedBackgroundColor tcell.Color
// If true, the selection is only shown when the list has focus.
selectedFocusOnly bool
// If true, the selection must remain visible when scrolling.
selectedAlwaysVisible bool
// If true, the selection must remain centered when scrolling.
selectedAlwaysCentered bool
// If true, the entire row is highlighted when selected.
highlightFullLine bool
// Whether or not navigating the list will wrap around.
wrapAround bool
// Whether or not hovering over an item will highlight it.
hover bool
// The number of list items and columns by which the list is scrolled
// down/to the right.
itemOffset, columnOffset int
// An optional function which is called when the user has navigated to a list
// item.
changed func(index int, item *ListItem)
// An optional function which is called when a list item was selected. This
// function will be called even if the list item defines its own callback.
selected func(index int, item *ListItem)
// An optional function which is called when the user presses the Escape key.
done func()
// The height of the list the last time it was drawn.
height int
sync.RWMutex
}
// NewList returns a new form.
func NewList() *List {
l := &List{
Box: NewBox(),
showSecondaryText: true,
scrollBarVisibility: ScrollBarAuto,
mainTextColor: Styles.PrimaryTextColor,
secondaryTextColor: Styles.TertiaryTextColor,
shortcutColor: Styles.SecondaryTextColor,
selectedTextColor: Styles.PrimitiveBackgroundColor,
scrollBarColor: Styles.ScrollBarColor,
selectedBackgroundColor: Styles.PrimaryTextColor,
}
l.ContextMenu = NewContextMenu(l)
l.focus = l
return l
}
// SetCurrentItem sets the currently selected item by its index, starting at 0
// for the first item. If a negative index is provided, items are referred to
// from the back (-1 = last item, -2 = second-to-last item, and so on). Out of
// range indices are clamped to the beginning/end.
//
// Calling this function triggers a "changed" event if the selection changes.
func (l *List) SetCurrentItem(index int) {
l.Lock()
if index < 0 {
index = len(l.items) + index
}
if index >= len(l.items) {
index = len(l.items) - 1
}
if index < 0 {
index = 0
}
previousItem := l.currentItem
l.currentItem = index
l.updateOffset()
if index != previousItem && index < len(l.items) && l.changed != nil {
item := l.items[index]
l.Unlock()
l.changed(index, item)
} else {
l.Unlock()
}
}
// GetCurrentItem returns the currently selected list item,
// Returns nil if no item is selected.
func (l *List) GetCurrentItem() *ListItem {
l.RLock()
defer l.RUnlock()
if len(l.items) == 0 || l.currentItem >= len(l.items) {
return nil
}
return l.items[l.currentItem]
}
// GetCurrentItemIndex returns the index of the currently selected list item,
// starting at 0 for the first item and its struct.
func (l *List) GetCurrentItemIndex() int {
l.RLock()
defer l.RUnlock()
return l.currentItem
}
// GetItems returns all list items.
func (l *List) GetItems() []*ListItem {
l.RLock()
defer l.RUnlock()
return l.items
}
// RemoveItem removes the item with the given index (starting at 0) from the
// list. If a negative index is provided, items are referred to from the back
// (-1 = last item, -2 = second-to-last item, and so on). Out of range indices
// are clamped to the beginning/end, i.e. unless the list is empty, an item is
// always removed.
//
// The currently selected item is shifted accordingly. If it is the one that is
// removed, a "changed" event is fired.
func (l *List) RemoveItem(index int) {
l.Lock()
if len(l.items) == 0 {
l.Unlock()
return
}
// Adjust index.
if index < 0 {
index = len(l.items) + index
}
if index >= len(l.items) {
index = len(l.items) - 1
}
if index < 0 {
index = 0
}
// Remove item.
l.items = append(l.items[:index], l.items[index+1:]...)
// If there is nothing left, we're done.
if len(l.items) == 0 {
l.Unlock()
return
}
// Shift current item.
previousItem := l.currentItem
if l.currentItem >= index && l.currentItem > 0 {
l.currentItem--
}
// Fire "changed" event for removed items.
if previousItem == index && index < len(l.items) && l.changed != nil {
item := l.items[l.currentItem]
l.Unlock()
l.changed(l.currentItem, item)
} else {
l.Unlock()
}
}
// SetOffset sets the number of list items and columns by which the list is
// scrolled down/to the right.
func (l *List) SetOffset(items, columns int) {
l.Lock()
defer l.Unlock()
if items < 0 {
items = 0
}
if columns < 0 {
columns = 0
}
l.itemOffset, l.columnOffset = items, columns
}
// GetOffset returns the number of list items and columns by which the list is
// scrolled down/to the right.
func (l *List) GetOffset() (int, int) {
l.Lock()
defer l.Unlock()
return l.itemOffset, l.columnOffset
}
// SetMainTextColor sets the color of the items' main text.
func (l *List) SetMainTextColor(color tcell.Color) {
l.Lock()
defer l.Unlock()
l.mainTextColor = color
}
// SetSecondaryTextColor sets the color of the items' secondary text.
func (l *List) SetSecondaryTextColor(color tcell.Color) {
l.Lock()
defer l.Unlock()
l.secondaryTextColor = color
}
// SetShortcutColor sets the color of the items' shortcut.
func (l *List) SetShortcutColor(color tcell.Color) {
l.Lock()
defer l.Unlock()
l.shortcutColor = color
}
// SetSelectedTextColor sets the text color of selected items.
func (l *List) SetSelectedTextColor(color tcell.Color) {
l.Lock()
defer l.Unlock()
l.selectedTextColor = color
}
// SetSelectedTextAttributes sets the style attributes of selected items.
func (l *List) SetSelectedTextAttributes(attr tcell.AttrMask) {
l.Lock()
defer l.Unlock()
l.selectedTextAttributes = attr
}
// SetSelectedBackgroundColor sets the background color of selected items.
func (l *List) SetSelectedBackgroundColor(color tcell.Color) {
l.Lock()
defer l.Unlock()
l.selectedBackgroundColor = color
}
// SetSelectedFocusOnly sets a flag which determines when the currently selected
// list item is highlighted. If set to true, selected items are only highlighted
// when the list has focus. If set to false, they are always highlighted.
func (l *List) SetSelectedFocusOnly(focusOnly bool) {
l.Lock()
defer l.Unlock()
l.selectedFocusOnly = focusOnly
}
// SetSelectedAlwaysVisible sets a flag which determines whether the currently
// selected list item must remain visible when scrolling.
func (l *List) SetSelectedAlwaysVisible(alwaysVisible bool) {
l.Lock()
defer l.Unlock()
l.selectedAlwaysVisible = alwaysVisible
}
// SetSelectedAlwaysCentered sets a flag which determines whether the currently
// selected list item must remain centered when scrolling.
func (l *List) SetSelectedAlwaysCentered(alwaysCentered bool) {
l.Lock()
defer l.Unlock()
l.selectedAlwaysCentered = alwaysCentered
}
// SetHighlightFullLine sets a flag which determines whether the colored
// background of selected items spans the entire width of the view. If set to
// true, the highlight spans the entire view. If set to false, only the text of
// the selected item from beginning to end is highlighted.
func (l *List) SetHighlightFullLine(highlight bool) {
l.Lock()
defer l.Unlock()
l.highlightFullLine = highlight
}
// ShowSecondaryText determines whether or not to show secondary item texts.
func (l *List) ShowSecondaryText(show bool) {
l.Lock()
defer l.Unlock()
l.showSecondaryText = show
return
}
// SetScrollBarVisibility specifies the display of the scroll bar.
func (l *List) SetScrollBarVisibility(visibility ScrollBarVisibility) {
l.Lock()
defer l.Unlock()
l.scrollBarVisibility = visibility
}
// SetScrollBarColor sets the color of the scroll bar.
func (l *List) SetScrollBarColor(color tcell.Color) {
l.Lock()
defer l.Unlock()
l.scrollBarColor = color
}
// SetHover sets the flag that determines whether hovering over an item will
// highlight it (without triggering callbacks set with SetSelectedFunc).
func (l *List) SetHover(hover bool) {
l.Lock()
defer l.Unlock()
l.hover = hover
}
// SetWrapAround sets the flag that determines whether navigating the list will
// wrap around. That is, navigating downwards on the last item will move the
// selection to the first item (similarly in the other direction). If set to
// false, the selection won't change when navigating downwards on the last item
// or navigating upwards on the first item.
func (l *List) SetWrapAround(wrapAround bool) {
l.Lock()
defer l.Unlock()
l.wrapAround = wrapAround
}
// SetChangedFunc sets the function which is called when the user navigates to
// a list item. The function receives the item's index in the list of items
// (starting with 0) and the list item.
//
// This function is also called when the first item is added or when
// SetCurrentItem() is called.
func (l *List) SetChangedFunc(handler func(index int, item *ListItem)) {
l.Lock()
defer l.Unlock()
l.changed = handler
}
// SetSelectedFunc sets the function which is called when the user selects a
// list item by pressing Enter on the current selection. The function receives
// the item's index in the list of items (starting with 0) and its struct.
func (l *List) SetSelectedFunc(handler func(int, *ListItem)) {
l.Lock()
defer l.Unlock()
l.selected = handler
}
// SetDoneFunc sets a function which is called when the user presses the Escape
// key.
func (l *List) SetDoneFunc(handler func()) {
l.Lock()
defer l.Unlock()
l.done = handler
}
// AddItem calls InsertItem() with an index of -1.
func (l *List) AddItem(item *ListItem) {
l.InsertItem(-1, item)
}
// InsertItem adds a new item to the list at the specified index. An index of 0
// will insert the item at the beginning, an index of 1 before the second item,
// and so on. An index of GetItemCount() or higher will insert the item at the
// end of the list. Negative indices are also allowed: An index of -1 will
// insert the item at the end of the list, an index of -2 before the last item,
// and so on. An index of -GetItemCount()-1 or lower will insert the item at the
// beginning.
//
// An item has a main text which will be highlighted when selected. It also has
// a secondary text which is shown underneath the main text (if it is set to
// visible) but which may remain empty.
//
// The shortcut is a key binding. If the specified rune is entered, the item
// is selected immediately. Set to 0 for no binding.
//
// The "selected" callback will be invoked when the user selects the item. You
// may provide nil if no such callback is needed or if all events are handled
// through the selected callback set with SetSelectedFunc().
//
// The currently selected item will shift its position accordingly. If the list
// was previously empty, a "changed" event is fired because the new item becomes
// selected.
func (l *List) InsertItem(index int, item *ListItem) {
l.Lock()
// Shift index to range.
if index < 0 {
index = len(l.items) + index + 1
}
if index < 0 {
index = 0
} else if index > len(l.items) {
index = len(l.items)
}
// Shift current item.
if l.currentItem < len(l.items) && l.currentItem >= index {
l.currentItem++
}
// Insert item (make space for the new item, then shift and insert).
l.items = append(l.items, nil)
if index < len(l.items)-1 { // -1 because l.items has already grown by one item.
copy(l.items[index+1:], l.items[index:])
}
l.items[index] = item
// Fire a "change" event for the first item in the list.
if len(l.items) == 1 && l.changed != nil {
item := l.items[0]
l.Unlock()
l.changed(0, item)
} else {
l.Unlock()
}
}
// GetItem returns the ListItem at the given index.
// Returns nil when index is out of bounds.
func (l *List) GetItem(index int) *ListItem {
if index > len(l.items)-1 {
return nil
}
return l.items[index]
}
// GetItemCount returns the number of items in the list.
func (l *List) GetItemCount() int {
l.RLock()
defer l.RUnlock()
return len(l.items)
}
// GetItemText returns an item's texts (main and secondary). Panics if the index
// is out of range.
func (l *List) GetItemText(index int) (main, secondary string) {
l.RLock()
defer l.RUnlock()
return string(l.items[index].mainText), string(l.items[index].secondaryText)
}
// SetItemText sets an item's main and secondary text. Panics if the index is
// out of range.
func (l *List) SetItemText(index int, main, secondary string) {
l.Lock()
defer l.Unlock()
item := l.items[index]
item.mainText = []byte(main)
item.secondaryText = []byte(secondary)
}
// SetItemEnabled sets whether an item is selectable. Panics if the index is
// out of range.
func (l *List) SetItemEnabled(index int, enabled bool) {
l.Lock()
defer l.Unlock()
item := l.items[index]
item.disabled = !enabled
}
// FindItems searches the main and secondary texts for the given strings and
// returns a list of item indices in which those strings are found. One of the
// two search strings may be empty, it will then be ignored. Indices are always
// returned in ascending order.
//
// If mustContainBoth is set to true, mainSearch must be contained in the main
// text AND secondarySearch must be contained in the secondary text. If it is
// false, only one of the two search strings must be contained.
//
// Set ignoreCase to true for case-insensitive search.
func (l *List) FindItems(mainSearch, secondarySearch string, mustContainBoth, ignoreCase bool) (indices []int) {
l.RLock()
defer l.RUnlock()
if mainSearch == "" && secondarySearch == "" {
return
}
if ignoreCase {
mainSearch = strings.ToLower(mainSearch)
secondarySearch = strings.ToLower(secondarySearch)
}
mainSearchBytes := []byte(mainSearch)
secondarySearchBytes := []byte(secondarySearch)
for index, item := range l.items {
mainText := item.mainText
secondaryText := item.secondaryText
if ignoreCase {
mainText = bytes.ToLower(mainText)
secondaryText = bytes.ToLower(secondaryText)
}
// strings.Contains() always returns true for a "" search.
mainContained := bytes.Contains(mainText, mainSearchBytes)
secondaryContained := bytes.Contains(secondaryText, secondarySearchBytes)
if mustContainBoth && mainContained && secondaryContained ||
!mustContainBoth && (len(mainText) > 0 && mainContained || len(secondaryText) > 0 && secondaryContained) {
indices = append(indices, index)
}
}
return
}
// Clear removes all items from the list.
func (l *List) Clear() {
l.Lock()
defer l.Unlock()
l.items = nil
l.currentItem = 0
l.itemOffset = 0
l.columnOffset = 0
}
// Focus is called by the application when the primitive receives focus.
func (l *List) Focus(delegate func(p Primitive)) {
l.Box.Focus(delegate)
if l.ContextMenu.open {
delegate(l.ContextMenu.list)
}
}
// HasFocus returns whether or not this primitive has focus.
func (l *List) HasFocus() bool {
if l.ContextMenu.open {
return l.ContextMenu.list.HasFocus()
}
l.RLock()
defer l.RUnlock()
return l.hasFocus
}
// Transform modifies the current selection.
func (l *List) Transform(tr Transformation) {
l.Lock()
previousItem := l.currentItem
l.transform(tr)
if l.currentItem != previousItem && l.currentItem < len(l.items) && l.changed != nil {
item := l.items[l.currentItem]
l.Unlock()
l.changed(l.currentItem, item)
} else {
l.Unlock()
}
}
func (l *List) transform(tr Transformation) {
var decreasing bool
pageItems := l.height
if l.showSecondaryText {
pageItems /= 2
}
if pageItems < 1 {
pageItems = 1
}
switch tr {
case TransformFirstItem:
l.currentItem = 0
l.itemOffset = 0
decreasing = true
case TransformLastItem:
l.currentItem = len(l.items) - 1
case TransformPreviousItem:
l.currentItem--
decreasing = true
case TransformNextItem:
l.currentItem++
case TransformPreviousPage:
l.currentItem -= pageItems
decreasing = true
case TransformNextPage:
l.currentItem += pageItems
l.itemOffset += pageItems
}
for i := 0; i < len(l.items); i++ {
if l.currentItem < 0 {
if l.wrapAround {
l.currentItem = len(l.items) - 1
} else {
l.currentItem = 0
l.itemOffset = 0
}
} else if l.currentItem >= len(l.items) {
if l.wrapAround {
l.currentItem = 0
l.itemOffset = 0
} else {
l.currentItem = len(l.items) - 1
}
}
item := l.items[l.currentItem]
if !item.disabled && (item.shortcut > 0 || len(item.mainText) > 0 || len(item.secondaryText) > 0) {
break
}
if decreasing {
l.currentItem--
} else {
l.currentItem++
}
}
l.updateOffset()
}
func (l *List) updateOffset() {
_, _, _, l.height = l.GetInnerRect()
h := l.height
if l.selectedAlwaysCentered {
h /= 2
}
if l.currentItem < l.itemOffset {
l.itemOffset = l.currentItem
} else if l.showSecondaryText {
if 2*(l.currentItem-l.itemOffset) >= h-1 {
l.itemOffset = (2*l.currentItem + 3 - h) / 2
}
} else {
if l.currentItem-l.itemOffset >= h {
l.itemOffset = l.currentItem + 1 - h
}
}
if l.showSecondaryText {
if l.itemOffset > len(l.items)-(l.height/2) {
l.itemOffset = len(l.items) - l.height/2
}
} else {
if l.itemOffset > len(l.items)-l.height {
l.itemOffset = len(l.items) - l.height
}
}
if l.itemOffset < 0 {
l.itemOffset = 0
}
// Maximum width of item text
maxWidth := 0
for _, option := range l.items {
strWidth := TaggedTextWidth(option.mainText)
secondaryWidth := TaggedTextWidth(option.secondaryText)
if secondaryWidth > strWidth {
strWidth = secondaryWidth
}
if option.shortcut != 0 {
strWidth += 4
}
if strWidth > maxWidth {
maxWidth = strWidth
}
}
// Additional width for scroll bar
addWidth := 0
if l.scrollBarVisibility == ScrollBarAlways ||
(l.scrollBarVisibility == ScrollBarAuto &&
((!l.showSecondaryText && len(l.items) > l.innerHeight) ||
(l.showSecondaryText && len(l.items) > l.innerHeight/2))) {
addWidth = 1
}
if l.columnOffset > (maxWidth-l.innerWidth)+addWidth {
l.columnOffset = (maxWidth - l.innerWidth) + addWidth
}
if l.columnOffset < 0 {
l.columnOffset = 0
}
}
// Draw draws this primitive onto the screen.
func (l *List) Draw(screen tcell.Screen) {
if !l.GetVisible() {
return
}
l.Box.Draw(screen)
hasFocus := l.GetFocusable().HasFocus()
l.Lock()
defer l.Unlock()
// Determine the dimensions.
x, y, width, height := l.GetInnerRect()
bottomLimit := y + height
l.height = height
screenWidth, _ := screen.Size()
scrollBarHeight := height
scrollBarX := x + (width - 1) + l.paddingLeft + l.paddingRight
if scrollBarX > screenWidth-1 {
scrollBarX = screenWidth - 1
}
// Halve scroll bar height when drawing two lines per list item.
if l.showSecondaryText {
scrollBarHeight /= 2
}
// Do we show any shortcuts?
var showShortcuts bool
for _, item := range l.items {
if item.shortcut != 0 {
showShortcuts = true
x += 4
width -= 4
break
}
}
// Adjust offset to keep the current selection in view.
if l.selectedAlwaysVisible || l.selectedAlwaysCentered {
l.updateOffset()
}
scrollBarCursor := int(float64(len(l.items)) * (float64(l.itemOffset) / float64(len(l.items)-height)))
// Draw the list items.
for index, item := range l.items {
if index < l.itemOffset {
continue
}
if y >= bottomLimit {
break
}
mainText := item.mainText
secondaryText := item.secondaryText
if l.columnOffset > 0 {
if l.columnOffset < len(mainText) {
mainText = mainText[l.columnOffset:]
} else {
mainText = nil
}
if l.columnOffset < len(secondaryText) {
secondaryText = secondaryText[l.columnOffset:]
} else {
secondaryText = nil
}
}
if len(item.mainText) == 0 && len(item.secondaryText) == 0 && item.shortcut == 0 { // Divider
Print(screen, bytes.Repeat([]byte(string(tcell.RuneHLine)), width+l.paddingLeft+l.paddingRight), x-l.paddingLeft, y, width+l.paddingLeft+l.paddingRight, AlignLeft, l.mainTextColor)
RenderScrollBar(screen, l.scrollBarVisibility, scrollBarX, y, scrollBarHeight, len(l.items), scrollBarCursor, index-l.itemOffset, l.hasFocus, l.scrollBarColor)
y++
continue
} else if item.disabled {
// Shortcuts.
if showShortcuts && item.shortcut != 0 {
Print(screen, []byte(fmt.Sprintf("(%c)", item.shortcut)), x-5, y, 4, AlignRight, tcell.ColorDarkSlateGray.TrueColor())
}
// Main text.
Print(screen, mainText, x, y, width, AlignLeft, tcell.ColorGray.TrueColor())
RenderScrollBar(screen, l.scrollBarVisibility, scrollBarX, y, scrollBarHeight, len(l.items), scrollBarCursor, index-l.itemOffset, l.hasFocus, l.scrollBarColor)
y++
continue
}
// Shortcuts.
if showShortcuts && item.shortcut != 0 {
Print(screen, []byte(fmt.Sprintf("(%c)", item.shortcut)), x-5, y, 4, AlignRight, l.shortcutColor)
}
// Main text.
Print(screen, mainText, x, y, width, AlignLeft, l.mainTextColor)
// Background color of selected text.
if index == l.currentItem && (!l.selectedFocusOnly || hasFocus) {
textWidth := width
if !l.highlightFullLine {
if w := TaggedTextWidth(mainText); w < textWidth {
textWidth = w
}
}
for bx := 0; bx < textWidth; bx++ {
m, c, style, _ := screen.GetContent(x+bx, y)
fg, _, _ := style.Decompose()
if fg == l.mainTextColor {
fg = l.selectedTextColor
}
style = SetAttributes(style.Background(l.selectedBackgroundColor).Foreground(fg), l.selectedTextAttributes)
screen.SetContent(x+bx, y, m, c, style)
}
}
RenderScrollBar(screen, l.scrollBarVisibility, scrollBarX, y, scrollBarHeight, len(l.items), scrollBarCursor, index-l.itemOffset, l.hasFocus, l.scrollBarColor)
y++
if y >= bottomLimit {
break
}
// Secondary text.
if l.showSecondaryText {
Print(screen, secondaryText, x, y, width, AlignLeft, l.secondaryTextColor)
RenderScrollBar(screen, l.scrollBarVisibility, scrollBarX, y, scrollBarHeight, len(l.items), scrollBarCursor, index-l.itemOffset, l.hasFocus, l.scrollBarColor)
y++
}
}
// Overdraw scroll bar when necessary.
for y < bottomLimit {
RenderScrollBar(screen, l.scrollBarVisibility, scrollBarX, y, scrollBarHeight, len(l.items), scrollBarCursor, bottomLimit-y, l.hasFocus, l.scrollBarColor)
y++
}
// Draw context menu.
if hasFocus && l.ContextMenu.open {
ctx := l.ContextMenuList()
x, y, width, height = l.GetInnerRect()
// What's the longest option text?
maxWidth := 0
for _, option := range ctx.items {
strWidth := TaggedTextWidth(option.mainText)
if option.shortcut != 0 {
strWidth += 4
}
if strWidth > maxWidth {
maxWidth = strWidth
}
}
lheight := len(ctx.items)
lwidth := maxWidth
// Add space for borders
lwidth += 2
lheight += 2
lwidth += ctx.paddingLeft + ctx.paddingRight
lheight += ctx.paddingTop + ctx.paddingBottom
cx, cy := l.ContextMenu.x, l.ContextMenu.y
if cx < 0 || cy < 0 {
offsetX := 7
if showShortcuts {
offsetX += 4
}
offsetY := l.currentItem
if l.showSecondaryText {
offsetY *= 2
}
x, y, _, _ := l.GetInnerRect()
cx, cy = x+offsetX, y+offsetY
}
_, sheight := screen.Size()
if cy+lheight >= sheight && cy-2 > lheight-cy {
for i := (cy + lheight) - sheight; i > 0; i-- {
cy--
if cy+lheight < sheight {
break
}
}
if cy < 0 {
cy = 0
}
}
if cy+lheight >= sheight {
lheight = sheight - cy
}
if ctx.scrollBarVisibility == ScrollBarAlways || (ctx.scrollBarVisibility == ScrollBarAuto && len(ctx.items) > lheight) {
lwidth++ // Add space for scroll bar
}
ctx.SetRect(cx, cy, lwidth, lheight)
ctx.Draw(screen)
}
}
// InputHandler returns the handler for this primitive.
func (l *List) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) {
return l.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p Primitive)) {
l.Lock()
if HitShortcut(event, Keys.Cancel) {
if l.ContextMenu.open {
l.Unlock()
l.ContextMenu.hide(setFocus)
return
}
if l.done != nil {
l.Unlock()
l.done()
} else {
l.Unlock()
}
return
} else if HitShortcut(event, Keys.Select, Keys.Select2) {
if l.currentItem >= 0 && l.currentItem < len(l.items) {
item := l.items[l.currentItem]
if !item.disabled {
if item.selected != nil {
l.Unlock()
item.selected()
l.Lock()
}
if l.selected != nil {
l.Unlock()
l.selected(l.currentItem, item)
l.Lock()
}
}
}
} else if HitShortcut(event, Keys.ShowContextMenu) {
defer l.ContextMenu.show(l.currentItem, -1, -1, setFocus)
} else if len(l.items) == 0 {
l.Unlock()
return
}
if event.Key() == tcell.KeyRune {
ch := event.Rune()
if ch != ' ' {
// It's not a space bar. Is it a shortcut?
for index, item := range l.items {
if !item.disabled && item.shortcut == ch {
// We have a shortcut.
l.currentItem = index
item := l.items[l.currentItem]
if item.selected != nil {
l.Unlock()
item.selected()
l.Lock()
}
if l.selected != nil {
l.Unlock()
l.selected(l.currentItem, item)
l.Lock()
}
l.Unlock()
return
}
}
}
}
previousItem := l.currentItem
if HitShortcut(event, Keys.MoveFirst, Keys.MoveFirst2) {
l.transform(TransformFirstItem)
} else if HitShortcut(event, Keys.MoveLast, Keys.MoveLast2) {
l.transform(TransformLastItem)
} else if HitShortcut(event, Keys.MoveUp, Keys.MoveUp2, Keys.MovePreviousField) {
l.transform(TransformPreviousItem)
} else if HitShortcut(event, Keys.MoveDown, Keys.MoveDown2, Keys.MoveNextField) {
l.transform(TransformNextItem)
} else if HitShortcut(event, Keys.MoveLeft, Keys.MoveLeft2) {
l.columnOffset--
l.updateOffset()
} else if HitShortcut(event, Keys.MoveRight, Keys.MoveRight2) {
l.columnOffset++
l.updateOffset()
} else if HitShortcut(event, Keys.MovePreviousPage) {
l.transform(TransformPreviousPage)
} else if HitShortcut(event, Keys.MoveNextPage) {
l.transform(TransformNextPage)
}
if l.currentItem != previousItem && l.currentItem < len(l.items) && l.changed != nil {
item := l.items[l.currentItem]
l.Unlock()
l.changed(l.currentItem, item)
} else {
l.Unlock()
}
})
}
// indexAtY returns the index of the list item found at the given Y position
// or a negative value if there is no such list item.
func (l *List) indexAtY(y int) int {
_, rectY, _, height := l.GetInnerRect()
if y < rectY || y >= rectY+height {
return -1
}
index := y - rectY
if l.showSecondaryText {
index /= 2
}
index += l.itemOffset
if index >= len(l.items) {
return -1
}
return index
}
// indexAtPoint returns the index of the list item found at the given position
// or a negative value if there is no such list item.
func (l *List) indexAtPoint(x, y int) int {
rectX, rectY, width, height := l.GetInnerRect()
if x < rectX || x >= rectX+width || y < rectY || y >= rectY+height {
return -1
}
index := y - rectY
if l.showSecondaryText {
index /= 2
}
index += l.itemOffset
if index >= len(l.items) {
return -1
}
return index
}
// MouseHandler returns the mouse handler for this primitive.
func (l *List) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
return l.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
l.Lock()
// Pass events to context menu.
if l.ContextMenuVisible() && l.ContextMenuList().InRect(event.Position()) {
defer l.ContextMenuList().MouseHandler()(action, event, setFocus)
consumed = true
l.Unlock()
return
}
if !l.InRect(event.Position()) {
l.Unlock()
return false, nil
}
// Process mouse event.
switch action {
case MouseLeftClick:
if l.ContextMenuVisible() {
defer l.ContextMenu.hide(setFocus)
consumed = true
l.Unlock()
return
}
l.Unlock()
setFocus(l)
l.Lock()
index := l.indexAtPoint(event.Position())
if index != -1 {
item := l.items[index]
if !item.disabled {
l.currentItem = index
if item.selected != nil {
l.Unlock()
item.selected()
l.Lock()
}
if l.selected != nil {
l.Unlock()
l.selected(index, item)
l.Lock()
}
if index != l.currentItem && l.changed != nil {
l.Unlock()
l.changed(index, item)
l.Lock()
}
}
}
consumed = true
case MouseMiddleClick:
if l.ContextMenu.open {
defer l.ContextMenu.hide(setFocus)
consumed = true
l.Unlock()
return
}
case MouseRightDown:
if len(l.ContextMenuList().items) == 0 {
l.Unlock()
return
}
x, y := event.Position()
index := l.indexAtPoint(event.Position())
if index != -1 {
item := l.items[index]
if !item.disabled {
l.currentItem = index
if index != l.currentItem && l.changed != nil {
l.Unlock()
l.changed(index, item)
l.Lock()
}
}
}
defer l.ContextMenu.show(l.currentItem, x, y, setFocus)
l.ContextMenu.drag = true
consumed = true
case MouseMove:
if l.hover {
_, y := event.Position()
index := l.indexAtY(y)
if index >= 0 {
item := l.items[index]
if !item.disabled {
l.currentItem = index
}
}
consumed = true
}
case MouseScrollUp:
if l.itemOffset > 0 {
l.itemOffset--
}
consumed = true
case MouseScrollDown:
lines := len(l.items) - l.itemOffset
if l.showSecondaryText {
lines *= 2
}
if _, _, _, height := l.GetInnerRect(); lines > height {
l.itemOffset++
}
consumed = true
}
l.Unlock()
return
})
}
package cview
import (
"sync"
"github.com/gdamore/tcell/v2"
)
// Modal is a centered message window used to inform the user or prompt them
// for an immediate decision. It needs to have at least one button (added via
// AddButtons) or it will never disappear. You may change the title and
// appearance of the window by modifying the Frame returned by GetFrame. You
// may include additional elements within the window by modifying the Form
// returned by GetForm.
type Modal struct {
*Box
// The Frame embedded in the Modal.
frame *Frame
// The Form embedded in the Modal's Frame.
form *Form
// The message text (original, not word-wrapped).
text string
// The text color.
textColor tcell.Color
// The optional callback for when the user clicked one of the buttons. It
// receives the index of the clicked button and the button's label.
done func(buttonIndex int, buttonLabel string)
sync.RWMutex
}
// NewModal returns a new centered message window.
func NewModal() *Modal {
m := &Modal{
Box: NewBox(),
textColor: Styles.PrimaryTextColor,
}
m.form = NewForm()
m.form.SetButtonsAlign(AlignCenter)
m.form.SetButtonBackgroundColor(Styles.PrimitiveBackgroundColor)
m.form.SetButtonTextColor(Styles.PrimaryTextColor)
m.form.SetBackgroundColor(Styles.ContrastBackgroundColor)
m.form.SetPadding(0, 0, 0, 0)
m.form.SetCancelFunc(func() {
if m.done != nil {
m.done(-1, "")
}
})
m.frame = NewFrame(m.form)
m.frame.SetBorder(true)
m.frame.SetBorders(0, 0, 1, 0, 0, 0)
m.frame.SetBackgroundColor(Styles.ContrastBackgroundColor)
m.frame.SetPadding(1, 1, 1, 1)
m.focus = m
return m
}
// SetBackgroundColor sets the color of the Modal Frame background.
func (m *Modal) SetBackgroundColor(color tcell.Color) {
m.Lock()
defer m.Unlock()
m.form.SetBackgroundColor(color)
m.frame.SetBackgroundColor(color)
}
// SetTextColor sets the color of the message text.
func (m *Modal) SetTextColor(color tcell.Color) {
m.Lock()
defer m.Unlock()
m.textColor = color
}
// SetButtonBackgroundColor sets the background color of the buttons.
func (m *Modal) SetButtonBackgroundColor(color tcell.Color) {
m.Lock()
defer m.Unlock()
m.form.SetButtonBackgroundColor(color)
}
// SetButtonTextColor sets the color of the button texts.
func (m *Modal) SetButtonTextColor(color tcell.Color) {
m.Lock()
defer m.Unlock()
m.form.SetButtonTextColor(color)
}
// SetDoneFunc sets a handler which is called when one of the buttons was
// pressed. It receives the index of the button as well as its label text. The
// handler is also called when the user presses the Escape key. The index will
// then be negative and the label text an empty string.
func (m *Modal) SetDoneFunc(handler func(buttonIndex int, buttonLabel string)) {
m.Lock()
defer m.Unlock()
m.done = handler
}
// SetText sets the message text of the window. The text may contain line
// breaks. Note that words are wrapped, too, based on the final size of the
// window.
func (m *Modal) SetText(text string) {
m.Lock()
defer m.Unlock()
m.text = text
}
// GetForm returns the Form embedded in the window. The returned Form may be
// modified to include additional elements (e.g. AddInputField, AddFormItem).
func (m *Modal) GetForm() *Form {
m.RLock()
defer m.RUnlock()
return m.form
}
// GetFrame returns the Frame embedded in the window.
func (m *Modal) GetFrame() *Frame {
m.RLock()
defer m.RUnlock()
return m.frame
}
// AddButtons adds buttons to the window. There must be at least one button and
// a "done" handler so the window can be closed again.
func (m *Modal) AddButtons(labels []string) {
m.Lock()
defer m.Unlock()
for index, label := range labels {
func(i int, l string) {
m.form.AddButton(label, func() {
if m.done != nil {
m.done(i, l)
}
})
button := m.form.GetButton(m.form.GetButtonCount() - 1)
button.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
switch event.Key() {
case tcell.KeyDown, tcell.KeyRight:
return tcell.NewEventKey(tcell.KeyTab, 0, tcell.ModNone)
case tcell.KeyUp, tcell.KeyLeft:
return tcell.NewEventKey(tcell.KeyBacktab, 0, tcell.ModNone)
}
return event
})
}(index, label)
}
}
// ClearButtons removes all buttons from the window.
func (m *Modal) ClearButtons() {
m.Lock()
defer m.Unlock()
m.form.ClearButtons()
}
// SetFocus shifts the focus to the button with the given index.
func (m *Modal) SetFocus(index int) {
m.Lock()
defer m.Unlock()
m.form.SetFocus(index)
}
// Focus is called when this primitive receives focus.
func (m *Modal) Focus(delegate func(p Primitive)) {
delegate(m.form)
}
// HasFocus returns whether or not this primitive has focus.
func (m *Modal) HasFocus() bool {
return m.GetForm().HasFocus()
}
// Draw draws this primitive onto the screen.
func (m *Modal) Draw(screen tcell.Screen) {
if !m.GetVisible() {
return
}
formItemCount := m.form.GetFormItemCount()
m.Lock()
defer m.Unlock()
// Calculate the width of this Modal.
buttonsWidth := 0
for _, button := range m.form.buttons {
buttonsWidth += TaggedTextWidth(button.label) + 4 + 2
}
buttonsWidth -= 2
screenWidth, screenHeight := screen.Size()
width := screenWidth / 3
if width < buttonsWidth {
width = buttonsWidth
}
// width is now without the box border.
// Reset the text and find out how wide it is.
m.frame.Clear()
lines := WordWrap(m.text, width)
for _, line := range lines {
m.frame.AddText(line, true, AlignCenter, m.textColor)
}
// Set the Modal's position and size.
height := len(lines) + (formItemCount * 2) + 6
width += 4
x := (screenWidth - width) / 2
y := (screenHeight - height) / 2
m.SetRect(x, y, width, height)
// Draw the frame.
m.frame.SetRect(x, y, width, height)
m.frame.Draw(screen)
}
// MouseHandler returns the mouse handler for this primitive.
func (m *Modal) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
return m.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
// Pass mouse events on to the form.
consumed, capture = m.form.MouseHandler()(action, event, setFocus)
if !consumed && action == MouseLeftClick && m.InRect(event.Position()) {
setFocus(m)
consumed = true
}
return
})
}
package cview
import (
"sync"
"github.com/gdamore/tcell/v2"
)
// panel represents a single panel of a Panels object.
type panel struct {
Name string // The panel's name.
Item Primitive // The panel's primitive.
Resize bool // Whether or not to resize the panel when it is drawn.
Visible bool // Whether or not this panel is visible.
}
// Panels is a container for other primitives often used as the application's
// root primitive. It allows to easily switch the visibility of the contained
// primitives.
type Panels struct {
*Box
// The contained panels. (Visible) panels are drawn from back to front.
panels []*panel
// We keep a reference to the function which allows us to set the focus to
// a newly visible panel.
setFocus func(p Primitive)
// An optional handler which is called whenever the visibility or the order of
// panels changes.
changed func()
sync.RWMutex
}
// NewPanels returns a new Panels object.
func NewPanels() *Panels {
p := &Panels{
Box: NewBox(),
}
p.focus = p
return p
}
// SetChangedFunc sets a handler which is called whenever the visibility or the
// order of any visible panels changes. This can be used to redraw the panels.
func (p *Panels) SetChangedFunc(handler func()) {
p.Lock()
defer p.Unlock()
p.changed = handler
}
// GetPanelCount returns the number of panels currently stored in this object.
func (p *Panels) GetPanelCount() int {
p.RLock()
defer p.RUnlock()
return len(p.panels)
}
// AddPanel adds a new panel with the given name and primitive. If there was
// previously a panel with the same name, it is overwritten. Leaving the name
// empty may cause conflicts in other functions so always specify a non-empty
// name.
//
// Visible panels will be drawn in the order they were added (unless that order
// was changed in one of the other functions). If "resize" is set to true, the
// primitive will be set to the size available to the Panels primitive whenever
// the panels are drawn.
func (p *Panels) AddPanel(name string, item Primitive, resize, visible bool) {
hasFocus := p.HasFocus()
p.Lock()
defer p.Unlock()
var added bool
for i, pg := range p.panels {
if pg.Name == name {
p.panels[i] = &panel{Item: item, Name: name, Resize: resize, Visible: visible}
added = true
break
}
}
if !added {
p.panels = append(p.panels, &panel{Item: item, Name: name, Resize: resize, Visible: visible})
}
if p.changed != nil {
p.Unlock()
p.changed()
p.Lock()
}
if hasFocus {
p.Unlock()
p.Focus(p.setFocus)
p.Lock()
}
}
// RemovePanel removes the panel with the given name. If that panel was the only
// visible panel, visibility is assigned to the last panel.
func (p *Panels) RemovePanel(name string) {
hasFocus := p.HasFocus()
p.Lock()
defer p.Unlock()
var isVisible bool
for index, panel := range p.panels {
if panel.Name == name {
isVisible = panel.Visible
p.panels = append(p.panels[:index], p.panels[index+1:]...)
if panel.Visible && p.changed != nil {
p.Unlock()
p.changed()
p.Lock()
}
break
}
}
if isVisible {
for index, panel := range p.panels {
if index < len(p.panels)-1 {
if panel.Visible {
break // There is a remaining visible panel.
}
} else {
panel.Visible = true // We need at least one visible panel.
}
}
}
if hasFocus {
p.Unlock()
p.Focus(p.setFocus)
p.Lock()
}
}
// HasPanel returns true if a panel with the given name exists in this object.
func (p *Panels) HasPanel(name string) bool {
p.RLock()
defer p.RUnlock()
for _, panel := range p.panels {
if panel.Name == name {
return true
}
}
return false
}
// ShowPanel sets a panel's visibility to "true" (in addition to any other panels
// which are already visible).
func (p *Panels) ShowPanel(name string) {
hasFocus := p.HasFocus()
p.Lock()
defer p.Unlock()
for _, panel := range p.panels {
if panel.Name == name {
panel.Visible = true
if p.changed != nil {
p.Unlock()
p.changed()
p.Lock()
}
break
}
}
if hasFocus {
p.Unlock()
p.Focus(p.setFocus)
p.Lock()
}
}
// HidePanel sets a panel's visibility to "false".
func (p *Panels) HidePanel(name string) {
hasFocus := p.HasFocus()
p.Lock()
defer p.Unlock()
for _, panel := range p.panels {
if panel.Name == name {
panel.Visible = false
if p.changed != nil {
p.Unlock()
p.changed()
p.Lock()
}
break
}
}
if hasFocus {
p.Unlock()
p.Focus(p.setFocus)
p.Lock()
}
}
// SetCurrentPanel sets a panel's visibility to "true" and all other panels'
// visibility to "false".
func (p *Panels) SetCurrentPanel(name string) {
hasFocus := p.HasFocus()
p.Lock()
defer p.Unlock()
for _, panel := range p.panels {
if panel.Name == name {
panel.Visible = true
} else {
panel.Visible = false
}
}
if p.changed != nil {
p.Unlock()
p.changed()
p.Lock()
}
if hasFocus {
p.Unlock()
p.Focus(p.setFocus)
p.Lock()
}
}
// SendToFront changes the order of the panels such that the panel with the given
// name comes last, causing it to be drawn last with the next update (if
// visible).
func (p *Panels) SendToFront(name string) {
hasFocus := p.HasFocus()
p.Lock()
defer p.Unlock()
for index, panel := range p.panels {
if panel.Name == name {
if index < len(p.panels)-1 {
p.panels = append(append(p.panels[:index], p.panels[index+1:]...), panel)
}
if panel.Visible && p.changed != nil {
p.Unlock()
p.changed()
p.Lock()
}
break
}
}
if hasFocus {
p.Unlock()
p.Focus(p.setFocus)
p.Lock()
}
}
// SendToBack changes the order of the panels such that the panel with the given
// name comes first, causing it to be drawn first with the next update (if
// visible).
func (p *Panels) SendToBack(name string) {
hasFocus := p.HasFocus()
p.Lock()
defer p.Unlock()
for index, pg := range p.panels {
if pg.Name == name {
if index > 0 {
p.panels = append(append([]*panel{pg}, p.panels[:index]...), p.panels[index+1:]...)
}
if pg.Visible && p.changed != nil {
p.Unlock()
p.changed()
p.Lock()
}
break
}
}
if hasFocus {
p.Unlock()
p.Focus(p.setFocus)
p.Lock()
}
}
// GetFrontPanel returns the front-most visible panel. If there are no visible
// panels, ("", nil) is returned.
func (p *Panels) GetFrontPanel() (name string, item Primitive) {
p.RLock()
defer p.RUnlock()
for index := len(p.panels) - 1; index >= 0; index-- {
if p.panels[index].Visible {
return p.panels[index].Name, p.panels[index].Item
}
}
return
}
// HasFocus returns whether or not this primitive has focus.
func (p *Panels) HasFocus() bool {
p.RLock()
defer p.RUnlock()
for _, panel := range p.panels {
if panel.Item.GetFocusable().HasFocus() {
return true
}
}
return false
}
// Focus is called by the application when the primitive receives focus.
func (p *Panels) Focus(delegate func(p Primitive)) {
p.Lock()
defer p.Unlock()
if delegate == nil {
return // We cannot delegate so we cannot focus.
}
p.setFocus = delegate
var topItem Primitive
for _, panel := range p.panels {
if panel.Visible {
topItem = panel.Item
}
}
if topItem != nil {
p.Unlock()
delegate(topItem)
p.Lock()
}
}
// Draw draws this primitive onto the screen.
func (p *Panels) Draw(screen tcell.Screen) {
if !p.GetVisible() {
return
}
p.Box.Draw(screen)
p.Lock()
defer p.Unlock()
x, y, width, height := p.GetInnerRect()
for _, panel := range p.panels {
if !panel.Visible {
continue
}
if panel.Resize {
panel.Item.SetRect(x, y, width, height)
}
panel.Item.Draw(screen)
}
}
// MouseHandler returns the mouse handler for this primitive.
func (p *Panels) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
return p.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
if !p.InRect(event.Position()) {
return false, nil
}
// Pass mouse events along to the last visible panel item that takes it.
for index := len(p.panels) - 1; index >= 0; index-- {
panel := p.panels[index]
if panel.Visible {
consumed, capture = panel.Item.MouseHandler()(action, event, setFocus)
if consumed {
return
}
}
}
return
})
}
// Support backwards compatibility with Pages.
type page = panel
// Pages is a wrapper around Panels.
//
// Deprecated: This type is provided for backwards compatibility.
// Developers should use Panels instead.
type Pages struct {
*Panels
}
// NewPages returns a new Panels object.
//
// Deprecated: This function is provided for backwards compatibility.
// Developers should use NewPanels instead.
func NewPages() *Pages {
return &Pages{NewPanels()}
}
// GetPageCount returns the number of panels currently stored in this object.
func (p *Pages) GetPageCount() int {
return p.GetPanelCount()
}
// AddPage adds a new panel with the given name and primitive.
func (p *Pages) AddPage(name string, item Primitive, resize, visible bool) {
p.AddPanel(name, item, resize, visible)
}
// AddAndSwitchToPage calls Add(), then SwitchTo() on that newly added panel.
func (p *Pages) AddAndSwitchToPage(name string, item Primitive, resize bool) {
p.AddPanel(name, item, resize, true)
p.SetCurrentPanel(name)
}
// RemovePage removes the panel with the given name.
func (p *Pages) RemovePage(name string) {
p.RemovePanel(name)
}
// HasPage returns true if a panel with the given name exists in this object.
func (p *Pages) HasPage(name string) bool {
return p.HasPanel(name)
}
// ShowPage sets a panel's visibility to "true".
func (p *Pages) ShowPage(name string) {
p.ShowPanel(name)
}
// HidePage sets a panel's visibility to "false".
func (p *Pages) HidePage(name string) {
p.HidePanel(name)
}
// SwitchToPage sets a panel's visibility to "true" and all other panels'
// visibility to "false".
func (p *Pages) SwitchToPage(name string) {
p.SetCurrentPanel(name)
}
// GetFrontPage returns the front-most visible panel.
func (p *Pages) GetFrontPage() (name string, item Primitive) {
return p.GetFrontPanel()
}
package cview
import (
"math"
"sync"
"github.com/gdamore/tcell/v2"
)
// ProgressBar indicates the progress of an operation.
type ProgressBar struct {
*Box
// Rune to use when rendering the empty area of the progress bar.
emptyRune rune
// Color of the empty area of the progress bar.
emptyColor tcell.Color
// Rune to use when rendering the filled area of the progress bar.
filledRune rune
// Color of the filled area of the progress bar.
filledColor tcell.Color
// If set to true, instead of filling from left to right, the bar is filled
// from bottom to top.
vertical bool
// Current progress.
progress int
// Progress required to fill the bar.
max int
sync.RWMutex
}
// NewProgressBar returns a new progress bar.
func NewProgressBar() *ProgressBar {
p := &ProgressBar{
Box: NewBox(),
emptyRune: tcell.RuneBlock,
emptyColor: Styles.PrimitiveBackgroundColor,
filledRune: tcell.RuneBlock,
filledColor: Styles.PrimaryTextColor,
max: 100,
}
p.SetBackgroundColor(Styles.PrimitiveBackgroundColor)
return p
}
// SetEmptyRune sets the rune used for the empty area of the progress bar.
func (p *ProgressBar) SetEmptyRune(empty rune) {
p.Lock()
defer p.Unlock()
p.emptyRune = empty
}
// SetEmptyColor sets the color of the empty area of the progress bar.
func (p *ProgressBar) SetEmptyColor(empty tcell.Color) {
p.Lock()
defer p.Unlock()
p.emptyColor = empty
}
// SetFilledRune sets the rune used for the filled area of the progress bar.
func (p *ProgressBar) SetFilledRune(filled rune) {
p.Lock()
defer p.Unlock()
p.filledRune = filled
}
// SetFilledColor sets the color of the filled area of the progress bar.
func (p *ProgressBar) SetFilledColor(filled tcell.Color) {
p.Lock()
defer p.Unlock()
p.filledColor = filled
}
// SetVertical sets the direction of the progress bar.
func (p *ProgressBar) SetVertical(vertical bool) {
p.Lock()
defer p.Unlock()
p.vertical = vertical
}
// SetMax sets the progress required to fill the bar.
func (p *ProgressBar) SetMax(max int) {
p.Lock()
defer p.Unlock()
p.max = max
}
// GetMax returns the progress required to fill the bar.
func (p *ProgressBar) GetMax() int {
p.RLock()
defer p.RUnlock()
return p.max
}
// AddProgress adds to the current progress.
func (p *ProgressBar) AddProgress(progress int) {
p.Lock()
defer p.Unlock()
p.progress += progress
if p.progress < 0 {
p.progress = 0
} else if p.progress > p.max {
p.progress = p.max
}
}
// SetProgress sets the current progress.
func (p *ProgressBar) SetProgress(progress int) {
p.Lock()
defer p.Unlock()
p.progress = progress
if p.progress < 0 {
p.progress = 0
} else if p.progress > p.max {
p.progress = p.max
}
}
// GetProgress gets the current progress.
func (p *ProgressBar) GetProgress() int {
p.RLock()
defer p.RUnlock()
return p.progress
}
// Complete returns whether the progress bar has been filled.
func (p *ProgressBar) Complete() bool {
p.RLock()
defer p.RUnlock()
return p.progress >= p.max
}
// Draw draws this primitive onto the screen.
func (p *ProgressBar) Draw(screen tcell.Screen) {
if !p.GetVisible() {
return
}
p.Box.Draw(screen)
p.Lock()
defer p.Unlock()
x, y, width, height := p.GetInnerRect()
barSize := height
maxLength := width
if p.vertical {
barSize = width
maxLength = height
}
barLength := int(math.RoundToEven(float64(maxLength) * (float64(p.progress) / float64(p.max))))
if barLength > maxLength {
barLength = maxLength
}
for i := 0; i < barSize; i++ {
for j := 0; j < barLength; j++ {
if p.vertical {
screen.SetContent(x+i, y+(height-1-j), p.filledRune, nil, tcell.StyleDefault.Foreground(p.filledColor).Background(p.backgroundColor))
} else {
screen.SetContent(x+j, y+i, p.filledRune, nil, tcell.StyleDefault.Foreground(p.filledColor).Background(p.backgroundColor))
}
}
for j := barLength; j < maxLength; j++ {
if p.vertical {
screen.SetContent(x+i, y+(height-1-j), p.emptyRune, nil, tcell.StyleDefault.Foreground(p.emptyColor).Background(p.backgroundColor))
} else {
screen.SetContent(x+j, y+i, p.emptyRune, nil, tcell.StyleDefault.Foreground(p.emptyColor).Background(p.backgroundColor))
}
}
}
}
package cview
import "github.com/gdamore/tcell/v2"
// Semigraphics provides an easy way to access unicode characters for drawing.
//
// Named like the unicode characters, 'Semigraphics'-prefix used if unicode block
// isn't prefixed itself.
const (
// Block: General 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)
}
package cview
import (
"math"
"sync"
"github.com/gdamore/tcell/v2"
)
// Slider is a progress bar which may be modified via keyboard and mouse.
type Slider struct {
*ProgressBar
// The text to be displayed before the slider.
label []byte
// The screen width of the label area. A value of 0 means use the width of
// the label text.
labelWidth int
// The label color.
labelColor tcell.Color
// The label color when focused.
labelColorFocused tcell.Color
// The background color of the input area.
fieldBackgroundColor tcell.Color
// The background color of the input area when focused.
fieldBackgroundColorFocused tcell.Color
// The text color of the input area.
fieldTextColor tcell.Color
// The text color of the input area when focused.
fieldTextColorFocused tcell.Color
// The amount to increment by when modified via keyboard.
increment int
// Set to true when mouse dragging is in progress.
dragging bool
// An optional function which is called when the user changes the value of
// this slider.
changed func(value int)
// An optional function which is called when the user indicated that they
// are done entering text. The key which was pressed is provided (tab,
// shift-tab, or escape).
done func(tcell.Key)
// A callback function set by the Form class and called when the user leaves
// this form item.
finished func(tcell.Key)
sync.RWMutex
}
// NewSlider returns a new slider.
func NewSlider() *Slider {
s := &Slider{
ProgressBar: NewProgressBar(),
increment: 10,
labelColor: Styles.SecondaryTextColor,
fieldBackgroundColor: Styles.ContrastBackgroundColor,
fieldTextColor: Styles.PrimaryTextColor,
labelColorFocused: ColorUnset,
fieldBackgroundColorFocused: ColorUnset,
fieldTextColorFocused: ColorUnset,
}
return s
}
// SetLabel sets the text to be displayed before the input area.
func (s *Slider) SetLabel(label string) {
s.Lock()
defer s.Unlock()
s.label = []byte(label)
}
// GetLabel returns the text to be displayed before the input area.
func (s *Slider) GetLabel() string {
s.RLock()
defer s.RUnlock()
return string(s.label)
}
// SetLabelWidth sets the screen width of the label. A value of 0 will cause the
// primitive to use the width of the label string.
func (s *Slider) SetLabelWidth(width int) {
s.Lock()
defer s.Unlock()
s.labelWidth = width
}
// SetLabelColor sets the color of the label.
func (s *Slider) SetLabelColor(color tcell.Color) {
s.Lock()
defer s.Unlock()
s.labelColor = color
}
// SetLabelColorFocused sets the color of the label when focused.
func (s *Slider) SetLabelColorFocused(color tcell.Color) {
s.Lock()
defer s.Unlock()
s.labelColorFocused = color
}
// SetFieldBackgroundColor sets the background color of the input area.
func (s *Slider) SetFieldBackgroundColor(color tcell.Color) {
s.Lock()
defer s.Unlock()
s.fieldBackgroundColor = color
}
// SetFieldBackgroundColorFocused sets the background color of the input area when focused.
func (s *Slider) SetFieldBackgroundColorFocused(color tcell.Color) {
s.Lock()
defer s.Unlock()
s.fieldBackgroundColorFocused = color
}
// SetFieldTextColor sets the text color of the input area.
func (s *Slider) SetFieldTextColor(color tcell.Color) {
s.Lock()
defer s.Unlock()
s.fieldTextColor = color
}
// SetFieldTextColorFocused sets the text color of the input area when focused.
func (s *Slider) SetFieldTextColorFocused(color tcell.Color) {
s.Lock()
defer s.Unlock()
s.fieldTextColorFocused = color
}
// GetFieldHeight returns the height of the field.
func (s *Slider) GetFieldHeight() int {
return 1
}
// GetFieldWidth returns this primitive's field width.
func (s *Slider) GetFieldWidth() int {
return 0
}
// SetIncrement sets the amount the slider is incremented by when modified via
// keyboard.
func (s *Slider) SetIncrement(increment int) {
s.Lock()
defer s.Unlock()
s.increment = increment
}
// SetChangedFunc sets a handler which is called when the value of this slider
// was changed by the user. The handler function receives the new value.
func (s *Slider) SetChangedFunc(handler func(value int)) {
s.Lock()
defer s.Unlock()
s.changed = handler
}
// SetDoneFunc sets a handler which is called when the user is done using the
// slider. The callback function is provided with the key that was pressed,
// which is one of the following:
//
// - KeyEscape: Abort text input.
// - KeyTab: Move to the next field.
// - KeyBacktab: Move to the previous field.
func (s *Slider) SetDoneFunc(handler func(key tcell.Key)) {
s.Lock()
defer s.Unlock()
s.done = handler
}
// SetFinishedFunc sets a callback invoked when the user leaves this form item.
func (s *Slider) SetFinishedFunc(handler func(key tcell.Key)) {
s.Lock()
defer s.Unlock()
s.finished = handler
}
// Draw draws this primitive onto the screen.
func (s *Slider) Draw(screen tcell.Screen) {
if !s.GetVisible() {
return
}
s.Box.Draw(screen)
hasFocus := s.GetFocusable().HasFocus()
s.Lock()
// Select colors
labelColor := s.labelColor
fieldBackgroundColor := s.fieldBackgroundColor
fieldTextColor := s.fieldTextColor
if hasFocus {
if s.labelColorFocused != ColorUnset {
labelColor = s.labelColorFocused
}
if s.fieldBackgroundColorFocused != ColorUnset {
fieldBackgroundColor = s.fieldBackgroundColorFocused
}
if s.fieldTextColorFocused != ColorUnset {
fieldTextColor = s.fieldTextColorFocused
}
}
// Prepare.
x, y, width, height := s.GetInnerRect()
rightLimit := x + width
if height < 1 || rightLimit <= x {
s.Unlock()
return
}
// Draw label.
if len(s.label) > 0 {
if s.vertical {
height--
// TODO draw label on bottom
} else {
if s.labelWidth > 0 {
labelWidth := s.labelWidth
if labelWidth > rightLimit-x {
labelWidth = rightLimit - x
}
Print(screen, []byte(s.label), x, y, labelWidth, AlignLeft, labelColor)
x += labelWidth + 1
width -= labelWidth + 1
} else {
_, drawnWidth := Print(screen, []byte(s.label), x, y, rightLimit-x, AlignLeft, labelColor)
x += drawnWidth + 1
width -= drawnWidth + 1
}
}
}
// Draw slider.
s.Unlock()
s.ProgressBar.SetRect(x, y, width, height)
s.ProgressBar.SetEmptyColor(fieldBackgroundColor)
s.ProgressBar.SetFilledColor(fieldTextColor)
s.ProgressBar.Draw(screen)
}
// InputHandler returns the handler for this primitive.
func (s *Slider) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) {
return s.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p Primitive)) {
if HitShortcut(event, Keys.Cancel, Keys.MovePreviousField, Keys.MoveNextField) {
if s.done != nil {
s.done(event.Key())
}
if s.finished != nil {
s.finished(event.Key())
}
return
}
previous := s.progress
if HitShortcut(event, Keys.MoveFirst, Keys.MoveFirst2) {
s.SetProgress(0)
} else if HitShortcut(event, Keys.MoveLast, Keys.MoveLast2) {
s.SetProgress(s.max)
} else if HitShortcut(event, Keys.MoveUp, Keys.MoveUp2, Keys.MoveRight, Keys.MoveRight2, Keys.MovePreviousField) {
s.AddProgress(s.increment)
} else if HitShortcut(event, Keys.MoveDown, Keys.MoveDown2, Keys.MoveLeft, Keys.MoveLeft2, Keys.MoveNextField) {
s.AddProgress(s.increment * -1)
}
if s.progress != previous && s.changed != nil {
s.changed(s.progress)
}
})
}
// MouseHandler returns the mouse handler for this primitive.
func (s *Slider) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
return s.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
x, y := event.Position()
if !s.InRect(x, y) {
s.dragging = false
return false, nil
}
// Process mouse event.
if action == MouseLeftClick {
setFocus(s)
consumed = true
}
handleMouse := func() {
if !s.ProgressBar.InRect(x, y) {
s.dragging = false
return
}
bx, by, bw, bh := s.GetInnerRect()
var clickPos, clickRange int
if s.ProgressBar.vertical {
clickPos = (bh - 1) - (y - by)
clickRange = bh - 1
} else {
clickPos = x - bx
clickRange = bw - 1
}
setValue := int(math.Floor(float64(s.max) * (float64(clickPos) / float64(clickRange))))
if setValue != s.progress {
s.SetProgress(setValue)
if s.changed != nil {
s.changed(s.progress)
}
}
}
// Handle dragging. Clicks are implicitly handled by this logic.
switch action {
case MouseLeftDown:
setFocus(s)
consumed = true
capture = s
s.dragging = true
handleMouse()
case MouseMove:
if s.dragging {
consumed = true
capture = s
handleMouse()
}
case MouseLeftUp:
if s.dragging {
consumed = true
s.dragging = false
handleMouse()
}
}
return
})
}
package cview
import (
"bytes"
"fmt"
"sync"
"github.com/gdamore/tcell/v2"
)
// TabbedPanels is a tabbed container for other primitives. The tab switcher
// may be positioned vertically or horizontally, before or after the content.
type TabbedPanels struct {
*Flex
Switcher *TextView
panels *Panels
tabLabels map[string]string
currentTab string
tabTextColor tcell.Color
tabTextColorFocused tcell.Color
tabBackgroundColor tcell.Color
tabBackgroundColorFocused tcell.Color
dividerStart string
dividerMid string
dividerEnd string
switcherVertical bool
switcherAfterContent bool
width, lastWidth int
setFocus func(Primitive)
sync.RWMutex
}
// NewTabbedPanels returns a new TabbedPanels object.
func NewTabbedPanels() *TabbedPanels {
t := &TabbedPanels{
Flex: NewFlex(),
Switcher: NewTextView(),
panels: NewPanels(),
tabTextColor: Styles.PrimaryTextColor,
tabTextColorFocused: Styles.InverseTextColor,
tabBackgroundColor: ColorUnset,
tabBackgroundColorFocused: Styles.PrimaryTextColor,
dividerMid: string(BoxDrawingsDoubleVertical),
dividerEnd: string(BoxDrawingsLightVertical),
tabLabels: make(map[string]string),
}
s := t.Switcher
s.SetDynamicColors(true)
s.SetRegions(true)
s.SetWrap(true)
s.SetWordWrap(true)
s.SetHighlightedFunc(func(added, removed, remaining []string) {
if len(added) == 0 {
return
}
t.SetCurrentTab(added[0])
if t.setFocus != nil {
t.setFocus(t.panels)
}
s.Highlight()
})
t.rebuild()
return t
}
// AddTab adds a new tab. Tab names should consist only of letters, numbers
// and spaces.
func (t *TabbedPanels) AddTab(name, label string, item Primitive) {
t.Lock()
t.tabLabels[name] = label
t.Unlock()
t.panels.AddPanel(name, item, true, false)
t.updateAll()
}
// RemoveTab removes a tab.
func (t *TabbedPanels) RemoveTab(name string) {
t.panels.RemovePanel(name)
t.updateAll()
}
// HasTab returns true if a tab with the given name exists in this object.
func (t *TabbedPanels) HasTab(name string) bool {
t.RLock()
defer t.RUnlock()
for _, panel := range t.panels.panels {
if panel.Name == name {
return true
}
}
return false
}
// SetCurrentTab sets the currently visible tab.
func (t *TabbedPanels) SetCurrentTab(name string) {
t.Lock()
if t.currentTab == name {
t.Unlock()
return
}
t.currentTab = name
t.updateAll()
t.Unlock()
t.Switcher.Highlight(t.currentTab)
}
// GetCurrentTab returns the currently visible tab.
func (t *TabbedPanels) GetCurrentTab() string {
t.RLock()
defer t.RUnlock()
return t.currentTab
}
// SetTabLabel sets the label of a tab.
func (t *TabbedPanels) SetTabLabel(name, label string) {
t.Lock()
defer t.Unlock()
if t.tabLabels[name] == label {
return
}
t.tabLabels[name] = label
t.updateTabLabels()
}
// SetTabTextColor sets the color of the tab text.
func (t *TabbedPanels) SetTabTextColor(color tcell.Color) {
t.Lock()
defer t.Unlock()
t.tabTextColor = color
}
// SetTabTextColorFocused sets the color of the tab text when the tab is in focus.
func (t *TabbedPanels) SetTabTextColorFocused(color tcell.Color) {
t.Lock()
defer t.Unlock()
t.tabTextColorFocused = color
}
// SetTabBackgroundColor sets the background color of the tab.
func (t *TabbedPanels) SetTabBackgroundColor(color tcell.Color) {
t.Lock()
defer t.Unlock()
t.tabBackgroundColor = color
}
// SetTabBackgroundColorFocused sets the background color of the tab when the
// tab is in focus.
func (t *TabbedPanels) SetTabBackgroundColorFocused(color tcell.Color) {
t.Lock()
defer t.Unlock()
t.tabBackgroundColorFocused = color
}
// SetTabSwitcherDivider sets the tab switcher divider text. Color tags are supported.
func (t *TabbedPanels) SetTabSwitcherDivider(start, mid, end string) {
t.Lock()
defer t.Unlock()
t.dividerStart, t.dividerMid, t.dividerEnd = start, mid, end
}
// SetTabSwitcherVertical sets the orientation of the tab switcher.
func (t *TabbedPanels) SetTabSwitcherVertical(vertical bool) {
t.Lock()
defer t.Unlock()
if t.switcherVertical == vertical {
return
}
t.switcherVertical = vertical
t.rebuild()
}
// SetTabSwitcherAfterContent sets whether the tab switcher is positioned after content.
func (t *TabbedPanels) SetTabSwitcherAfterContent(after bool) {
t.Lock()
defer t.Unlock()
if t.switcherAfterContent == after {
return
}
t.switcherAfterContent = after
t.rebuild()
}
func (t *TabbedPanels) rebuild() {
f := t.Flex
if t.switcherVertical {
f.SetDirection(FlexColumn)
} else {
f.SetDirection(FlexRow)
}
f.RemoveItem(t.panels)
f.RemoveItem(t.Switcher)
if t.switcherAfterContent {
f.AddItem(t.panels, 0, 1, true)
f.AddItem(t.Switcher, 1, 1, false)
} else {
f.AddItem(t.Switcher, 1, 1, false)
f.AddItem(t.panels, 0, 1, true)
}
t.updateTabLabels()
}
func (t *TabbedPanels) updateTabLabels() {
if len(t.panels.panels) == 0 {
t.Switcher.SetText("")
t.Flex.ResizeItem(t.Switcher, 0, 1)
return
}
maxWidth := 0
for _, panel := range t.panels.panels {
label := t.tabLabels[panel.Name]
if len(label) > maxWidth {
maxWidth = len(label)
}
}
var b bytes.Buffer
if !t.switcherVertical {
b.WriteString(t.dividerStart)
}
l := len(t.panels.panels)
spacer := []byte(" ")
for i, panel := range t.panels.panels {
if i > 0 && t.switcherVertical {
b.WriteRune('\n')
}
if t.switcherVertical && t.switcherAfterContent {
b.WriteString(t.dividerMid)
b.WriteRune(' ')
}
textColor := t.tabTextColor
backgroundColor := t.tabBackgroundColor
if panel.Name == t.currentTab {
textColor = t.tabTextColorFocused
backgroundColor = t.tabBackgroundColorFocused
}
label := t.tabLabels[panel.Name]
if !t.switcherVertical {
label = " " + label
}
if t.switcherVertical {
spacer = bytes.Repeat([]byte(" "), maxWidth-len(label)+1)
}
b.WriteString(fmt.Sprintf(`["%s"][%s:%s]%s%s[-:-][""]`, panel.Name, ColorHex(textColor), ColorHex(backgroundColor), label, spacer))
if i == l-1 && !t.switcherVertical {
b.WriteString(t.dividerEnd)
} else if !t.switcherAfterContent {
b.WriteString(t.dividerMid)
}
}
t.Switcher.SetText(b.String())
var reqLines int
if t.switcherVertical {
reqLines = maxWidth + 2
} else {
reqLines = len(WordWrap(t.Switcher.GetText(true), t.width))
if reqLines < 1 {
reqLines = 1
}
}
t.Flex.ResizeItem(t.Switcher, reqLines, 1)
}
func (t *TabbedPanels) updateVisibleTabs() {
allPanels := t.panels.panels
var newTab string
var foundCurrent bool
for _, panel := range allPanels {
if panel.Name == t.currentTab {
newTab = panel.Name
foundCurrent = true
break
}
}
if !foundCurrent {
for _, panel := range allPanels {
if panel.Name != "" {
newTab = panel.Name
break
}
}
}
if t.currentTab != newTab {
t.SetCurrentTab(newTab)
return
}
for _, panel := range allPanels {
if panel.Name == t.currentTab {
t.panels.ShowPanel(panel.Name)
} else {
t.panels.HidePanel(panel.Name)
}
}
}
func (t *TabbedPanels) updateAll() {
t.updateTabLabels()
t.updateVisibleTabs()
}
// Draw draws this primitive onto the screen.
func (t *TabbedPanels) Draw(screen tcell.Screen) {
if !t.GetVisible() {
return
}
t.Box.Draw(screen)
_, _, t.width, _ = t.GetInnerRect()
if t.width != t.lastWidth {
t.updateTabLabels()
}
t.lastWidth = t.width
t.Flex.Draw(screen)
}
// InputHandler returns the handler for this primitive.
func (t *TabbedPanels) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) {
return t.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p Primitive)) {
if t.setFocus == nil {
t.setFocus = setFocus
}
t.Flex.InputHandler()(event, setFocus)
})
}
// MouseHandler returns the mouse handler for this primitive.
func (t *TabbedPanels) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
return t.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
if t.setFocus == nil {
t.setFocus = setFocus
}
x, y := event.Position()
if !t.InRect(x, y) {
return false, nil
}
if t.Switcher.InRect(x, y) {
if t.setFocus != nil {
defer t.setFocus(t.panels)
}
defer t.Switcher.MouseHandler()(action, event, setFocus)
return true, nil
}
return t.Flex.MouseHandler()(action, event, setFocus)
})
}
package cview
import (
"bytes"
"sort"
"sync"
"github.com/gdamore/tcell/v2"
colorful "github.com/lucasb-eyer/go-colorful"
)
// TableCell represents one cell inside a Table. You can instantiate this type
// directly but all colors (background and text) will be set to their default
// which is black.
type TableCell struct {
// The reference object.
Reference interface{}
// The text to be displayed in the table cell.
Text []byte
// The alignment of the cell text. One of AlignLeft (default), AlignCenter,
// or AlignRight.
Align int
// The maximum width of the cell in screen space. This is used to give a
// column a maximum width. Any cell text whose screen width exceeds this width
// is cut off. Set to 0 if there is no maximum width.
MaxWidth int
// If the total table width is less than the available width, this value is
// used to add extra width to a column. See SetExpansion() for details.
Expansion int
// The color of the cell text.
Color tcell.Color
// The background color of the cell.
BackgroundColor tcell.Color
// The style attributes of the cell.
Attributes tcell.AttrMask
// If set to true, this cell cannot be selected.
NotSelectable bool
// The position and width of the cell the last time table was drawn.
x, y, width int
sync.RWMutex
}
// NewTableCell returns a new table cell with sensible defaults. That is, left
// aligned text with the primary text color (see Styles) and a transparent
// background (using the background of the Table).
func NewTableCell(text string) *TableCell {
return &TableCell{
Text: []byte(text),
Align: AlignLeft,
Color: Styles.PrimaryTextColor,
BackgroundColor: tcell.ColorDefault,
}
}
// SetBytes sets the cell's text.
func (c *TableCell) SetBytes(text []byte) {
c.Lock()
defer c.Unlock()
c.Text = text
}
// SetText sets the cell's text.
func (c *TableCell) SetText(text string) {
c.SetBytes([]byte(text))
}
// GetBytes returns the cell's text.
func (c *TableCell) GetBytes() []byte {
c.RLock()
defer c.RUnlock()
return c.Text
}
// GetText returns the cell's text.
func (c *TableCell) GetText() string {
return string(c.GetBytes())
}
// SetAlign sets the cell's text alignment, one of AlignLeft, AlignCenter, or
// AlignRight.
func (c *TableCell) SetAlign(align int) {
c.Lock()
defer c.Unlock()
c.Align = align
}
// SetMaxWidth sets maximum width of the cell in screen space. This is used to
// give a column a maximum width. Any cell text whose screen width exceeds this
// width is cut off. Set to 0 if there is no maximum width.
func (c *TableCell) SetMaxWidth(maxWidth int) {
c.Lock()
defer c.Unlock()
c.MaxWidth = maxWidth
}
// SetExpansion sets the value by which the column of this cell expands if the
// available width for the table is more than the table width (prior to applying
// this expansion value). This is a proportional value. The amount of unused
// horizontal space is divided into widths to be added to each column. How much
// extra width a column receives depends on the expansion value: A value of 0
// (the default) will not cause the column to increase in width. Other values
// are proportional, e.g. a value of 2 will cause a column to grow by twice
// the amount of a column with a value of 1.
//
// Since this value affects an entire column, the maximum over all visible cells
// in that column is used.
//
// This function panics if a negative value is provided.
func (c *TableCell) SetExpansion(expansion int) {
c.Lock()
defer c.Unlock()
if expansion < 0 {
panic("Table cell expansion values may not be negative")
}
c.Expansion = expansion
}
// SetTextColor sets the cell's text color.
func (c *TableCell) SetTextColor(color tcell.Color) {
c.Lock()
defer c.Unlock()
c.Color = color
}
// SetBackgroundColor sets the cell's background color. Set to
// tcell.ColorDefault to use the table's background color.
func (c *TableCell) SetBackgroundColor(color tcell.Color) {
c.Lock()
defer c.Unlock()
c.BackgroundColor = color
}
// SetAttributes sets the cell's text attributes. You can combine different
// attributes using bitmask operations:
//
// cell.SetAttributes(tcell.AttrUnderline | tcell.AttrBold)
func (c *TableCell) SetAttributes(attr tcell.AttrMask) {
c.Lock()
defer c.Unlock()
c.Attributes = attr
}
// SetStyle sets the cell's style (foreground color, background color, and
// attributes) all at once.
func (c *TableCell) SetStyle(style tcell.Style) {
c.Lock()
defer c.Unlock()
c.Color, c.BackgroundColor, c.Attributes = style.Decompose()
}
// SetSelectable sets whether or not this cell can be selected by the user.
func (c *TableCell) SetSelectable(selectable bool) {
c.Lock()
defer c.Unlock()
c.NotSelectable = !selectable
}
// SetReference allows you to store a reference of any type in this cell. This
// will allow you to establish a mapping between the cell and your
// actual data.
func (c *TableCell) SetReference(reference interface{}) {
c.Lock()
defer c.Unlock()
c.Reference = reference
}
// GetReference returns this cell's reference object.
func (c *TableCell) GetReference() interface{} {
c.RLock()
defer c.RUnlock()
return c.Reference
}
// GetLastPosition returns the position of the table cell the last time it was
// drawn on screen. If the cell is not on screen, the return values are
// undefined.
//
// Because the Table class will attempt to keep selected cells on screen, this
// function is most useful in response to a "selected" event (see
// SetSelectedFunc()) or a "selectionChanged" event (see
// SetSelectionChangedFunc()).
func (c *TableCell) GetLastPosition() (x, y, width int) {
c.RLock()
defer c.RUnlock()
return c.x, c.y, c.width
}
// Table visualizes two-dimensional data consisting of rows and columns. Each
// Table cell is defined via SetCell() by the TableCell type. They can be added
// dynamically to the table and changed any time.
//
// Each row of the table must have the same number of columns when it is drawn
// or navigated. This isn't strictly enforced, however you may encounter issues
// when navigating a table with rows of varied column sizes.
//
// The most compact display of a table is without borders. Each row will then
// occupy one row on screen and columns are separated by the rune defined via
// SetSeparator() (a space character by default).
//
// When borders are turned on (via SetBorders()), each table cell is surrounded
// by lines. Therefore one table row will require two rows on screen.
//
// Columns will use as much horizontal space as they need. You can constrain
// their size with the MaxWidth parameter of the TableCell type.
//
// Fixed Columns
//
// You can define fixed rows and rolumns via SetFixed(). They will always stay
// in their place, even when the table is scrolled. Fixed rows are always the
// top rows. Fixed columns are always the leftmost columns.
//
// Selections
//
// You can call SetSelectable() to set columns and/or rows to "selectable". If
// the flag is set only for columns, entire columns can be selected by the user.
// If it is set only for rows, entire rows can be selected. If both flags are
// set, individual cells can be selected. The "selected" handler set via
// SetSelectedFunc() is invoked when the user presses Enter on a selection.
//
// Navigation
//
// If the table extends beyond the available space, it can be navigated with
// key bindings similar to Vim:
//
// - h, left arrow: Move left by one column.
// - l, right arrow: Move right by one column.
// - j, down arrow: Move down by one row.
// - k, up arrow: Move up by one row.
// - g, home: Move to the top.
// - G, end: Move to the bottom.
// - Ctrl-F, page down: Move down by one page.
// - Ctrl-B, page up: Move up by one page.
//
// When there is no selection, this affects the entire table (except for fixed
// rows and columns). When there is a selection, the user moves the selection.
// The class will attempt to keep the selection from moving out of the screen.
//
// Use SetInputCapture() to override or modify keyboard input.
type Table struct {
*Box
// Whether or not this table has borders around each cell.
cellBorders bool
// The color of the cell borders or the separator.
cellBorderColor tcell.Color
// If there are no borders, the column separator.
separator rune
// The size of the padding between each cell
rowPadding, columnPadding int
// The cells of the table. Rows first, then columns.
cells [][]*TableCell
// The rightmost column in the data set.
lastColumn int
// If true, when calculating the widths of the columns, all rows are evaluated
// instead of only the visible ones.
evaluateAllRows bool
// The number of fixed rows / columns.
fixedRows, fixedColumns int
// Whether or not rows or columns can be selected. If both are set to true,
// cells can be selected.
rowsSelectable, columnsSelectable bool
// The currently selected row and column.
selectedRow, selectedColumn int
// The number of rows/columns by which the table is scrolled down/to the
// right.
rowOffset, columnOffset int
// If set to true, the table's last row will always be visible.
trackEnd bool
// The sort function of the table. Defaults to a case-sensitive comparison.
sortFunc func(column, i, j int) bool
// Whether or not the table should be sorted when a fixed row is clicked.
sortClicked bool
// The last direction the table was sorted by when clicked.
sortClickedDescending bool
// The last column the table was sorted by when clicked.
sortClickedColumn int
// The number of visible rows the last time the table was drawn.
visibleRows int
// The indices of the visible columns as of the last time the table was drawn.
visibleColumnIndices []int
// The net widths of the visible columns as of the last time the table was
// drawn.
visibleColumnWidths []int
// Visibility of the scroll bar.
scrollBarVisibility ScrollBarVisibility
// The scroll bar color.
scrollBarColor tcell.Color
// The style of the selected rows. If this value is StyleDefault, 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.
selected func(row, column int)
// An optional function which gets called when the user changes the selection.
// If entire rows selected, the column value is undefined.
// Likewise for entire columns.
selectionChanged func(row, column int)
// An optional function which gets called when the user presses Escape, Tab,
// or Backtab. Also when the user presses Enter if nothing is selectable.
done func(key tcell.Key)
sync.RWMutex
}
// NewTable returns a new table.
func NewTable() *Table {
return &Table{
Box: NewBox(),
scrollBarVisibility: ScrollBarAuto,
scrollBarColor: Styles.ScrollBarColor,
cellBorderColor: Styles.GraphicsColor,
separator: ' ',
sortClicked: true,
lastColumn: -1,
}
}
// Clear removes all table data.
func (t *Table) Clear() {
t.Lock()
defer t.Unlock()
t.cells = nil
t.lastColumn = -1
}
// SetCellBorders sets whether or not each cell in the table is surrounded by a
// border.
func (t *Table) SetCellBorders(show bool) {
t.Lock()
defer t.Unlock()
t.cellBorders = show
}
// SetCellBorderColor sets the color of the cell borders.
func (t *Table) SetCellBorderColor(color tcell.Color) {
t.Lock()
defer t.Unlock()
t.cellBorderColor = color
}
// SetCellPadding sets the size of the padding between each cell.
func (t *Table) SetCellPadding(row, column int) {
t.Lock()
defer t.Unlock()
if t.rowPadding < 0 {
t.rowPadding = 0
}
if t.columnPadding < 0 {
t.columnPadding = 0
}
t.rowPadding, t.columnPadding = row, column
}
// SetScrollBarVisibility specifies the display of the scroll bar.
func (t *Table) SetScrollBarVisibility(visibility ScrollBarVisibility) {
t.Lock()
defer t.Unlock()
t.scrollBarVisibility = visibility
}
// SetScrollBarColor sets the color of the scroll bar.
func (t *Table) SetScrollBarColor(color tcell.Color) {
t.Lock()
defer t.Unlock()
t.scrollBarColor = color
}
// 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) {
t.Lock()
defer t.Unlock()
t.selectedStyle = SetAttributes(tcell.StyleDefault.Foreground(foregroundColor).Background(backgroundColor), attributes)
}
// 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 Borders.Vertical (or any other rune) if the column
// separation should be more visible. If cell borders are activated, this is
// ignored.
//
// Separators have the same color as borders.
func (t *Table) SetSeparator(separator rune) {
t.Lock()
defer t.Unlock()
t.separator = separator
}
// SetFixed sets the number of fixed rows and columns which are always visible
// even when the rest of the cells are scrolled out of view. Rows are always the
// top-most ones. Columns are always the left-most ones.
func (t *Table) SetFixed(rows, columns int) {
t.Lock()
defer t.Unlock()
t.fixedRows, t.fixedColumns = rows, columns
}
// SetSelectable sets the flags which determine what can be selected in a table.
// There are three selection modi:
//
// - rows = false, columns = false: Nothing can be selected.
// - rows = true, columns = false: Rows can be selected.
// - rows = false, columns = true: Columns can be selected.
// - rows = true, columns = true: Individual cells can be selected.
func (t *Table) SetSelectable(rows, columns bool) {
t.Lock()
defer t.Unlock()
t.rowsSelectable, t.columnsSelectable = rows, columns
}
// GetSelectable returns what can be selected in a table. Refer to
// SetSelectable() for details.
func (t *Table) GetSelectable() (rows, columns bool) {
t.RLock()
defer t.RUnlock()
return t.rowsSelectable, t.columnsSelectable
}
// GetSelection returns the position of the current selection.
// If entire rows are selected, the column index is undefined.
// Likewise for entire columns.
func (t *Table) GetSelection() (row, column int) {
t.RLock()
defer t.RUnlock()
return t.selectedRow, t.selectedColumn
}
// Select sets the selected cell. Depending on the selection settings
// specified via SetSelectable(), this may be an entire row or column, or even
// ignored completely. The "selection changed" event is fired if such a callback
// is available (even if the selection ends up being the same as before and even
// if cells are not selectable).
func (t *Table) Select(row, column int) {
t.Lock()
defer t.Unlock()
t.selectedRow, t.selectedColumn = row, column
if t.selectionChanged != nil {
t.Unlock()
t.selectionChanged(row, column)
t.Lock()
}
}
// SetOffset sets how many rows and columns should be skipped when drawing the
// table. This is useful for large tables that do not fit on the screen.
// Navigating a selection can change these values.
//
// Fixed rows and columns are never skipped.
func (t *Table) SetOffset(row, column int) {
t.Lock()
defer t.Unlock()
t.rowOffset, t.columnOffset = row, column
t.trackEnd = false
}
// GetOffset returns the current row and column offset. This indicates how many
// rows and columns the table is scrolled down and to the right.
func (t *Table) GetOffset() (row, column int) {
t.RLock()
defer t.RUnlock()
return t.rowOffset, t.columnOffset
}
// SetEvaluateAllRows sets a flag which determines the rows to be evaluated when
// calculating the widths of the table's columns. When false, only visible rows
// are evaluated. When true, all rows in the table are evaluated.
//
// Set this flag to true to avoid shifting column widths when the table is
// scrolled. (May be slower for large tables.)
func (t *Table) SetEvaluateAllRows(all bool) {
t.Lock()
defer t.Unlock()
t.evaluateAllRows = all
}
// SetSelectedFunc sets a handler which is called whenever the user presses the
// Enter key on a selected cell/row/column. The handler receives the position of
// the selection and its cell contents. If entire rows are selected, the column
// index is undefined. Likewise for entire columns.
func (t *Table) SetSelectedFunc(handler func(row, column int)) {
t.Lock()
defer t.Unlock()
t.selected = handler
}
// SetSelectionChangedFunc sets a handler which is called whenever the current
// selection changes. The handler receives the position of the new selection.
// If entire rows are selected, the column index is undefined. Likewise for
// entire columns.
func (t *Table) SetSelectionChangedFunc(handler func(row, column int)) {
t.Lock()
defer t.Unlock()
t.selectionChanged = handler
}
// SetDoneFunc sets a handler which is called whenever the user presses the
// Escape, Tab, or Backtab key. If nothing is selected, it is also called when
// user presses the Enter key (because pressing Enter on a selection triggers
// the "selected" handler set via SetSelectedFunc()).
func (t *Table) SetDoneFunc(handler func(key tcell.Key)) {
t.Lock()
defer t.Unlock()
t.done = handler
}
// SetCell sets the content of a cell the specified position. It is ok to
// 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
// automatically extend the internal table representation, e.g. starting with
// a row of 100,000 will immediately create 100,000 empty rows.
//
// To avoid unnecessary garbage collection, fill columns from left to right.
func (t *Table) SetCell(row, column int, cell *TableCell) {
t.Lock()
defer t.Unlock()
if row >= len(t.cells) {
t.cells = append(t.cells, make([][]*TableCell, row-len(t.cells)+1)...)
}
rowLen := len(t.cells[row])
if column >= rowLen {
t.cells[row] = append(t.cells[row], make([]*TableCell, column-rowLen+1)...)
for c := rowLen; c < column; c++ {
t.cells[row][c] = &TableCell{}
}
}
t.cells[row][column] = cell
if column > t.lastColumn {
t.lastColumn = column
}
}
// SetCellSimple calls SetCell() with the given text, left-aligned, in white.
func (t *Table) SetCellSimple(row, column int, text string) {
t.SetCell(row, column, NewTableCell(text))
}
// GetCell returns the contents of the cell at the specified position. A valid
// TableCell object is always returned but it will be uninitialized if the cell
// was not previously set. Such an uninitialized object will not automatically
// be inserted. Therefore, repeated calls to this function may return different
// pointers for uninitialized cells.
func (t *Table) GetCell(row, column int) *TableCell {
t.RLock()
defer t.RUnlock()
if row >= len(t.cells) || column >= len(t.cells[row]) {
return &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) {
t.Lock()
defer t.Unlock()
if row < 0 || row >= len(t.cells) {
return
}
t.cells = append(t.cells[:row], t.cells[row+1:]...)
}
// 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) {
t.Lock()
defer t.Unlock()
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:]...)
}
}
// InsertRow inserts a row before the row with the given index. Cells on the
// given row and below will be shifted to the bottom by one row. If "row" is
// equal or larger than the current number of rows, this function has no effect.
func (t *Table) InsertRow(row int) {
t.Lock()
defer t.Unlock()
if row >= len(t.cells) {
return
}
t.cells = append(t.cells, nil) // Extend by one.
copy(t.cells[row+1:], t.cells[row:]) // Shift down.
t.cells[row] = nil // New row is uninitialized.
}
// InsertColumn inserts a column before the column with the given index. Cells
// in the given column and to its right will be shifted to the right by one
// column. Rows that have fewer initialized cells than "column" will remain
// unchanged.
func (t *Table) InsertColumn(column int) {
t.Lock()
defer t.Unlock()
for row := range t.cells {
if column >= len(t.cells[row]) {
continue
}
t.cells[row] = append(t.cells[row], nil) // Extend by one.
copy(t.cells[row][column+1:], t.cells[row][column:]) // Shift to the right.
t.cells[row][column] = &TableCell{} // New element is an uninitialized table cell.
}
}
// GetRowCount returns the number of rows in the table.
func (t *Table) GetRowCount() int {
t.RLock()
defer t.RUnlock()
return len(t.cells)
}
// GetColumnCount returns the (maximum) number of columns in the table.
func (t *Table) GetColumnCount() int {
t.RLock()
defer t.RUnlock()
if len(t.cells) == 0 {
return 0
}
return t.lastColumn + 1
}
// cellAt returns the row and column located at the given screen coordinates.
// Each returned value may be negative if there is no row and/or cell. This
// function will also process coordinates outside the table's inner rectangle so
// callers will need to check for bounds themselves.
func (t *Table) cellAt(x, y int) (row, column int) {
rectX, rectY, _, _ := t.GetInnerRect()
// Determine row as seen on screen.
if t.cellBorders {
row = (y - rectY - 1) / 2
} else {
row = y - rectY
}
// Respect fixed rows and row offset.
if row >= 0 {
if row >= t.fixedRows {
row += t.rowOffset
}
if row >= len(t.cells) {
row = -1
}
}
// Search for the clicked column.
column = -1
if x >= rectX {
columnX := rectX
if t.cellBorders {
columnX++
}
for index, width := range t.visibleColumnWidths {
columnX += width + 1
if x < columnX {
column = t.visibleColumnIndices[index]
break
}
}
}
return
}
// ScrollToBeginning scrolls the table to the beginning to that the top left
// corner of the table is shown. Note that this position may be corrected if
// there is a selection.
func (t *Table) ScrollToBeginning() {
t.Lock()
defer t.Unlock()
t.trackEnd = false
t.columnOffset = 0
t.rowOffset = 0
}
// ScrollToEnd scrolls the table to the beginning to that the bottom left corner
// of the table is shown. Adding more rows to the table will cause it to
// automatically scroll with the new data. Note that this position may be
// corrected if there is a selection.
func (t *Table) ScrollToEnd() {
t.Lock()
defer t.Unlock()
t.trackEnd = true
t.columnOffset = 0
t.rowOffset = len(t.cells)
}
// SetSortClicked sets a flag which determines whether the table is sorted when
// a fixed row is clicked. This flag is enabled by default.
func (t *Table) SetSortClicked(sortClicked bool) {
t.Lock()
defer t.Unlock()
t.sortClicked = sortClicked
}
// SetSortFunc sets the sorting function used for the table. When unset, a
// case-sensitive string comparison is used.
func (t *Table) SetSortFunc(sortFunc func(column, i, j int) bool) {
t.Lock()
defer t.Unlock()
t.sortFunc = sortFunc
}
// Sort sorts the table by the column at the given index. You may set a custom
// sorting function with SetSortFunc.
func (t *Table) Sort(column int, descending bool) {
t.Lock()
defer t.Unlock()
if len(t.cells) == 0 || column < 0 || column >= len(t.cells[0]) {
return
}
if t.sortFunc == nil {
t.sortFunc = func(column, i, j int) bool {
return bytes.Compare(t.cells[i][column].Text, t.cells[j][column].Text) == -1
}
}
sort.SliceStable(t.cells, func(i, j int) bool {
if i < t.fixedRows {
return i < j
} else if j < t.fixedRows {
return j > i
}
if !descending {
return t.sortFunc(column, i, j)
}
return t.sortFunc(column, j, i)
})
}
// Draw draws this primitive onto the screen.
func (t *Table) Draw(screen tcell.Screen) {
if !t.GetVisible() {
return
}
t.Box.Draw(screen)
t.Lock()
defer t.Unlock()
// What's our available screen space?
x, y, width, height := t.GetInnerRect()
if t.cellBorders {
t.visibleRows = height / 2
} else {
t.visibleRows = height / (t.rowPadding + 1)
}
showVerticalScrollBar := t.scrollBarVisibility == ScrollBarAlways || (t.scrollBarVisibility == ScrollBarAuto && len(t.cells) > t.visibleRows-t.fixedRows)
if showVerticalScrollBar {
width-- // Subtract space for scroll bar.
}
// Return the cell at the specified position (nil if it doesn't exist).
getCell := func(row, column int) *TableCell {
if row < 0 || column < 0 || row >= len(t.cells) || column >= len(t.cells[row]) {
return nil
}
return t.cells[row][column]
}
// If this cell is not selectable, find the next one.
if t.rowsSelectable || t.columnsSelectable {
if t.selectedColumn < 0 {
t.selectedColumn = 0
}
if t.selectedRow < 0 {
t.selectedRow = 0
}
for t.selectedRow < len(t.cells) {
cell := getCell(t.selectedRow, t.selectedColumn)
if cell == nil || !cell.NotSelectable {
break
}
t.selectedColumn++
if t.selectedColumn > t.lastColumn {
t.selectedColumn = 0
t.selectedRow++
}
}
}
// Clamp row offsets.
if t.rowsSelectable {
if t.selectedRow >= t.fixedRows && t.selectedRow < t.fixedRows+t.rowOffset {
t.rowOffset = t.selectedRow - t.fixedRows
t.trackEnd = false
}
if t.cellBorders {
if 2*(t.selectedRow+1-t.rowOffset) >= height {
t.rowOffset = t.selectedRow + 1 - height/2
t.trackEnd = false
}
} else {
if t.selectedRow-t.rowOffset >= height/(t.rowPadding+1) {
t.rowOffset = t.selectedRow - height/(t.rowPadding+1)
t.trackEnd = false
}
}
}
if t.cellBorders {
if 2*(len(t.cells)-t.rowOffset) < height {
t.trackEnd = true
}
} else {
if len(t.cells)-t.rowOffset < height/(t.rowPadding+1) {
t.trackEnd = true
}
}
if t.trackEnd {
if t.cellBorders {
t.rowOffset = len(t.cells) - height/2
} else {
t.rowOffset = len(t.cells) - height/(t.rowPadding+1)
}
}
if t.rowOffset < 0 {
t.rowOffset = 0
}
// Clamp column offset. (Only left side here. The right side is more
// difficult and we'll do it below.)
if t.columnsSelectable && t.selectedColumn >= t.fixedColumns && t.selectedColumn < t.fixedColumns+t.columnOffset {
t.columnOffset = t.selectedColumn - t.fixedColumns
}
if t.columnOffset < 0 {
t.columnOffset = 0
}
if t.selectedColumn < 0 {
t.selectedColumn = 0
}
// Determine the indices and widths of the columns and rows which fit on the
// screen.
var (
columns, rows, allRows, widths []int
tableHeight, tableWidth int
)
columnPadding := t.columnPadding
if t.cellBorders {
columnPadding++
}
rowStep := t.rowPadding + 1
if t.cellBorders {
rowStep++ // With borders, every table row takes two screen rows.
tableWidth = 1 // We start at the second character because of the left table border.
}
if t.evaluateAllRows {
allRows = make([]int, len(t.cells))
for row := range t.cells {
allRows[row] = row
}
}
indexRow := func(row int) bool { // Determine if this row is visible, store its index.
if tableHeight >= height {
return false
}
rows = append(rows, row)
tableHeight += rowStep
for i := 0; i < rowStep-1; i++ {
rows = append(rows, -1)
}
return true
}
for row := 0; row < t.fixedRows && row < len(t.cells); row++ { // Do the fixed rows first.
if !indexRow(row) {
break
}
}
for row := t.fixedRows + t.rowOffset; row < len(t.cells); row++ { // Then the remaining rows.
if !indexRow(row) {
break
}
}
var (
skipped, lastTableWidth, expansionTotal int
expansions []int
)
ColumnLoop:
for column := 0; ; column++ {
// If we've moved beyond the right border, we stop or skip a column.
for tableWidth-1 >= width { // -1 because we include one extra column if the separator falls on the right end of the box.
// We've moved beyond the available space.
if column < t.fixedColumns {
break ColumnLoop // We're in the fixed area. We're done.
}
if !t.columnsSelectable && skipped >= t.columnOffset {
break ColumnLoop // There is no selection and we've already reached the offset.
}
if t.columnsSelectable && t.selectedColumn-skipped == t.fixedColumns {
break ColumnLoop // The selected column reached the leftmost point before disappearing.
}
if t.columnsSelectable && skipped >= t.columnOffset &&
(t.selectedColumn < column && lastTableWidth < width-1 && tableWidth < width-1 || t.selectedColumn < column-1) {
break ColumnLoop // We've skipped as many as requested and the selection is visible.
}
if len(columns) <= t.fixedColumns {
break // Nothing to skip.
}
// We need to skip a column.
skipped++
lastTableWidth -= widths[t.fixedColumns] + 1
tableWidth -= widths[t.fixedColumns] + 1
columns = append(columns[:t.fixedColumns], columns[t.fixedColumns+1:]...)
widths = append(widths[:t.fixedColumns], widths[t.fixedColumns+1:]...)
expansions = append(expansions[:t.fixedColumns], expansions[t.fixedColumns+1:]...)
}
// What's this column's width (without expansion)?
maxWidth := -1
expansion := 0
evaluationRows := rows
if t.evaluateAllRows {
evaluationRows = allRows
}
for _, row := range evaluationRows {
if row == -1 {
continue
}
if cell := getCell(row, column); cell != nil {
_, _, _, _, _, _, cellWidth := decomposeText(cell.Text, true, false)
if cell.MaxWidth > 0 && cell.MaxWidth < cellWidth {
cellWidth = cell.MaxWidth
}
if cellWidth > maxWidth {
maxWidth = cellWidth
}
if cell.Expansion > expansion {
expansion = cell.Expansion
}
}
}
if maxWidth < 0 {
break // No more cells found in this column.
}
// Store new column info at the end.
columns = append(columns, column)
widths = append(widths, maxWidth)
lastTableWidth = tableWidth
tableWidth += maxWidth + columnPadding
expansions = append(expansions, expansion)
expansionTotal += expansion
}
t.columnOffset = skipped
// If we have space left, distribute it.
if tableWidth < width {
toDistribute := width - tableWidth
for index, expansion := range expansions {
if expansionTotal <= 0 {
break
}
expWidth := toDistribute * expansion / expansionTotal
widths[index] += expWidth
toDistribute -= expWidth
expansionTotal -= expansion
}
tableWidth = width - toDistribute
}
// Helper function which draws border runes.
borderStyle := tcell.StyleDefault.Background(t.backgroundColor).Foreground(t.cellBorderColor)
drawBorder := func(colX, rowY int, ch rune) {
screen.SetContent(x+colX, y+rowY, ch, nil, borderStyle)
}
// Draw the cells (and borders).
var columnX int
if !t.cellBorders {
columnX--
}
for columnIndex, column := range columns {
columnWidth := widths[columnIndex]
for rowY, row := range rows {
if t.cellBorders {
// Draw borders.
rowY *= 2
for pos := 0; pos < columnWidth+columnPadding && columnX+columnPadding+pos < width; pos++ {
if row == -1 {
continue
}
drawBorder(columnX+pos+1, rowY, Borders.Horizontal)
}
ch := Borders.Cross
if columnIndex == 0 {
if rowY == 0 {
ch = Borders.TopLeft
} else {
ch = Borders.LeftT
}
} else if rowY == 0 {
ch = Borders.TopT
}
if row == -1 {
ch = Borders.Vertical
}
drawBorder(columnX, rowY, ch)
rowY++
if rowY >= height {
break // No space for the text anymore.
}
drawBorder(columnX, rowY, Borders.Vertical)
} else if columnIndex > 0 && columnPadding != 0 {
// Draw separator.
drawBorder(columnX, rowY, t.separator)
}
// Get the cell.
cell := getCell(row, column)
if cell == nil {
continue
}
// Draw text.
finalWidth := columnWidth
if columnX+columnPadding+columnWidth >= width {
finalWidth = width - columnX - columnPadding
}
cell.x, cell.y, cell.width = x+columnX+columnPadding, y+rowY, finalWidth
_, printed := PrintStyle(screen, cell.Text, x+columnX+1, y+rowY, finalWidth, cell.Align, SetAttributes(tcell.StyleDefault.Foreground(cell.Color), cell.Attributes))
if TaggedTextWidth(cell.Text)-printed > 0 && printed > 0 {
_, _, style, _ := screen.GetContent(x+columnX+finalWidth, y+rowY)
PrintStyle(screen, []byte(string(SemigraphicsHorizontalEllipsis)), x+columnX+finalWidth, y+rowY, t.rowPadding, AlignLeft, style)
//PrintStyle(screen, []byte(string(SemigraphicsHorizontalEllipsis)), x+columnX+finalWidth, y+rowY, 1, AlignLeft, style)
}
}
// Draw bottom border.
if rowY := 2 * len(rows); t.cellBorders && rowY < height {
for pos := 0; pos < columnWidth+columnPadding && columnX+columnPadding+pos < width; pos++ {
drawBorder(columnX+pos+1, rowY, Borders.Horizontal)
}
ch := Borders.BottomT
if columnIndex == 0 {
ch = Borders.BottomLeft
}
drawBorder(columnX, rowY, ch)
}
columnX += columnWidth + columnPadding
}
// Draw right border.
if t.cellBorders && len(t.cells) > 0 && columnX < width {
for rowY, row := range rows {
rowY *= 2
if rowY+1 < height {
drawBorder(columnX, rowY+1, Borders.Vertical)
}
ch := Borders.RightT
if rowY == 0 {
ch = Borders.TopRight
}
if row == -1 {
ch = Borders.Vertical
}
drawBorder(columnX, rowY, ch)
}
if rowY := 2 * len(rows); rowY < height {
drawBorder(columnX, rowY, Borders.BottomRight)
}
}
if showVerticalScrollBar {
// Calculate scroll bar position and dimensions.
rows := len(t.cells)
scrollBarItems := rows - t.fixedRows
scrollBarHeight := (t.visibleRows*(t.rowPadding+1) - t.fixedRows) + 1
scrollBarX := x + width
scrollBarY := y + t.fixedRows
if scrollBarX > x+tableWidth {
scrollBarX = x + tableWidth
}
padTotalOffset := 1
if t.cellBorders {
padTotalOffset = 2
scrollBarItems *= 2
scrollBarHeight = (scrollBarHeight * 2) - 1
scrollBarY += t.fixedRows + 1
}
// Draw scroll bar.
extra := 3
if t.rowPadding == 0 {
extra = 16
}
cursor := int(float64(scrollBarItems+extra) * (float64(t.rowOffset) / float64(((rows-t.fixedRows)-t.visibleRows)+padTotalOffset)))
for printed := 0; printed < scrollBarHeight; printed++ {
RenderScrollBar(screen, t.scrollBarVisibility, scrollBarX, scrollBarY+printed, scrollBarHeight, scrollBarItems, cursor, printed, t.hasFocus, t.scrollBarColor)
}
}
// TODO Draw horizontal scroll bar
// Helper function which colors the background of a box.
// 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)
fg, bg, a := style.Decompose()
if invert {
if fg == textColor || fg == t.cellBorderColor {
fg = backgroundColor
}
if fg == tcell.ColorDefault {
fg = t.backgroundColor
}
style = style.Background(textColor).Foreground(fg)
} else {
if backgroundColor != tcell.ColorDefault {
bg = backgroundColor
}
if textColor != tcell.ColorDefault {
fg = textColor
}
if attr != 0 {
a = attr
}
style = SetAttributes(style.Background(bg).Foreground(fg), a)
}
screen.SetContent(fromX+bx, fromY+by, m, c, style)
}
}
}
// Color the cell backgrounds. To avoid undesirable artefacts, we combine
// the drawing of a cell by background color, selected cells last.
type cellInfo struct {
x, y, w, h int
color tcell.Color
selected bool
}
cellsByBackgroundColor := make(map[tcell.Color][]*cellInfo)
var backgroundColors []tcell.Color
for rowY, row := range rows {
columnX := 0
rowSelected := t.rowsSelectable && !t.columnsSelectable && row == t.selectedRow
for columnIndex, column := range columns {
columnWidth := widths[columnIndex]
cell := getCell(row, column)
if cell == nil {
continue
}
bx, by, bw, bh := x+columnX, y+rowY, columnWidth+columnPadding, 1
if t.cellBorders {
by = y + rowY*2
bw++
bh = 3
}
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, &cellInfo{
x: bx,
y: by,
w: bw,
h: bh,
color: cell.Color,
selected: cellSelected,
})
if !ok {
backgroundColors = append(backgroundColors, cell.BackgroundColor)
}
columnX += columnWidth + columnPadding
}
}
sort.Slice(backgroundColors, func(i int, j int) bool {
// Draw brightest colors last (i.e. on top).
r, g, b := backgroundColors[i].RGB()
c := colorful.Color{R: float64(r) / 255, G: float64(g) / 255, B: float64(b) / 255}
_, _, li := c.Hcl()
r, g, b = backgroundColors[j].RGB()
c = colorful.Color{R: float64(r) / 255, G: float64(g) / 255, B: float64(b) / 255}
_, _, 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 {
if t.selectedStyle != tcell.StyleDefault {
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.color, 0, true)
}
} else {
colorBackground(cell.x, cell.y, cell.w, cell.h, bgColor, tcell.ColorDefault, 0, false)
}
}
}
// Remember column infos.
t.visibleColumnIndices, t.visibleColumnWidths = columns, widths
}
// InputHandler returns the handler for this primitive.
func (t *Table) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) {
return t.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p Primitive)) {
t.Lock()
defer t.Unlock()
key := event.Key()
if (!t.rowsSelectable && !t.columnsSelectable && key == tcell.KeyEnter) ||
key == tcell.KeyEscape ||
key == tcell.KeyTab ||
key == tcell.KeyBacktab {
if t.done != nil {
t.Unlock()
t.done(key)
t.Lock()
}
return
}
// Movement functions.
previouslySelectedRow, previouslySelectedColumn := t.selectedRow, t.selectedColumn
var (
validSelection = func(row, column int) bool {
if row < t.fixedRows || row >= len(t.cells) || column < t.fixedColumns || column > t.lastColumn {
return false
}
cell := t.cells[row][column]
return cell == nil || !cell.NotSelectable
}
home = func() {
if t.rowsSelectable {
t.selectedRow = 0
t.selectedColumn = 0
} else {
t.trackEnd = false
t.rowOffset = 0
t.columnOffset = 0
}
}
end = func() {
if t.rowsSelectable {
t.selectedRow = len(t.cells) - 1
t.selectedColumn = t.lastColumn
} else {
t.trackEnd = true
t.columnOffset = 0
}
}
down = func() {
if t.rowsSelectable {
if validSelection(t.selectedRow+1, t.selectedColumn) {
t.selectedRow++
}
} else {
t.rowOffset++
}
}
up = func() {
if t.rowsSelectable {
if validSelection(t.selectedRow-1, t.selectedColumn) {
t.selectedRow--
}
} else {
t.trackEnd = false
t.rowOffset--
}
}
left = func() {
if t.columnsSelectable {
if validSelection(t.selectedRow, t.selectedColumn-1) {
t.selectedColumn--
}
} else {
t.columnOffset--
}
}
right = func() {
if t.columnsSelectable {
if validSelection(t.selectedRow, t.selectedColumn+1) {
t.selectedColumn++
}
} else {
t.columnOffset++
}
}
pageDown = func() {
offsetAmount := t.visibleRows - t.fixedRows
if offsetAmount < 0 {
offsetAmount = 0
}
if t.rowsSelectable {
t.selectedRow += offsetAmount
if t.selectedRow >= len(t.cells) {
t.selectedRow = len(t.cells) - 1
}
} else {
t.rowOffset += offsetAmount
}
}
pageUp = func() {
offsetAmount := t.visibleRows - t.fixedRows
if offsetAmount < 0 {
offsetAmount = 0
}
if t.rowsSelectable {
t.selectedRow -= offsetAmount
if t.selectedRow < 0 {
t.selectedRow = 0
}
} else {
t.trackEnd = false
t.rowOffset -= offsetAmount
}
}
)
if HitShortcut(event, Keys.MoveFirst, Keys.MoveFirst2) {
home()
} else if HitShortcut(event, Keys.MoveLast, Keys.MoveLast2) {
end()
} else if HitShortcut(event, Keys.MoveUp, Keys.MoveUp2, Keys.MovePreviousField) {
up()
} else if HitShortcut(event, Keys.MoveDown, Keys.MoveDown2, Keys.MoveNextField) {
down()
} else if HitShortcut(event, Keys.MoveLeft, Keys.MoveLeft2) {
left()
} else if HitShortcut(event, Keys.MoveRight, Keys.MoveRight2) {
right()
} else if HitShortcut(event, Keys.MovePreviousPage) {
pageUp()
} else if HitShortcut(event, Keys.MoveNextPage) {
pageDown()
} else if HitShortcut(event, Keys.Select) {
if (t.rowsSelectable || t.columnsSelectable) && t.selected != nil {
t.Unlock()
t.selected(t.selectedRow, t.selectedColumn)
t.Lock()
}
}
// If the selection has changed, notify the handler.
if t.selectionChanged != nil && ((t.rowsSelectable && previouslySelectedRow != t.selectedRow) || (t.columnsSelectable && previouslySelectedColumn != t.selectedColumn)) {
t.Unlock()
t.selectionChanged(t.selectedRow, t.selectedColumn)
t.Lock()
}
})
}
// MouseHandler returns the mouse handler for this primitive.
func (t *Table) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
return t.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
x, y := event.Position()
if !t.InRect(x, y) {
return false, nil
}
switch action {
case MouseLeftClick:
_, tableY, _, _ := t.GetInnerRect()
mul := 1
maxY := tableY
if t.cellBorders {
mul = 2
maxY = tableY + 1
}
if t.sortClicked && t.fixedRows > 0 && (y >= tableY && y < maxY+(t.fixedRows*mul)) {
_, column := t.cellAt(x, y)
if t.sortClickedColumn != column {
t.sortClickedColumn = column
t.sortClickedDescending = false
} else {
t.sortClickedDescending = !t.sortClickedDescending
}
t.Sort(column, t.sortClickedDescending)
if t.columnsSelectable {
t.selectedColumn = column
}
} else if t.rowsSelectable || t.columnsSelectable {
t.Select(t.cellAt(x, y))
}
consumed = true
setFocus(t)
case MouseScrollUp:
t.trackEnd = false
t.rowOffset--
consumed = true
case MouseScrollDown:
t.rowOffset++
consumed = true
}
return
})
}
// SetBorders sets whether or not each cell in the table is surrounded by a border.
//
// Deprecated: This function is provided for backwards compatibility.
// Developers should use SetCellBorder instead.
func (t *Table) SetBorders(show bool) {
t.SetCellBorders(show)
}
// SetBordersColor sets the color of the cell borders.
//
// Deprecated: This function is provided for backwards compatibility.
// Developers should use SetCellBorderColor instead.
func (t *Table) SetBordersColor(color tcell.Color) {
t.SetCellBorderColor(color)
}
package cview
import (
"bytes"
"regexp"
"sync"
"unicode"
"unicode/utf8"
"github.com/gdamore/tcell/v2"
"github.com/lucasb-eyer/go-colorful"
"github.com/mattn/go-runewidth"
"github.com/rivo/uniseg"
)
var (
openColorRegex = regexp.MustCompile(`\[([a-zA-Z]*|#[0-9a-zA-Z]*)$`)
openRegionRegex = regexp.MustCompile(`\["[a-zA-Z0-9_,;: \-\.]*"?$`)
newLineRegex = regexp.MustCompile(`\r?\n`)
// TabSize is the number of spaces with which a tab character will be replaced.
TabSize = 4
)
// textViewIndex contains information about each line displayed in the text
// view.
type textViewIndex struct {
Line int // The index into the "buffer" variable.
Pos int // The index into the "buffer" line ([]byte position).
NextPos int // The (byte) index of the next character in this buffer line.
Width int // The screen width of this line.
ForegroundColor string // The starting foreground color ("" = don't change, "-" = reset).
BackgroundColor string // The starting background color ("" = don't change, "-" = reset).
Attributes string // The starting attributes ("" = don't change, "-" = reset).
Region []byte // The starting region ID.
}
// textViewRegion contains information about a region.
type textViewRegion struct {
// The region ID.
ID []byte
// The starting and end screen position of the region as determined the last
// time Draw() was called. A negative value indicates out-of-rect positions.
FromX, FromY, ToX, ToY int
}
// 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. (See SetChangedFunc() for more details.)
//
// Navigation
//
// If the text view is scrollable (the default), text is kept in a buffer which
// may be larger than the screen and can be navigated similarly to Vim:
//
// - h, left arrow: Move left.
// - l, right arrow: Move right.
// - j, down arrow: Move down.
// - k, up arrow: Move up.
// - g, home: Move to the top.
// - G, end: Move to the bottom.
// - Ctrl-F, page down: Move down by one page.
// - Ctrl-B, page up: Move up by one page.
//
// If the text is not scrollable, any text above the top visible line is
// discarded.
//
// Use SetInputCapture() to override or modify keyboard input.
//
// Colors
//
// If dynamic colors are enabled via SetDynamicColors(), text color can be
// changed dynamically by embedding color strings in square brackets. This works
// the same way as anywhere else. Please see the package documentation for more
// information.
//
// Regions and Highlights
//
// If regions are enabled via SetRegions(), you can define text regions within
// the text and assign region IDs to them. Text regions start with region tags.
// Region tags are square brackets that contain a region ID in double quotes,
// for example:
//
// We define a ["rg"]region[""] here.
//
// A text region ends with the next region tag. Tags with no region ID ([""])
// don't start new regions. They can therefore be used to mark the end of a
// region. Region IDs must satisfy the following regular expression:
//
// [a-zA-Z0-9_,;: \-\.]+
//
// Regions can be highlighted by calling the Highlight() function with one or
// more region IDs. This can be used to display search results, for example.
//
// The ScrollToHighlight() function can be used to jump to the currently
// highlighted region once when the text view is drawn the next time.
type TextView struct {
*Box
// The text buffer.
buffer [][]byte
// The last bytes that have been received but are not part of the buffer yet.
recentBytes []byte
// The last width and height of the text view.
lastWidth, lastHeight int
// The processed line index. This is nil if the buffer has changed and needs
// to be re-indexed.
index []*textViewIndex
// The width of the text view buffer index.
indexWidth int
// If set to true, the buffer will be reindexed each time it is modified.
reindex bool
// The text alignment, one of AlignLeft, AlignCenter, or AlignRight.
align int
// Information about visible regions as of the last call to Draw().
regionInfos []*textViewRegion
// Indices into the "index" slice which correspond to the first line of the
// first highlight and the last line of the last highlight. This is calculated
// 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{}
// The screen width of the longest line in the index (not the buffer).
longestLine int
// The index of the first line shown in the text view.
lineOffset int
// The maximum number of newlines the text view will hold (0 = unlimited).
maxLines int
// If set to true, the text view will always remain at the end of the content.
trackEnd bool
// The number of characters to be skipped on each line (not in wrap mode).
columnOffset int
// The height of the content the last time the text view was drawn.
pageSize int
// If set to true, the text view will keep a buffer of text which can be
// navigated when the text is longer than what fits into the box.
scrollable bool
// Visibility of the scroll bar.
scrollBarVisibility ScrollBarVisibility
// The scroll bar color.
scrollBarColor tcell.Color
// If set to true, lines that are longer than the available width are wrapped
// onto the next line. If set to false, any characters beyond the available
// width are discarded.
wrap bool
// The maximum line width when wrapping (0 = use TextView width).
wrapWidth int
// If set to true and if wrap is also true, lines are split at spaces or
// after punctuation characters.
wordWrap bool
// The (starting) color of the text.
textColor tcell.Color
// If set to true, the text color can be changed dynamically by piping color
// strings in square brackets to the text view.
dynamicColors bool
// If set to true, region tags can be used to define regions.
regions bool
// A temporary flag which, when true, will automatically bring the current
// highlight(s) into the visible screen.
scrollToHighlights bool
// If true, setting new highlights will be a XOR instead of an overwrite
// operation.
toggleHighlights bool
// An optional function which is called when the content of the text view has
// changed.
changed func()
// An optional function which is called when the user presses one of the
// following keys: Escape, Enter, Tab, Backtab.
done func(tcell.Key)
// An optional function which is called when one or more regions were
// highlighted.
highlighted func(added, removed, remaining []string)
sync.RWMutex
}
// NewTextView returns a new text view.
func NewTextView() *TextView {
return &TextView{
Box: NewBox(),
highlights: make(map[string]struct{}),
lineOffset: -1,
reindex: true,
scrollable: true,
scrollBarVisibility: ScrollBarAuto,
scrollBarColor: Styles.ScrollBarColor,
align: AlignLeft,
wrap: true,
textColor: Styles.PrimaryTextColor,
}
}
// SetScrollable sets the flag that decides whether or not the text view is
// scrollable. If true, text is kept in a buffer and can be navigated. If false,
// the last line will always be visible.
func (t *TextView) SetScrollable(scrollable bool) {
t.Lock()
defer t.Unlock()
t.scrollable = scrollable
if !scrollable {
t.trackEnd = true
}
}
// SetScrollBarVisibility specifies the display of the scroll bar.
func (t *TextView) SetScrollBarVisibility(visibility ScrollBarVisibility) {
t.Lock()
defer t.Unlock()
t.scrollBarVisibility = visibility
}
// SetScrollBarColor sets the color of the scroll bar.
func (t *TextView) SetScrollBarColor(color tcell.Color) {
t.Lock()
defer t.Unlock()
t.scrollBarColor = color
}
// SetWrap sets the flag that, if true, leads to lines that are longer than the
// available width being wrapped onto the next line. If false, any characters
// beyond the available width are not displayed.
func (t *TextView) SetWrap(wrap bool) {
t.Lock()
defer t.Unlock()
if t.wrap != wrap {
t.index = nil
}
t.wrap = wrap
}
// SetWordWrap sets the flag that, if true and if the "wrap" flag is also true
// (see SetWrap()), wraps the line at spaces or after punctuation marks. Note
// that trailing spaces will not be printed.
//
// This flag is ignored if the "wrap" flag is false.
func (t *TextView) SetWordWrap(wrapOnWords bool) {
t.Lock()
defer t.Unlock()
if t.wordWrap != wrapOnWords {
t.index = nil
}
t.wordWrap = wrapOnWords
}
// SetTextAlign sets the text alignment within the text view. This must be
// either AlignLeft, AlignCenter, or AlignRight.
func (t *TextView) SetTextAlign(align int) {
t.Lock()
defer t.Unlock()
if t.align != align {
t.index = nil
}
t.align = align
}
// SetTextColor sets the initial color of the text (which can be changed
// dynamically by sending color strings in square brackets to the text view if
// dynamic colors are enabled).
func (t *TextView) SetTextColor(color tcell.Color) {
t.Lock()
defer t.Unlock()
t.textColor = color
}
// SetBytes sets the text of this text view to the provided byte slice.
// Previously contained text will be removed.
func (t *TextView) SetBytes(text []byte) {
t.Lock()
defer t.Unlock()
t.clear()
t.write(text)
}
// SetText sets the text of this text view to the provided string. Previously
// contained text will be removed.
func (t *TextView) SetText(text string) {
t.SetBytes([]byte(text))
}
// GetBytes returns the current text of this text view. If "stripTags" is set
// to true, any region/color tags are stripped from the text.
func (t *TextView) GetBytes(stripTags bool) []byte {
t.RLock()
defer t.RUnlock()
if !stripTags {
if len(t.recentBytes) > 0 {
return bytes.Join(append(t.buffer, t.recentBytes), []byte("\n"))
}
return bytes.Join(t.buffer, []byte("\n"))
}
buffer := bytes.Join(t.buffer, []byte("\n"))
return StripTags(buffer, t.dynamicColors, t.regions)
}
// GetText returns the current text of this text view. If "stripTags" is set
// to true, any region/color tags are stripped from the text.
func (t *TextView) GetText(stripTags bool) string {
return string(t.GetBytes(stripTags))
}
// SetDynamicColors sets the flag that allows the text color to be changed
// dynamically. See class description for details.
func (t *TextView) SetDynamicColors(dynamic bool) {
t.Lock()
defer t.Unlock()
if t.dynamicColors != dynamic {
t.index = nil
}
t.dynamicColors = dynamic
}
// SetRegions sets the flag that allows to define regions in the text. See class
// description for details.
func (t *TextView) SetRegions(regions bool) {
t.Lock()
defer t.Unlock()
if t.regions != regions {
t.index = nil
}
t.regions = regions
}
// SetChangedFunc sets a handler function which is called when the text of the
// text view has changed. This is useful when text is written to this io.Writer
// in a separate goroutine. Doing so 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()) {
t.Lock()
defer t.Unlock()
t.changed = handler
}
// SetDoneFunc sets a handler which is called when the user presses on the
// following keys: Escape, Enter, Tab, Backtab. The key is passed to the
// handler.
func (t *TextView) SetDoneFunc(handler func(key tcell.Key)) {
t.Lock()
defer t.Unlock()
t.done = handler
}
// SetHighlightedFunc sets a handler which is called when the list of currently
// highlighted regions change. It receives a list of region IDs which were newly
// highlighted, those that are not highlighted anymore, and those that remain
// highlighted.
//
// Note that because regions are only determined during drawing, this function
// can only fire for regions that have existed during the last call to Draw().
func (t *TextView) SetHighlightedFunc(handler func(added, removed, remaining []string)) {
t.highlighted = handler
}
func (t *TextView) clipBuffer() {
if t.maxLines <= 0 {
return
}
lenbuf := len(t.buffer)
if lenbuf > t.maxLines {
t.buffer = t.buffer[lenbuf-t.maxLines:]
}
}
// SetMaxLines sets the maximum number of newlines the text view will hold
// before discarding older data from the buffer.
func (t *TextView) SetMaxLines(maxLines int) {
t.maxLines = maxLines
t.clipBuffer()
}
// ScrollTo scrolls to the specified row and column (both starting with 0).
func (t *TextView) ScrollTo(row, column int) {
t.Lock()
defer t.Unlock()
if !t.scrollable {
return
}
t.lineOffset = row
t.columnOffset = column
t.trackEnd = false
}
// ScrollToBeginning scrolls to the top left corner of the text if the text view
// is scrollable.
func (t *TextView) ScrollToBeginning() {
t.Lock()
defer t.Unlock()
if !t.scrollable {
return
}
t.trackEnd = false
t.lineOffset = 0
t.columnOffset = 0
}
// ScrollToEnd scrolls to the bottom left corner of the text if the text view
// is scrollable. Adding new rows to the end of the text view will cause it to
// scroll with the new data.
func (t *TextView) ScrollToEnd() {
t.Lock()
defer t.Unlock()
if !t.scrollable {
return
}
t.trackEnd = true
t.columnOffset = 0
}
// 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) {
t.RLock()
defer t.RUnlock()
return t.lineOffset, t.columnOffset
}
// Clear removes all text from the buffer.
func (t *TextView) Clear() {
t.Lock()
defer t.Unlock()
t.clear()
}
func (t *TextView) clear() {
t.buffer = nil
t.recentBytes = nil
if t.reindex {
t.index = nil
}
}
// Highlight specifies which regions should be highlighted. If highlight
// toggling is set to true (see SetToggleHighlights()), the highlight of the
// provided regions is toggled (highlighted regions are un-highlighted and vice
// versa). If toggling is set to false, the provided regions are highlighted and
// all other regions will not be highlighted (you may also provide nil to turn
// off all highlights).
//
// For more information on regions, see class description. Empty region strings
// are ignored.
//
// Text in highlighted regions will be drawn inverted, i.e. with their
// background and foreground colors swapped.
func (t *TextView) Highlight(regionIDs ...string) {
t.Lock()
// Toggle highlights.
if t.toggleHighlights {
var newIDs []string
HighlightLoop:
for regionID := range t.highlights {
for _, id := range regionIDs {
if regionID == id {
continue HighlightLoop
}
}
newIDs = append(newIDs, regionID)
}
for _, regionID := range regionIDs {
if _, ok := t.highlights[regionID]; !ok {
newIDs = append(newIDs, regionID)
}
}
regionIDs = newIDs
} // Now we have a list of region IDs that end up being highlighted.
// Determine added and removed regions.
var added, removed, remaining []string
if t.highlighted != nil {
for _, regionID := range regionIDs {
if _, ok := t.highlights[regionID]; ok {
remaining = append(remaining, regionID)
delete(t.highlights, regionID)
} else {
added = append(added, regionID)
}
}
for regionID := range t.highlights {
removed = append(removed, regionID)
}
}
// Make new selection.
t.highlights = make(map[string]struct{})
for _, id := range regionIDs {
if id == "" {
continue
}
t.highlights[id] = struct{}{}
}
t.index = nil
// Notify.
if t.highlighted != nil && (len(added) > 0 || len(removed) > 0) {
t.Unlock()
t.highlighted(added, removed, remaining)
} else {
t.Unlock()
}
}
// GetHighlights returns the IDs of all currently highlighted regions.
func (t *TextView) GetHighlights() (regionIDs []string) {
t.RLock()
defer t.RUnlock()
for id := range t.highlights {
regionIDs = append(regionIDs, id)
}
return
}
// SetToggleHighlights sets a flag to determine how regions are highlighted.
// When set to true, the Highlight() function (or a mouse click) will toggle the
// provided/selected regions. When set to false, Highlight() (or a mouse click)
// will simply highlight the provided regions.
func (t *TextView) SetToggleHighlights(toggle bool) {
t.toggleHighlights = toggle
}
// ScrollToHighlight will cause the visible area to be scrolled so that the
// highlighted regions appear in the visible area of the text view. This
// repositioning happens the next time the text view is drawn. It happens only
// once so you will need to call this function repeatedly to always keep
// highlighted regions in view.
//
// Nothing happens if there are no highlighted regions or if the text view is
// not scrollable.
func (t *TextView) ScrollToHighlight() {
t.Lock()
defer t.Unlock()
if len(t.highlights) == 0 || !t.scrollable || !t.regions {
return
}
t.index = nil
t.scrollToHighlights = true
t.trackEnd = false
}
// GetRegionText returns the text of the region with the given ID. If dynamic
// colors are enabled, color tags are stripped from the text. Newlines are
// always returned as '\n' runes.
//
// If the region does not exist or if regions are turned off, an empty string
// is returned.
func (t *TextView) GetRegionText(regionID string) string {
t.RLock()
defer t.RUnlock()
if !t.regions || len(regionID) == 0 {
return ""
}
var (
buffer bytes.Buffer
currentRegionID string
)
for _, str := range t.buffer {
// Find all color tags in this line.
var colorTagIndices [][]int
if t.dynamicColors {
colorTagIndices = colorPattern.FindAllIndex(str, -1)
}
// Find all regions in this line.
var (
regionIndices [][]int
regions [][][]byte
)
if t.regions {
regionIndices = regionPattern.FindAllIndex(str, -1)
regions = regionPattern.FindAllSubmatch(str, -1)
}
// Analyze this line.
var currentTag, currentRegion int
for pos, ch := range str {
// Skip any color tags.
if currentTag < len(colorTagIndices) && pos >= colorTagIndices[currentTag][0] && pos < colorTagIndices[currentTag][1] {
if pos == colorTagIndices[currentTag][1]-1 {
currentTag++
}
if colorTagIndices[currentTag][1]-colorTagIndices[currentTag][0] > 2 {
continue
}
}
// Skip any regions.
if currentRegion < len(regionIndices) && pos >= regionIndices[currentRegion][0] && pos < regionIndices[currentRegion][1] {
if pos == regionIndices[currentRegion][1]-1 {
if currentRegionID == regionID {
// This is the end of the requested region. We're done.
return buffer.String()
}
currentRegionID = string(regions[currentRegion][1])
currentRegion++
}
continue
}
// Add this rune.
if currentRegionID == regionID {
buffer.WriteByte(ch)
}
}
// Add newline.
if currentRegionID == regionID {
buffer.WriteRune('\n')
}
}
return escapePattern.ReplaceAllString(buffer.String(), `[$1$2]`)
}
// Focus is called when this primitive receives focus.
func (t *TextView) Focus(delegate func(p Primitive)) {
t.Lock()
defer t.Unlock()
// Implemented here with locking because this is used by layout primitives.
t.hasFocus = true
}
// HasFocus returns whether or not this primitive has focus.
func (t *TextView) HasFocus() bool {
t.RLock()
defer t.RUnlock()
// Implemented here with locking because this may be used in the "changed"
// callback.
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) {
t.Lock()
changed := t.changed
if changed != nil {
// Notify at the end.
defer changed()
}
defer t.Unlock()
return t.write(p)
}
func (t *TextView) write(p []byte) (n int, err error) {
// Copy data over.
newBytes := append(t.recentBytes, p...)
t.recentBytes = nil
// If we have a trailing invalid UTF-8 byte, we'll wait.
if r, _ := utf8.DecodeLastRune(p); r == utf8.RuneError {
t.recentBytes = newBytes
return len(p), nil
}
// If we have a trailing open dynamic color, exclude it.
if t.dynamicColors {
location := openColorRegex.FindIndex(newBytes)
if location != nil {
t.recentBytes = newBytes[location[0]:]
newBytes = newBytes[:location[0]]
}
}
// If we have a trailing open region, exclude it.
if t.regions {
location := openRegionRegex.FindIndex(newBytes)
if location != nil {
t.recentBytes = newBytes[location[0]:]
newBytes = newBytes[:location[0]]
}
}
// Transform the new bytes into strings.
newBytes = bytes.Replace(newBytes, []byte{'\t'}, bytes.Repeat([]byte{' '}, TabSize), -1)
for index, line := range bytes.Split(newBytes, []byte("\n")) {
if index == 0 {
if len(t.buffer) == 0 {
t.buffer = [][]byte{line}
} else {
t.buffer[len(t.buffer)-1] = append(t.buffer[len(t.buffer)-1], line...)
}
} else {
t.buffer = append(t.buffer, line)
}
}
t.clipBuffer()
// Reset the index.
if t.reindex {
t.index = nil
}
return len(p), nil
}
// SetWrapWidth set the maximum width of lines when wrapping is enabled.
// When set to 0 the width of the TextView is used.
func (t *TextView) SetWrapWidth(width int) {
t.Lock()
defer t.Unlock()
t.wrapWidth = width
}
// SetReindexBuffer set a flag controlling whether the buffer is reindexed when
// it is modified. This improves the performance of TextViews whose contents
// always have line-breaks in the same location. This must be called after the
// buffer has been indexed.
func (t *TextView) SetReindexBuffer(reindex bool) {
t.Lock()
defer t.Unlock()
t.reindex = reindex
if reindex {
t.index = nil
}
}
// reindexBuffer re-indexes the buffer such that we can use it to easily draw
// the buffer onto the screen. Each line in the index will contain a pointer
// into the buffer from which on we will print text. It will also contain the
// color with which the line starts.
func (t *TextView) reindexBuffer(width int) {
if t.index != nil && (!t.wrap || width == t.indexWidth) {
return // Nothing has changed. We can still use the current index.
}
t.index = nil
t.indexWidth = width
t.fromHighlight, t.toHighlight, t.posHighlight = -1, -1, -1
// If there's no space, there's no index.
if width < 1 {
return
}
if t.wrapWidth > 0 && t.wrapWidth < width {
width = t.wrapWidth
}
// Initial states.
var regionID []byte
var (
highlighted bool
foregroundColor, backgroundColor, attributes string
)
// Go through each line in the buffer.
for bufferIndex, buf := range t.buffer {
colorTagIndices, colorTags, regionIndices, regions, escapeIndices, strippedStr, _ := decomposeText(buf, t.dynamicColors, t.regions)
// Split the line if required.
var splitLines []string
str := string(strippedStr)
if t.wrap && len(str) > 0 {
for len(str) > 0 {
extract := runewidth.Truncate(str, width, "")
if len(extract) == 0 {
// We'll extract at least one grapheme cluster.
gr := uniseg.NewGraphemes(str)
gr.Next()
_, to := gr.Positions()
extract = str[:to]
}
if t.wordWrap && len(extract) < len(str) {
// Add any spaces from the next line.
if spaces := spacePattern.FindStringIndex(str[len(extract):]); spaces != nil && spaces[0] == 0 {
extract = str[:len(extract)+spaces[1]]
}
// Can we split before the mandatory end?
matches := boundaryPattern.FindAllStringIndex(extract, -1)
if len(matches) > 0 {
// Yes. Let's split there.
extract = extract[:matches[len(matches)-1][1]]
}
}
splitLines = append(splitLines, extract)
str = str[len(extract):]
}
} else {
// No need to split the line.
splitLines = []string{str}
}
// Create index from split lines.
var originalPos, colorPos, regionPos, escapePos int
for _, splitLine := range splitLines {
line := &textViewIndex{
Line: bufferIndex,
Pos: originalPos,
ForegroundColor: foregroundColor,
BackgroundColor: backgroundColor,
Attributes: attributes,
Region: regionID,
}
// Shift original position with tags.
lineLength := len(splitLine)
remainingLength := lineLength
tagEnd := originalPos
totalTagLength := 0
for {
// 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.
foregroundColor, backgroundColor, attributes = styleFromTag(foregroundColor, backgroundColor, attributes, colorTags[colorPos])
colorPos++
case 1:
// Process region tags.
regionID = regions[regionPos][1]
_, highlighted = t.highlights[string(regionID)]
// Update highlight range.
if highlighted {
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++
case 2:
// Process escape tags.
escapePos++
}
}
// Advance to next line.
originalPos += lineLength + totalTagLength
// Append this line.
line.NextPos = originalPos
line.Width = runewidth.StringWidth(splitLine)
t.index = append(t.index, line)
}
// Word-wrapped lines may have trailing whitespace. Remove it.
if t.wrap && t.wordWrap {
for _, line := range t.index {
str := t.buffer[line.Line][line.Pos:line.NextPos]
trimmed := bytes.TrimRightFunc(str, unicode.IsSpace)
if len(trimmed) != len(str) {
oldNextPos := line.NextPos
line.NextPos -= len(str) - len(trimmed)
line.Width -= runewidth.StringWidth(string(t.buffer[line.Line][line.NextPos:oldNextPos]))
}
}
}
}
// Calculate longest line.
t.longestLine = 0
for _, line := range t.index {
if line.Width > t.longestLine {
t.longestLine = line.Width
}
}
}
// Draw draws this primitive onto the screen.
func (t *TextView) Draw(screen tcell.Screen) {
if !t.GetVisible() {
return
}
t.Box.Draw(screen)
t.Lock()
defer t.Unlock()
// Get the available size.
x, y, width, height := t.GetInnerRect()
if height == 0 {
return
}
t.pageSize = height
if t.index == nil || width != t.lastWidth || height != t.lastHeight {
t.reindexBuffer(width)
}
t.lastWidth, t.lastHeight = width, height
showVerticalScrollBar := t.scrollBarVisibility == ScrollBarAlways || (t.scrollBarVisibility == ScrollBarAuto && len(t.index) > height)
if showVerticalScrollBar {
width-- // Subtract space for scroll bar.
}
t.reindexBuffer(width)
if t.regions {
t.regionInfos = nil
}
// If we don't have an index, there's nothing to draw.
if t.index == nil {
return
}
// Move to highlighted regions.
if t.regions && t.scrollToHighlights && t.fromHighlight >= 0 {
// Do we fit the entire height?
if t.toHighlight-t.fromHighlight+1 < height {
// Yes, let's center the highlights.
t.lineOffset = (t.fromHighlight + t.toHighlight - height) / 2
} else {
// 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 offscreen on the left, move it onscreen.
if t.posHighlight-t.columnOffset < 0 {
t.columnOffset = t.posHighlight - width/4
}
}
t.scrollToHighlights = false
// Adjust line offset.
if t.lineOffset+height > len(t.index) {
t.trackEnd = true
}
if t.trackEnd {
t.lineOffset = len(t.index) - height
}
if t.lineOffset < 0 {
t.lineOffset = 0
}
// Adjust column offset.
if t.align == AlignLeft {
if t.columnOffset+width > t.longestLine {
t.columnOffset = t.longestLine - width
}
if t.columnOffset < 0 {
t.columnOffset = 0
}
} else if t.align == AlignRight {
if t.columnOffset-width < -t.longestLine {
t.columnOffset = width - t.longestLine
}
if t.columnOffset > 0 {
t.columnOffset = 0
}
} else { // AlignCenter.
half := (t.longestLine - width) / 2
if half > 0 {
if t.columnOffset > half {
t.columnOffset = half
}
if t.columnOffset < -half {
t.columnOffset = -half
}
} else {
t.columnOffset = 0
}
}
// Draw the buffer.
defaultStyle := tcell.StyleDefault.Foreground(t.textColor)
for line := t.lineOffset; line < len(t.index); line++ {
// Are we done?
if line-t.lineOffset >= height {
break
}
// Get the text for this line.
index := t.index[line]
text := t.buffer[index.Line][index.Pos:index.NextPos]
foregroundColor := index.ForegroundColor
backgroundColor := index.BackgroundColor
attributes := index.Attributes
regionID := index.Region
if t.regions && len(regionID) > 0 && (len(t.regionInfos) == 0 || !bytes.Equal(t.regionInfos[len(t.regionInfos)-1].ID, regionID)) {
t.regionInfos = append(t.regionInfos, &textViewRegion{
ID: regionID,
FromX: x,
FromY: y + line - t.lineOffset,
ToX: -1,
ToY: -1,
})
}
// Process tags.
colorTagIndices, colorTags, regionIndices, regions, escapeIndices, strippedText, _ := decomposeText(text, t.dynamicColors, t.regions)
// Calculate the position of the line.
var skip, posX int
if t.align == AlignLeft {
posX = -t.columnOffset
} else if t.align == AlignRight {
posX = width - index.Width - t.columnOffset
} else { // AlignCenter.
posX = (width-index.Width)/2 - t.columnOffset
}
if posX < 0 {
skip = -posX
posX = 0
}
// Print the line.
if y+line-t.lineOffset >= 0 {
var colorPos, regionPos, escapePos, tagOffset, skipped int
iterateString(string(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.
if len(regionID) > 0 && len(t.regionInfos) > 0 && bytes.Equal(t.regionInfos[len(t.regionInfos)-1].ID, regionID) {
// End last region.
t.regionInfos[len(t.regionInfos)-1].ToX = x + posX
t.regionInfos[len(t.regionInfos)-1].ToY = y + line - t.lineOffset
}
regionID = regions[regionPos][1]
if len(regionID) > 0 {
// Start new region.
t.regionInfos = append(t.regionInfos, &textViewRegion{
ID: regionID,
FromX: x + posX,
FromY: y + line - t.lineOffset,
ToX: -1,
ToY: -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.
_, _, existingStyle, _ := screen.GetContent(x+posX, y+line-t.lineOffset)
_, background, _ := existingStyle.Decompose()
style := overlayStyle(background, defaultStyle, foregroundColor, backgroundColor, attributes)
// Do we highlight this character?
var highlighted bool
if len(regionID) > 0 {
if _, ok := t.highlights[string(regionID)]; ok {
highlighted = true
}
}
if highlighted {
fg, bg, _ := style.Decompose()
if bg == tcell.ColorDefault {
r, g, b := fg.RGB()
c := colorful.Color{R: float64(r) / 255, G: float64(g) / 255, B: float64(b) / 255}
_, _, li := c.Hcl()
if li < .5 {
bg = tcell.ColorWhite.TrueColor()
} else {
bg = tcell.ColorBlack.TrueColor()
}
}
style = style.Background(fg).Foreground(bg)
}
// Skip to the right.
if !t.wrap && skipped < skip {
skipped += screenWidth
return false
}
// Stop at the right border.
if posX+screenWidth > width {
return true
}
// 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)
}
}
// Advance.
posX += screenWidth
return false
})
}
}
// Draw scroll bar.
if showVerticalScrollBar {
cursor := int(float64(len(t.index)) * (float64(t.lineOffset) / float64(len(t.index)-height)))
for printed := 0; printed < height; printed++ {
RenderScrollBar(screen, t.scrollBarVisibility, x+width, y+printed, height, len(t.index), cursor, printed, t.hasFocus, t.scrollBarColor)
}
}
// If this view is not scrollable, we'll purge the buffer of lines that have
// scrolled out of view.
if !t.scrollable && t.lineOffset > 0 {
if t.lineOffset >= len(t.index) {
t.buffer = nil
} else {
t.buffer = t.buffer[t.index[t.lineOffset].Line:]
}
t.index = nil
t.lineOffset = 0
}
}
// InputHandler returns the handler for this primitive.
func (t *TextView) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) {
return t.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p Primitive)) {
key := event.Key()
if HitShortcut(event, Keys.Cancel, Keys.Select, Keys.Select2, Keys.MovePreviousField, Keys.MoveNextField) {
if t.done != nil {
t.done(key)
}
return
}
t.Lock()
defer t.Unlock()
if !t.scrollable {
return
}
if HitShortcut(event, Keys.MoveFirst, Keys.MoveFirst2) {
t.trackEnd = false
t.lineOffset = 0
t.columnOffset = 0
} else if HitShortcut(event, Keys.MoveLast, Keys.MoveLast2) {
t.trackEnd = true
t.columnOffset = 0
} else if HitShortcut(event, Keys.MoveUp, Keys.MoveUp2) {
t.trackEnd = false
t.lineOffset--
} else if HitShortcut(event, Keys.MoveDown, Keys.MoveDown2) {
t.lineOffset++
} else if HitShortcut(event, Keys.MoveLeft, Keys.MoveLeft2) {
t.columnOffset--
} else if HitShortcut(event, Keys.MoveRight, Keys.MoveRight2) {
t.columnOffset++
} else if HitShortcut(event, Keys.MovePreviousPage) {
t.trackEnd = false
t.lineOffset -= t.pageSize
} else if HitShortcut(event, Keys.MoveNextPage) {
t.lineOffset += t.pageSize
}
})
}
// MouseHandler returns the mouse handler for this primitive.
func (t *TextView) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
return t.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
x, y := event.Position()
if !t.InRect(x, y) {
return false, nil
}
switch action {
case MouseLeftClick:
if t.regions {
// Find a region to highlight.
for _, region := range t.regionInfos {
if y == region.FromY && x < region.FromX ||
y == region.ToY && x >= region.ToX ||
region.FromY >= 0 && y < region.FromY ||
region.ToY >= 0 && y > region.ToY {
continue
}
t.Highlight(string(region.ID))
break
}
}
consumed = true
setFocus(t)
case MouseScrollUp:
if t.scrollable {
t.trackEnd = false
t.lineOffset--
consumed = true
}
case MouseScrollDown:
if t.scrollable {
t.lineOffset++
consumed = true
}
}
return
})
}
package cview
import (
"sync"
"github.com/gdamore/tcell/v2"
)
// 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 focused and 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 focuses this node.
focused func()
// 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.
sync.RWMutex
}
// 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) {
n.Lock()
defer n.Unlock()
n.walk(callback)
}
func (n *TreeNode) walk(callback func(node, parent *TreeNode) bool) {
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])
}
}
}
// 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{}) {
n.Lock()
defer n.Unlock()
n.reference = reference
}
// GetReference returns this node's reference object.
func (n *TreeNode) GetReference() interface{} {
n.RLock()
defer n.RUnlock()
return n.reference
}
// SetChildren sets this node's child nodes.
func (n *TreeNode) SetChildren(childNodes []*TreeNode) {
n.Lock()
defer n.Unlock()
n.children = childNodes
}
// GetText returns this node's text.
func (n *TreeNode) GetText() string {
n.RLock()
defer n.RUnlock()
return n.text
}
// GetChildren returns this node's children.
func (n *TreeNode) GetChildren() []*TreeNode {
n.RLock()
defer n.RUnlock()
return n.children
}
// ClearChildren removes all child nodes from this node.
func (n *TreeNode) ClearChildren() {
n.Lock()
defer n.Unlock()
n.children = nil
}
// AddChild adds a new child node to this node.
func (n *TreeNode) AddChild(node *TreeNode) {
n.Lock()
defer n.Unlock()
n.children = append(n.children, node)
}
// SetSelectable sets a flag indicating whether this node can be focused and
// selected by the user.
func (n *TreeNode) SetSelectable(selectable bool) {
n.Lock()
defer n.Unlock()
n.selectable = selectable
}
// SetFocusedFunc sets the function which is called when the user navigates to
// this node.
//
// This function is also called when the user selects this node.
func (n *TreeNode) SetFocusedFunc(handler func()) {
n.Lock()
defer n.Unlock()
n.focused = handler
}
// SetSelectedFunc sets a function which is called when the user selects this
// node by hitting Enter when it is focused.
func (n *TreeNode) SetSelectedFunc(handler func()) {
n.Lock()
defer n.Unlock()
n.selected = handler
}
// SetExpanded sets whether or not this node's child nodes should be displayed.
func (n *TreeNode) SetExpanded(expanded bool) {
n.Lock()
defer n.Unlock()
n.expanded = expanded
}
// Expand makes the child nodes of this node appear.
func (n *TreeNode) Expand() {
n.Lock()
defer n.Unlock()
n.expanded = true
}
// Collapse makes the child nodes of this node disappear.
func (n *TreeNode) Collapse() {
n.Lock()
defer n.Unlock()
n.expanded = false
}
// ExpandAll expands this node and all descendent nodes.
func (n *TreeNode) ExpandAll() {
n.Walk(func(node, parent *TreeNode) bool {
node.expanded = true
return true
})
}
// CollapseAll collapses this node and all descendent nodes.
func (n *TreeNode) CollapseAll() {
n.Walk(func(node, parent *TreeNode) bool {
n.expanded = false
return true
})
}
// IsExpanded returns whether the child nodes of this node are visible.
func (n *TreeNode) IsExpanded() bool {
n.RLock()
defer n.RUnlock()
return n.expanded
}
// SetText sets the node's text which is displayed.
func (n *TreeNode) SetText(text string) {
n.Lock()
defer n.Unlock()
n.text = text
}
// GetColor returns the node's color.
func (n *TreeNode) GetColor() tcell.Color {
n.RLock()
defer n.RUnlock()
return n.color
}
// SetColor sets the node's text color.
func (n *TreeNode) SetColor(color tcell.Color) {
n.Lock()
defer n.Unlock()
n.color = color
}
// 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) {
n.Lock()
defer n.Unlock()
n.indent = indent
}
// 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 focused 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.
type TreeView struct {
*Box
// The root node.
root *TreeNode
// The currently focused node or nil if no node is focused.
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 [][]byte
// 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 text color for selected items.
selectedTextColor *tcell.Color
// The background color for selected items.
selectedBackgroundColor *tcell.Color
// The color of the lines.
graphicsColor tcell.Color
// Visibility of the scroll bar.
scrollBarVisibility ScrollBarVisibility
// The scroll bar color.
scrollBarColor tcell.Color
// An optional function called when the focused tree item changes.
changed func(node *TreeNode)
// An optional function called when a tree item is selected.
selected func(node *TreeNode)
// An optional function called when the user moves away from this primitive.
done func(key tcell.Key)
// The visible nodes, top-down, as set by process().
nodes []*TreeNode
sync.RWMutex
}
// NewTreeView returns a new tree view.
func NewTreeView() *TreeView {
return &TreeView{
Box: NewBox(),
scrollBarVisibility: ScrollBarAuto,
graphics: true,
graphicsColor: Styles.GraphicsColor,
scrollBarColor: Styles.ScrollBarColor,
}
}
// SetRoot sets the root node of the tree.
func (t *TreeView) SetRoot(root *TreeNode) {
t.Lock()
defer t.Unlock()
t.root = root
}
// GetRoot returns the root node of the tree. If no such node was previously
// set, nil is returned.
func (t *TreeView) GetRoot() *TreeNode {
t.RLock()
defer t.RUnlock()
return t.root
}
// SetCurrentNode focuses a node or, when provided with nil, clears focus.
// 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) {
t.Lock()
defer t.Unlock()
t.currentNode = node
if t.currentNode.focused != nil {
t.Unlock()
t.currentNode.focused()
t.Lock()
}
}
// GetCurrentNode returns the currently selected node or nil of no node is
// currently selected.
func (t *TreeView) GetCurrentNode() *TreeNode {
t.RLock()
defer t.RUnlock()
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) {
t.Lock()
defer t.Unlock()
t.topLevel = topLevel
}
// 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) {
t.Lock()
defer t.Unlock()
t.prefixes = make([][]byte, len(prefixes))
for i := range prefixes {
t.prefixes[i] = []byte(prefixes[i])
}
}
// 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) {
t.Lock()
defer t.Unlock()
t.align = align
}
// 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) {
t.Lock()
defer t.Unlock()
t.graphics = showGraphics
}
// SetSelectedTextColor sets the text color of selected items.
func (t *TreeView) SetSelectedTextColor(color tcell.Color) {
t.Lock()
defer t.Unlock()
t.selectedTextColor = &color
}
// SetSelectedBackgroundColor sets the background color of selected items.
func (t *TreeView) SetSelectedBackgroundColor(color tcell.Color) {
t.Lock()
defer t.Unlock()
t.selectedBackgroundColor = &color
}
// SetGraphicsColor sets the colors of the lines used to draw the tree structure.
func (t *TreeView) SetGraphicsColor(color tcell.Color) {
t.Lock()
defer t.Unlock()
t.graphicsColor = color
}
// SetScrollBarVisibility specifies the display of the scroll bar.
func (t *TreeView) SetScrollBarVisibility(visibility ScrollBarVisibility) {
t.Lock()
defer t.Unlock()
t.scrollBarVisibility = visibility
}
// SetScrollBarColor sets the color of the scroll bar.
func (t *TreeView) SetScrollBarColor(color tcell.Color) {
t.Lock()
defer t.Unlock()
t.scrollBarColor = color
}
// SetChangedFunc sets the function which is called when the user navigates to
// a new tree node.
func (t *TreeView) SetChangedFunc(handler func(node *TreeNode)) {
t.Lock()
defer t.Unlock()
t.changed = handler
}
// 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)) {
t.Lock()
defer t.Unlock()
t.selected = handler
}
// SetDoneFunc sets a handler which is called whenever the user presses the
// Escape, Tab, or Backtab key.
func (t *TreeView) SetDoneFunc(handler func(key tcell.Key)) {
t.Lock()
defer t.Unlock()
t.done = handler
}
// GetScrollOffset returns the number of node rows that were skipped at the top
// of the tree view. Note that when the user navigates the tree view, this value
// is only updated after the tree view has been redrawn.
func (t *TreeView) GetScrollOffset() int {
t.RLock()
defer t.RUnlock()
return t.offsetY
}
// GetRowCount returns the number of "visible" nodes. This includes nodes which
// fall outside the tree view's box but notably does not include the children
// of collapsed nodes. Note that this value is only up to date after the tree
// view has been drawn.
func (t *TreeView) GetRowCount() int {
t.RLock()
defer t.RUnlock()
return len(t.nodes)
}
// Transform modifies the current selection.
func (t *TreeView) Transform(tr Transformation) {
t.Lock()
defer t.Unlock()
switch tr {
case TransformFirstItem:
t.movement = treeHome
case TransformLastItem:
t.movement = treeEnd
case TransformPreviousItem:
t.movement = treeUp
case TransformNextItem:
t.movement = treeDown
case TransformPreviousPage:
t.movement = treePageUp
case TransformNextPage:
t.movement = treePageDown
}
t.process()
}
// 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
}
// Add the node to the list.
if node.level >= t.topLevel {
// This node will be visible.
if node.textX > maxTextX {
maxTextX = node.textX
}
if node == t.currentNode && node.selectable {
selectedIndex = len(t.nodes)
}
// Maybe we want to skip this level.
if t.topLevel == node.level && (topLevelGraphicsX < 0 || node.graphicsX < topLevelGraphicsX) {
topLevelGraphicsX = node.graphicsX
}
t.nodes = append(t.nodes, node)
}
// Recurse if desired.
return node.expanded
})
// Post-process positions.
for _, node := range t.nodes {
// If text must align, we correct the positions.
if t.align && node.level > t.topLevel {
node.textX = maxTextX
}
// If we skipped levels, shift to the left.
if topLevelGraphicsX > 0 {
node.graphicsX -= topLevelGraphicsX
node.textX -= topLevelGraphicsX
}
}
// Process selection. (Also trigger events if necessary.)
if selectedIndex >= 0 {
// Move the selection.
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 treePageDown:
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 treePageUp:
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.Unlock()
t.changed(t.currentNode)
t.Lock()
}
if t.currentNode.focused != nil {
t.Unlock()
t.currentNode.focused()
t.Lock()
}
}
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) {
if !t.GetVisible() {
return
}
t.Box.Draw(screen)
t.Lock()
defer t.Unlock()
if t.root == nil {
return
}
t.process()
// 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
}
// Calculate scroll bar position.
rows := len(t.nodes)
cursor := int(float64(rows) * (float64(t.offsetY) / float64(rows-height)))
// 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 {
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 {
backgroundColor := node.color
foregroundColor := t.backgroundColor
if t.selectedTextColor != nil {
foregroundColor = *t.selectedTextColor
}
if t.selectedBackgroundColor != nil {
backgroundColor = *t.selectedBackgroundColor
}
style = tcell.StyleDefault.Background(backgroundColor).Foreground(foregroundColor)
}
PrintStyle(screen, []byte(node.text), x+node.textX+prefixWidth, posY, width-node.textX-prefixWidth, AlignLeft, style)
}
}
// Draw scroll bar.
RenderScrollBar(screen, t.scrollBarVisibility, x+(width-1), posY, height, rows, cursor, posY-y, t.hasFocus, t.scrollBarColor)
// Advance.
posY++
}
}
// InputHandler returns the handler for this primitive.
func (t *TreeView) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) {
return t.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p Primitive)) {
selectNode := func() {
t.Lock()
currentNode := t.currentNode
t.Unlock()
if currentNode == nil {
return
}
if t.selected != nil {
t.selected(currentNode)
}
if currentNode.focused != nil {
currentNode.focused()
}
if currentNode.selected != nil {
currentNode.selected()
}
}
t.Lock()
defer t.Unlock()
// Because the tree is flattened into a list only at drawing time, we also
// postpone the (selection) movement to drawing time.
if HitShortcut(event, Keys.Cancel, Keys.MovePreviousField, Keys.MoveNextField) {
if t.done != nil {
t.Unlock()
t.done(event.Key())
t.Lock()
}
} else if HitShortcut(event, Keys.MoveFirst, Keys.MoveFirst2) {
t.movement = treeHome
} else if HitShortcut(event, Keys.MoveLast, Keys.MoveLast2) {
t.movement = treeEnd
} else if HitShortcut(event, Keys.MoveUp, Keys.MoveUp2, Keys.MovePreviousField) {
t.movement = treeUp
} else if HitShortcut(event, Keys.MoveDown, Keys.MoveDown2, Keys.MoveNextField) {
t.movement = treeDown
} else if HitShortcut(event, Keys.MovePreviousPage) {
t.movement = treePageUp
} else if HitShortcut(event, Keys.MoveNextPage) {
t.movement = treePageDown
} else if HitShortcut(event, Keys.Select, Keys.Select2) {
t.Unlock()
selectNode()
t.Lock()
}
t.process()
})
}
// MouseHandler returns the mouse handler for this primitive.
func (t *TreeView) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
return t.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
x, y := event.Position()
if !t.InRect(x, y) {
return false, nil
}
switch action {
case MouseLeftClick:
_, rectY, _, _ := t.GetInnerRect()
y -= rectY
if y >= 0 && y < len(t.nodes) {
node := t.nodes[y]
if node.selectable {
if t.currentNode != node && t.changed != nil {
t.changed(node)
}
if t.selected != nil {
t.selected(node)
}
t.currentNode = node
}
}
consumed = true
setFocus(t)
case MouseScrollUp:
t.movement = treeUp
consumed = true
case MouseScrollDown:
t.movement = treeDown
consumed = true
}
return
})
}
package cview
import (
"fmt"
"math"
"regexp"
"sort"
"strconv"
"github.com/gdamore/tcell/v2"
"github.com/mattn/go-runewidth"
"github.com/rivo/uniseg"
)
// ColorUnset represents an unset color. This is necessary because the zero
// value of color, ColorDefault, results in default terminal colors.
var ColorUnset = tcell.ColorSpecial | 108
// Text alignment within a box.
const (
AlignLeft = iota
AlignCenter
AlignRight
)
// Common regular expressions.
var (
colorPattern = regexp.MustCompile(`\[([a-zA-Z]+|#[0-9a-zA-Z]{6}|\-)?(:([a-zA-Z]+|#[0-9a-zA-Z]{6}|\-)?(:([bdilrsu]+|\-)?)?)?\]`)
regionPattern = regexp.MustCompile(`\["([a-zA-Z0-9_,;: \-\.]*)"\]`)
escapePattern = regexp.MustCompile(`\[([a-zA-Z0-9_,;: \-\."#]+)\[(\[*)\]`)
nonEscapePattern = regexp.MustCompile(`(\[[a-zA-Z0-9_,;: \-\."#]+\[*)\]`)
boundaryPattern = regexp.MustCompile(`(([,\.\-:;!\?&#+]|\n)[ \t\f\r]*|([ \t\f\r]+))`)
spacePattern = regexp.MustCompile(`\s+`)
)
// Positions of substrings in regular expressions.
const (
colorForegroundPos = 1
colorBackgroundPos = 3
colorFlagPos = 5
)
// Predefined InputField acceptance functions.
var (
// InputFieldInteger accepts integers.
InputFieldInteger func(text string, ch rune) bool
// InputFieldFloat accepts floating-point numbers.
InputFieldFloat func(text string, ch rune) bool
// InputFieldMaxLength returns an input field accept handler which accepts
// input strings up to a given length. Use it like this:
//
// inputField.SetAcceptanceFunc(InputFieldMaxLength(10)) // Accept up to 10 characters.
InputFieldMaxLength func(maxLength int) func(text string, ch rune) bool
)
// Transformation describes a widget state modification.
type Transformation int
// Widget transformations.
const (
TransformFirstItem Transformation = 1
TransformLastItem Transformation = 2
TransformPreviousItem Transformation = 3
TransformNextItem Transformation = 4
TransformPreviousPage Transformation = 5
TransformNextPage Transformation = 6
)
// Package initialization.
func init() {
runewidth.EastAsianWidth = true
// Initialize the predefined input field handlers.
InputFieldInteger = func(text string, ch rune) bool {
if text == "-" {
return true
}
_, err := strconv.Atoi(text)
return err == nil
}
InputFieldFloat = func(text string, ch rune) bool {
if text == "-" || text == "." || text == "-." {
return true
}
_, err := strconv.ParseFloat(text, 64)
return err == nil
}
InputFieldMaxLength = func(maxLength int) func(text string, ch rune) bool {
return func(text string, ch rune) bool {
return len([]rune(text)) <= maxLength
}
}
}
// StripTags returns the provided text without color and/or region tags.
func StripTags(text []byte, colors bool, regions bool) []byte {
if !colors && !regions {
stripped := make([]byte, len(text))
copy(stripped, text)
return stripped
}
var stripped []byte
src := text
if regions {
stripped = regionPattern.ReplaceAll(text, nil)
src = stripped
}
if colors {
stripped = colorPattern.ReplaceAllFunc(src, func(match []byte) []byte {
if len(match) > 2 {
return nil
}
return match
})
}
return escapePattern.ReplaceAll(stripped, []byte(`[$1$2]`))
}
// ColorHex returns the hexadecimal value of a color as a string, prefixed with #.
// If the color is invalid, a blank string is returned.
func ColorHex(c tcell.Color) string {
if !c.Valid() {
return ""
}
r, g, b := c.RGB()
return fmt.Sprintf("#%02x%02x%02x", r, g, b)
}
// styleFromTag takes the given style, defined by a foreground color (fgColor),
// a background color (bgColor), and style attributes, and modifies it based on
// the substrings (tagSubstrings) extracted by the regular expression for color
// tags. The new colors and attributes are returned where empty strings mean
// "don't modify" and a dash ("-") means "reset to default".
func styleFromTag(fgColor, bgColor, attributes string, tagSubstrings [][]byte) (newFgColor, newBgColor, newAttributes string) {
if len(tagSubstrings[colorForegroundPos]) > 0 {
color := string(tagSubstrings[colorForegroundPos])
if color == "-" {
fgColor = "-"
} else if color != "" {
fgColor = color
}
}
if len(tagSubstrings[colorBackgroundPos-1]) > 0 {
color := string(tagSubstrings[colorBackgroundPos])
if color == "-" {
bgColor = "-"
} else if color != "" {
bgColor = color
}
}
if len(tagSubstrings[colorFlagPos-1]) > 0 {
flags := string(tagSubstrings[colorFlagPos])
if flags == "-" {
attributes = "-"
} else if flags != "" {
attributes = flags
}
}
return fgColor, bgColor, attributes
}
// overlayStyle mixes a background color with a foreground color (fgColor),
// a (possibly new) background color (bgColor), and style attributes, and
// returns the resulting style. For a definition of the colors and attributes,
// see styleFromTag(). Reset instructions cause the corresponding part of the
// default style to be used.
func overlayStyle(background tcell.Color, defaultStyle tcell.Style, fgColor, bgColor, attributes string) tcell.Style {
defFg, defBg, defAttr := defaultStyle.Decompose()
style := defaultStyle.Background(background)
style = style.Foreground(defFg)
if fgColor != "" {
if fgColor == "-" {
style = style.Foreground(defFg)
} else {
style = style.Foreground(tcell.GetColor(fgColor).TrueColor())
}
}
if bgColor == "-" || bgColor == "" && defBg != tcell.ColorDefault {
style = style.Background(defBg)
} else if bgColor != "" {
style = style.Background(tcell.GetColor(bgColor).TrueColor())
}
if attributes == "-" {
style = style.Bold(defAttr&tcell.AttrBold > 0)
style = style.Dim(defAttr&tcell.AttrDim > 0)
style = style.Italic(defAttr&tcell.AttrItalic > 0)
style = style.Blink(defAttr&tcell.AttrBlink > 0)
style = style.Reverse(defAttr&tcell.AttrReverse > 0)
style = style.StrikeThrough(defAttr&tcell.AttrStrikeThrough > 0)
style = style.Underline(defAttr&tcell.AttrUnderline > 0)
} else if attributes != "" {
style = style.Normal()
for _, flag := range attributes {
switch flag {
case 'b':
style = style.Bold(true)
case 'd':
style = style.Dim(true)
case 'i':
style = style.Italic(true)
case 'l':
style = style.Blink(true)
case 'r':
style = style.Reverse(true)
case 's':
style = style.StrikeThrough(true)
case 'u':
style = style.Underline(true)
}
}
}
return style
}
// SetAttributes sets attributes on a style.
func SetAttributes(style tcell.Style, attrs tcell.AttrMask) tcell.Style {
return style.
Bold(attrs&tcell.AttrBold != 0).
Dim(attrs&tcell.AttrDim != 0).
Italic(attrs&tcell.AttrItalic != 0).
Blink(attrs&tcell.AttrBlink != 0).
Reverse(attrs&tcell.AttrReverse != 0).
StrikeThrough(attrs&tcell.AttrStrikeThrough != 0).
Underline(attrs&tcell.AttrUnderline != 0)
}
// decomposeText returns information about a string which may contain color
// tags or region tags, depending on which ones are requested to be found. It
// returns the indices of the color tags (as returned by
// re.FindAllStringIndex()), the color tags themselves (as returned by
// re.FindAllStringSubmatch()), the indices of region tags and the region tags
// themselves, the indices of an escaped tags (only if at least color tags or
// region tags are requested), the string stripped by any tags and escaped, and
// the screen width of the stripped string.
func decomposeText(text []byte, findColors, findRegions bool) (colorIndices [][]int, colors [][][]byte, regionIndices [][]int, regions [][][]byte, escapeIndices [][]int, stripped []byte, width int) {
// Shortcut for the trivial case.
if !findColors && !findRegions {
return nil, nil, nil, nil, nil, text, runewidth.StringWidth(string(text))
}
// Get positions of any tags.
if findColors {
colorIndices = colorPattern.FindAllIndex(text, -1)
colors = colorPattern.FindAllSubmatch(text, -1)
}
if findRegions {
regionIndices = regionPattern.FindAllIndex(text, -1)
regions = regionPattern.FindAllSubmatch(text, -1)
}
escapeIndices = escapePattern.FindAllIndex(text, -1)
// Because the color pattern detects empty tags, we need to filter them out.
for i := len(colorIndices) - 1; i >= 0; i-- {
if colorIndices[i][1]-colorIndices[i][0] == 2 {
colorIndices = append(colorIndices[:i], colorIndices[i+1:]...)
colors = append(colors[:i], colors[i+1:]...)
}
}
// Make a (sorted) list of all tags.
var allIndices [][]int
if findColors && findRegions {
allIndices = colorIndices
allIndices = make([][]int, len(colorIndices)+len(regionIndices))
copy(allIndices, colorIndices)
copy(allIndices[len(colorIndices):], regionIndices)
sort.Slice(allIndices, func(i int, j int) bool {
return allIndices[i][0] < allIndices[j][0]
})
} else if findColors {
allIndices = colorIndices
} else {
allIndices = regionIndices
}
// Remove the tags from the original string.
var from int
buf := make([]byte, 0, len(text))
for _, indices := range allIndices {
buf = append(buf, text[from:indices[0]]...)
from = indices[1]
}
buf = append(buf, text[from:]...)
// Escape string.
stripped = escapePattern.ReplaceAll(buf, []byte("[$1$2]"))
// Get the width of the stripped string.
width = runewidth.StringWidth(string(stripped))
return
}
// Print prints text onto the screen into the given box at (x,y,maxWidth,1),
// not exceeding that box. "align" is one of AlignLeft, AlignCenter, or
// AlignRight. The screen's background color will not be changed.
//
// You can change the colors and text styles mid-text by inserting a color tag.
// See the package description for details.
//
// 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 []byte, x, y, maxWidth, align int, color tcell.Color) (int, int) {
return PrintStyle(screen, text, x, y, maxWidth, align, tcell.StyleDefault.Foreground(color))
}
// PrintStyle works like Print() but it takes a style instead of just a
// foreground color.
func PrintStyle(screen tcell.Screen, text []byte, x, y, maxWidth, align int, style tcell.Style) (int, int) {
if maxWidth <= 0 || len(text) == 0 {
return 0, 0
}
// Decompose the text.
colorIndices, colors, _, _, escapeIndices, strippedText, strippedWidth := decomposeText(text, true, false)
// We want to reduce all alignments to AlignLeft.
if align == AlignRight {
if strippedWidth <= maxWidth {
// There's enough space for the entire text.
return PrintStyle(screen, text, x+maxWidth-strippedWidth, y, maxWidth, AlignLeft, style)
}
// Trim characters off the beginning.
var (
bytes, width, colorPos, escapePos, tagOffset int
foregroundColor, backgroundColor, attributes string
)
_, originalBackground, _ := style.Decompose()
iterateString(string(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++
}
if escapePos < len(escapeIndices) && textPos+tagOffset >= escapeIndices[escapePos][0] && textPos+tagOffset < escapeIndices[escapePos][1] {
tagOffset++
escapePos++
}
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 = append(text[:escapeCharPos], text[escapeCharPos+1:]...)
}
// Print and return.
bytes, width = PrintStyle(screen, text[textPos+tagOffset:], x, y, maxWidth, AlignLeft, style)
return true
}
return false
})
return bytes, width
} else if align == AlignCenter {
if strippedWidth == maxWidth {
// Use the exact space.
return PrintStyle(screen, text, x, y, maxWidth, AlignLeft, style)
} else if strippedWidth < maxWidth {
// We have more space than we need.
half := (maxWidth - strippedWidth) / 2
return PrintStyle(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(strippedText)
for rightIndex-1 > leftIndex && strippedWidth-choppedLeft-choppedRight > maxWidth {
if choppedLeft < choppedRight {
// Iterate on the left by one character.
iterateString(string(strippedText[leftIndex:]), func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool {
choppedLeft += screenWidth
leftIndex += textWidth
return true
})
} else {
// Iterate on the right by one character.
iterateStringReverse(string(strippedText[leftIndex:rightIndex]), func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool {
choppedRight += screenWidth
rightIndex -= textWidth
return true
})
}
}
// 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 = append(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 PrintStyle(screen, text[leftIndex+tagOffset:], x, y, maxWidth, AlignLeft, style)
}
}
// Draw text.
var (
drawn, drawnWidth, colorPos, escapePos, tagOffset int
foregroundColor, backgroundColor, attributes string
)
iterateString(string(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
}
// Handle color tags.
for 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 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++
}
}
// 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)
}
}
// Advance.
drawn += length
drawnWidth += screenWidth
return false
})
return drawn + tagOffset + len(escapeIndices), drawnWidth
}
// PrintSimple prints white text to the screen at the given position.
func PrintSimple(screen tcell.Screen, text []byte, x, y int) {
Print(screen, text, x, y, math.MaxInt32, AlignLeft, Styles.PrimaryTextColor)
}
// TaggedTextWidth returns the width of the given string needed to print it on
// screen. The text may contain color tags which are not counted.
func TaggedTextWidth(text []byte) int {
_, _, _, _, _, _, width := decomposeText(text, true, false)
return width
}
// TaggedStringWidth returns the width of the given string needed to print it on
// screen. The text may contain color tags which are not counted.
func TaggedStringWidth(text string) int {
return TaggedTextWidth([]byte(text))
}
// WordWrap splits a text such that each resulting line does not exceed the
// given screen width. Possible split points are after any punctuation or
// whitespace. Whitespace after split points will be dropped.
//
// This function considers color tags to have no width.
//
// Text is always split at newline characters ('\n').
//
// Text must not be escaped.
func WordWrap(text string, width int) (lines []string) {
colorTagIndices, _, _, _, escapeIndices, strippedText, _ := decomposeText([]byte(text), true, false)
// Find candidate breakpoints.
breakpoints := boundaryPattern.FindAllSubmatchIndex(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, overflow int
forceBreak 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
if pos < 0 || pos > len(substr) {
return substr
}
return substr[:pos] + substr[pos+1:]
}
}
return substr
}
iterateString(string(strippedText), func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool {
// Handle tags.
for {
if colorPos < len(colorTagIndices) && textPos+tagOffset >= colorTagIndices[colorPos][0] && textPos+tagOffset < colorTagIndices[colorPos][1] {
// Colour tags.
tagOffset += colorTagIndices[colorPos][1] - colorTagIndices[colorPos][0]
colorPos++
} else if escapePos < len(escapeIndices) && textPos+tagOffset == escapeIndices[escapePos][1]-2 {
// Escape tags.
tagOffset++
escapePos++
} else {
break
}
}
// Is this a breakpoint?
if breakpointPos < len(breakpoints) && textPos+tagOffset == breakpoints[breakpointPos][0]+1 {
// Yes, it is. Set up breakpoint infos depending on its type.
lastBreakpoint = breakpoints[breakpointPos][0] + tagOffset
lastContinuation = breakpoints[breakpointPos][1] + tagOffset
overflow = 0
forceBreak = main == '\n'
if breakpoints[breakpointPos][6] < 0 && !forceBreak {
lastBreakpoint++ // Don't skip punctuation.
}
breakpointPos++
}
// Check if a break is warranted.
if forceBreak || lineWidth > 0 && lineWidth+screenWidth > width {
breakpoint := lastBreakpoint
continuation := lastContinuation
if forceBreak {
breakpoint = textPos + tagOffset
continuation = textPos + tagOffset + 1
lastBreakpoint = 0
overflow = 0
} else if lastBreakpoint <= currentLineStart {
breakpoint = textPos + tagOffset
continuation = textPos + tagOffset
overflow = 0
}
lines = append(lines, unescape(text[currentLineStart:breakpoint], currentLineStart))
currentLineStart, lineWidth, forceBreak = continuation, overflow, false
}
// Remember the characters since the last breakpoint.
if lastBreakpoint > 0 && lastContinuation <= textPos+tagOffset {
overflow += screenWidth
}
// Advance.
lineWidth += screenWidth
// But if we're still inside a breakpoint, skip next character (whitespace).
if textPos+tagOffset < currentLineStart {
lineWidth -= screenWidth
}
return false
})
// Flush the rest.
if currentLineStart < len(text) {
lines = append(lines, unescape(text[currentLineStart:], currentLineStart))
}
return
}
// EscapeBytes escapes the given text such that color and/or region tags are not
// recognized and substituted by the print functions of this package. For
// example, to include a tag-like string in a box title or in a TextView:
//
// box.SetTitle(cview.Escape("[squarebrackets]"))
// fmt.Fprint(textView, cview.EscapeBytes(`["quoted"]`))
func EscapeBytes(text []byte) []byte {
return nonEscapePattern.ReplaceAll(text, []byte("$1[]"))
}
// Escape escapes the given text such that color and/or region tags are not
// recognized and substituted by the print functions of this package. For
// example, to include a tag-like string in a box title or in a TextView:
//
// box.SetTitle(cview.Escape("[squarebrackets]"))
// fmt.Fprint(textView, cview.Escape(`["quoted"]`))
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 screenPos int
gr := uniseg.NewGraphemes(text)
for gr.Next() {
r := gr.Runes()
from, to := gr.Positions()
width := runewidth.StringWidth(gr.Str())
var comb []rune
if len(r) > 1 {
comb = r[1:]
}
if callback(r[0], comb, from, to-from, screenPos, width) {
return true
}
screenPos += width
}
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 cluster struct {
main rune
comb []rune
textPos, textWidth, screenPos, screenWidth int
}
// Create the grapheme clusters.
var clusters []cluster
iterateString(text, func(main rune, comb []rune, textPos int, textWidth int, screenPos int, screenWidth int) bool {
clusters = append(clusters, cluster{
main: main,
comb: comb,
textPos: textPos,
textWidth: textWidth,
screenPos: screenPos,
screenWidth: screenWidth,
})
return false
})
// Iterate in reverse.
for index := len(clusters) - 1; index >= 0; index-- {
if callback(
clusters[index].main,
clusters[index].comb,
clusters[index].textPos,
clusters[index].textWidth,
clusters[index].screenPos,
clusters[index].screenWidth,
) {
return true
}
}
return false
}
// ScrollBarVisibility specifies the display of a scroll bar.
type ScrollBarVisibility int
const (
// ScrollBarNever never shows a scroll bar.
ScrollBarNever ScrollBarVisibility = iota
// ScrollBarAuto shows a scroll bar when there are items offscreen.
ScrollBarAuto
// ScrollBarAlways always shows a scroll bar.
ScrollBarAlways
)
// Scroll bar render text (must be one cell wide)
var (
ScrollBarArea = []byte("[-:-:-]░")
ScrollBarAreaFocused = []byte("[-:-:-]▒")
ScrollBarHandle = []byte("[-:-:-]▓")
ScrollBarHandleFocused = []byte("[::r] [-:-:-]")
)
// RenderScrollBar renders a scroll bar at the specified position.
func RenderScrollBar(screen tcell.Screen, visibility ScrollBarVisibility, x int, y int, height int, items int, cursor int, printed int, focused bool, color tcell.Color) {
if visibility == ScrollBarNever || (visibility == ScrollBarAuto && items <= height) {
return
}
// Place cursor at top when there are no items offscreen.
if items <= height {
cursor = 0
}
// Handle negative cursor.
if cursor < 0 {
cursor = 0
}
// Calculate handle position.
handlePosition := int(float64(height-1) * (float64(cursor) / float64(items-1)))
// Print scroll bar.
var text []byte
if printed == handlePosition {
if focused {
text = ScrollBarHandleFocused
} else {
text = ScrollBarHandle
}
} else {
if focused {
text = ScrollBarAreaFocused
} else {
text = ScrollBarArea
}
}
Print(screen, text, x, y, 1, AlignLeft, color)
}
package cview
import (
"sync"
"github.com/gdamore/tcell/v2"
)
// Window is a draggable, resizable frame around a primitive. Windows must be
// added to a WindowManager.
type Window struct {
*Box
primitive Primitive
fullscreen bool
normalX, normalY int
normalW, normalH int
dragX, dragY int
dragWX, dragWY int
sync.RWMutex
}
// NewWindow returns a new window around the given primitive.
func NewWindow(primitive Primitive) *Window {
w := &Window{
Box: NewBox(),
primitive: primitive,
dragWX: -1,
dragWY: -1,
}
w.Box.focus = w
return w
}
// SetFullscreen sets the flag indicating whether or not the the window should
// be drawn fullscreen.
func (w *Window) SetFullscreen(fullscreen bool) {
w.Lock()
defer w.Unlock()
if w.fullscreen == fullscreen {
return
}
w.fullscreen = fullscreen
if w.fullscreen {
w.normalX, w.normalY, w.normalW, w.normalH = w.GetRect()
} else {
w.SetRect(w.normalX, w.normalY, w.normalW, w.normalH)
}
}
// Focus is called when this primitive receives focus.
func (w *Window) Focus(delegate func(p Primitive)) {
w.Lock()
defer w.Unlock()
w.Box.Focus(delegate)
w.primitive.Focus(delegate)
}
// Blur is called when this primitive loses focus.
func (w *Window) Blur() {
w.Lock()
defer w.Unlock()
w.Box.Blur()
w.primitive.Blur()
}
// HasFocus returns whether or not this primitive has focus.
func (w *Window) HasFocus() bool {
w.RLock()
defer w.RUnlock()
focusable := w.primitive.GetFocusable()
if focusable != nil {
return focusable.HasFocus()
}
return w.Box.HasFocus()
}
// Draw draws this primitive onto the screen.
func (w *Window) Draw(screen tcell.Screen) {
if !w.GetVisible() {
return
}
w.RLock()
defer w.RUnlock()
w.Box.Draw(screen)
x, y, width, height := w.GetInnerRect()
w.primitive.SetRect(x, y, width, height)
w.primitive.Draw(screen)
}
// InputHandler returns the handler for this primitive.
func (w *Window) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) {
return w.primitive.InputHandler()
}
// MouseHandler returns the mouse handler for this primitive.
func (w *Window) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
return w.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
if !w.InRect(event.Position()) {
return false, nil
}
if action == MouseLeftDown || action == MouseMiddleDown || action == MouseRightDown {
setFocus(w)
}
if action == MouseLeftDown {
x, y, width, height := w.GetRect()
mouseX, mouseY := event.Position()
leftEdge := mouseX == x
rightEdge := mouseX == x+width-1
bottomEdge := mouseY == y+height-1
topEdge := mouseY == y
if mouseY >= y && mouseY <= y+height-1 {
if leftEdge {
w.dragX = -1
} else if rightEdge {
w.dragX = 1
}
}
if mouseX >= x && mouseX <= x+width-1 {
if bottomEdge {
w.dragY = -1
} else if topEdge {
if leftEdge || rightEdge {
w.dragY = 1
} else {
w.dragWX = mouseX - x
w.dragWY = mouseY - y
}
}
}
}
_, capture = w.primitive.MouseHandler()(action, event, setFocus)
return true, capture
})
}
package cview
import (
"sync"
"github.com/gdamore/tcell/v2"
)
// WindowManager provides an area which windows may be added to.
type WindowManager struct {
*Box
windows []*Window
sync.RWMutex
}
// NewWindowManager returns a new window manager.
func NewWindowManager() *WindowManager {
return &WindowManager{
Box: NewBox(),
}
}
// Add adds a window to the manager.
func (wm *WindowManager) Add(w ...*Window) {
wm.Lock()
defer wm.Unlock()
for _, window := range w {
window.SetBorder(true)
}
wm.windows = append(wm.windows, w...)
}
// Clear removes all windows from the manager.
func (wm *WindowManager) Clear() {
wm.Lock()
defer wm.Unlock()
wm.windows = nil
}
// Focus is called when this primitive receives focus.
func (wm *WindowManager) Focus(delegate func(p Primitive)) {
wm.Lock()
defer wm.Unlock()
if len(wm.windows) == 0 {
return
}
wm.windows[len(wm.windows)-1].Focus(delegate)
}
// HasFocus returns whether or not this primitive has focus.
func (wm *WindowManager) HasFocus() bool {
wm.RLock()
defer wm.RUnlock()
for _, w := range wm.windows {
if w.HasFocus() {
return true
}
}
return false
}
// Draw draws this primitive onto the screen.
func (wm *WindowManager) Draw(screen tcell.Screen) {
if !wm.GetVisible() {
return
}
wm.RLock()
defer wm.RUnlock()
wm.Box.Draw(screen)
x, y, width, height := wm.GetInnerRect()
var hasFullScreen bool
for _, w := range wm.windows {
if !w.fullscreen || !w.GetVisible() {
continue
}
hasFullScreen = true
w.SetRect(x-1, y, width+2, height+1)
w.Draw(screen)
}
if hasFullScreen {
return
}
for _, w := range wm.windows {
if !w.GetVisible() {
continue
}
// Reposition out of bounds windows
margin := 3
wx, wy, ww, wh := w.GetRect()
ox, oy := wx, wy
if wx > x+width-margin {
wx = x + width - margin
}
if wx+ww < x+margin {
wx = x - ww + margin
}
if wy > y+height-margin {
wy = y + height - margin
}
if wy < y {
wy = y // No top margin
}
if wx != ox || wy != oy {
w.SetRect(wx, wy, ww, wh)
}
w.Draw(screen)
}
}
// MouseHandler returns the mouse handler for this primitive.
func (wm *WindowManager) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
return wm.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
if !wm.InRect(event.Position()) {
return false, nil
}
if action == MouseMove {
mouseX, mouseY := event.Position()
for _, w := range wm.windows {
if w.dragWX != -1 || w.dragWY != -1 {
offsetX := w.x - mouseX
offsetY := w.y - mouseY
w.x -= offsetX + w.dragWX
w.y -= offsetY + w.dragWY
w.updateInnerRect()
consumed = true
}
if w.dragX != 0 {
if w.dragX == -1 {
offsetX := w.x - mouseX
if w.width+offsetX >= Styles.WindowMinWidth {
w.x -= offsetX
w.width += offsetX
}
} else {
offsetX := mouseX - (w.x + w.width) + 1
if w.width+offsetX >= Styles.WindowMinWidth {
w.width += offsetX
}
}
w.updateInnerRect()
consumed = true
}
if w.dragY != 0 {
if w.dragY == -1 {
offsetY := mouseY - (w.y + w.height) + 1
if w.height+offsetY >= Styles.WindowMinHeight {
w.height += offsetY
}
} else {
offsetY := w.y - mouseY
if w.height+offsetY >= Styles.WindowMinHeight {
w.y -= offsetY
w.height += offsetY
}
}
w.updateInnerRect()
consumed = true
}
}
} else if action == MouseLeftUp {
for _, w := range wm.windows {
w.dragX, w.dragY = 0, 0
w.dragWX, w.dragWY = -1, -1
}
}
// Focus window on mousedown
var (
focusWindow *Window
focusWindowIndex int
)
for i := len(wm.windows) - 1; i >= 0; i-- {
if wm.windows[i].InRect(event.Position()) {
focusWindow = wm.windows[i]
focusWindowIndex = i
break
}
}
if focusWindow != nil {
if action == MouseLeftDown || action == MouseMiddleDown || action == MouseRightDown {
for _, w := range wm.windows {
if w != focusWindow {
w.Blur()
}
}
wm.windows = append(append(wm.windows[:focusWindowIndex], wm.windows[focusWindowIndex+1:]...), focusWindow)
}
return focusWindow.MouseHandler()(action, event, setFocus)
}
return consumed, nil
})
}
// Package cpick is an interactive color picker in the terminal using cview
package cpick
import (
"fmt"
color "github.com/ethanbaker/colors"
"github.com/ethanbaker/cpick/cview"
"github.com/gdamore/tcell/v2"
)
// Default keys, runes, and mod masks for simplicity
var dk tcell.Key = tcell.KeyF64
var dr rune = '_'
var dm tcell.ModMask
// Easy way to simulate a tcell event
func simEvent(key tcell.Key, r rune, m tcell.ModMask) *tcell.EventKey {
return tcell.NewEventKey(key, r, m)
}
// Keys that will be used commonly
var enter tcell.Key = tcell.KeyEnter
var escape tcell.Key = tcell.KeyEscape
var tab tcell.Key = tcell.KeyTab
var movementRunes = []rune{'l', 'h', 'j', 'k', 'G'}
var movementKeys = []tcell.Key{tcell.KeyLeft, tcell.KeyRight, tcell.KeyUp, tcell.KeyDown}
// Tester funcion used to test functions used in cpick without having
// to export them or use them in an interactive application.
func tester() error {
// Test screen setups
hScreenSetup()
svScreenSetup()
// Test non-error returning functions
testHelp()
// Test error returning functions
var errFuncs = [...]func() error{testColorPages, testHTable, testSVTable, testSearch, testInputCapture}
for _, v := range errFuncs {
err := v()
if err != nil {
return err
}
}
_, err := Start(false)
return err
}
func testColorPages() error {
app.SetFocus(colorPages)
// Test setup
colorPageSetup()
// Test done function
colorPageDoneFunc(escape)
colorPageDoneFunc(tab)
// Test selected function
colorPageSelectedFunc(0, 0)
colorPageSelectedFunc(0, 3)
// Test selection changed function
for i := 0; i < len(colorInfo); i++ {
colorInfo[i].table.Select(1, 1)
}
// Test capture handler
err := testColorPageCaptureHandler()
if err != nil {
return err
}
// Test color name getter function
var hsvs = [...]color.HSV{{H: 0, S: 100, V: 100}, {H: 0, S: 100, V: 99}}
var altHsvs = [...]color.HSV{{H: 0, S: 100, V: 99}, {H: 0, S: 100, V: 98}}
for i := 0; i < len(hsvs); i++ {
name := getColorName(hsvs[i], altHsvs[i])
switch i {
case 0:
if name != "red" {
return fmt.Errorf(fmt.Sprintf("Error! getColorName(%v, %v) is not properly returning red!\nOutput: %v\n", hsvs[i], altHsvs[i], name))
}
case 1:
if name != "custom color" {
return fmt.Errorf(fmt.Sprintf("Error! getColorName(%v, %v) is not properly returning custom color!\nOutput: %v\n", hsvs[i], altHsvs[i], name))
}
}
}
// Test path getter function
_, err = getPath()
if err != nil {
return err
}
// Test colors getter function
var paths = [...]string{"", "./testing/colors.json"}
for _, v := range paths {
data := getCustomColors(v)
if data.COLORLIST[0].NAME != "css" {
return fmt.Errorf(fmt.Sprintf("Error! getCustomColors(%v) is not properly returning presetData!\nOutput: %v\n", v, data))
}
}
return nil
}
// Test capture handler
func testColorPageCaptureHandler() error {
var eventRunes = [...]rune{'C', 'c', 'C', 'c', ' ', 'n', 'N'}
for i, v := range eventRunes {
setEvent := simEvent(dk, v, dm)
returnEvent := colorPageCaptureHandler(setEvent)
switch i {
case 0:
colorInfo[colorPageIndex].table.Select(3, 1)
case 1:
colorInfo[colorPageIndex].table.Select(0, 0)
}
if setEvent != returnEvent {
return fmt.Errorf(fmt.Sprintf("Error! colorPageCaptureHandler(%v) is not properly returning event!\nOutput: %v\n", setEvent, returnEvent))
}
}
for _, v := range movementRunes {
setEvent := simEvent(dk, v, dm)
returnEvent := colorPageCaptureHandler(setEvent)
if v == 'G' {
if returnEvent != nil {
return fmt.Errorf(fmt.Sprintf("Error! colorPageCaptureHandler(%v) is not properly returning nil for movement rune 'G'!\nOutput: %v\n", setEvent, returnEvent))
}
} else {
if returnEvent != setEvent {
return fmt.Errorf(fmt.Sprintf("Error! colorPageCaptureHandler(%v) is not properly returning nil for movement runes!\nOutput: %v\n", setEvent, returnEvent))
}
}
}
colorInfo[0].table.Select(1, 1)
for _, v := range movementKeys {
setEvent := simEvent(v, dr, dm)
returnEvent := colorPageCaptureHandler(setEvent)
if v == 'G' {
if returnEvent != nil {
return fmt.Errorf(fmt.Sprintf("Error! colorPageCaptureHandler(%v) is not properly returning nil for movement rune 'G'!\nOutput: %v\n", setEvent, returnEvent))
}
} else {
if returnEvent != setEvent {
return fmt.Errorf(fmt.Sprintf("Error! colorPageCaptureHandler(%v) is not properly returning nil for movement runes!\nOutput: %v\n", setEvent, returnEvent))
}
}
}
return nil
}
func testHTable() error {
app.SetFocus(hTable)
// Test setup function
hTableSetup()
// Test done function
hTableDoneFunc(escape)
hTableDoneFunc(tab)
// Test selected function
hTableSelectedFunc(0, 0)
// Test selection changed function
hTableSelectionChangedFunc(0, 0)
// Test capture handler
for i := 0; i < 2; i++ {
setEvent := simEvent(dk, ' ', dm)
returnEvent := hCaptureHandler(setEvent)
if setEvent != returnEvent {
return fmt.Errorf(fmt.Sprintf("Error! hCaptureHandler(%v) is not properly returning event!\nOutput: %v\n", setEvent, returnEvent))
}
colorInfo[0].table.Select(0, 1)
}
return nil
}
func testSVTable() error {
app.SetFocus(svTable)
// Test setup function
svTableSetup()
// Test done function
svTableDoneFunc(escape)
svTableDoneFunc(tab)
// Test selected function
svTableSelectedFunc(0, 0)
// Test selection changed function
svTableSelectionChangedFunc(0, 0)
svTableSelectionChangedFunc(50, 0)
// Test capture handler
setEvent := simEvent(dk, dr, dm)
returnEvent := svCaptureHandler(setEvent)
if setEvent != returnEvent {
return fmt.Errorf(fmt.Sprintf("Error! svCaptureHandler(%v) is not properly returning event!\nOutput: %v\n", setEvent, returnEvent))
}
// Test draw function
drawSVTable()
// Test block drawing
darkHSV1 := color.HSV{H: 0, S: 101, V: 101}
darkHSV2 := color.HSV{H: -1, S: -1, V: -1}
lightHSV1 := color.HSV{H: 0, S: 101, V: 101}
lightHSV2 := color.HSV{H: -1, S: -1, V: -1}
setColorValues(darkHSV1, darkSVBlock, darkSVText, lightHSV1, lightSVBlock, lightSVText)
setColorValues(darkHSV2, darkSVBlock, darkSVText, lightHSV2, lightSVBlock, lightSVText)
return nil
}
func testHelp() {
app.SetFocus(helpModal)
// Test setup function
helpPageSetup()
var primitives = [...]cview.Primitive{colorPages, hTable, svTable}
// Test done function and show help function
for _, v := range primitives {
helpFocus = v
helpModalDoneFunc(0, "Exit help")
app.SetFocus(v)
showHelp()
}
}
func testSearch() error {
app.SetFocus(searchInput)
// Test setup function
searchInputSetup()
// Test done function
searchInputDoneFunc(escape)
searchInput.SetText("red")
searchInputDoneFunc(enter)
// Test autocomplete function
searchInputAutocompleteFunc("")
searchInputAutocompleteFunc("red")
searchInputAutocompleteFunc("lightgoldenrodyellow")
searchInputAutocompleteFunc("?")
// Test parsing function
parseSearchText("#ffffff")
parseSearchText("#fffffff")
parseSearchText("rgb:a")
parseSearchText("rgb:0 0 0")
parseSearchText("rgb:0 0 -1")
parseSearchText("rgb:0 0 0 0")
parseSearchText("hsv:0 0 0")
parseSearchText("hsv:-1 0 0")
parseSearchText("hsv:0 0 -1")
parseSearchText("hsv:0 0 0 0")
parseSearchText("hsl:0 0 0")
parseSearchText("hsl:-1 0 0")
parseSearchText("hsl:0 0 -1")
parseSearchText("hsl:0 0 0 0")
parseSearchText("cmyk: 0 0 0 0")
parseSearchText("cmyk: 0 0 0 -1")
parseSearchText("cmyk: 0 0 0 0 0")
parseSearchText("decimal: 0")
parseSearchText("decimal: -1")
parseSearchText("decimal: 0 0")
parseSearchText("ansi:a")
// Test capture handler
var eventRunes = [...]rune{'n', 'N'}
for i, v := range eventRunes {
searchIndexes = [][]int{{0, 0, 0}, {1, 1, 1}}
switch i {
case 0:
searchIndex = 0
case 1:
searchIndex = len(searchIndexes) - 1
}
setEvent := simEvent(dk, v, dm)
returnEvent := searchInputCaptureHandler(setEvent)
if setEvent != returnEvent {
return fmt.Errorf(fmt.Sprintf("Error! searchInputCaptureHandler(%v) is not properly returning event!\nOutput: %v\n", setEvent, returnEvent))
}
}
return nil
}
func testInputCapture() error {
var eventKeys = [...]rune{'q', 'q', '`', '?'}
for i, v := range eventKeys {
switch i {
case 1:
app.SetFocus(searchInput)
case 3:
app.SetFocus(svTable)
}
setEvent := simEvent(dk, v, dm)
returnEvent := inputCaptureHandler(setEvent)
if i == 3 {
setEvent = nil
}
if setEvent != returnEvent {
return fmt.Errorf(fmt.Sprintf("Error! inputCaptureHandler(%v) is not properly returning event!\nOutput: %v\n", simEvent(dk, v, dm), returnEvent))
}
}
var primitives = [...]cview.Primitive{hTable, colorPages, svTable}
for _, v := range primitives {
app.SetFocus(v)
setEvent := simEvent(dk, dr, dm)
returnEvent := inputCaptureHandler(setEvent)
if setEvent != returnEvent {
return fmt.Errorf(fmt.Sprintf("Error! inputCaptureHandler(%v) is not properly returning event!\nOutput: %v\n", simEvent(dk, dr, dm), returnEvent))
}
}
return nil
}