tui/styles.go (368 lines of code) (raw):
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package tui
import (
"fmt"
"os"
"strings"
"github.com/charmbracelet/bubbles/list"
"github.com/charmbracelet/bubbles/table"
"github.com/charmbracelet/lipgloss"
"github.com/muesli/termenv"
"golang.org/x/term"
)
type ansi16colors []ansi16color
func (a ansi16colors) code(s string) string {
for _, v := range a {
if s == v.name {
return v.colorCode
}
}
return ""
}
func (a ansi16colors) codeByID(i int) string {
for _, v := range a {
if i == v.id {
return v.colorCode
}
}
return ""
}
func (a ansi16colors) color(s string) ansi16color {
for _, v := range a {
if strings.TrimSpace(s) == strings.TrimSpace(v.name) {
return v
}
}
return ansi16color{}
}
func (a ansi16colors) colorByID(i int) ansi16color {
for _, v := range a {
if i == v.id {
return v
}
}
return ansi16color{}
}
type ansi16color struct {
id int
name string
colorCode string
}
var textColors = ansi16colors{
{id: 0, name: "black", colorCode: "\033[0;30m"},
{id: 1, name: "red", colorCode: "\033[0;31m"},
{id: 2, name: "green", colorCode: "\033[0;32m"},
{id: 3, name: "yellow", colorCode: "\033[0;33m"},
{id: 4, name: "blue", colorCode: "\033[0;34m"},
{id: 5, name: "magenta", colorCode: "\033[0;35m"},
{id: 6, name: "cyan", colorCode: "\033[0;36m"},
{id: 7, name: "white", colorCode: "\033[0;37m"},
{id: 8, name: "bright black", colorCode: "\033[1;30m"},
{id: 9, name: "bright red", colorCode: "\033[1;31m"},
{id: 10, name: "bright green", colorCode: "\033[1;32m"},
{id: 11, name: "bright yellow", colorCode: "\033[1;33m"},
{id: 12, name: "bright blue", colorCode: "\033[1;34m"},
{id: 13, name: "bright magenta", colorCode: "\033[1;35m"},
{id: 14, name: "bright cyan", colorCode: "\033[1;36m"},
{id: 15, name: "bright white", colorCode: "\033[1;37m"},
{id: 8, name: "dark grey", colorCode: "\033[1;30m"},
{id: 8, name: "dark gray", colorCode: "\033[1;30m"},
{id: 7, name: "light grey", colorCode: "\033[0;37m"},
{id: 7, name: "light gray", colorCode: "\033[0;37m"},
}
var backgroundColors = ansi16colors{
{id: -1, name: "blank", colorCode: ""},
{id: 0, name: "black", colorCode: "\033[0;40m"},
{id: 1, name: "red", colorCode: "\033[0;41m"},
{id: 2, name: "green", colorCode: "\033[0;42m"},
{id: 3, name: "yellow", colorCode: "\033[0;43m"},
{id: 4, name: "blue", colorCode: "\033[0;44m"},
{id: 5, name: "magenta", colorCode: "\033[0;45m"},
{id: 6, name: "cyan", colorCode: "\033[0;46m"},
{id: 7, name: "white", colorCode: "\033[0;47m"},
{id: 8, name: "bold on black", colorCode: "\033[0;40m"},
{id: 9, name: "bold on red", colorCode: "\033[0;41m"},
{id: 10, name: "bold on green", colorCode: "\033[0;42m"},
{id: 11, name: "bold on yellow", colorCode: "\033[0;43m"},
{id: 12, name: "bold on blue", colorCode: "\033[0;44m"},
{id: 13, name: "bold on magenta", colorCode: "\033[0;45m"},
{id: 14, name: "bold on cyan", colorCode: "\033[0;46m"},
{id: 15, name: "bold on white", colorCode: "\033[0;47m"},
}
type dsAdaptiveColor struct {
light ansi16color
dark ansi16color
blankOnCloudShell bool
}
func (a dsAdaptiveColor) code() string {
if a.blankOnCloudShell && os.Getenv("GOOGLE_CLOUD_SHELL") != "" {
return clear
}
if termenv.HasDarkBackground() {
return a.dark.colorCode
}
return a.light.colorCode
}
var clear = "\033[0m"
type dsStyle struct {
style lipgloss.Style
foreground dsAdaptiveColor
background dsAdaptiveColor
bright bool
underline bool
bold bool
}
func (d dsStyle) Render(s string) string {
startFg := d.foreground.code()
if d.underline {
// Replace the right character with the underline trigger
sl := strings.Split(startFg, "")
sl[2] = "4"
startFg = strings.Join(sl, "")
}
startBg := d.background.code()
content := d.style.Render(s)
return fmt.Sprintf("%s%s%s%s", startFg, startBg, content, clear)
}
func newDsStyle() dsStyle {
blankBG := backgroundColors.color("blank")
black := textColors.color("black")
white := textColors.color("light grey")
r := dsStyle{style: lipgloss.NewStyle()}
r.foreground = dsAdaptiveColor{light: black, dark: white, blankOnCloudShell: true}
r.background = dsAdaptiveColor{light: blankBG, dark: blankBG}
return r
}
func (d dsStyle) Foreground(a dsAdaptiveColor) dsStyle {
d.foreground = a
return d
}
func (d dsStyle) Background(a dsAdaptiveColor) dsStyle {
d.background = a
return d
}
func (d dsStyle) Bright(t bool) dsStyle {
d.bright = t
return d
}
func (d dsStyle) Bold(t bool) dsStyle {
d.bold = t
return d
}
func (d dsStyle) Underline(t bool) dsStyle {
d.underline = t
return d
}
func (d dsStyle) Width(i int) dsStyle {
d.style = d.style.Width(i)
return d
}
func (d dsStyle) Height(i int) dsStyle {
d.style = d.style.Height(i)
return d
}
func (d dsStyle) MarginLeft(i int) dsStyle {
d.style = d.style.MarginLeft(i)
return d
}
func (d dsStyle) MarginTop(i int) dsStyle {
d.style = d.style.MarginTop(i)
return d
}
func (d dsStyle) MarginRight(i int) dsStyle {
d.style = d.style.MarginRight(i)
return d
}
func (d dsStyle) MarginBottom(i int) dsStyle {
d.style = d.style.MarginBottom(i)
return d
}
func (d dsStyle) Margin(i ...int) dsStyle {
d.style = d.style.Margin(i...)
return d
}
func (d dsStyle) PaddingLeft(i int) dsStyle {
d.style = d.style.PaddingLeft(i)
return d
}
func (d dsStyle) PaddingTop(i int) dsStyle {
d.style = d.style.PaddingTop(i)
return d
}
func (d dsStyle) PaddingRight(i int) dsStyle {
d.style = d.style.PaddingRight(i)
return d
}
func (d dsStyle) PaddingBottom(i int) dsStyle {
d.style = d.style.PaddingBottom(i)
return d
}
func (d dsStyle) Padding(i ...int) dsStyle {
d.style = d.style.Padding(i...)
return d
}
func (d dsStyle) MaxWidth(i int) dsStyle {
d.style = d.style.MaxWidth(i)
return d
}
func (d dsStyle) Italic(t bool) dsStyle {
d.style = d.style.Italic(t)
return d
}
func (d dsStyle) Copy() dsStyle {
r := dsStyle{}
r.style = d.style.Copy()
r.foreground = d.foreground
r.background = d.background
r.bright = d.bright
r.underline = d.underline
r.bold = d.bold
return r
}
func (d dsStyle) BorderLeft(t bool) dsStyle {
d.style = d.style.BorderLeft(t)
return d
}
func (d dsStyle) BorderTop(t bool) dsStyle {
d.style = d.style.BorderTop(t)
return d
}
func (d dsStyle) BorderRight(t bool) dsStyle {
d.style = d.style.BorderRight(t)
return d
}
func (d dsStyle) BorderBottom(t bool) dsStyle {
d.style = d.style.BorderBottom(t)
return d
}
func (d dsStyle) BorderStyle(b lipgloss.Border) dsStyle {
d.style = d.style.Border(b)
return d
}
func (d dsStyle) BorderForeground(b lipgloss.TerminalColor) dsStyle {
d.style = d.style.BorderForeground(b)
return d
}
var (
width = 100
hardWidthLimit = width
lgbasicText = lipgloss.AdaptiveColor{Light: "0", Dark: "15"}
lggray = lipgloss.AdaptiveColor{Light: "7", Dark: "8"}
lggrayWeak = lipgloss.AdaptiveColor{Light: "8", Dark: "7"}
lgalert = lipgloss.AdaptiveColor{Light: "1", Dark: "9"}
gray = dsAdaptiveColor{light: textColors.color("white"), dark: textColors.color("dark grey")}
grayWeak = dsAdaptiveColor{light: textColors.color("dark grey"), dark: textColors.color("white")}
simClearColor = dsAdaptiveColor{light: textColors.color("bright white"), dark: textColors.colorByID(0)}
highlight = dsAdaptiveColor{light: textColors.color("cyan"), dark: textColors.color("bright cyan")}
basicText = dsAdaptiveColor{light: textColors.color("black"), dark: textColors.color("light grey"), blankOnCloudShell: true}
alert = dsAdaptiveColor{light: textColors.color("red"), dark: textColors.color("bright red")}
completeColor = dsAdaptiveColor{light: textColors.color("dark grey"), dark: textColors.color("dark grey")}
pendingColor = dsAdaptiveColor{light: textColors.color("cyan"), dark: textColors.color("bright cyan")}
highlightBG = dsAdaptiveColor{light: backgroundColors.color("bold on cyan"), dark: backgroundColors.color("cyan")}
strong = newDsStyle().
Foreground(highlight)
normal = newDsStyle().
Foreground(basicText)
url = newDsStyle().
Foreground(highlight).
Underline(true)
titleStyle = newDsStyle().
Bold(true).
Foreground(basicText)
purchaseStyle = newDsStyle().
Bold(true).
Foreground(alert).
Background(gray)
subTitleStyle = newDsStyle().
MaxWidth(hardWidthLimit).
Bold(false).
Foreground(basicText)
headerCopyStyle = newDsStyle().
MaxWidth(hardWidthLimit)
headerStyle = newDsStyle().
MarginLeft(0).
MarginRight(0).
Padding(0, 3).
BorderStyle(lipgloss.ThickBorder()).
BorderTop(false).
BorderLeft(false).
BorderRight(false).
BorderBottom(true).
MaxWidth(hardWidthLimit).
BorderForeground(lggray).
Width(width)
cursorPromptStyle = newDsStyle().
Foreground(highlight)
bodyStyle = newDsStyle().
MarginLeft(0).
MarginRight(0).
Padding(0, 3).
Foreground(basicText).
Width(width).
MaxWidth(hardWidthLimit)
docStyle = newDsStyle().
Foreground(basicText).
Padding(0, 2)
promptStyle = newDsStyle().
Bold(true).
Background(highlightBG).
Foreground(dsAdaptiveColor{light: textColors.color("white"), dark: textColors.color("white")})
alertStyle = bodyStyle.Copy().
Foreground(alert)
alertStrongStyle = bodyStyle.Copy().
Foreground(alert).
PaddingLeft(3).Bold(true)
instructionStyle = newDsStyle().
PaddingLeft(3)
textStyle = newDsStyle().
Foreground(basicText)
textInputDefaultStyle = newDsStyle().
Foreground(highlight)
tableStyle = table.DefaultStyles()
inputText = bodyStyle.Copy().
Foreground(highlight)
componentStyle = newDsStyle().
PaddingLeft(1).
MarginLeft(0)
billingDisabledStyle = newDsStyle().
Foreground(gray)
itemStyle = newDsStyle().
PaddingLeft(4)
selectedItemStyle = newDsStyle().
PaddingLeft(2).
Background(highlightBG).
Foreground(basicText)
paginationStyle = list.DefaultStyles().
PaginationStyle.PaddingLeft(4)
helpStyle = list.DefaultStyles().
HelpStyle.
PaddingLeft(4).
PaddingBottom(1).
Foreground(lggrayWeak)
quitTextStyle = newDsStyle().
Margin(1, 0, 2, 4)
spinnerStyle = newDsStyle().Foreground(highlight)
textInputPrompt = helpStyle.Copy().
PaddingLeft(3)
completeStyle = newDsStyle().Foreground(highlight)
pendingStyle = newDsStyle().Foreground(grayWeak)
errorAlertStyle = lipgloss.NewStyle().
Width(100).
Border(lipgloss.NormalBorder()).
BorderForeground(lgalert).
PaddingLeft(3).
Foreground(lggrayWeak)
boldAlert = lipgloss.NewStyle().Bold(true).Foreground(lgalert)
cmdStyle = lipgloss.NewStyle().Background(lggrayWeak).Foreground(lgalert)
)
func init() {
width, _, _ = term.GetSize(int(os.Stdout.Fd()))
tableStyle.Header.
BorderStyle(lipgloss.HiddenBorder()).
BorderBottom(true).
Bold(false)
tableStyle.Selected.
Foreground(lgbasicText).
Bold(false)
tableStyle.Cell.Foreground(lgbasicText).
Padding(0)
tableStyle.Header.Padding(0)
}