Initial commit. Add the nuts and bolts.

This commit is contained in:
Christian Rocha 2021-03-01 18:29:00 -05:00
commit 5c53233775
No known key found for this signature in database
GPG Key ID: D6CC7A16E5878018
6 changed files with 447 additions and 0 deletions

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2021 Charmbracelet, Inc
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

66
align.go Normal file
View File

@ -0,0 +1,66 @@
package lipgloss
import (
"strings"
"github.com/muesli/reflow/ansi"
)
type Align int
const (
AlignLeft Align = iota
AlignRight
AlignCenter
)
// Perform text alignment. If the string is multi-lined, we also make all lines
// the same width by padding them with spaces.
func align(s string, t Align) string {
if strings.Count(s, "\n") == 0 {
return s
}
lines, widest := getLines(s)
var b strings.Builder
for i, l := range lines {
w := ansi.PrintableRuneWidth(l)
if n := widest - w; n > 0 {
switch t {
case AlignRight:
l = strings.Repeat(" ", n) + l
case AlignCenter:
left := n / 2
right := left + n%2 // note that we put the remainder on the right
l = strings.Repeat(" ", left) + l + strings.Repeat(" ", right)
default:
l += strings.Repeat(" ", n)
}
}
b.WriteString(l)
if i < len(lines)-1 {
b.WriteRune('\n')
}
}
return b.String()
}
// Split a string into lines, additionally returning the size of the widest
// line.
func getLines(s string) (lines []string, widest int) {
lines = strings.Split(s, "\n")
for _, l := range lines {
w := ansi.PrintableRuneWidth(l)
if widest < w {
widest = w
}
}
return
}

58
color.go Normal file
View File

@ -0,0 +1,58 @@
package lipgloss
import "github.com/muesli/termenv"
// ColorType is an interface used in color specifications.
type ColorType interface {
value() string
}
// NoColor is used to specify the absence of color styling. When this is active
// foreground colors will be rendered with the terminal's default text color,
// and background colors will not be drawn at all.
//
// Example usage:
//
// color := NoColor
//
var NoColor = noColor{}
type noColor struct{}
func (n noColor) value() string {
return ""
}
// Color specifies a color by hex or ANSI value. For example:
//
// ansiColor := Color("21")
// hexColor := Color("#0000ff")
//
type Color string
func (c Color) value() string {
return string(c)
}
// AdaptiveColor provides color alternatives for light and dark backgrounds.
// The appropriate color with be returned based on the darkness of the terminal
// background color determined at runtime.
//
// Example usage:
//
// color := AdaptiveColor{Light: "#0000ff", Dark: "#000099"}
//
type AdaptiveColor struct {
Light string
Dark string
}
func (a AdaptiveColor) value() string {
if !darkBackgroundQueried {
hasDarkBackground = termenv.HasDarkBackground()
}
if hasDarkBackground {
return a.Dark
}
return a.Light
}

8
go.mod Normal file
View File

@ -0,0 +1,8 @@
module github.com/charmbracelet/lipgloss
go 1.15
require (
github.com/muesli/reflow v0.2.1-0.20210115123740-9e1d0d53df68
github.com/muesli/termenv v0.7.4
)

16
go.sum Normal file
View File

@ -0,0 +1,16 @@
github.com/lucasb-eyer/go-colorful v1.0.3 h1:QIbQXiugsb+q10B+MI+7DI1oQLdmnep86tWFlaaUAac=
github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.10 h1:CoZ3S2P7pvtP45xOtBw+/mDL2z0RKI576gSkzRRpdGg=
github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/muesli/reflow v0.2.1-0.20210115123740-9e1d0d53df68 h1:y1p/ycavWjGT9FnmSjdbWUlLGvcxrY0Rw3ATltrxOhk=
github.com/muesli/reflow v0.2.1-0.20210115123740-9e1d0d53df68/go.mod h1:Xk+z4oIWdQqJzsxyjgl3P22oYZnHdZ8FFTHAQQt5BMQ=
github.com/muesli/termenv v0.7.4 h1:/pBqvU5CpkY53tU0vVn+xgs2ZTX63aH5nY+SSps5Xa8=
github.com/muesli/termenv v0.7.4/go.mod h1:pZ7qY9l3F7e5xsAOS0zCew2tME+p7bWeBkotCEcIIcc=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42 h1:vEOn+mP2zCOVzKckCZy6YsCtDblrpj/w7B9nxGNELpg=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=

278
style.go Normal file
View File

