Skip to content

Commit

Permalink
feat: add option to set a custom lipgloss renderer
Browse files Browse the repository at this point in the history
Lip Gloss defaults to `os.Stdout` to detect the background color and
color profile of the terminal. This can be a problem when the
application is using a different output stream like `os.Stderr` or a
remote session like in a SSH server.

When using an SSH server, usually the server provides to the session a
`$SSH_TTY` environment variable pointing to the allocated PTY to be used
for the application. The value of `$SSH_TTY` points to the terminal TTY
  FD that _should_ be used to run the application.

Starting with Lip Gloss v0.7, you can use custom renderers to specify a
different Lip Gloss output to detect the background color and color
profile. This PR adds the necessary "glue" to the Bubbles in order to
change the renderer being used to detect the color profile and
background.
  • Loading branch information
aymanbagabas committed Aug 3, 2023
1 parent 95d7be5 commit 7b958c0
Show file tree
Hide file tree
Showing 11 changed files with 395 additions and 96 deletions.
28 changes: 26 additions & 2 deletions cursor/cursor.go
Expand Up @@ -72,11 +72,23 @@ type Model struct {
blinkTag int
// mode determines the behavior of the cursor
mode Mode

re *lipgloss.Renderer
}

// Option is used to set options in New.
type Option func(*Model)

// WithRenderer sets the Lip Gloss renderer for the cursor.
func WithRenderer(r *lipgloss.Renderer) Option {
return func(m *Model) {
m.re = r
}
}

