From f2eb07ce2bb6d6a94506334581eae384f08b6ca6 Mon Sep 17 00:00:00 2001 From: Patrick Dewey Date: Wed, 10 Jun 2026 17:20:56 -0400 Subject: [PATCH] add styles package --- README.md | 12 ++++++ internal/app/app.go | 30 ++++---------- internal/app/forms.go | 78 +++++++++++------------------------ internal/app/styles.go | 94 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 138 insertions(+), 76 deletions(-) create mode 100644 internal/app/styles.go diff --git a/README.md b/README.md index eb6ec5c..a813082 100644 --- a/README.md +++ b/README.md @@ -27,3 +27,15 @@ semble-tui # or go run main.go ``` + +## Demo recording + +Record the VHS demo tape from a Nix shell: + +```sh +nix shell nixpkgs#vhs --command vhs tapes/demo.tape +``` + +The tape sources `env.sh` before launching the app, so keep `SEMBLE_API_KEY` +there when recording locally. The rendered GIF is written to +`media/semble-demo.gif`. diff --git a/internal/app/app.go b/internal/app/app.go index 7c52420..bd4cb76 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -33,18 +33,6 @@ const ( var screenNames = []string{"Profile", "Cards", "Collections", "Links", "Notifications", "Feed"} -var ( - titleStyle = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("63")).Padding(0, 1) - helpStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("241")) - errStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("196")) - okStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("42")) - tabStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("246")).Padding(0, 2) - activeTab = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("231")).Padding(0, 2) - activeText = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("63")) - toastStyle = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("232")).Background(lipgloss.Color("42")).Padding(0, 1) - errToast = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("231")).Background(lipgloss.Color("196")).Padding(0, 1) -) - type layout struct { width, height int barWidth int @@ -418,16 +406,16 @@ func (m model) footerView() string { } right := "" if m.err != nil { - right = errToast.Render(m.err.Error()) + right = theme.ErrorBadge.Render(m.err.Error()) } else if m.status != "" { - right = toastStyle.Render(m.status) + right = theme.SuccessBadge.Render(m.status) } if right != "" && l.barWidth > 0 { maxRight := min(l.barWidth/2, max(8, l.barWidth-lipgloss.Width("? help · q quit")-5)) if m.err != nil { - right = errToast.Render(truncateVisible(m.err.Error(), max(1, maxRight-2))) + right = theme.ErrorBadge.Render(truncateVisible(m.err.Error(), max(1, maxRight-2))) } else { - right = toastStyle.Render(truncateVisible(m.status, max(1, maxRight-2))) + right = theme.SuccessBadge.Render(truncateVisible(m.status, max(1, maxRight-2))) } } if right != "" && l.barWidth > 0 && lipgloss.Width(help)+lipgloss.Width(right)+2 > l.barWidth { @@ -437,7 +425,7 @@ func (m model) footerView() string { help = m.tinyHelpText() } bar := m.barStyle() - left := helpStyle.Background(m.barBackground()).Render(help) + left := theme.Help.Background(m.barBackground()).Render(help) line := left if right != "" { line = m.withRight(left, right, l.barWidth) @@ -635,7 +623,7 @@ func (m model) tabsView() string { parts[i] = m.activeTabStyle().Render(name) continue } - parts[i] = tabStyle.Background(bg).Render(name) + parts[i] = theme.Tab.Background(bg).Render(name) } return lipgloss.JoinHorizontal(lipgloss.Top, parts...) } @@ -649,15 +637,15 @@ func (m model) compactTabsView() string { parts[i] = m.activeTabStyle().Padding(0, 1).Render(label) continue } - parts[i] = tabStyle.Background(bg).Padding(0, 1).Render(label) + parts[i] = theme.Tab.Background(bg).Padding(0, 1).Render(label) } return lipgloss.JoinHorizontal(lipgloss.Top, parts...) } func (m model) titleView() string { - return titleStyle.Background(m.barBackground()).Render("Semble") + return theme.AppTitle.Background(m.barBackground()).Render("Semble") } func (m model) activeTabStyle() lipgloss.Style { - return activeTab.Background(lipgloss.Color("63")) + return theme.ActiveTabStyle() } diff --git a/internal/app/forms.go b/internal/app/forms.go index 120adda..a79b61e 100644 --- a/internal/app/forms.go +++ b/internal/app/forms.go @@ -7,7 +7,6 @@ import ( "charm.land/bubbles/v2/textarea" "charm.land/bubbles/v2/textinput" tea "charm.land/bubbletea/v2" - "charm.land/lipgloss/v2" semble "tangled.org/pdewey.com/semble" ) @@ -24,19 +23,12 @@ var linkTypes = []semble.ConnectionType{ var accessTypes = []semble.AccessType{semble.AccessOpen, semble.AccessClosed} -var ( - formPanelStyle = lipgloss.NewStyle().Border(lipgloss.RoundedBorder()).BorderForeground(lipgloss.Color("63")).Padding(1, 2).MarginTop(1) - labelStyle = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("250")) - formTitleStyle = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("63")) - fieldHintStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("244")) -) - func newInput(placeholder string) textinput.Model { t := textinput.New() t.Prompt = " " t.Placeholder = placeholder t.CharLimit = 512 - t.SetStyles(inputStyles()) + t.SetStyles(theme.InputStyles()) return t } func newCardForm() form { @@ -69,34 +61,10 @@ func newNote(placeholder string) textarea.Model { t.ShowLineNumbers = false t.Prompt = " " t.SetHeight(5) - t.SetStyles(noteStyles()) + t.SetStyles(theme.TextareaStyles()) return t } -func inputStyles() textinput.Styles { - s := textinput.DefaultDarkStyles() - s.Focused.Prompt = activeText - s.Focused.Text = lipgloss.NewStyle().Foreground(lipgloss.Color("252")) - s.Focused.Placeholder = lipgloss.NewStyle().Foreground(lipgloss.Color("244")) - s.Blurred.Prompt = helpStyle - s.Blurred.Text = lipgloss.NewStyle().Foreground(lipgloss.Color("245")) - s.Blurred.Placeholder = lipgloss.NewStyle().Foreground(lipgloss.Color("240")) - return s -} - -func noteStyles() textarea.Styles { - s := textarea.DefaultDarkStyles() - s.Focused.Prompt = activeText - s.Focused.Text = lipgloss.NewStyle().Foreground(lipgloss.Color("252")) - s.Focused.CursorLine = lipgloss.NewStyle() - s.Focused.Placeholder = lipgloss.NewStyle().Foreground(lipgloss.Color("244")) - s.Blurred.Prompt = helpStyle - s.Blurred.Text = lipgloss.NewStyle().Foreground(lipgloss.Color("245")) - s.Blurred.CursorLine = lipgloss.NewStyle() - s.Blurred.Placeholder = lipgloss.NewStyle().Foreground(lipgloss.Color("240")) - return s -} - func linkTypeIndex(typ semble.ConnectionType) int { for i, candidate := range linkTypes { if candidate == typ { @@ -284,7 +252,7 @@ func (m *model) toggleCurrentCollection() { func (m model) formView() string { width := m.formWidth() var b strings.Builder - b.WriteString(formTitleStyle.Render(m.formTitle()) + "\n") + b.WriteString(theme.PanelTitle.Render(m.formTitle()) + "\n") b.WriteString("\n") for i, f := range m.form.fields { if m.mode == modeLinkForm && i == 2 { @@ -299,7 +267,7 @@ func (m model) formView() string { b.WriteString(accessTypeSelectorView(m.form) + "\n\n") } b.WriteString(m.noteView(m.form.focus == m.noteFocusIndex())) - return formPanelStyle.Width(width).Render(strings.TrimRight(b.String(), "\n")) + return theme.FormPanel.Width(width).Render(strings.TrimRight(b.String(), "\n")) } func (m model) formTitle() string { @@ -331,9 +299,9 @@ func accessTypeSelectorView(f form) string { var b strings.Builder title := "Access" if f.focus == 1 { - title = activeText.Render("▸ " + title) + title = theme.ActiveText.Render("▸ " + title) } else { - title = fieldHintStyle.Render(" " + title) + title = theme.FieldHint.Render(" " + title) } b.WriteString(title + "\n") for i, typ := range accessTypes { @@ -341,12 +309,12 @@ func accessTypeSelectorView(f form) string { value := string(typ) if i == f.accessTypeIndex { cursor = "> " - value = okStyle.Render(value) + value = theme.SuccessText.Render(value) } b.WriteString(cursor + value + "\n") } if f.focus == 1 { - b.WriteString(helpStyle.Render(" j/k or ↑/↓ select access") + "\n") + b.WriteString(theme.Help.Render(" j/k or ↑/↓ select access") + "\n") } return strings.TrimRight(b.String(), "\n") } @@ -355,28 +323,28 @@ func (m model) collectionPickerView() string { var b strings.Builder title := "Collections" if m.form.focus == m.selectorFocusIndex() { - title = activeText.Render("▸ " + title) + title = theme.ActiveText.Render("▸ " + title) } else { - title = fieldHintStyle.Render(" " + title) + title = theme.FieldHint.Render(" " + title) } b.WriteString(title + "\n") if len(m.form.collections) == 0 { - b.WriteString(helpStyle.Render(" No collections loaded") + "\n") + b.WriteString(theme.Help.Render(" No collections loaded") + "\n") return strings.TrimRight(b.String(), "\n") } for i, c := range m.form.collections { id := formCollectionID(c) mark := "[ ]" if m.form.selectedCollectionIDs[id] { - mark = okStyle.Render("[x]") + mark = theme.SuccessText.Render("[x]") } cursor := " " name := truncateVisible(c.Name, max(12, m.formWidth()-18)) if i == m.form.collectionCursor { cursor = "> " - name = activeText.Render(name) + name = theme.ActiveText.Render(name) } - b.WriteString(fmt.Sprintf("%s%s %s %s\n", cursor, mark, name, helpStyle.Render(fmt.Sprintf("%d cards", c.CardCount)))) + b.WriteString(fmt.Sprintf("%s%s %s %s\n", cursor, mark, name, theme.Help.Render(fmt.Sprintf("%d cards", c.CardCount)))) } page := m.form.collectionPage if page == 0 { @@ -386,7 +354,7 @@ func (m model) collectionPickerView() string { if m.form.collectionPageInfo.TotalPages > 0 { pageText = fmt.Sprintf(" page %d/%d", page, m.form.collectionPageInfo.TotalPages) } - b.WriteString(helpStyle.Render(pageText+" · space toggle · h/l page") + "\n") + b.WriteString(theme.Help.Render(pageText+" · space toggle · h/l page") + "\n") return strings.TrimRight(b.String(), "\n") } @@ -420,19 +388,19 @@ func (m *model) resizeForm(bodyWidth int) { } func (m model) inputView(label string, input textinput.Model, focused bool) string { - labelText := labelStyle.Render(label) + labelText := theme.Label.Render(label) if focused { - labelText = activeText.Render("▸ " + label) + labelText = theme.ActiveText.Render("▸ " + label) } else { - labelText = fieldHintStyle.Render(" " + label) + labelText = theme.FieldHint.Render(" " + label) } return labelText + "\n" + input.View() + "\n" } func (m model) noteView(focused bool) string { - labelText := fieldHintStyle.Render(" Note") + labelText := theme.FieldHint.Render(" Note") if focused { - labelText = activeText.Render("▸ Note") + labelText = theme.ActiveText.Render("▸ Note") } return labelText + "\n" + m.form.note.View() } @@ -441,7 +409,7 @@ func linkTypeSelectorView(f form) string { var b strings.Builder title := "Type:" if f.focus == 2 { - title = activeText.Render(title) + title = theme.ActiveText.Render(title) } b.WriteString(title + "\n") for i, typ := range linkTypes { @@ -449,12 +417,12 @@ func linkTypeSelectorView(f form) string { value := string(typ) if i == f.linkTypeIndex { cursor = "> " - value = okStyle.Render(value) + value = theme.SuccessText.Render(value) } b.WriteString(cursor + value + "\n") } if f.focus == 2 { - b.WriteString(helpStyle.Render(" j/k or ↑/↓ select link type") + "\n") + b.WriteString(theme.Help.Render(" j/k or ↑/↓ select link type") + "\n") } return strings.TrimRight(b.String(), "\n") } diff --git a/internal/app/styles.go b/internal/app/styles.go new file mode 100644 index 0000000..ee206c9 --- /dev/null +++ b/internal/app/styles.go @@ -0,0 +1,94 @@ +package app + +import ( + "image/color" + + "charm.land/bubbles/v2/textarea" + "charm.land/bubbles/v2/textinput" + "charm.land/lipgloss/v2" +) + +type Theme struct { + Accent color.Color + AccentText color.Color + Success color.Color + Error color.Color + Muted color.Color + Dim color.Color + PanelBorder color.Color + + AppTitle lipgloss.Style + Help lipgloss.Style + ErrorText lipgloss.Style + SuccessText lipgloss.Style + Tab lipgloss.Style + ActiveTab lipgloss.Style + ActiveText lipgloss.Style + SuccessBadge lipgloss.Style + ErrorBadge lipgloss.Style + + FormPanel lipgloss.Style + Label lipgloss.Style + PanelTitle lipgloss.Style + FieldHint lipgloss.Style +} + +func NewTheme() Theme { + accent := lipgloss.Color("63") + return Theme{ + Accent: accent, + AccentText: lipgloss.Color("231"), + Success: lipgloss.Color("42"), + Error: lipgloss.Color("196"), + Muted: lipgloss.Color("241"), + Dim: lipgloss.Color("244"), + PanelBorder: accent, + + AppTitle: lipgloss.NewStyle().Bold(true).Foreground(accent).Padding(0, 1), + Help: lipgloss.NewStyle().Foreground(lipgloss.Color("241")), + ErrorText: lipgloss.NewStyle().Foreground(lipgloss.Color("196")), + SuccessText: lipgloss.NewStyle().Foreground(lipgloss.Color("42")), + Tab: lipgloss.NewStyle().Foreground(lipgloss.Color("246")).Padding(0, 2), + ActiveTab: lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("231")).Padding(0, 2), + ActiveText: lipgloss.NewStyle().Bold(true).Foreground(accent), + SuccessBadge: lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("232")).Background(lipgloss.Color("42")).Padding(0, 1), + ErrorBadge: lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("231")).Background(lipgloss.Color("196")).Padding(0, 1), + + FormPanel: lipgloss.NewStyle().Border(lipgloss.RoundedBorder()).BorderForeground(accent).Padding(1, 2).MarginTop(1), + Label: lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("250")), + PanelTitle: lipgloss.NewStyle().Bold(true).Foreground(accent), + FieldHint: lipgloss.NewStyle().Foreground(lipgloss.Color("244")), + } +} + +func (t Theme) ActiveTabStyle() lipgloss.Style { + return t.ActiveTab.Background(t.Accent) +} + +func (t Theme) InputStyles() textinput.Styles { + s := textinput.DefaultDarkStyles() + s.Focused.Prompt = t.ActiveText + s.Focused.Text = lipgloss.NewStyle().Foreground(lipgloss.Color("252")) + s.Focused.Placeholder = lipgloss.NewStyle().Foreground(t.Dim) + s.Blurred.Prompt = t.Help + s.Blurred.Text = lipgloss.NewStyle().Foreground(lipgloss.Color("245")) + s.Blurred.Placeholder = lipgloss.NewStyle().Foreground(lipgloss.Color("240")) + + return s +} + +func (t Theme) TextareaStyles() textarea.Styles { + s := textarea.DefaultDarkStyles() + s.Focused.Prompt = t.ActiveText + s.Focused.Text = lipgloss.NewStyle().Foreground(lipgloss.Color("252")) + s.Focused.CursorLine = lipgloss.NewStyle() + s.Focused.Placeholder = lipgloss.NewStyle().Foreground(t.Dim) + s.Blurred.Prompt = t.Help + s.Blurred.Text = lipgloss.NewStyle().Foreground(lipgloss.Color("245")) + s.Blurred.CursorLine = lipgloss.NewStyle() + s.Blurred.Placeholder = lipgloss.NewStyle().Foreground(lipgloss.Color("240")) + + return s +} + +var theme = NewTheme() -- 2.51.2