Detailed changes
@@ -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))
}
@@ -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
}
@@ -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
@@ -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"))
@@ -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 {
@@ -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))
-}
@@ -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(" • ")