@ -0,0 +1,278 @@
package lipgloss
import (
"strings"
"unicode"
"github.com/muesli/reflow/indent"
"github.com/muesli/reflow/truncate"
"github.com/muesli/reflow/wordwrap"
"github.com/muesli/termenv"
)
// ANSI reset sequence.
const resetSeq = termenv.CSI + termenv.ResetSeq + "m"
var (
// Cache termenv lookups
color func(string) termenv.Color = termenv.ColorProfile().Color
darkBackgroundQueried bool
hasDarkBackground bool
)
// Style describes formatting instructions for a given string.
type Style struct {
Bold bool
Italic bool
Underline bool
Strikethrough bool
Reverse bool
Blink bool
Faint bool
Foreground ColorType
Background ColorType
// If the string contains multiple lines, they'll wrap at this value.
// Lines will also be padded with spaces so they'll all be the same width.
Width int
// Text alignment.
Align Align
// Padding. This will be colored according to the Background value if
// StylePadding is true.
LeftPadding int
RightPadding int
TopPadding int
BottomPadding int
// Whether or not to apply styling to the padding. Most notably, this
// determines whether or not the indent background color is styled.
StylePadding bool
// Margins. These will never be colored.
LeftMargin int
RightMargin int
TopMargin int
BottomMargin int
// If set, we truncate lines at this value after all other style has been
// applied. That is to say, the physical width of strings will not exceed
// this value, so this can be handy when building user interfaces.
MaxWidth int
// Whether or not to remove trailing spaces with no background color. By
// default we leave them in.
RenderClearTrailingSpaces bool
// Whether to draw underlines on spaces (like padding). We don't do this by
// default as it's likely not what people want, but you can turn it on if
// you so desire.
RenderUnderlinesOnSpaces bool
}
// Apply applies formatting to a given string.
func (s Style) Apply(str string) string {
return s.apply(str, false)
}
// Apply as applies formatting to the given string, removing newlines and
// skipping over block-level rules. Use this internally if you require an
// inline-style only in your library or component.
func (s Style) ApplyInline(str string) string {
return s.apply(str, true)
}
// WithMaxWidth applies a max width to a given style. This is useful in
// enforcing a certain width at render time, particularly with aribtrary
// strings and styles.
//
// Example:
//
// var userInput string = "..."
// var userStyle = text.Style{ /* ... */ }
// fmt.Println(userStyle.WithMaxWidth(16).Apply(userInput))
//
func (s Style) WithMaxWidth(n int) Style {
s.MaxWidth = n
return s
}
func (s Style) apply(str string, singleLine bool) string {
var (
styler = termenv.Style{}
// A copy of the main termenv styler, but without underlines. Used to
// not render underlines on spaces, if applicable. It's a pointer so
// we can treat it like a maybe monad, since it won't always be
// applicable.
noUnderlineStyler *termenv.Style
)
if s.Bold {
styler = styler.Bold()
}
if s.Italic {
styler = styler.Italic()
}
if s.Strikethrough {
styler = styler.CrossOut()
}
if s.Reverse {
styler = styler.Reverse()
}
if s.Blink {
styler = styler.Blink()
}
if s.Faint {
styler = styler.Faint()
}
switch c := s.Foreground.(type) {
case Color, AdaptiveColor:
styler = styler.Foreground(color(c.value()))
}
switch c := s.Background.(type) {
case Color, AdaptiveColor:
styler = styler.Background(color(c.value()))
}
if s.Underline {
if s.Underline && !s.RenderUnderlinesOnSpaces {
stylerCopy := styler
noUnderlineStyler = &stylerCopy
}
styler = styler.Underline()
}
// Strip spaces in single line mode
if singleLine {
str = strings.Replace(str, "\n", "", -1)
}
if !s.StylePadding {
str = styler.Styled(str)
}
// Word wrap
if !singleLine && s.Width > 0 {
str = wordwrap.String(str, s.Width-s.LeftPadding-s.RightPadding)
}
// Is a background color set?
backgroundColorSet := true
switch s.Background.(type) {
case nil, noColor:
backgroundColorSet = false
}
// Left/right padding
str = padLeft(str, s.LeftPadding)
if !s.RenderClearTrailingSpaces || backgroundColorSet {
str = padRight(str, s.RightPadding, s.StylePadding)
}
// Top/bottom padding
if !singleLine && s.TopPadding > 0 {
str = strings.Repeat("\n", s.TopPadding) + str
}
if !singleLine && s.BottomPadding > 0 {
str += strings.Repeat("\n", s.BottomPadding)
}
numLines := strings.Count(str, "\n")
// Set alignment. This will also pad short lines with spaces so that all
// lines are the same length.
if numLines > 0 && (!s.RenderClearTrailingSpaces || backgroundColorSet || s.Align != AlignLeft) {
str = align(str, s.Align)
}
if s.StylePadding {
// We have to do some extra work to not render underlines on spaces
if noUnderlineStyler != nil {
var b strings.Builder
for _, c := range str {
if unicode.IsSpace(c) {
b.WriteString(noUnderlineStyler.Styled(string(c)))
continue
}
b.WriteString(styler.Styled(string(c)))
}
str = b.String()
} else {
str = styler.Styled(str)
}
}
// Add left margin
str = padLeft(str, s.LeftMargin)
// Add right margin
if !s.RenderClearTrailingSpaces {
str = padRight(str, s.RightMargin, false)
}
// Top/bottom margin
if !singleLine {
var maybeSpaces string
if s.RenderClearTrailingSpaces {
_, width := getLines(str)
maybeSpaces = strings.Repeat(" ", width)
}
if s.TopMargin > 0 {
str = strings.Repeat(maybeSpaces+"\n", s.TopMargin) + str
}
if s.BottomMargin > 0 {
str += strings.Repeat("\n"+maybeSpaces, s.BottomMargin) + "\n"
}
}
// Truncate accoridng to MaxWidth
if s.MaxWidth > 0 {
lines := strings.Split(str, "\n")
for i := range lines {
lines[i] = truncate.String(lines[i], uint(s.MaxWidth))
}
str = strings.Join(lines, "\n")
}
return str
}
// Apply left padding.
func padLeft(str string, n int) string {
if n == 0 || str == "" {
return str
}
return indent.String(str, uint(n))
}
// Apply right right padding.
func padRight(str string, n int, stylePadding bool) string {
if n == 0 || str == "" {
return str
}
padding := strings.Repeat(" ", n)
var maybeReset string
if !stylePadding {
maybeReset = resetSeq
}
lines := strings.Split(str, "\n")
for i := range lines {
lines[i] += maybeReset + padding
}
return strings.Join(lines, "\n")
}