Detailed changes
@@ -2,9 +2,9 @@ module github.com/charmbracelet/crush
go 1.24.3
-replace github.com/charmbracelet/bubbletea/v2 => github.com/charmbracelet/bubbletea-internal/v2 v2.0.0-20250708152737-144080f3d891
+replace github.com/charmbracelet/bubbletea/v2 => github.com/charmbracelet/bubbletea-internal/v2 v2.0.0-20250710185017-3c0ffd25e595
-replace github.com/charmbracelet/lipgloss/v2 => github.com/charmbracelet/lipgloss-internal/v2 v2.0.0-20250708152830-0fa4ef151093
+replace github.com/charmbracelet/lipgloss/v2 => github.com/charmbracelet/lipgloss-internal/v2 v2.0.0-20250710185058-03664cb9cecb
require (
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0
@@ -75,7 +75,7 @@ require (
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/aymerick/douceur v0.2.0 // indirect
github.com/charmbracelet/colorprofile v0.3.1 // indirect
- github.com/charmbracelet/ultraviolet v0.0.0-20250708152637-0fe0235c8db9
+ github.com/charmbracelet/ultraviolet v0.0.0-20250708152637-0fe0235c8db9 // indirect
github.com/charmbracelet/x/cellbuf v0.0.14-0.20250516160309-24eee56f89fa // indirect
github.com/charmbracelet/x/exp/slice v0.0.0-20250611152503-f53cdd7e01ef
github.com/charmbracelet/x/term v0.2.1
@@ -70,16 +70,16 @@ github.com/charlievieth/fastwalk v1.0.11 h1:5sLT/q9+d9xMdpKExawLppqvXFZCVKf6JHnr
github.com/charlievieth/fastwalk v1.0.11/go.mod h1:yGy1zbxog41ZVMcKA/i8ojXLFsuayX5VvwhQVoj9PBI=
github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250607113720-eb5e1cf3b09e h1:99Ugtt633rqauFsXjZobZmtkNpeaWialfj8dl6COC6A=
github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250607113720-eb5e1cf3b09e/go.mod h1:6HamsBKWqEC/FVHuQMHgQL+knPyvHH55HwJDHl/adMw=
-github.com/charmbracelet/bubbletea-internal/v2 v2.0.0-20250708152737-144080f3d891 h1:wh6N1dR4XkDh6XsiZh1/tImJAZvYB0yVLmaUKvJXvK0=
-github.com/charmbracelet/bubbletea-internal/v2 v2.0.0-20250708152737-144080f3d891/go.mod h1:SwBB+WoaQVMMOM9hknbN/7FNT86kgKG0LSHGTmLphX8=
+github.com/charmbracelet/bubbletea-internal/v2 v2.0.0-20250710185017-3c0ffd25e595 h1:wLMjzOqrwoM7Em9UR9sGbn4375G8WuxcwFB3kjZiqHo=
+github.com/charmbracelet/bubbletea-internal/v2 v2.0.0-20250710185017-3c0ffd25e595/go.mod h1:+Tl7rePElw6OKt382t04zXwtPFoPXxAaJzNrYmtsLds=
github.com/charmbracelet/colorprofile v0.3.1 h1:k8dTHMd7fgw4bnFd7jXTLZrSU/CQrKnL3m+AxCzDz40=
github.com/charmbracelet/colorprofile v0.3.1/go.mod h1:/GkGusxNs8VB/RSOh3fu0TJmQ4ICMMPApIIVn0KszZ0=
github.com/charmbracelet/fang v0.1.0 h1:SlZS2crf3/zQh7Mr4+W+7QR1k+L08rrPX5rm5z3d7Wg=
github.com/charmbracelet/fang v0.1.0/go.mod h1:Zl/zeUQ8EtQuGyiV0ZKZlZPDowKRTzu8s/367EpN/fc=
github.com/charmbracelet/glamour/v2 v2.0.0-20250516160903-6f1e2c8f9ebe h1:i6ce4CcAlPpTj2ER69m1DBeLZ3RRcHnKExuwhKa3GfY=
github.com/charmbracelet/glamour/v2 v2.0.0-20250516160903-6f1e2c8f9ebe/go.mod h1:p3Q+aN4eQKeM5jhrmXPMgPrlKbmc59rWSnMsSA3udhk=
-github.com/charmbracelet/lipgloss-internal/v2 v2.0.0-20250708152830-0fa4ef151093 h1:c9vOmNJQUwy/lp/pNOB5ZDMhOuXJ3Y2LL9uZMYGgJxQ=
-github.com/charmbracelet/lipgloss-internal/v2 v2.0.0-20250708152830-0fa4ef151093/go.mod h1:XmxjFJcMEfYIHa4Mw4ra+uMjploDkTlkKIs7wLt9v4Q=
+github.com/charmbracelet/lipgloss-internal/v2 v2.0.0-20250710185058-03664cb9cecb h1:lswj7CYZVYbLn2OhYJsXOMRQQGdRIfyuSnh5FdVSMr0=
+github.com/charmbracelet/lipgloss-internal/v2 v2.0.0-20250710185058-03664cb9cecb/go.mod h1:wEc/TRrTAIDJYjVCg3+y8WeKaN+F88gpYfGbUuP6W3A=
github.com/charmbracelet/log/v2 v2.0.0-20250226163916-c379e29ff706 h1:WkwO6Ks3mSIGnGuSdKl9qDSyfbYK50z2wc2gGMggegE=
github.com/charmbracelet/log/v2 v2.0.0-20250226163916-c379e29ff706/go.mod h1:mjJGp00cxcfvD5xdCa+bso251Jt4owrQvuimJtVmEmM=
github.com/charmbracelet/ultraviolet v0.0.0-20250708152637-0fe0235c8db9 h1:+LLFCLxtb/sHegwY3zYdFAbaOgI/I9pv/pxdUlI1Q9s=
@@ -88,8 +88,6 @@ github.com/charmbracelet/x/ansi v0.9.3 h1:BXt5DHS/MKF+LjuK4huWrC6NCvHtexww7dMayh
github.com/charmbracelet/x/ansi v0.9.3/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE=
github.com/charmbracelet/x/cellbuf v0.0.14-0.20250516160309-24eee56f89fa h1:lphz0Z3rsiOtMYiz8axkT24i9yFiueDhJbzyNUADmME=
github.com/charmbracelet/x/cellbuf v0.0.14-0.20250516160309-24eee56f89fa/go.mod h1:xBlh2Yi3DL3zy/2n15kITpg0YZardf/aa/hgUaIM6Rk=
-github.com/charmbracelet/x/exp/charmtone v0.0.0-20250627134340-c144409e381c h1:2GELBLPgfSbHU53bsQhR9XIgNuVZ6w+Rz8RWV5Lq+A4=
-github.com/charmbracelet/x/exp/charmtone v0.0.0-20250627134340-c144409e381c/go.mod h1:T9jr8CzFpjhFVHjNjKwbAD7KwBNyFnj2pntAO7F2zw0=
github.com/charmbracelet/x/exp/charmtone v0.0.0-20250708181618-a60a724ba6c3 h1:1xwHZg6eMZ9Wv5TE1UGub6ARubyOd1Lo5kPUI/6VL50=
github.com/charmbracelet/x/exp/charmtone v0.0.0-20250708181618-a60a724ba6c3/go.mod h1:T9jr8CzFpjhFVHjNjKwbAD7KwBNyFnj2pntAO7F2zw0=
github.com/charmbracelet/x/exp/golden v0.0.0-20250207160936-21c02780d27a h1:FsHEJ52OC4VuTzU8t+n5frMjLvpYWEznSr/u8tnkCYw=
@@ -624,6 +624,10 @@ func (p *Program) eventLoop(model Model, cmds chan Cmd) (Model, error) {
switch msg.Mode {
case ansi.AltScreenSaveCursorMode:
p.renderer.enterAltScreen()
+ // Main and alternate screen have their own Kitty keyboard
+ // stack. We need to request keyboard enhancements again
+ // when entering/exiting the alternate screen.
+ p.requestKeyboardEnhancements()
case ansi.TextCursorEnableMode:
p.renderer.showCursor()
case ansi.GraphemeClusteringMode:
@@ -645,6 +649,10 @@ func (p *Program) eventLoop(model Model, cmds chan Cmd) (Model, error) {
switch msg.Mode {
case ansi.AltScreenSaveCursorMode:
p.renderer.exitAltScreen()
+ // Main and alternate screen have their own Kitty keyboard
+ // stack. We need to request keyboard enhancements again
+ // when entering/exiting the alternate screen.
+ p.requestKeyboardEnhancements()
case ansi.TextCursorEnableMode:
p.renderer.hideCursor()
default:
@@ -1410,13 +1418,16 @@ func (p *Program) stopRenderer(kill bool) {
// requestKeyboardEnhancements tries to enable keyboard enhancements and read
// the active keyboard enhancements from the terminal.
func (p *Program) requestKeyboardEnhancements() {
+ // XXX: We write to the renderer directly so that we synchronize with the
+ // alt-screen state of the renderer. This is because the main screen and
+ // alternate screen have their own Kitty keyboard state stack.
if p.requestedEnhancements.modifyOtherKeys > 0 {
- p.execute(ansi.KeyModifierOptions(4, p.requestedEnhancements.modifyOtherKeys)) //nolint:mnd
- p.execute(ansi.QueryModifyOtherKeys)
+ _, _ = p.renderer.writeString(ansi.KeyModifierOptions(4, p.requestedEnhancements.modifyOtherKeys)) //nolint:mnd
+ _, _ = p.renderer.writeString(ansi.QueryModifyOtherKeys)
}
if p.requestedEnhancements.kittyFlags > 0 {
- p.execute(ansi.PushKittyKeyboard(p.requestedEnhancements.kittyFlags))
- p.execute(ansi.RequestKittyKeyboard)
+ _, _ = p.renderer.writeString(ansi.PushKittyKeyboard(p.requestedEnhancements.kittyFlags))
+ _, _ = p.renderer.writeString(ansi.RequestKittyKeyboard)
}
}
@@ -135,6 +135,16 @@ func (s Style) GetPaddingLeft() int {
return s.getAsInt(paddingLeftKey)
}
+// GetPaddingChar returns the style's padding character. If no value is set a
+// space (`\u0020`) is returned.
+func (s Style) GetPaddingChar() rune {
+ char := s.getAsRune(paddingCharKey)
+ if char == 0 {
+ return ' '
+ }
+ return char
+}
+
// GetHorizontalPadding returns the style's left and right padding. Unset
// values are measured as 0.
func (s Style) GetHorizontalPadding() int {
@@ -186,6 +196,16 @@ func (s Style) GetMarginLeft() int {
return s.getAsInt(marginLeftKey)
}
+// GetMarginChar returns the style's padding character. If no value is set a
+// space (`\u0020`) is returned.
+func (s Style) GetMarginChar() rune {
+ char := s.getAsRune(marginCharKey)
+ if char == 0 {
+ return ' '
+ }
+ return char
+}
+
// GetHorizontalMargins returns the style's left and right margins. Unset
// values are measured as 0.
func (s Style) GetHorizontalMargins() int {
@@ -432,6 +452,19 @@ func (s Style) isSet(k propKey) bool {
return s.props.has(k)
}
+func (s Style) getAsRune(k propKey) rune {
+ if !s.isSet(k) {
+ return 0
+ }
+ switch k { //nolint:exhaustive
+ case paddingCharKey:
+ return s.paddingChar
+ case marginCharKey:
+ return s.marginChar
+ }
+ return 0
+}
+
func (s Style) getAsBool(k propKey, defaultVal bool) bool {
if !s.isSet(k) {
return defaultVal
@@ -29,6 +29,8 @@ func (s *Style) set(key propKey, value any) {
s.paddingBottom = max(0, value.(int))
case paddingLeftKey:
s.paddingLeft = max(0, value.(int))
+ case paddingCharKey:
+ s.paddingChar = value.(rune)
case marginTopKey:
s.marginTop = max(0, value.(int))
case marginRightKey:
@@ -39,6 +41,8 @@ func (s *Style) set(key propKey, value any) {
s.marginLeft = max(0, value.(int))
case marginBackgroundKey:
s.marginBgColor = colorOrNil(value)
+ case marginCharKey:
+ s.marginChar = value.(rune)
case borderStyleKey:
s.borderStyle = value.(Border)
case borderTopForegroundKey:
@@ -111,6 +115,8 @@ func (s *Style) setFrom(key propKey, i Style) {
s.set(paddingBottomKey, i.paddingBottom)
case paddingLeftKey:
s.set(paddingLeftKey, i.paddingLeft)
+ case paddingCharKey:
+ s.set(paddingCharKey, i.paddingChar)
case marginTopKey:
s.set(marginTopKey, i.marginTop)
case marginRightKey:
@@ -121,6 +127,8 @@ func (s *Style) setFrom(key propKey, i Style) {
s.set(marginLeftKey, i.marginLeft)
case marginBackgroundKey:
s.set(marginBackgroundKey, i.marginBgColor)
+ case marginCharKey:
+ s.set(marginCharKey, i.marginChar)
case borderStyleKey:
s.set(borderStyleKey, i.borderStyle)
case borderTopForegroundKey:
@@ -320,6 +328,18 @@ func (s Style) PaddingBottom(i int) Style {
return s
}
+// PaddingChar sets the character used for padding. This is useful for
+// rendering blocks with a specific character, such as a space or a dot.
+// Example of using [NBSP] as padding to prevent line breaks:
+//
+// ```go
+// s := lipgloss.NewStyle().PaddingChar(lipgloss.NBSP)
+// ```
+func (s Style) PaddingChar(r rune) Style {
+ s.set(paddingCharKey, r)
+ return s
+}
+
// ColorWhitespace determines whether or not the background color should be
// applied to the padding. This is true by default as it's more than likely the
// desired and expected behavior, but it can be disabled for certain graphic
@@ -390,6 +410,13 @@ func (s Style) MarginBackground(c color.Color) Style {
return s
}
+// MarginChar sets the character used for the margin. This is useful for
+// rendering blocks with a specific character, such as a space or a dot.
+func (s Style) MarginChar(r rune) Style {
+ s.set(marginCharKey, r)
+ return s
+}
+
// Border is shorthand for setting the border style and which sides should
// have a border at once. The variadic argument sides works as follows:
//
@@ -10,7 +10,8 @@ import (
)
const (
- nbsp = '\u00A0'
+ // NBSP is the non-breaking space rune.
+ NBSP = '\u00A0'
tabWidthDefault = 4
)
@@ -44,6 +45,7 @@ const (
paddingRightKey
paddingBottomKey
paddingLeftKey
+ paddingCharKey
// Margins.
marginTopKey
@@ -51,6 +53,7 @@ const (
marginBottomKey
marginLeftKey
marginBackgroundKey
+ marginCharKey
// Border runes.
borderStyleKey
@@ -128,12 +131,14 @@ type Style struct {
paddingRight int
paddingBottom int
paddingLeft int
+ paddingChar rune
marginTop int
marginRight int
marginBottom int
marginLeft int
marginBgColor color.Color
+ marginChar rune
borderStyle Border
borderTopFgColor color.Color
@@ -387,23 +392,24 @@ func (s Style) Render(strs ...string) string {
// Padding
if !inline { //nolint:nestif
+ padChar := s.paddingChar
+ if padChar == 0 {
+ padChar = ' '
+ }
if leftPadding > 0 {
var st *ansi.Style
if colorWhitespace || styleWhitespace {
st = &teWhitespace
}
- str = padLeft(str, leftPadding, st, nbsp)
+ str = padLeft(str, leftPadding, st, padChar)
}
- // XXX: We use a non-breaking space to pad so that the padding is
- // preserved when the string is copied and pasted.
-
if rightPadding > 0 {
var st *ansi.Style
if colorWhitespace || styleWhitespace {
st = &teWhitespace
}
- str = padRight(str, rightPadding, st, nbsp)
+ str = padRight(str, rightPadding, st, padChar)
}
if topPadding > 0 {
@@ -494,8 +500,12 @@ func (s Style) applyMargins(str string, inline bool) string {
}
// Add left and right margin
- str = padLeft(str, leftMargin, &style, ' ')
- str = padRight(str, rightMargin, &style, ' ')
+ marginChar := s.marginChar
+ if marginChar == 0 {
+ marginChar = ' '
+ }
+ str = padLeft(str, leftMargin, &style, marginChar)
+ str = padRight(str, rightMargin, &style, marginChar)
// Top/bottom margin
if !inline {
@@ -96,6 +96,13 @@ func (s Style) UnsetPadding() Style {
s.unset(paddingRightKey)
s.unset(paddingTopKey)
s.unset(paddingBottomKey)
+ s.unset(paddingCharKey)
+ return s
+}
+
+// UnsetPaddingChar removes the padding character style rule, if set.
+func (s Style) UnsetPaddingChar() Style {
+ s.unset(paddingCharKey)
return s
}
@@ -1,109 +0,0 @@
-// Package screen provides functions and helpers to manipulate a [uv.Screen].
-package screen
-
-import uv "github.com/charmbracelet/ultraviolet"
-
-// Clear clears the screen with empty cells. This is equivalent to filling the
-// screen with empty cells.
-//
-// If the screen implements a [Clear] method, it will be called instead of
-// filling the screen with empty cells.
-func Clear(scr uv.Screen) {
- if c, ok := scr.(interface {
- Clear()
- }); ok {
- c.Clear()
- return
- }
- Fill(scr, nil)
-}
-
-// ClearArea clears the given area of the screen with empty cells. This is
-// equivalent to filling the area with empty cells.
-//
-// If the screen implements a [ClearArea] method, it will be called instead of
-// filling the area with empty cells.
-func ClearArea(scr uv.Screen, area uv.Rectangle) {
- if c, ok := scr.(interface {
- ClearArea(area uv.Rectangle)
- }); ok {
- c.ClearArea(area)
- return
- }
- FillArea(scr, nil, area)
-}
-
-// Fill fills the screen with the given cell. If the cell is nil, it fills the
-// screen with empty cells.
-//
-// If the screen implements a [Fill] method, it will be called instead of
-// filling the screen with empty cells.
-func Fill(scr uv.Screen, cell *uv.Cell) {
- if f, ok := scr.(interface {
- Fill(cell *uv.Cell)
- }); ok {
- f.Fill(cell)
- return
- }
- FillArea(scr, cell, scr.Bounds())
-}
-
-// FillArea fills the given area of the screen with the given cell. If the cell
-// is nil, it fills the area with empty cells.
-//
-// If the screen implements a [FillArea] method, it will be called instead of
-// filling the area with empty cells.
-func FillArea(scr uv.Screen, cell *uv.Cell, area uv.Rectangle) {
- if f, ok := scr.(interface {
- FillArea(cell *uv.Cell, area uv.Rectangle)
- }); ok {
- f.FillArea(cell, area)
- return
- }
- for y := area.Min.Y; y < area.Max.Y; y++ {
- for x := area.Min.X; x < area.Max.X; x++ {
- scr.SetCell(x, y, cell)
- }
- }
-}
-
-// CloneArea clones the given area of the screen and returns a new buffer
-// with the same size as the area. The new buffer will contain the same cells
-// as the area in the screen.
-// Use [uv.Buffer.Draw] to draw the cloned buffer to a screen again.
-//
-// If the screen implements a [CloneArea] method, it will be called instead of
-// cloning the area manually.
-func CloneArea(scr uv.Screen, area uv.Rectangle) *uv.Buffer {
- if c, ok := scr.(interface {
- CloneArea(area uv.Rectangle) *uv.Buffer
- }); ok {
- return c.CloneArea(area)
- }
- buf := uv.NewBuffer(area.Dx(), area.Dy())
- for y := area.Min.Y; y < area.Max.Y; y++ {
- for x := area.Min.X; x < area.Max.X; x++ {
- cell := scr.CellAt(x, y)
- if cell == nil || cell.IsZero() {
- continue
- }
- buf.SetCell(x-area.Min.X, y-area.Min.Y, cell.Clone())
- }
- }
- return buf
-}
-
-// Clone creates a new [uv.Buffer] clone of the given screen. The new buffer will
-// have the same size as the screen and will contain the same cells.
-// Use [uv.Buffer.Draw] to draw the cloned buffer to a screen again.
-//
-// If the screen implements a [Clone] method, it will be called instead of
-// cloning the entire screen manually.
-func Clone(scr uv.Screen) *uv.Buffer {
- if c, ok := scr.(interface {
- Clone() *uv.Buffer
- }); ok {
- return c.Clone()
- }
- return CloneArea(scr, scr.Bounds())
-}
@@ -254,8 +254,8 @@ github.com/charmbracelet/bubbles/v2/spinner
github.com/charmbracelet/bubbles/v2/textarea
github.com/charmbracelet/bubbles/v2/textinput
github.com/charmbracelet/bubbles/v2/viewport
-# github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.1 => github.com/charmbracelet/bubbletea-internal/v2 v2.0.0-20250708152737-144080f3d891
-## explicit; go 1.24.3
+# github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.1 => github.com/charmbracelet/bubbletea-internal/v2 v2.0.0-20250710185017-3c0ffd25e595
+## explicit; go 1.24.0
github.com/charmbracelet/bubbletea/v2
# github.com/charmbracelet/colorprofile v0.3.1
## explicit; go 1.23.0
@@ -269,7 +269,7 @@ github.com/charmbracelet/glamour/v2
github.com/charmbracelet/glamour/v2/ansi
github.com/charmbracelet/glamour/v2/internal/autolink
github.com/charmbracelet/glamour/v2/styles
-# github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.2.0.20250703152125-8e1c474f8a71 => github.com/charmbracelet/lipgloss-internal/v2 v2.0.0-20250708152830-0fa4ef151093
+# github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.2.0.20250703152125-8e1c474f8a71 => github.com/charmbracelet/lipgloss-internal/v2 v2.0.0-20250710185058-03664cb9cecb
## explicit; go 1.24.2
github.com/charmbracelet/lipgloss/v2
github.com/charmbracelet/lipgloss/v2/table
@@ -280,7 +280,6 @@ github.com/charmbracelet/log/v2
# github.com/charmbracelet/ultraviolet v0.0.0-20250708152637-0fe0235c8db9
## explicit; go 1.24.0
github.com/charmbracelet/ultraviolet
-github.com/charmbracelet/ultraviolet/screen
# github.com/charmbracelet/x/ansi v0.9.3
## explicit; go 1.23.0
github.com/charmbracelet/x/ansi
@@ -839,5 +838,5 @@ mvdan.cc/sh/v3/fileutil
mvdan.cc/sh/v3/interp
mvdan.cc/sh/v3/pattern
mvdan.cc/sh/v3/syntax
-# github.com/charmbracelet/bubbletea/v2 => github.com/charmbracelet/bubbletea-internal/v2 v2.0.0-20250708152737-144080f3d891
-# github.com/charmbracelet/lipgloss/v2 => github.com/charmbracelet/lipgloss-internal/v2 v2.0.0-20250708152830-0fa4ef151093
+# github.com/charmbracelet/bubbletea/v2 => github.com/charmbracelet/bubbletea-internal/v2 v2.0.0-20250710185017-3c0ffd25e595
+# github.com/charmbracelet/lipgloss/v2 => github.com/charmbracelet/lipgloss-internal/v2 v2.0.0-20250710185058-03664cb9cecb