From 6c1fa6c3a8faa754723a788d4fdb244b99b06d9a Mon Sep 17 00:00:00 2001 From: Christian Rocha Date: Thu, 19 Aug 2021 12:05:49 -0400 Subject: [PATCH] Generally "smooth" out style and layout --- tui/bubble.go | 84 ++++++++++++++++++++++++--------- tui/bubbles/repo/bubble.go | 46 +++++++++++++----- tui/bubbles/selection/bubble.go | 14 ++++-- tui/bubbles/selection/style.go | 11 ----- tui/commands.go | 20 ++++---- tui/help.go | 12 ----- tui/style.go | 77 +++++++++++++++++++----------- 7 files changed, 166 insertions(+), 98 deletions(-) delete mode 100644 tui/bubbles/selection/style.go delete mode 100644 tui/help.go diff --git a/tui/bubble.go b/tui/bubble.go index eca06e297..8c937fa27 100644 --- a/tui/bubble.go +++ b/tui/bubble.go @@ -2,7 +2,6 @@ package tui import ( "fmt" - "io" "smoothie/git" "smoothie/tui/bubbles/commits" "smoothie/tui/bubbles/repo" @@ -91,7 +90,7 @@ func (b *Bubble) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg.String() { case "q", "ctrl+c": return b, tea.Quit - case "tab": + case "tab", "shift+tab": b.activeBox = (b.activeBox + 1) % 2 case "h", "left": if b.activeBox > 0 { @@ -118,6 +117,7 @@ func (b *Bubble) Update(msg tea.Msg) (tea.Model, tea.Cmd) { cmds = append(cmds, cmd) } } + // XXX: maybe propagate size changes to child bubbles (particularly height) case selection.SelectedMsg: b.activeBox = 1 rb := b.repoMenu[msg.Index].bubble @@ -138,48 +138,88 @@ func (b *Bubble) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return b, tea.Batch(cmds...) } -func (b *Bubble) viewForBox(i int, width int, height int) string { - var ls lipgloss.Style - if i == b.activeBox { - ls = activeBoxStyle.Copy() - } else { - ls = inactiveBoxStyle.Copy() - } - ls.Width(width) - if height > 0 { - ls.Height(height).MarginBottom(3) +func (b *Bubble) viewForBox(i int) string { + box := b.boxes[i] + isActive := i == b.activeBox + var s lipgloss.Style + var menuHeightFix int // TODO: figure out why we need this + switch box.(type) { + case *selection.Bubble: + menuHeightFix = 1 + if isActive { + s = menuActiveStyle + break + } + s = menuStyle + case *repo.Bubble: + if isActive { + s = contentBoxActiveStyle + } else { + s = contentBoxStyle + } + const repoWidthFix = 1 // TODO: figure out why we need this + w := b.width - + lipgloss.Width(b.viewForBox(0)) - + appBoxStyle.GetHorizontalFrameSize() - + s.GetHorizontalFrameSize() + repoWidthFix + s = s.Copy().Width(w) + default: + panic(fmt.Sprintf("unknown box type %T", box)) } - return ls.Render(b.boxes[i].View()) + h := b.height - + lipgloss.Height(b.headerView()) - + lipgloss.Height(b.footerView()) - + s.GetVerticalFrameSize() - + appBoxStyle.GetVerticalFrameSize() + + menuHeightFix + return s.Copy().Height(h).Render(box.View()) } -func (b Bubble) footerView(w io.Writer) { +func (b Bubble) headerView() string { + w := b.width - appBoxStyle.GetHorizontalFrameSize() + return headerStyle.Copy().Width(w).Render(b.config.Name) +} + +func (b Bubble) footerView() string { + w := &strings.Builder{} h := []helpEntry{ {"tab", "section"}, {"↑/↓", "navigate"}, {"q", "quit"}, } + if _, ok := b.boxes[b.activeBox].(*repo.Bubble); ok { + h = append(h[:2], helpEntry{"f/b", "pgup/pgdown"}, h[2]) + } for i, v := range h { fmt.Fprint(w, v) if i != len(h)-1 { fmt.Fprint(w, helpDivider) } } + return footerStyle.Render(w.String()) } -func (b *Bubble) View() string { +func (b Bubble) View() string { s := strings.Builder{} - w := b.width - 3 - s.WriteString(headerStyle.Width(w - 2).Render(b.config.Name)) + s.WriteString(b.headerView()) s.WriteRune('\n') switch b.state { case loadedState: - lb := b.viewForBox(0, boxLeftWidth, 0) - rb := b.viewForBox(1, b.width-boxLeftWidth-10, b.height-8) + lb := b.viewForBox(0) + rb := b.viewForBox(1) s.WriteString(lipgloss.JoinHorizontal(lipgloss.Top, lb, rb)) case errorState: s.WriteString(errorStyle.Render(fmt.Sprintf("Bummer: %s", b.error))) } - s.WriteRune('\n') - b.footerView(&s) - return appBoxStyle.Width(w).Height(b.height).Render(s.String()) + s.WriteString(b.footerView()) + return appBoxStyle.Render(s.String()) +} + +type helpEntry struct { + key string + val string +} + +func (h helpEntry) String() string { + return fmt.Sprintf("%s %s", helpKeyStyle.Render(h.key), helpValueStyle.Render(h.val)) } diff --git a/tui/bubbles/repo/bubble.go b/tui/bubbles/repo/bubble.go index 8c30c6068..001a9f48b 100644 --- a/tui/bubbles/repo/bubble.go +++ b/tui/bubbles/repo/bubble.go @@ -8,8 +8,11 @@ import ( "github.com/charmbracelet/bubbles/viewport" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/glamour" + "github.com/charmbracelet/lipgloss" ) +const glamourMaxWidth = 120 + type ErrMsg struct { Error error } @@ -28,21 +31,18 @@ type Bubble struct { } func NewBubble(rs *git.RepoSource, name string, width, wm, height, hm int, tmp interface{}) *Bubble { - return &Bubble{ + b := &Bubble{ templateObject: tmp, repoSource: rs, name: name, - height: height, - width: width, heightMargin: hm, widthMargin: wm, readmeViewport: &ViewportBubble{ - Viewport: &viewport.Model{ - Width: width - wm, - Height: height - hm, - }, + Viewport: &viewport.Model{}, }, } + b.SetSize(width, height) + return b } func (b *Bubble) Init() tea.Cmd { @@ -53,8 +53,10 @@ func (b *Bubble) Update(msg tea.Msg) (tea.Model, tea.Cmd) { var cmds []tea.Cmd switch msg := msg.(type) { case tea.WindowSizeMsg: - b.readmeViewport.Viewport.Width = msg.Width - b.widthMargin - b.readmeViewport.Viewport.Height = msg.Height - b.heightMargin + b.SetSize(msg.Width, msg.Height) + // XXX: if we find that longer readmes take more than a few + // milliseconds to render we may need to move Glamour rendering into a + // command. md, err := b.glamourize(b.readme) if err != nil { return b, nil @@ -63,12 +65,17 @@ func (b *Bubble) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } rv, cmd := b.readmeViewport.Update(msg) b.readmeViewport = rv.(*ViewportBubble) - if cmd != nil { - cmds = append(cmds, cmd) - } + cmds = append(cmds, cmd) return b, tea.Batch(cmds...) } +func (b *Bubble) SetSize(w, h int) { + b.width = w + b.height = h + b.readmeViewport.Viewport.Width = w - b.widthMargin + b.readmeViewport.Viewport.Height = h - b.heightMargin +} + func (b *Bubble) GotoTop() { b.readmeViewport.Viewport.GotoTop() } @@ -116,9 +123,14 @@ func (b *Bubble) templatize(mdt string) (string, error) { } func (b *Bubble) glamourize(md string) (string, error) { + // TODO: read gaps in appropriate style to remove the magic number below. + w := b.width - b.widthMargin - 2 + if w > glamourMaxWidth { + w = glamourMaxWidth + } tr, err := glamour.NewTermRenderer( glamour.WithStandardStyle("dark"), - glamour.WithWordWrap(b.width-b.widthMargin), + glamour.WithWordWrap(w), ) if err != nil { @@ -128,5 +140,13 @@ func (b *Bubble) glamourize(md string) (string, error) { if err != nil { return "", err } + // Enforce a maximum width for cases when glamour lines run long. + // + // TODO: use Reflow's unconditional wrapping to force-wrap long lines. This + // should utlimately happen as a Glamour option. + // + // See: + // https://github.com/muesli/reflow#unconditional-wrapping + mdt = lipgloss.NewStyle().MaxWidth(w).Render(mdt) return mdt, nil } diff --git a/tui/bubbles/selection/bubble.go b/tui/bubbles/selection/bubble.go index 69d7c2b98..75736b268 100644 --- a/tui/bubbles/selection/bubble.go +++ b/tui/bubbles/selection/bubble.go @@ -18,14 +18,16 @@ type ActiveMsg struct { type Bubble struct { NormalStyle lipgloss.Style SelectedStyle lipgloss.Style + Cursor string Items []string SelectedItem int } -func NewBubble(items []string) *Bubble { +func NewBubble(items []string, normalStyle, selectedStyle lipgloss.Style, cursor string) *Bubble { return &Bubble{ NormalStyle: normalStyle, SelectedStyle: selectedStyle, + Cursor: cursor, Items: items, } } @@ -34,13 +36,17 @@ func (b *Bubble) Init() tea.Cmd { return nil } -func (b *Bubble) View() string { +func (b Bubble) View() string { s := "" for i, item := range b.Items { if i == b.SelectedItem { - s += b.SelectedStyle.Render(item) + "\n" + s += b.Cursor + s += b.SelectedStyle.Render(item) } else { - s += b.NormalStyle.Render(item) + "\n" + s += b.NormalStyle.Render(item) + } + if i < len(b.Items)-1 { + s += "\n" } } return s diff --git a/tui/bubbles/selection/style.go b/tui/bubbles/selection/style.go deleted file mode 100644 index 120fc75a7..000000000 --- a/tui/bubbles/selection/style.go +++ /dev/null @@ -1,11 +0,0 @@ -package selection - -import ( - "github.com/charmbracelet/lipgloss" -) - -var normalStyle = lipgloss.NewStyle(). - Foreground(lipgloss.Color("#707070")) - -var selectedStyle = lipgloss.NewStyle(). - Foreground(lipgloss.Color("#FFFFFF")) diff --git a/tui/commands.go b/tui/commands.go index 68f54fd6b..a02fc8a14 100644 --- a/tui/commands.go +++ b/tui/commands.go @@ -2,7 +2,6 @@ package tui import ( "fmt" - "smoothie/tui/bubbles/commits" "smoothie/tui/bubbles/repo" "smoothie/tui/bubbles/selection" @@ -49,7 +48,10 @@ func (b *Bubble) setupCmd() tea.Msg { if me.Repo == "config" { tmplConfig = b.config } - rb := repo.NewBubble(b.repoSource, me.Repo, b.width, boxLeftWidth+12, b.height, 12, tmplConfig) + width := b.width + boxLeftWidth := menuStyle.GetWidth() + menuStyle.GetHorizontalFrameSize() + const heightMargin = 12 // TODO: figure out why this needs to be 12 + rb := repo.NewBubble(b.repoSource, me.Repo, width, boxLeftWidth, b.height, heightMargin, tmplConfig) initCmd := rb.Init() msg := initCmd() switch msg := msg.(type) { @@ -60,13 +62,15 @@ func (b *Bubble) setupCmd() tea.Msg { b.repoMenu = append(b.repoMenu, me) rs = append(rs, me.Name) } - b.repoSelect = selection.NewBubble(rs) + b.repoSelect = selection.NewBubble(rs, menuItemStyle, selectedMenuItemStyle, menuCursor.String()) b.boxes[0] = b.repoSelect - b.commitsLog = commits.NewBubble( - b.height-verticalPadding-2, - boxRightWidth-horizontalPadding-2, - b.repoSource.GetCommits(200), - ) + /* + b.commitsLog = commits.NewBubble( + b.height-verticalPadding-2, + boxRightWidth-horizontalPadding-2, + b.repoSource.GetCommits(200), + ) + */ ir := -1 if b.initialRepo != "" { for i, me := range b.repoMenu { diff --git a/tui/help.go b/tui/help.go deleted file mode 100644 index 19b290aff..000000000 --- a/tui/help.go +++ /dev/null @@ -1,12 +0,0 @@ -package tui - -import "fmt" - -type helpEntry struct { - key string - val string -} - -func (h helpEntry) String() string { - return fmt.Sprintf("%s %s", helpKeyStyle.Render(h.key), helpValueStyle.Render(h.val)) -} diff --git a/tui/style.go b/tui/style.go index d4ff52ae7..998de114e 100644 --- a/tui/style.go +++ b/tui/style.go @@ -4,42 +4,48 @@ import ( "github.com/charmbracelet/lipgloss" ) -const boxLeftWidth = 25 -const boxRightWidth = 85 -const headerHeight = 1 -const footerHeight = 2 -const appPadding = 1 -const boxPadding = 1 -const viewportHeightConstant = 7 // TODO figure out why this needs to be 7 -const horizontalPadding = appPadding * 2 -const verticalPadding = headerHeight + footerHeight + (appPadding * 2) - -var appBoxStyle = lipgloss.NewStyle(). - PaddingLeft(appPadding). - PaddingRight(appPadding) - -var inactiveBoxStyle = lipgloss.NewStyle(). - Foreground(lipgloss.Color("#606060")). +var activeBorderColor = lipgloss.Color("243") +var inactiveBorderColor = lipgloss.Color("236") + +var hiddenBorder = lipgloss.Border{ + TopLeft: " ", + Top: " ", + TopRight: " ", + BottomLeft: " ", + Bottom: " ", + BottomRight: " ", +} + +var appBoxStyle = lipgloss.NewStyle() + +var menuStyle = lipgloss.NewStyle(). + BorderStyle(lipgloss.RoundedBorder()). + BorderForeground(inactiveBorderColor). + Padding(1, 2). + MarginRight(1). + Width(24) + +var menuActiveStyle = menuStyle.Copy(). + BorderStyle(lipgloss.RoundedBorder()). + BorderForeground(activeBorderColor) + +var contentBoxStyle = lipgloss.NewStyle(). BorderStyle(lipgloss.RoundedBorder()). - BorderForeground(lipgloss.Color("#303030")). - Padding(boxPadding) + BorderForeground(inactiveBorderColor). + PaddingRight(1). + MarginBottom(1) -var activeBoxStyle = lipgloss.NewStyle(). - Foreground(lipgloss.Color("#FFFFFF")). +var contentBoxActiveStyle = contentBoxStyle.Copy(). BorderStyle(lipgloss.RoundedBorder()). - BorderForeground(lipgloss.Color("#714C7B")). - Padding(boxPadding) + BorderForeground(activeBorderColor) var headerStyle = lipgloss.NewStyle(). - Foreground(lipgloss.Color("#714C7B")). + Foreground(lipgloss.Color("61")). Align(lipgloss.Right). Bold(true) -var normalStyle = lipgloss.NewStyle(). - Foreground(lipgloss.Color("#FFFFFF")) - -var errorStyle = lipgloss.NewStyle(). - Foreground(lipgloss.Color("#FF00000")) +var footerStyle = lipgloss.NewStyle(). + MarginTop(1) var helpKeyStyle = lipgloss.NewStyle(). Foreground(lipgloss.Color("241")) @@ -47,6 +53,21 @@ var helpKeyStyle = lipgloss.NewStyle(). var helpValueStyle = lipgloss.NewStyle(). Foreground(lipgloss.Color("239")) +var menuItemStyle = lipgloss.NewStyle(). + Foreground(lipgloss.Color("252")). + PaddingLeft(2) + +var selectedMenuItemStyle = lipgloss.NewStyle(). + Foreground(lipgloss.Color("207")). + PaddingLeft(1) + +var menuCursor = lipgloss.NewStyle(). + Foreground(lipgloss.Color("213")). + SetString(">") + +var errorStyle = lipgloss.NewStyle(). + Foreground(lipgloss.Color("#FF00000")) + var helpDivider = lipgloss.NewStyle(). Foreground(lipgloss.Color("237")). SetString(" • ")