From 49c25d9b68cc207dceb2c1676cd54f1f8054bd99 Mon Sep 17 00:00:00 2001 From: yashranjan1 Date: Sat, 12 Sep 2026 00:04:45 +0200 Subject: [PATCH 1/2] feat: add custom loading component for tui - Implement animated loader component in internal/tui/components/Loader - Integrate loader into AppModel footer and request view lifecycle - Update view messages to trigger loading states asynchronously --- .gitignore | 1 + internal/tui/app/model.go | 38 +++++++- internal/tui/components/Loader/component.go | 81 +++++++++++++++++ .../components/OptionsProvider/component.go | 3 + internal/tui/components/ViewPort/component.go | 1 + internal/tui/messages/messages.go | 4 + internal/tui/styles/app-styles.go | 6 +- internal/tui/styles/functions.go | 2 +- internal/tui/styles/loader-styles.go | 87 +++++++++++++++++++ internal/tui/styles/request-styles.go | 2 +- internal/tui/styles/theme.go | 1 + internal/tui/views/request-view.go | 30 ++++--- 12 files changed, 236 insertions(+), 20 deletions(-) create mode 100644 internal/tui/components/Loader/component.go create mode 100644 internal/tui/styles/loader-styles.go diff --git a/.gitignore b/.gitignore index 4c97fa6..e648db4 100644 --- a/.gitignore +++ b/.gitignore @@ -50,3 +50,4 @@ internal/old_tui/ docs/node_modules/ docs/.vitepress/dist/ docs/.vitepress/cache/ +test_files/ diff --git a/internal/tui/app/model.go b/internal/tui/app/model.go index 1769063..17f9dca 100644 --- a/internal/tui/app/model.go +++ b/internal/tui/app/model.go @@ -4,12 +4,14 @@ import ( "fmt" "sort" "strings" + "time" "charm.land/bubbles/v2/help" "charm.land/bubbles/v2/key" tea "charm.land/bubbletea/v2" "charm.land/lipgloss/v2" "github.com/yashranjan1/relay/internal/log" + loader "github.com/yashranjan1/relay/internal/tui/components/Loader" toast "github.com/yashranjan1/relay/internal/tui/components/Toast" "github.com/yashranjan1/relay/internal/tui/keybinds" "github.com/yashranjan1/relay/internal/tui/messages" @@ -40,6 +42,8 @@ type AppModel struct { keys []key.Binding help help.Model errorMsg string + loading bool + loader *loader.Loader } func (a AppModel) Init() tea.Cmd { @@ -56,11 +60,23 @@ func (a AppModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case messages.ItemAdded: a.Views[Collections], cmd = a.Views[Collections].Update(messages.RefreshItemsList{}) cmds = append(cmds, cmd) + case messages.StartLoader: + a.loading = true + a.loader.SetMessage(msg.Message) + cmd = a.loader.Tick() + cmds = append(cmds, cmd) + case messages.Response: + a.loading = false case messages.RemoveToast: a.toast.RemoveToast(msg.ID) case messages.AddToast: cmd = a.toast.AddToast(msg.Type, msg.Message) cmds = append(cmds, cmd) + case loader.TickMsg: + if a.loading { + cmd = a.loader.Update(msg) + cmds = append(cmds, cmd) + } case tea.WindowSizeMsg: a.height = msg.Height a.width = msg.Width @@ -92,6 +108,9 @@ func (a AppModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { cmd = a.Views[a.focusedView].OnFocus() cmds = append(cmds, cmd) + if a.loading { + } + return a, tea.Batch(cmds...) case tea.KeyPressMsg: @@ -184,10 +203,21 @@ func (a AppModel) Header() string { } func (a AppModel) Footer() string { - name := styles.ApplyGradientToFooter("REQ") + name := styles.ApplyGradientToFooter("RELAY") footerText := styles.FooterSegmentStyle.Render(a.Views[a.focusedView].GetFooterSegment()) - version := styles.FooterVersionStyle.Width(a.width - lipgloss.Width(name) - lipgloss.Width(footerText)).Render(a.ctx.Version) - return lipgloss.JoinHorizontal(lipgloss.Left, name, footerText, version) + version := styles.FooterVersionStyle.Render(a.ctx.Version) + + var loader string + residualWidth := a.width - (lipgloss.Width(name) + lipgloss.Width(footerText) + lipgloss.Width(version)) + style := styles.LoaderStyle.Width(residualWidth) + + if a.loading { + loader = style.Render(a.loader.View()) + } else { + loader = style.Render("") + } + + return lipgloss.JoinHorizontal(lipgloss.Left, name, footerText, loader, version) } func NewAppModel(ctx *Context) AppModel { @@ -202,6 +232,8 @@ func NewAppModel(ctx *Context) AppModel { ctx: ctx, help: help.New(), keys: appKeybinds, + loader: loader.NewLoader(50*time.Millisecond, 9), + loading: false, toast: toasts, } diff --git a/internal/tui/components/Loader/component.go b/internal/tui/components/Loader/component.go new file mode 100644 index 0000000..0c48768 --- /dev/null +++ b/internal/tui/components/Loader/component.go @@ -0,0 +1,81 @@ +package loader + +import ( + "math" + "strings" + "time" + + tea "charm.land/bubbletea/v2" + "github.com/yashranjan1/relay/internal/tui/styles" +) + +type TickMsg time.Time + +type Loader struct { + start time.Time + speed time.Duration + message string + direction int + index int + width int +} + +func NewLoader(speed time.Duration, width int) *Loader { + return &Loader{ + start: time.Now(), + speed: speed, + direction: 1, + width: width, + message: " Generating commit", + } +} + +func (l *Loader) SetMessage(message string) { + l.message = message +} + +func (l *Loader) Tick() tea.Cmd { + return tea.Tick(l.speed, func(t time.Time) tea.Msg { + return TickMsg(t) + }) +} + +func (l *Loader) Update(msg tea.Msg) tea.Cmd { + switch msg.(type) { + case TickMsg: + l.index += l.direction + if l.index >= l.width-1 { + l.direction = -1 + } else if l.index <= 0 { + l.direction = 1 + } + return l.Tick() + } + return nil +} + +func (l *Loader) View() string { + var result strings.Builder + + for i := 0; i < l.width; i++ { + ch := "■" + + dist := float64(abs(i-l.index)) / float64(l.width) + sin := math.Sin((1.0 - dist) * math.Pi / 2) + alpha := math.Pow(sin, 8) + + char := styles.GetLoaderCharStyle(alpha)(ch) + + result.WriteString(char) + + } + result.WriteString(styles.LoaderMessageStyle.Render(l.message)) + return result.String() +} + +func abs(x int) int { + if x < 0 { + return -x + } + return x +} diff --git a/internal/tui/components/OptionsProvider/component.go b/internal/tui/components/OptionsProvider/component.go index 966956d..09d01f2 100644 --- a/internal/tui/components/OptionsProvider/component.go +++ b/internal/tui/components/OptionsProvider/component.go @@ -73,6 +73,9 @@ func (o OptionsProvider[T, U]) Update(msg tea.Msg) (OptionsProvider[T, U], tea.C } return o, func() tea.Msg { return messages.DeleteItem{ItemID: int64(o.GetSelected().ID)} } case key.Matches(msg, o.keys.Choose): + if o.GetSelected().ID == -1 { + return o, tea.Batch(cmds...) + } return o, func() tea.Msg { return messages.ChooseItem[Option]{ Item: o.GetSelected(), diff --git a/internal/tui/components/ViewPort/component.go b/internal/tui/components/ViewPort/component.go index c43a11f..fb6497e 100644 --- a/internal/tui/components/ViewPort/component.go +++ b/internal/tui/components/ViewPort/component.go @@ -32,6 +32,7 @@ func (v *Viewport) EraseState() { v.status = 0 v.body = "" v.headers = nil + v.viewport.SetContent(v.body) } func (v *Viewport) SetWidth(width int) { diff --git a/internal/tui/messages/messages.go b/internal/tui/messages/messages.go index 47ba904..4d5b44a 100644 --- a/internal/tui/messages/messages.go +++ b/internal/tui/messages/messages.go @@ -51,3 +51,7 @@ type Response struct { } type RefreshItemsList struct{} + +type StartLoader struct { + Message string +} diff --git a/internal/tui/styles/app-styles.go b/internal/tui/styles/app-styles.go index aeba518..0f1659e 100644 --- a/internal/tui/styles/app-styles.go +++ b/internal/tui/styles/app-styles.go @@ -8,7 +8,8 @@ var ( footerNameStyle = lipgloss.NewStyle().Bold(true).Background(AppTheme.FooterNameBG) footerNameBGStyle = lipgloss.NewStyle().Background(AppTheme.FooterNameBG).Padding(0, 3, 0) FooterSegmentStyle = lipgloss.NewStyle().Background(AppTheme.FooterSegmentBG).PaddingLeft(2).Foreground(AppTheme.FooterSegmentFG) - FooterVersionStyle = lipgloss.NewStyle().Background(AppTheme.FooterSegmentBG).AlignHorizontal(lipgloss.Right).PaddingRight(2).Foreground(AppTheme.FooterSegmentFG) + LoaderStyle = lipgloss.NewStyle().Background(AppTheme.FooterSegmentBG).PaddingLeft(2).AlignHorizontal(lipgloss.Right) + FooterVersionStyle = lipgloss.NewStyle().Background(AppTheme.FooterNameBG).AlignHorizontal(lipgloss.Right).Padding(0, 2).Foreground(AppTheme.FooterSegmentFG) TabHeadingInactive = lipgloss.NewStyle().Width(25).AlignHorizontal(lipgloss.Center).Border(lipgloss.NormalBorder(), false, false, false, true) TabHeadingActive = lipgloss.NewStyle().Background(AppTheme.Accent).Foreground(AppTheme.HeadingForeground).Width(25).AlignHorizontal(lipgloss.Center).Border(lipgloss.NormalBorder(), false, false, false, true) HelpStyle = lipgloss.NewStyle().Padding(1, 0, 1, 2) @@ -20,7 +21,8 @@ func initAppStyles() { footerNameStyle = lipgloss.NewStyle().Bold(true).Background(AppTheme.FooterNameBG) footerNameBGStyle = lipgloss.NewStyle().Background(AppTheme.FooterNameBG).Padding(0, 3, 0) FooterSegmentStyle = lipgloss.NewStyle().Background(AppTheme.FooterSegmentBG).PaddingLeft(2).Foreground(AppTheme.FooterSegmentFG) - FooterVersionStyle = lipgloss.NewStyle().Background(AppTheme.FooterSegmentBG).AlignHorizontal(lipgloss.Right).PaddingRight(2).Foreground(AppTheme.FooterSegmentFG) + LoaderStyle = lipgloss.NewStyle().Background(AppTheme.FooterSegmentBG).PaddingLeft(2).AlignHorizontal(lipgloss.Right) + FooterVersionStyle = lipgloss.NewStyle().Background(AppTheme.FooterNameBG).AlignHorizontal(lipgloss.Right).Padding(0, 2).Foreground(AppTheme.FooterSegmentFG) TabHeadingInactive = lipgloss.NewStyle().Width(25).AlignHorizontal(lipgloss.Center).Border(lipgloss.NormalBorder(), false, false, false, true) TabHeadingActive = lipgloss.NewStyle().Background(AppTheme.Accent).Foreground(AppTheme.HeadingForeground).Width(25).AlignHorizontal(lipgloss.Center).Border(lipgloss.NormalBorder(), false, false, false, true) HelpStyle = lipgloss.NewStyle().Padding(1, 0, 1, 2) diff --git a/internal/tui/styles/functions.go b/internal/tui/styles/functions.go index 07c9f03..89b3789 100644 --- a/internal/tui/styles/functions.go +++ b/internal/tui/styles/functions.go @@ -33,5 +33,5 @@ func interpolateColor(start, end stdcolor.Color, ratio float64) stdcolor.Color { } func ApplyGradientToFooter(text string) string { - return gradientText("REQ", AppTheme.FooterNameFGFrom, AppTheme.FooterNameFGTo, footerNameStyle, footerNameBGStyle) + return gradientText(text, AppTheme.FooterNameFGFrom, AppTheme.FooterNameFGTo, footerNameStyle, footerNameBGStyle) } diff --git a/internal/tui/styles/loader-styles.go b/internal/tui/styles/loader-styles.go new file mode 100644 index 0000000..883775b --- /dev/null +++ b/internal/tui/styles/loader-styles.go @@ -0,0 +1,87 @@ +package styles + +import ( + "charm.land/lipgloss/v2" + "fmt" + "image/color" + "strconv" + "strings" +) + +func ParseHex(s string) (color.RGBA, error) { + s = strings.TrimPrefix(s, "#") + + if len(s) == 3 { + s = string([]byte{s[0], s[0], s[1], s[1], s[2], s[2]}) + } + + if len(s) != 6 { + return color.RGBA{}, fmt.Errorf("invalid hex color: %q", s) + } + + val, err := strconv.ParseUint(s, 16, 32) + if err != nil { + return color.RGBA{}, fmt.Errorf("invalid hex color: %q", s) + } + + return color.RGBA{ + R: uint8(val >> 16), + G: uint8(val >> 8), + B: uint8(val), + A: 255, + }, nil +} + +func FakeOpacity(fg, bg color.RGBA, alpha float64) (string, error) { + + blend := func(fg, bg uint8) uint8 { + return uint8(float64(fg)*alpha + float64(bg)*(1-alpha)) + } + + result := color.RGBA{ + R: blend(fg.R, bg.R), + G: blend(fg.G, bg.G), + B: blend(fg.B, bg.B), + } + + return fmt.Sprintf("#%02X%02X%02X", result.R, result.G, result.B), nil +} + +func GetLoaderCharStyle(alpha float64) func(...string) string { + fgR, fgG, fgB, _ := AppTheme.Accent.RGBA() + + fg := color.RGBA{ + R: uint8(fgR), + G: uint8(fgG), + B: uint8(fgB), + } + + bgR, bgG, bgB, _ := AppTheme.FooterSegmentBG.RGBA() + + bg := color.RGBA{ + R: uint8(bgR), + G: uint8(bgG), + B: uint8(bgB), + } + + color, _ := FakeOpacity(fg, bg, alpha) + + return lipgloss.NewStyle(). + Foreground(lipgloss.Color(color)). + Background(AppTheme.FooterSegmentBG). + Render +} + +var ( + LoaderMessageStyle = lipgloss.NewStyle(). + PaddingRight(2). + Foreground(AppTheme.Accent). + Background(AppTheme.FooterSegmentBG) +) + +func initLoaderStyles() { + LoaderMessageStyle = lipgloss.NewStyle(). + PaddingRight(2). + Foreground(AppTheme.Accent). + Background(AppTheme.FooterSegmentBG) +} diff --git a/internal/tui/styles/request-styles.go b/internal/tui/styles/request-styles.go index 11c17e4..3c497a0 100644 --- a/internal/tui/styles/request-styles.go +++ b/internal/tui/styles/request-styles.go @@ -30,7 +30,7 @@ func UrlInputStyle(active bool) lipgloss.Style { } func ResponseContentStyle(height, width int) func(...string) string { - return lipgloss.NewStyle().Padding(1, 0, 1, 1).Height(height).Width(width).Render + return lipgloss.NewStyle().Padding(1, 0, 0, 1).Height(height).Width(width).Render } func ResponseStyle(active bool) func(...string) string { diff --git a/internal/tui/styles/theme.go b/internal/tui/styles/theme.go index ffab9fc..38741b1 100644 --- a/internal/tui/styles/theme.go +++ b/internal/tui/styles/theme.go @@ -35,4 +35,5 @@ func SetTheme(newTheme Theme) { initRequestStyles() initCollectionStyles() initAppStyles() + initLoaderStyles() } diff --git a/internal/tui/views/request-view.go b/internal/tui/views/request-view.go index 2c58d74..cbf7f52 100644 --- a/internal/tui/views/request-view.go +++ b/internal/tui/views/request-view.go @@ -8,7 +8,6 @@ import ( "charm.land/bubbles/v2/help" "charm.land/bubbles/v2/key" "charm.land/bubbles/v2/list" - "charm.land/bubbles/v2/spinner" tea "charm.land/bubbletea/v2" "charm.land/lipgloss/v2" "github.com/yashranjan1/relay/internal/backend/endpoints" @@ -48,7 +47,6 @@ type RequestView struct { loading bool help help.Model keys *keybinds.ListKeyMap - spinner spinner.Model client *http.HTTPManager order int update func(context.Context, int64, endpoints.EndpointData) (endpoints.EndpointEntity, error) @@ -93,7 +91,7 @@ func (r *RequestView) Update(msg tea.Msg) (ViewInterface, tea.Cmd) { w := r.components[methodPicker].GetWidth() r.components[urlInput].SetWidth(r.width - w) - topMenu := 4 + topMenu := 5 r.components[responseView], cmd = r.components[responseView].Update(tea.WindowSizeMsg{ Height: msg.Height - topMenu, Width: r.width - topMenu, @@ -115,6 +113,9 @@ func (r *RequestView) Update(msg tea.Msg) (ViewInterface, tea.Cmd) { case tea.KeyPressMsg: switch { case key.Matches(msg, keybinds.Keys.Back): + if settable, ok := r.components[responseView].(componenttypes.ResponseSettable); ok { + settable.EraseState() + } return r, func() tea.Msg { return messages.NavigateToView{ ViewName: Endpoints, @@ -140,11 +141,22 @@ func (r *RequestView) Update(msg tea.Msg) (ViewInterface, tea.Cmd) { Err: err, } } - return messages.Response{ + + data := messages.Response{ Data: res, } + + return data + } + + loaderMsg := func() tea.Msg { + return messages.StartLoader{ + Message: " Sending", + } } - return r, tea.Batch(r.spinner.Tick, sendMsg) + + return r, tea.Batch(loaderMsg, sendMsg) + case key.Matches(msg, keybinds.Keys.Save): r.Save() } @@ -154,11 +166,6 @@ func (r *RequestView) Update(msg tea.Msg) (ViewInterface, tea.Cmd) { cmds = append(cmds, cmd) - if r.loading { - r.spinner, cmd = r.spinner.Update(msg) - cmds = append(cmds, cmd) - } - return r, tea.Batch(cmds...) } @@ -282,8 +289,6 @@ func NewRequestView(cfg RequestViewConfig) *RequestView { mpConfig := createMethodPickerConfig() uiConfig := createURLInputConfig() - s := spinner.New() - s.Spinner = spinner.Dot return &RequestView{ components: map[reqFocused]componenttypes.FocusableComponent{ @@ -297,6 +302,5 @@ func NewRequestView(cfg RequestViewConfig) *RequestView { order: cfg.Order, update: cfg.Update, client: cfg.Client, - spinner: s, } } From cab0630d54ad82a3eef56f9bdf7d2d343cc9ffe5 Mon Sep 17 00:00:00 2001 From: yashranjan1 Date: Sat, 12 Sep 2026 00:15:32 +0200 Subject: [PATCH 2/2] refactor: relocate color and loader utilities - Move ParseHex, FakeOpacity, and GetLoaderCharStyle to functions.go - Clean up imports and simplify color package references across styles package --- internal/tui/styles/functions.go | 75 ++++++++++++++++++++++++++-- internal/tui/styles/loader-styles.go | 68 ------------------------- 2 files changed, 71 insertions(+), 72 deletions(-) diff --git a/internal/tui/styles/functions.go b/internal/tui/styles/functions.go index 89b3789..9fc5712 100644 --- a/internal/tui/styles/functions.go +++ b/internal/tui/styles/functions.go @@ -1,12 +1,15 @@ package styles import ( - stdcolor "image/color" + "fmt" + "image/color" + "strconv" + "strings" "charm.land/lipgloss/v2" ) -func gradientText(text string, startColor, endColor stdcolor.Color, base, additional lipgloss.Style) string { +func gradientText(text string, startColor, endColor color.Color, base, additional lipgloss.Style) string { n := len(text) result := "" @@ -21,7 +24,7 @@ func gradientText(text string, startColor, endColor stdcolor.Color, base, additi return additional.Render(result) } -func interpolateColor(start, end stdcolor.Color, ratio float64) stdcolor.Color { +func interpolateColor(start, end color.Color, ratio float64) color.Color { r1, g1, b1, _ := start.RGBA() r2, g2, b2, _ := end.RGBA() @@ -29,9 +32,73 @@ func interpolateColor(start, end stdcolor.Color, ratio float64) stdcolor.Color { g := uint8(float64(g1>>8) + (float64(g2>>8)-float64(g1>>8))*ratio) b := uint8(float64(b1>>8) + (float64(b2>>8)-float64(b1>>8))*ratio) - return stdcolor.RGBA{R: r, G: g, B: b, A: 0xff} + return color.RGBA{R: r, G: g, B: b, A: 0xff} } func ApplyGradientToFooter(text string) string { return gradientText(text, AppTheme.FooterNameFGFrom, AppTheme.FooterNameFGTo, footerNameStyle, footerNameBGStyle) } + +func ParseHex(s string) (color.RGBA, error) { + s = strings.TrimPrefix(s, "#") + + if len(s) == 3 { + s = string([]byte{s[0], s[0], s[1], s[1], s[2], s[2]}) + } + + if len(s) != 6 { + return color.RGBA{}, fmt.Errorf("invalid hex color: %q", s) + } + + val, err := strconv.ParseUint(s, 16, 32) + if err != nil { + return color.RGBA{}, fmt.Errorf("invalid hex color: %q", s) + } + + return color.RGBA{ + R: uint8(val >> 16), + G: uint8(val >> 8), + B: uint8(val), + A: 255, + }, nil +} + +func FakeOpacity(fg, bg color.RGBA, alpha float64) (string, error) { + + blend := func(fg, bg uint8) uint8 { + return uint8(float64(fg)*alpha + float64(bg)*(1-alpha)) + } + + result := color.RGBA{ + R: blend(fg.R, bg.R), + G: blend(fg.G, bg.G), + B: blend(fg.B, bg.B), + } + + return fmt.Sprintf("#%02X%02X%02X", result.R, result.G, result.B), nil +} + +func GetLoaderCharStyle(alpha float64) func(...string) string { + fgR, fgG, fgB, _ := AppTheme.Accent.RGBA() + + fg := color.RGBA{ + R: uint8(fgR), + G: uint8(fgG), + B: uint8(fgB), + } + + bgR, bgG, bgB, _ := AppTheme.FooterSegmentBG.RGBA() + + bg := color.RGBA{ + R: uint8(bgR), + G: uint8(bgG), + B: uint8(bgB), + } + + color, _ := FakeOpacity(fg, bg, alpha) + + return lipgloss.NewStyle(). + Foreground(lipgloss.Color(color)). + Background(AppTheme.FooterSegmentBG). + Render +} diff --git a/internal/tui/styles/loader-styles.go b/internal/tui/styles/loader-styles.go index 883775b..79321ed 100644 --- a/internal/tui/styles/loader-styles.go +++ b/internal/tui/styles/loader-styles.go @@ -2,76 +2,8 @@ package styles import ( "charm.land/lipgloss/v2" - "fmt" - "image/color" - "strconv" - "strings" ) -func ParseHex(s string) (color.RGBA, error) { - s = strings.TrimPrefix(s, "#") - - if len(s) == 3 { - s = string([]byte{s[0], s[0], s[1], s[1], s[2], s[2]}) - } - - if len(s) != 6 { - return color.RGBA{}, fmt.Errorf("invalid hex color: %q", s) - } - - val, err := strconv.ParseUint(s, 16, 32) - if err != nil { - return color.RGBA{}, fmt.Errorf("invalid hex color: %q", s) - } - - return color.RGBA{ - R: uint8(val >> 16), - G: uint8(val >> 8), - B: uint8(val), - A: 255, - }, nil -} - -func FakeOpacity(fg, bg color.RGBA, alpha float64) (string, error) { - - blend := func(fg, bg uint8) uint8 { - return uint8(float64(fg)*alpha + float64(bg)*(1-alpha)) - } - - result := color.RGBA{ - R: blend(fg.R, bg.R), - G: blend(fg.G, bg.G), - B: blend(fg.B, bg.B), - } - - return fmt.Sprintf("#%02X%02X%02X", result.R, result.G, result.B), nil -} - -func GetLoaderCharStyle(alpha float64) func(...string) string { - fgR, fgG, fgB, _ := AppTheme.Accent.RGBA() - - fg := color.RGBA{ - R: uint8(fgR), - G: uint8(fgG), - B: uint8(fgB), - } - - bgR, bgG, bgB, _ := AppTheme.FooterSegmentBG.RGBA() - - bg := color.RGBA{ - R: uint8(bgR), - G: uint8(bgG), - B: uint8(bgB), - } - - color, _ := FakeOpacity(fg, bg, alpha) - - return lipgloss.NewStyle(). - Foreground(lipgloss.Color(color)). - Background(AppTheme.FooterSegmentBG). - Render -} - var ( LoaderMessageStyle = lipgloss.NewStyle(). PaddingRight(2).