Detailed changes
@@ -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