diff --git a/internal/tui/detail.go b/internal/tui/detail.go index 2130ddd..80a2d55 100644 --- a/internal/tui/detail.go +++ b/internal/tui/detail.go @@ -115,7 +115,7 @@ func (d detailModel) previewView(width int) string { bodyWidth = 20 } r, _ := glamour.NewTermRenderer( - glamour.WithStylePath("dark"), + glamour.WithStylePath(glamourStyle()), glamour.WithWordWrap(bodyWidth), ) rendered, err := r.Render(e.Body) diff --git a/internal/tui/help.go b/internal/tui/help.go index ef3bd18..a5e3958 100644 --- a/internal/tui/help.go +++ b/internal/tui/help.go @@ -33,6 +33,7 @@ func renderHelp(width, height int) string { {"2", "cards view"}, {"s", "cycle sort (cards)"}, {"i", "cycle intent (cards)"}, + {"T", "cycle theme"}, }}, {"Actions", [][2]string{ {"d", "delete (with confirm)"}, diff --git a/internal/tui/keys.go b/internal/tui/keys.go index e68b7be..a0d1723 100644 --- a/internal/tui/keys.go +++ b/internal/tui/keys.go @@ -34,6 +34,7 @@ type keyMap struct { Tab key.Binding ToggleRail key.Binding Stumble key.Binding + Theme key.Binding } var keys = keyMap{ @@ -68,4 +69,5 @@ var keys = keyMap{ 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")), Stumble: key.NewBinding(key.WithKeys("S"), key.WithHelp("S", "stumble")), + Theme: key.NewBinding(key.WithKeys("T"), key.WithHelp("T", "theme")), } diff --git a/internal/tui/model.go b/internal/tui/model.go index 5e51c8e..abdf619 100644 --- a/internal/tui/model.go +++ b/internal/tui/model.go @@ -105,6 +105,8 @@ type model struct { } func newModel(store *db.Store) model { + loadTheme() + applyTheme() inp := newInputModel() inp.ti.Focus() return model{ @@ -565,6 +567,10 @@ func (m model) updateBrowse(msg tea.KeyMsg) (tea.Model, tea.Cmd) { } return m, nil + case "T": + t := cycleTheme() + return m, m.setStatus("theme: " + t.Name) + case "i": if m.mode == modeCards && m.state == stateList { m.cards.setIntent(m.cards.intent.next()) diff --git a/internal/tui/stumble.go b/internal/tui/stumble.go index e35eaf4..d897bdb 100644 --- a/internal/tui/stumble.go +++ b/internal/tui/stumble.go @@ -111,7 +111,7 @@ func (s stumbleModel) view() string { bodyWidth = 20 } r, _ := glamour.NewTermRenderer( - glamour.WithStylePath("dark"), + glamour.WithStylePath(glamourStyle()), glamour.WithWordWrap(bodyWidth), ) rendered, err := r.Render(e.Body) diff --git a/internal/tui/styles.go b/internal/tui/styles.go index 637e0f5..f744ca2 100644 --- a/internal/tui/styles.go +++ b/internal/tui/styles.go @@ -3,147 +3,97 @@ package tui import "github.com/charmbracelet/lipgloss" var ( - subtle = lipgloss.AdaptiveColor{Light: "#D9DCCF", Dark: "#383838"} - highlight = lipgloss.AdaptiveColor{Light: "#874BFD", Dark: "#7D56F4"} - dim = lipgloss.AdaptiveColor{Light: "#A49FA5", Dark: "#777777"} - - titleStyle = lipgloss.NewStyle(). - Bold(true). - Foreground(highlight). - PaddingLeft(1) - - statusStyle = lipgloss.NewStyle(). - Foreground(dim). - PaddingLeft(1) - - listItemStyle = lipgloss.NewStyle(). - PaddingLeft(4) - - selectedItemStyle = lipgloss.NewStyle(). - PaddingLeft(1). - Bold(true). - Foreground(highlight). - SetString("›") - - glyphStyle = lipgloss.NewStyle(). - Width(2) - - completedGlyphStyle = lipgloss.NewStyle(). - Width(2). - Foreground(dim) - - tagStyle = lipgloss.NewStyle(). - Foreground(lipgloss.AdaptiveColor{Light: "#43BF6D", Dark: "#73F59F"}) - - idStyle = lipgloss.NewStyle(). - Foreground(dim) - - inputPromptStyle = lipgloss.NewStyle(). - Foreground(highlight). - Bold(true) - - 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"}) + titleStyle lipgloss.Style + statusStyle lipgloss.Style + listItemStyle lipgloss.Style + selectedItemStyle lipgloss.Style + glyphStyle lipgloss.Style + completedGlyphStyle lipgloss.Style + tagStyle lipgloss.Style + idStyle lipgloss.Style + inputPromptStyle lipgloss.Style + detailHeaderStyle lipgloss.Style + detailBodyStyle lipgloss.Style + helpStyle lipgloss.Style + errorStyle lipgloss.Style + dateHeaderStyle lipgloss.Style + pinnedStyle lipgloss.Style + filterPillStyle lipgloss.Style + helpKeyStyle lipgloss.Style + helpDescStyle lipgloss.Style + affordanceStyle lipgloss.Style + useCountStyle lipgloss.Style + modeStyle lipgloss.Style + detailLabelStyle lipgloss.Style + detailValueStyle lipgloss.Style + checkDoneStyle lipgloss.Style + checkPendingStyle lipgloss.Style + searchPillStyle lipgloss.Style + gutterStyle lipgloss.Style + drawerBorderStyle lipgloss.Style + drawerHintsStyle lipgloss.Style + drawerPreviewStyle lipgloss.Style + separatorStyle lipgloss.Style + hintKeyStyle lipgloss.Style + hintDescStyle lipgloss.Style + railHeaderStyle lipgloss.Style + railTagStyle lipgloss.Style + railActiveTagStyle lipgloss.Style + railCountStyle lipgloss.Style + stumbleAgeStyle lipgloss.Style ) + +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) +} diff --git a/internal/tui/tagrail.go b/internal/tui/tagrail.go index 3ddbe80..ac5097b 100644 --- a/internal/tui/tagrail.go +++ b/internal/tui/tagrail.go @@ -4,6 +4,7 @@ import ( "fmt" "strings" + "github.com/charmbracelet/lipgloss" "github.com/lerko/nib/internal/db" ) @@ -78,7 +79,7 @@ func (r tagRailModel) view(focused bool) string { headerStyle := railHeaderStyle if focused { - headerStyle = headerStyle.Foreground(highlight) + headerStyle = headerStyle.Foreground(lipgloss.Color(activeTheme().Accent)) } b.WriteString(headerStyle.Render("tags")) b.WriteString("\n") diff --git a/internal/tui/theme.go b/internal/tui/theme.go new file mode 100644 index 0000000..6c24cd1 --- /dev/null +++ b/internal/tui/theme.go @@ -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) +}