Merge pull request 'feat(tui): add 13 preloaded themes matching web design system' (#39) from feat/tui-theme into main

Reviewed-on: #39
This commit was merged in pull request #39.
This commit is contained in:
2026-05-21 00:27:49 +00:00
8 changed files with 196 additions and 146 deletions
+1 -1
View File
@@ -115,7 +115,7 @@ func (d detailModel) previewView(width int) string {
bodyWidth = 20 bodyWidth = 20
} }
r, _ := glamour.NewTermRenderer( r, _ := glamour.NewTermRenderer(
glamour.WithStylePath("dark"), glamour.WithStylePath(glamourStyle()),
glamour.WithWordWrap(bodyWidth), glamour.WithWordWrap(bodyWidth),
) )
rendered, err := r.Render(e.Body) rendered, err := r.Render(e.Body)
+1
View File
@@ -33,6 +33,7 @@ func renderHelp(width, height int) string {
{"2", "cards view"}, {"2", "cards view"},
{"s", "cycle sort (cards)"}, {"s", "cycle sort (cards)"},
{"i", "cycle intent (cards)"}, {"i", "cycle intent (cards)"},
{"T", "cycle theme"},
}}, }},
{"Actions", [][2]string{ {"Actions", [][2]string{
{"d", "delete (with confirm)"}, {"d", "delete (with confirm)"},
+2
View File
@@ -34,6 +34,7 @@ type keyMap struct {
Tab key.Binding Tab key.Binding
ToggleRail key.Binding ToggleRail key.Binding
Stumble key.Binding Stumble key.Binding
Theme key.Binding
} }
var keys = keyMap{ var keys = keyMap{
@@ -68,4 +69,5 @@ var keys = keyMap{
Tab: key.NewBinding(key.WithKeys("tab"), key.WithHelp("tab", "focus cycle")), Tab: key.NewBinding(key.WithKeys("tab"), key.WithHelp("tab", "focus cycle")),
ToggleRail: key.NewBinding(key.WithKeys("ctrl+b"), key.WithHelp("ctrl+b", "toggle tag rail")), ToggleRail: key.NewBinding(key.WithKeys("ctrl+b"), key.WithHelp("ctrl+b", "toggle tag rail")),
Stumble: key.NewBinding(key.WithKeys("S"), key.WithHelp("S", "stumble")), Stumble: key.NewBinding(key.WithKeys("S"), key.WithHelp("S", "stumble")),
Theme: key.NewBinding(key.WithKeys("T"), key.WithHelp("T", "theme")),
} }
+6
View File
@@ -105,6 +105,8 @@ type model struct {
} }
func newModel(store *db.Store) model { func newModel(store *db.Store) model {
loadTheme()
applyTheme()
inp := newInputModel() inp := newInputModel()
inp.ti.Focus() inp.ti.Focus()
return model{ return model{
@@ -565,6 +567,10 @@ func (m model) updateBrowse(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
} }
return m, nil return m, nil
case "T":
t := cycleTheme()
return m, m.setStatus("theme: " + t.Name)
case "i": case "i":
if m.mode == modeCards && m.state == stateList { if m.mode == modeCards && m.state == stateList {
m.cards.setIntent(m.cards.intent.next()) m.cards.setIntent(m.cards.intent.next())
+1 -1
View File
@@ -111,7 +111,7 @@ func (s stumbleModel) view() string {
bodyWidth = 20 bodyWidth = 20
} }
r, _ := glamour.NewTermRenderer( r, _ := glamour.NewTermRenderer(
glamour.WithStylePath("dark"), glamour.WithStylePath(glamourStyle()),
glamour.WithWordWrap(bodyWidth), glamour.WithWordWrap(bodyWidth),
) )
rendered, err := r.Render(e.Body) rendered, err := r.Render(e.Body)
+93 -143
View File
@@ -3,147 +3,97 @@ package tui
import "github.com/charmbracelet/lipgloss" import "github.com/charmbracelet/lipgloss"
var ( var (
subtle = lipgloss.AdaptiveColor{Light: "#D9DCCF", Dark: "#383838"} titleStyle lipgloss.Style
highlight = lipgloss.AdaptiveColor{Light: "#874BFD", Dark: "#7D56F4"} statusStyle lipgloss.Style
dim = lipgloss.AdaptiveColor{Light: "#A49FA5", Dark: "#777777"} listItemStyle lipgloss.Style
selectedItemStyle lipgloss.Style
titleStyle = lipgloss.NewStyle(). glyphStyle lipgloss.Style
Bold(true). completedGlyphStyle lipgloss.Style
Foreground(highlight). tagStyle lipgloss.Style
PaddingLeft(1) idStyle lipgloss.Style
inputPromptStyle lipgloss.Style
statusStyle = lipgloss.NewStyle(). detailHeaderStyle lipgloss.Style
Foreground(dim). detailBodyStyle lipgloss.Style
PaddingLeft(1) helpStyle lipgloss.Style
errorStyle lipgloss.Style
listItemStyle = lipgloss.NewStyle(). dateHeaderStyle lipgloss.Style
PaddingLeft(4) pinnedStyle lipgloss.Style
filterPillStyle lipgloss.Style
selectedItemStyle = lipgloss.NewStyle(). helpKeyStyle lipgloss.Style
PaddingLeft(1). helpDescStyle lipgloss.Style
Bold(true). affordanceStyle lipgloss.Style
Foreground(highlight). useCountStyle lipgloss.Style
SetString("") modeStyle lipgloss.Style
detailLabelStyle lipgloss.Style
glyphStyle = lipgloss.NewStyle(). detailValueStyle lipgloss.Style
Width(2) checkDoneStyle lipgloss.Style
checkPendingStyle lipgloss.Style
completedGlyphStyle = lipgloss.NewStyle(). searchPillStyle lipgloss.Style
Width(2). gutterStyle lipgloss.Style
Foreground(dim) drawerBorderStyle lipgloss.Style
drawerHintsStyle lipgloss.Style
tagStyle = lipgloss.NewStyle(). drawerPreviewStyle lipgloss.Style
Foreground(lipgloss.AdaptiveColor{Light: "#43BF6D", Dark: "#73F59F"}) separatorStyle lipgloss.Style
hintKeyStyle lipgloss.Style
idStyle = lipgloss.NewStyle(). hintDescStyle lipgloss.Style
Foreground(dim) railHeaderStyle lipgloss.Style
railTagStyle lipgloss.Style
inputPromptStyle = lipgloss.NewStyle(). railActiveTagStyle lipgloss.Style
Foreground(highlight). railCountStyle lipgloss.Style
Bold(true) stumbleAgeStyle lipgloss.Style
detailHeaderStyle = lipgloss.NewStyle().
Bold(true).
Foreground(highlight).
MarginBottom(1)
detailBodyStyle = lipgloss.NewStyle().
PaddingLeft(2).
PaddingTop(1)
helpStyle = lipgloss.NewStyle().
Foreground(dim).
PaddingLeft(1)
errorStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("#FF0000")).
PaddingLeft(1)
dateHeaderStyle = lipgloss.NewStyle().
Foreground(dim).
PaddingLeft(1)
pinnedStyle = lipgloss.NewStyle().
Foreground(lipgloss.AdaptiveColor{Light: "#D4A017", Dark: "#FFD700"})
filterPillStyle = lipgloss.NewStyle().
Foreground(lipgloss.AdaptiveColor{Light: "#43BF6D", Dark: "#73F59F"}).
Bold(true)
helpKeyStyle = lipgloss.NewStyle().
Foreground(highlight).
Bold(true).
Width(18)
helpDescStyle = lipgloss.NewStyle().
Foreground(dim)
affordanceStyle = lipgloss.NewStyle().
Foreground(lipgloss.AdaptiveColor{Light: "#5B8EF0", Dark: "#7AAFFF"}).
Bold(true)
useCountStyle = lipgloss.NewStyle().
Foreground(lipgloss.AdaptiveColor{Light: "#B07D3A", Dark: "#D4A54A"})
modeStyle = lipgloss.NewStyle().
Foreground(dim).
Bold(true)
detailLabelStyle = lipgloss.NewStyle().
Foreground(highlight).
Bold(true)
detailValueStyle = lipgloss.NewStyle().
Foreground(lipgloss.AdaptiveColor{Light: "#555555", Dark: "#BBBBBB"})
checkDoneStyle = lipgloss.NewStyle().
Foreground(lipgloss.AdaptiveColor{Light: "#43BF6D", Dark: "#73F59F"})
checkPendingStyle = lipgloss.NewStyle().
Foreground(dim)
searchPillStyle = lipgloss.NewStyle().
Foreground(lipgloss.AdaptiveColor{Light: "#E06C75", Dark: "#E06C75"}).
Bold(true)
gutterStyle = lipgloss.NewStyle().
Foreground(dim)
drawerBorderStyle = lipgloss.NewStyle().
Foreground(dim)
drawerHintsStyle = lipgloss.NewStyle().
Foreground(dim).
PaddingLeft(2)
drawerPreviewStyle = lipgloss.NewStyle().
Foreground(lipgloss.AdaptiveColor{Light: "#555555", Dark: "#AAAAAA"}).
PaddingLeft(2)
separatorStyle = lipgloss.NewStyle().
Foreground(dim)
hintKeyStyle = lipgloss.NewStyle().
Foreground(highlight).
Bold(true)
hintDescStyle = lipgloss.NewStyle().
Foreground(dim)
railHeaderStyle = lipgloss.NewStyle().
Bold(true).
Foreground(dim)
railTagStyle = lipgloss.NewStyle().
Foreground(lipgloss.AdaptiveColor{Light: "#43BF6D", Dark: "#73F59F"})
railActiveTagStyle = lipgloss.NewStyle().
Foreground(lipgloss.AdaptiveColor{Light: "#43BF6D", Dark: "#73F59F"}).
Bold(true)
railCountStyle = lipgloss.NewStyle().
Foreground(dim)
stumbleAgeStyle = lipgloss.NewStyle().
Foreground(lipgloss.AdaptiveColor{Light: "#cc4400", Dark: "#fab387"})
) )
func init() {
applyTheme()
}
func applyTheme() {
t := activeTheme()
accent := lipgloss.Color(t.Accent)
dim := lipgloss.Color(t.Dim)
muted := lipgloss.Color(t.Muted)
ok := lipgloss.Color(t.Ok)
todo := lipgloss.Color(t.Todo)
event := lipgloss.Color(t.Event)
remind := lipgloss.Color(t.Remind)
danger := lipgloss.Color(t.Danger)
titleStyle = lipgloss.NewStyle().Bold(true).Foreground(accent).PaddingLeft(1)
statusStyle = lipgloss.NewStyle().Foreground(dim).PaddingLeft(1)
listItemStyle = lipgloss.NewStyle().PaddingLeft(4)
selectedItemStyle = lipgloss.NewStyle().PaddingLeft(1).Bold(true).Foreground(accent).SetString("")
glyphStyle = lipgloss.NewStyle().Width(2)
completedGlyphStyle = lipgloss.NewStyle().Width(2).Foreground(dim)
tagStyle = lipgloss.NewStyle().Foreground(ok)
idStyle = lipgloss.NewStyle().Foreground(dim)
inputPromptStyle = lipgloss.NewStyle().Foreground(accent).Bold(true)
detailHeaderStyle = lipgloss.NewStyle().Bold(true).Foreground(accent).MarginBottom(1)
detailBodyStyle = lipgloss.NewStyle().PaddingLeft(2).PaddingTop(1)
helpStyle = lipgloss.NewStyle().Foreground(dim).PaddingLeft(1)
errorStyle = lipgloss.NewStyle().Foreground(danger).PaddingLeft(1)
dateHeaderStyle = lipgloss.NewStyle().Foreground(dim).PaddingLeft(1)
pinnedStyle = lipgloss.NewStyle().Foreground(todo)
filterPillStyle = lipgloss.NewStyle().Foreground(ok).Bold(true)
helpKeyStyle = lipgloss.NewStyle().Foreground(accent).Bold(true).Width(18)
helpDescStyle = lipgloss.NewStyle().Foreground(dim)
affordanceStyle = lipgloss.NewStyle().Foreground(event).Bold(true)
useCountStyle = lipgloss.NewStyle().Foreground(remind)
modeStyle = lipgloss.NewStyle().Foreground(dim).Bold(true)
detailLabelStyle = lipgloss.NewStyle().Foreground(accent).Bold(true)
detailValueStyle = lipgloss.NewStyle().Foreground(muted)
checkDoneStyle = lipgloss.NewStyle().Foreground(ok)
checkPendingStyle = lipgloss.NewStyle().Foreground(dim)
searchPillStyle = lipgloss.NewStyle().Foreground(danger).Bold(true)
gutterStyle = lipgloss.NewStyle().Foreground(dim)
drawerBorderStyle = lipgloss.NewStyle().Foreground(dim)
drawerHintsStyle = lipgloss.NewStyle().Foreground(dim).PaddingLeft(2)
drawerPreviewStyle = lipgloss.NewStyle().Foreground(muted).PaddingLeft(2)
separatorStyle = lipgloss.NewStyle().Foreground(dim)
hintKeyStyle = lipgloss.NewStyle().Foreground(accent).Bold(true)
hintDescStyle = lipgloss.NewStyle().Foreground(dim)
railHeaderStyle = lipgloss.NewStyle().Bold(true).Foreground(dim)
railTagStyle = lipgloss.NewStyle().Foreground(ok)
railActiveTagStyle = lipgloss.NewStyle().Foreground(ok).Bold(true)
railCountStyle = lipgloss.NewStyle().Foreground(dim)
stumbleAgeStyle = lipgloss.NewStyle().Foreground(remind)
}
+2 -1
View File
@@ -4,6 +4,7 @@ import (
"fmt" "fmt"
"strings" "strings"
"github.com/charmbracelet/lipgloss"
"github.com/lerko/nib/internal/db" "github.com/lerko/nib/internal/db"
) )
@@ -78,7 +79,7 @@ func (r tagRailModel) view(focused bool) string {
headerStyle := railHeaderStyle headerStyle := railHeaderStyle
if focused { if focused {
headerStyle = headerStyle.Foreground(highlight) headerStyle = headerStyle.Foreground(lipgloss.Color(activeTheme().Accent))
} }
b.WriteString(headerStyle.Render("tags")) b.WriteString(headerStyle.Render("tags"))
b.WriteString("\n") b.WriteString("\n")
+90
View File
@@ -0,0 +1,90 @@
package tui
import (
"os"
"path/filepath"
"strings"
)
type Theme struct {
Name string
Dark bool
Accent string
Dim string
Muted string
Ok string
Todo string
Event string
Remind string
Danger string
}
var themes = []Theme{
{Name: "dark", Dark: true, Accent: "#c8942a", Dim: "#504840", Muted: "#8c8070", Ok: "#7aab72", Todo: "#d4a84b", Event: "#6898c8", Remind: "#c8784a", Danger: "#b85858"},
{Name: "tinycard", Dark: true, Accent: "#ad8ee6", Dim: "#555a6a", Muted: "#8b90a0", Ok: "#4ade80", Todo: "#fbbf24", Event: "#22d3ee", Remind: "#e8845a", Danger: "#ef4444"},
{Name: "catppuccin", Dark: true, Accent: "#cba6f7", Dim: "#6c7086", Muted: "#a6adc8", Ok: "#a6e3a1", Todo: "#f9e2af", Event: "#89b4fa", Remind: "#fab387", Danger: "#f38ba8"},
{Name: "nord", Dark: true, Accent: "#88c0d0", Dim: "#4c566a", Muted: "#d8dee9", Ok: "#a3be8c", Todo: "#ebcb8b", Event: "#81a1c1", Remind: "#d08770", Danger: "#bf616a"},
{Name: "dracula", Dark: true, Accent: "#bd93f9", Dim: "#6272a4", Muted: "#bfbfbf", Ok: "#50fa7b", Todo: "#f1fa8c", Event: "#8be9fd", Remind: "#ffb86c", Danger: "#ff5555"},
{Name: "gruvbox", Dark: true, Accent: "#fabd2f", Dim: "#665c54", Muted: "#a89984", Ok: "#b8bb26", Todo: "#fabd2f", Event: "#83a598", Remind: "#fe8019", Danger: "#fb4934"},
{Name: "rosepine", Dark: true, Accent: "#c4a7e7", Dim: "#6e6a86", Muted: "#908caa", Ok: "#a6da95", Todo: "#f6c177", Event: "#31748f", Remind: "#ea9a97", Danger: "#eb6f92"},
{Name: "tokyonight", Dark: true, Accent: "#7aa2f7", Dim: "#565f89", Muted: "#a9b1d6", Ok: "#9ece6a", Todo: "#e0af68", Event: "#7aa2f7", Remind: "#ff9e64", Danger: "#f7768e"},
{Name: "solarized", Dark: true, Accent: "#268bd2", Dim: "#586e75", Muted: "#657b83", Ok: "#859900", Todo: "#b58900", Event: "#268bd2", Remind: "#cb4b16", Danger: "#dc322f"},
{Name: "paper", Dark: false, Accent: "#8a6018", Dim: "#a09080", Muted: "#6a5e50", Ok: "#2a6828", Todo: "#7a5c00", Event: "#245890", Remind: "#984020", Danger: "#882030"},
{Name: "catppuccin-latte", Dark: false, Accent: "#8839ef", Dim: "#9ca0b0", Muted: "#6c6f85", Ok: "#40a02b", Todo: "#df8e1d", Event: "#1e66f5", Remind: "#fe640b", Danger: "#d20f39"},
{Name: "rosepine-dawn", Dark: false, Accent: "#907aa9", Dim: "#9893a5", Muted: "#797593", Ok: "#56949f", Todo: "#ea9d34", Event: "#286983", Remind: "#d7827e", Danger: "#b4637a"},
{Name: "solarized-light", Dark: false, Accent: "#268bd2", Dim: "#93a1a1", Muted: "#586e75", Ok: "#859900", Todo: "#b58900", Event: "#268bd2", Remind: "#cb4b16", Danger: "#dc322f"},
}
var activeThemeIndex int
func activeTheme() Theme {
return themes[activeThemeIndex]
}
func cycleTheme() Theme {
activeThemeIndex = (activeThemeIndex + 1) % len(themes)
applyTheme()
saveTheme()
return themes[activeThemeIndex]
}
func glamourStyle() string {
if themes[activeThemeIndex].Dark {
return "dark"
}
return "light"
}
func themePath() string {
home, err := os.UserHomeDir()
if err != nil {
return ""
}
return filepath.Join(home, ".nib", "theme")
}
func loadTheme() {
p := themePath()
if p == "" {
return
}
data, err := os.ReadFile(p)
if err != nil {
return
}
name := strings.TrimSpace(string(data))
for i, t := range themes {
if t.Name == name {
activeThemeIndex = i
return
}
}
}
func saveTheme() {
p := themePath()
if p == "" {
return
}
_ = os.WriteFile(p, []byte(themes[activeThemeIndex].Name+"\n"), 0o644)
}