feat(tui): add 13 preloaded themes matching web design system
Port all web CSS token themes to TUI via shared vocabulary (accent, dim, muted, ok, todo, event, remind, danger). Styles rebuild from active theme on switch. Press T to cycle, persists to ~/.nib/theme. Glamour markdown renderer respects light/dark per theme.
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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)"},
|
||||
|
||||
@@ -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")),
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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)
|
||||
|
||||
+93
-143
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user