// New creates a new model with default settings.
func New() Model {
return Model{
func New(opts ...Option) Model {
m := Model{
BlinkSpeed: defaultBlinkSpeed,

Blink: true,
Expand All @@ -86,6 +98,18 @@ func New() Model {
ctx: context.Background(),
},
}

for _, opt := range opts {
opt(&m)
}

if m.re == nil {
m.re = lipgloss.DefaultRenderer()
}

m.Style = m.Style.Renderer(m.re)

return m
}

// Update updates the cursor.
Expand Down
74 changes: 57 additions & 17 deletions filepicker/filepicker.go
Expand Up @@ -27,9 +27,19 @@ func nextID() int {
return lastID
}

// Option is used to set options in New.
type Option func(*Model)

// WithRenderer sets the Lip Gloss renderer for the filepicker.
func WithRenderer(r *lipgloss.Renderer) Option {
return func(m *Model) {
m.re = r
}
}

// New returns a new filepicker model with default styling and key bindings.
func New() Model {
return Model{
func New(opts ...Option) Model {
m := Model{
id: nextID(),
CurrentDirectory: ".",
Cursor: ">",
Expand All @@ -46,8 +56,19 @@ func New() Model {
minStack: newStack(),
maxStack: newStack(),
KeyMap: DefaultKeyMap(),
Styles: DefaultStyles(),
}

for _, opt := range opts {
opt(&m)
}

if m.re == nil {
m.re = lipgloss.DefaultRenderer()
}

m.Styles = DefaultStyles().Renderer(m.re)

return m
}

type errorMsg struct {
Expand Down Expand Up @@ -108,32 +129,51 @@ type Styles struct {
EmptyDirectory lipgloss.Style
}

// Renderer returns a new Styles copy with the given Lip Gloss renderer.
func (s Styles) Renderer(re *lipgloss.Renderer) Styles {
s.DisabledCursor = s.DisabledCursor.Copy().Renderer(re)
s.Cursor = s.Cursor.Copy().Renderer(re)
s.Symlink = s.Symlink.Copy().Renderer(re)
s.Directory = s.Directory.Copy().Renderer(re)
s.File = s.File.Copy().Renderer(re)
s.DisabledFile = s.DisabledFile.Copy().Renderer(re)
s.Permission = s.Permission.Copy().Renderer(re)
s.Selected = s.Selected.Copy().Renderer(re)
s.DisabledSelected = s.DisabledSelected.Copy().Renderer(re)
s.FileSize = s.FileSize.Copy().Renderer(re)
s.EmptyDirectory = s.EmptyDirectory.Copy().Renderer(re)
return s
}

// DefaultStyles defines the default styling for the file picker.
func DefaultStyles() Styles {
return DefaultStylesWithRenderer(lipgloss.DefaultRenderer())
return Styles{
DisabledCursor: lipgloss.NewStyle().Foreground(lipgloss.Color("247")),
Cursor: lipgloss.NewStyle().Foreground(lipgloss.Color("212")),
Symlink: lipgloss.NewStyle().Foreground(lipgloss.Color("36")),
Directory: lipgloss.NewStyle().Foreground(lipgloss.Color("99")),
File: lipgloss.NewStyle(),
DisabledFile: lipgloss.NewStyle().Foreground(lipgloss.Color("243")),
DisabledSelected: lipgloss.NewStyle().Foreground(lipgloss.Color("247")),
Permission: lipgloss.NewStyle().Foreground(lipgloss.Color("244")),
Selected: lipgloss.NewStyle().Foreground(lipgloss.Color("212")).Bold(true),
FileSize: lipgloss.NewStyle().Foreground(lipgloss.Color("240")).Width(fileSizeWidth).Align(lipgloss.Right),
EmptyDirectory: lipgloss.NewStyle().Foreground(lipgloss.Color("240")).PaddingLeft(paddingLeft).SetString("Bummer. No Files Found."),
}
}

// DefaultStylesWithRenderer defines the default styling for the file picker,
// with a given Lip Gloss renderer.
//
// Deprecated: use Styles.Renderer instead.
func DefaultStylesWithRenderer(r *lipgloss.Renderer) Styles {
return Styles{
DisabledCursor: r.NewStyle().Foreground(lipgloss.Color("247")),
Cursor: r.NewStyle().Foreground(lipgloss.Color("212")),
Symlink: r.NewStyle().Foreground(lipgloss.Color("36")),
Directory: r.NewStyle().Foreground(lipgloss.Color("99")),
File: r.NewStyle(),
DisabledFile: r.NewStyle().Foreground(lipgloss.Color("243")),
DisabledSelected: r.NewStyle().Foreground(lipgloss.Color("247")),
Permission: r.NewStyle().Foreground(lipgloss.Color("244")),
Selected: r.NewStyle().Foreground(lipgloss.Color("212")).Bold(true),
FileSize: r.NewStyle().Foreground(lipgloss.Color("240")).Width(fileSizeWidth).Align(lipgloss.Right),
EmptyDirectory: r.NewStyle().Foreground(lipgloss.Color("240")).PaddingLeft(paddingLeft).SetString("Bummer. No Files Found."),
}
return DefaultStyles().Renderer(r)
}

// Model represents a file picker.
type Model struct {
id int
re *lipgloss.Renderer

// Path is the path which the user has selected with the file picker.
Path string
Expand Down
87 changes: 62 additions & 25 deletions help/help.go
Expand Up @@ -42,6 +42,43 @@ type Styles struct {
FullSeparator lipgloss.Style
}

// DefaultStyles returns a set of default styles for the help bubble.
func DefaultStyles() Styles {
keyStyle := lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{
Light: "#909090",
Dark: "#626262",
})
descStyle := lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{
Light: "#B2B2B2",
Dark: "#4A4A4A",
})
sepStyle := lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{
Light: "#DDDADA",
Dark: "#3C3C3C",
})
return Styles{
ShortKey: keyStyle,
ShortDesc: descStyle,
ShortSeparator: sepStyle,
Ellipsis: sepStyle.Copy(),
FullKey: keyStyle.Copy(),
FullDesc: descStyle.Copy(),
FullSeparator: sepStyle.Copy(),
}
}

// Renderer returns a copy of Styles with the given Lip Gloss renderer set.
func (s Styles) Renderer(r *lipgloss.Renderer) Styles {
s.Ellipsis = s.Ellipsis.Copy().Renderer(r)
s.ShortKey = s.ShortKey.Copy().Renderer(r)
s.ShortDesc = s.ShortDesc.Copy().Renderer(r)
s.ShortSeparator = s.ShortSeparator.Copy().Renderer(r)
s.FullKey = s.FullKey.Copy().Renderer(r)
s.FullDesc = s.FullDesc.Copy().Renderer(r)
s.FullSeparator = s.FullSeparator.Copy().Renderer(r)
return s
}

// Model contains the state of the help view.
type Model struct {
Width int
Expand All @@ -55,39 +92,39 @@ type Model struct {
Ellipsis string

Styles Styles
}

// New creates a new help view with some useful defaults.
func New() Model {
keyStyle := lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{
Light: "#909090",
Dark: "#626262",
})
re *lipgloss.Renderer
}

descStyle := lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{
Light: "#B2B2B2",
Dark: "#4A4A4A",
})
// Option is used to set options in New.
type Option func(*Model)

sepStyle := lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{
Light: "#DDDADA",
Dark: "#3C3C3C",
})
// WithRenderer sets the Lip Gloss renderer for the help model.
func WithRenderer(r *lipgloss.Renderer) Option {
return func(m *Model) {
m.re = r
}
}

return Model{
// New creates a new help view with some useful defaults.
func New(opts ...Option) Model {
m := Model{
ShortSeparator: " • ",
FullSeparator: " ",
Ellipsis: "…",
Styles: Styles{
ShortKey: keyStyle,
ShortDesc: descStyle,
ShortSeparator: sepStyle,
Ellipsis: sepStyle.Copy(),
FullKey: keyStyle.Copy(),
FullDesc: descStyle.Copy(),
FullSeparator: sepStyle.Copy(),
},
}

for _, opt := range opts {
opt(&m)
}

if m.re == nil {
m.re = lipgloss.DefaultRenderer()
}

m.Styles = DefaultStyles().Renderer(m.re)

return m
}

// NewModel creates a new help view with some useful defaults.
Expand Down
73 changes: 47 additions & 26 deletions list/list.go
Expand Up @@ -134,8 +134,20 @@ func (f FilterState) String() string {
}[f]
}

// Option is used to set options in New.
type Option func(*Model)

// WithRenderer sets the Lip Gloss renderer for the list model.
func WithRenderer(r *lipgloss.Renderer) Option {
return func(m *Model) {
m.re = r
}
}

// Model contains the state of this component.
type Model struct {
re *lipgloss.Renderer

showTitle bool
showFilter bool
showStatusBar bool
Expand Down Expand Up @@ -195,8 +207,37 @@ type Model struct {
}

// New returns a new model with sensible defaults.
func New(items []Item, delegate ItemDelegate, width, height int) Model {
styles := DefaultStyles()
func New(items []Item, delegate ItemDelegate, width, height int, opts ...Option) Model {
m := Model{
showTitle: true,
showFilter: true,
showStatusBar: true,
showPagination: true,
showHelp: true,
itemNameSingular: "item",
itemNamePlural: "items",
filteringEnabled: true,
KeyMap: DefaultKeyMap(),
Filter: DefaultFilter,
Title: "List",
StatusMessageLifetime: time.Second,

width: width,
height: height,
delegate: delegate,
items: items,
Help: help.New(),
}

for _, opt := range opts {
opt(&m)
}

if m.re == nil {
m.re = lipgloss.DefaultRenderer()
}

styles := DefaultStyles().Renderer(m.re)

sp := spinner.New()
sp.Spinner = spinner.Line
Expand All @@ -214,30 +255,10 @@ func New(items []Item, delegate ItemDelegate, width, height int) Model {
p.ActiveDot = styles.ActivePaginationDot.String()
p.InactiveDot = styles.InactivePaginationDot.String()

m := Model{
showTitle: true,
showFilter: true,
showStatusBar: true,
showPagination: true,
showHelp: true,
itemNameSingular: "item",
itemNamePlural: "items",
filteringEnabled: true,
KeyMap: DefaultKeyMap(),
Filter: DefaultFilter,
Styles: styles,
Title: "List",
FilterInput: filterInput,
StatusMessageLifetime: time.Second,

width: width,
height: height,
delegate: delegate,
items: items,
Paginator: p,
spinner: sp,
Help: help.New(),
}
m.Styles = styles
m.FilterInput = filterInput
m.Paginator = p
m.spinner = sp

m.updatePagination()
m.updateKeybindings()
Expand Down
22 changes: 22 additions & 0 deletions list/style.go
Expand Up @@ -39,6 +39,28 @@ type Styles struct {
DividerDot lipgloss.Style
}

// Renderer returns a copy of Styles with the given Lip Gloss renderer set.
func (s Styles) Renderer(r *lipgloss.Renderer) Styles {
s.TitleBar = s.TitleBar.Copy().Renderer(r)
s.Title = s.Title.Copy().Renderer(r)
s.Spinner = s.Spinner.Copy().Renderer(r)
s.FilterPrompt = s.FilterPrompt.Copy().Renderer(r)
s.FilterCursor = s.FilterCursor.Copy().Renderer(r)
s.DefaultFilterCharacterMatch = s.DefaultFilterCharacterMatch.Copy().Renderer(r)
s.StatusBar = s.StatusBar.Copy().Renderer(r)
s.StatusEmpty = s.StatusEmpty.Copy().Renderer(r)
s.StatusBarActiveFilter = s.StatusBarActiveFilter.Copy().Renderer(r)
s.StatusBarFilterCount = s.StatusBarFilterCount.Copy().Renderer(r)
s.NoItems = s.NoItems.Copy().Renderer(r)
s.PaginationStyle = s.PaginationStyle.Copy().Renderer(r)
s.HelpStyle = s.HelpStyle.Copy().Renderer(r)
s.ActivePaginationDot = s.ActivePaginationDot.Copy().Renderer(r)
s.InactivePaginationDot = s.InactivePaginationDot.Copy().Renderer(r)
s.ArabicPagination = s.ArabicPagination.Copy().Renderer(r)
s.DividerDot = s.DividerDot.Copy().Renderer(r)
return s
}

// DefaultStyles returns a set of default style definitions for this list
// component.
func DefaultStyles() (s Styles) {
Expand Down

0 comments on commit 7b958c0

Please sign in to comment.