Merge pull request 'feat(tui): theme system with 5 curated dark palettes' (#24) from feat/themes into main
Reviewed-on: lerko/uptime#24
This commit was merged in pull request #24.
This commit is contained in:
@@ -319,7 +319,7 @@ func (m *Model) initAlertHuhForm() tea.Cmd {
|
||||
).Title("Gotify Settings").WithHideFunc(func() bool {
|
||||
return m.alertFormData.AlertType != "gotify"
|
||||
}),
|
||||
).WithTheme(huh.ThemeDracula())
|
||||
).WithTheme(m.theme.HuhTheme())
|
||||
|
||||
return m.huhForm.Init()
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
)
|
||||
|
||||
var maintStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#bb9af7"))
|
||||
var maintStyle lipgloss.Style
|
||||
|
||||
type maintFormData struct {
|
||||
Title string
|
||||
@@ -187,7 +187,7 @@ func (m *Model) initMaintHuhForm() tea.Cmd {
|
||||
).Title("Duration").WithHideFunc(func() bool {
|
||||
return m.maintFormData.Type == "incident"
|
||||
}),
|
||||
).WithTheme(huh.ThemeDracula())
|
||||
).WithTheme(m.theme.HuhTheme())
|
||||
|
||||
return m.huhForm.Init()
|
||||
}
|
||||
|
||||
@@ -37,10 +37,7 @@ func typeIcon(siteType string, collapsed bool) string {
|
||||
}
|
||||
}
|
||||
|
||||
var siteGroupStyle = lipgloss.NewStyle().
|
||||
Padding(0, 1).
|
||||
Bold(true).
|
||||
Foreground(lipgloss.Color("#7D56F4"))
|
||||
var siteGroupStyle lipgloss.Style
|
||||
|
||||
type siteFormData struct {
|
||||
Name string
|
||||
@@ -340,7 +337,7 @@ func (m Model) viewSitesTab() string {
|
||||
if len(m.sites) == 0 {
|
||||
welcome := lipgloss.NewStyle().
|
||||
Border(lipgloss.RoundedBorder()).
|
||||
BorderForeground(lipgloss.Color("#7D56F4")).
|
||||
BorderForeground(m.theme.Accent).
|
||||
Padding(1, 3).
|
||||
Render(
|
||||
titleStyle.Render("Go-Upkeep") + "\n\n" +
|
||||
@@ -651,7 +648,7 @@ func (m *Model) initSiteHuhForm() tea.Cmd {
|
||||
).Title("Advanced").WithHideFunc(func() bool {
|
||||
return m.siteFormData.SiteType == "group"
|
||||
}),
|
||||
).WithTheme(huh.ThemeDracula())
|
||||
).WithTheme(m.theme.HuhTheme())
|
||||
|
||||
return m.huhForm.Init()
|
||||
}
|
||||
|
||||
@@ -94,7 +94,7 @@ func (m *Model) initUserHuhForm() tea.Cmd {
|
||||
huh.NewOption("Admin", "admin"),
|
||||
).Value(&m.userFormData.Role),
|
||||
).Title("SSH Access"),
|
||||
).WithTheme(huh.ThemeDracula())
|
||||
).WithTheme(m.theme.HuhTheme())
|
||||
|
||||
return m.huhForm.Init()
|
||||
}
|
||||
|
||||
@@ -6,25 +6,11 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
tableHeaderStyle = lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#7D56F4")).
|
||||
Bold(true).
|
||||
Padding(0, 1)
|
||||
|
||||
tableCellStyle = lipgloss.NewStyle().Padding(0, 1)
|
||||
|
||||
tableSelectedStyle = lipgloss.NewStyle().
|
||||
Padding(0, 1).
|
||||
Bold(true).
|
||||
Foreground(lipgloss.Color("#ffffff")).
|
||||
Background(lipgloss.Color("#3b3b5c"))
|
||||
|
||||
tableBorderStyle = lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#444"))
|
||||
|
||||
tableZebraStyle = lipgloss.NewStyle().
|
||||
Padding(0, 1).
|
||||
Background(lipgloss.Color("#1a1a2e"))
|
||||
tableHeaderStyle lipgloss.Style
|
||||
tableCellStyle lipgloss.Style
|
||||
tableSelectedStyle lipgloss.Style
|
||||
tableBorderStyle lipgloss.Style
|
||||
tableZebraStyle lipgloss.Style
|
||||
)
|
||||
|
||||
type StyleOverride func(row, col int) *lipgloss.Style
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
package tui
|
||||
|
||||
import (
|
||||
"github.com/charmbracelet/huh"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
)
|
||||
|
||||
type Theme struct {
|
||||
Name string
|
||||
|
||||
// Base layers
|
||||
Bg lipgloss.Color
|
||||
Surface lipgloss.Color
|
||||
Panel lipgloss.Color
|
||||
Border lipgloss.Color
|
||||
|
||||
// Text
|
||||
Fg lipgloss.Color
|
||||
Muted lipgloss.Color
|
||||
Subtle lipgloss.Color
|
||||
|
||||
// Semantic
|
||||
Success lipgloss.Color
|
||||
Warning lipgloss.Color
|
||||
Danger lipgloss.Color
|
||||
Info lipgloss.Color
|
||||
Accent lipgloss.Color
|
||||
Purple lipgloss.Color
|
||||
|
||||
// Table
|
||||
ZebraBg lipgloss.Color
|
||||
|
||||
// Selection
|
||||
SelectedFg lipgloss.Color
|
||||
SelectedBg lipgloss.Color
|
||||
}
|
||||
|
||||
var themes = []Theme{
|
||||
themeFlexokiDark,
|
||||
themeTokyoNight,
|
||||
themeCatppuccinMocha,
|
||||
themeNord,
|
||||
themeGruvbox,
|
||||
}
|
||||
|
||||
var themeFlexokiDark = Theme{
|
||||
Name: "Flexoki Dark",
|
||||
Bg: "#1C1B1A",
|
||||
Surface: "#282726",
|
||||
Panel: "#343331",
|
||||
Border: "#575653",
|
||||
Fg: "#CECDC3",
|
||||
Muted: "#878580",
|
||||
Subtle: "#6F6E69",
|
||||
Success: "#879A39",
|
||||
Warning: "#D0A215",
|
||||
Danger: "#D14D41",
|
||||
Info: "#4385BE",
|
||||
Accent: "#3AA99F",
|
||||
Purple: "#8B7EC8",
|
||||
ZebraBg: "#222120",
|
||||
SelectedFg: "#FFFCF0",
|
||||
SelectedBg: "#403E3C",
|
||||
}
|
||||
|
||||
var themeTokyoNight = Theme{
|
||||
Name: "Tokyo Night",
|
||||
Bg: "#1a1b26",
|
||||
Surface: "#24283b",
|
||||
Panel: "#292e42",
|
||||
Border: "#3b4261",
|
||||
Fg: "#c0caf5",
|
||||
Muted: "#a9b1d6",
|
||||
Subtle: "#565f89",
|
||||
Success: "#9ece6a",
|
||||
Warning: "#e0af68",
|
||||
Danger: "#f7768e",
|
||||
Info: "#7aa2f7",
|
||||
Accent: "#7dcfff",
|
||||
Purple: "#bb9af7",
|
||||
ZebraBg: "#1c1d28",
|
||||
SelectedFg: "#c0caf5",
|
||||
SelectedBg: "#292e42",
|
||||
}
|
||||
|
||||
var themeGruvbox = Theme{
|
||||
Name: "Gruvbox",
|
||||
Bg: "#282828",
|
||||
Surface: "#3c3836",
|
||||
Panel: "#504945",
|
||||
Border: "#665c54",
|
||||
Fg: "#ebdbb2",
|
||||
Muted: "#bdae93",
|
||||
Subtle: "#7c6f64",
|
||||
Success: "#b8bb26",
|
||||
Warning: "#fabd2f",
|
||||
Danger: "#fb4934",
|
||||
Info: "#83a598",
|
||||
Accent: "#8ec07c",
|
||||
Purple: "#d3869b",
|
||||
ZebraBg: "#2a2a2a",
|
||||
SelectedFg: "#fbf1c7",
|
||||
SelectedBg: "#504945",
|
||||
}
|
||||
|
||||
var themeCatppuccinMocha = Theme{
|
||||
Name: "Catppuccin Mocha",
|
||||
Bg: "#1e1e2e",
|
||||
Surface: "#313244",
|
||||
Panel: "#45475a",
|
||||
Border: "#585b70",
|
||||
Fg: "#cdd6f4",
|
||||
Muted: "#a6adc8",
|
||||
Subtle: "#6c7086",
|
||||
Success: "#a6e3a1",
|
||||
Warning: "#f9e2af",
|
||||
Danger: "#f38ba8",
|
||||
Info: "#89b4fa",
|
||||
Accent: "#94e2d5",
|
||||
Purple: "#cba6f7",
|
||||
ZebraBg: "#232334",
|
||||
SelectedFg: "#cdd6f4",
|
||||
SelectedBg: "#45475a",
|
||||
}
|
||||
|
||||
var themeNord = Theme{
|
||||
Name: "Nord",
|
||||
Bg: "#2e3440",
|
||||
Surface: "#3b4252",
|
||||
Panel: "#434c5e",
|
||||
Border: "#4c566a",
|
||||
Fg: "#d8dee9",
|
||||
Muted: "#d8dee9",
|
||||
Subtle: "#4c566a",
|
||||
Success: "#a3be8c",
|
||||
Warning: "#ebcb8b",
|
||||
Danger: "#bf616a",
|
||||
Info: "#81a1c1",
|
||||
Accent: "#88c0d0",
|
||||
Purple: "#b48ead",
|
||||
ZebraBg: "#323845",
|
||||
SelectedFg: "#eceff4",
|
||||
SelectedBg: "#434c5e",
|
||||
}
|
||||
|
||||
func (t Theme) HuhTheme() *huh.Theme {
|
||||
ht := huh.ThemeBase()
|
||||
|
||||
ht.Focused.Base = ht.Focused.Base.BorderForeground(t.Border)
|
||||
ht.Focused.Card = ht.Focused.Base
|
||||
ht.Focused.Title = ht.Focused.Title.Foreground(t.Accent).Bold(true)
|
||||
ht.Focused.NoteTitle = ht.Focused.NoteTitle.Foreground(t.Accent).Bold(true).MarginBottom(1)
|
||||
ht.Focused.Description = ht.Focused.Description.Foreground(t.Muted)
|
||||
ht.Focused.ErrorIndicator = ht.Focused.ErrorIndicator.Foreground(t.Danger)
|
||||
ht.Focused.ErrorMessage = ht.Focused.ErrorMessage.Foreground(t.Danger)
|
||||
ht.Focused.SelectSelector = ht.Focused.SelectSelector.Foreground(t.Purple)
|
||||
ht.Focused.NextIndicator = ht.Focused.NextIndicator.Foreground(t.Purple)
|
||||
ht.Focused.PrevIndicator = ht.Focused.PrevIndicator.Foreground(t.Purple)
|
||||
ht.Focused.Option = ht.Focused.Option.Foreground(t.Fg)
|
||||
ht.Focused.MultiSelectSelector = ht.Focused.MultiSelectSelector.Foreground(t.Purple)
|
||||
ht.Focused.SelectedOption = ht.Focused.SelectedOption.Foreground(t.Success)
|
||||
ht.Focused.SelectedPrefix = lipgloss.NewStyle().Foreground(t.Success).SetString("✓ ")
|
||||
ht.Focused.UnselectedPrefix = lipgloss.NewStyle().Foreground(t.Subtle).SetString("• ")
|
||||
ht.Focused.UnselectedOption = ht.Focused.UnselectedOption.Foreground(t.Fg)
|
||||
ht.Focused.FocusedButton = ht.Focused.FocusedButton.Foreground(t.Bg).Background(t.Accent)
|
||||
ht.Focused.Next = ht.Focused.FocusedButton
|
||||
ht.Focused.BlurredButton = ht.Focused.BlurredButton.Foreground(t.Fg).Background(t.Surface)
|
||||
ht.Focused.TextInput.Cursor = ht.Focused.TextInput.Cursor.Foreground(t.Accent)
|
||||
ht.Focused.TextInput.Placeholder = ht.Focused.TextInput.Placeholder.Foreground(t.Subtle)
|
||||
ht.Focused.TextInput.Prompt = ht.Focused.TextInput.Prompt.Foreground(t.Purple)
|
||||
|
||||
ht.Blurred = ht.Focused
|
||||
ht.Blurred.Base = ht.Focused.Base.BorderStyle(lipgloss.HiddenBorder())
|
||||
ht.Blurred.Card = ht.Blurred.Base
|
||||
ht.Blurred.NextIndicator = lipgloss.NewStyle()
|
||||
ht.Blurred.PrevIndicator = lipgloss.NewStyle()
|
||||
|
||||
ht.Group.Title = ht.Focused.Title
|
||||
ht.Group.Description = ht.Focused.Description
|
||||
|
||||
return ht
|
||||
}
|
||||
|
||||
func themeByName(name string) Theme {
|
||||
for _, t := range themes {
|
||||
if t.Name == name {
|
||||
return t
|
||||
}
|
||||
}
|
||||
return themes[0]
|
||||
}
|
||||
+54
-14
@@ -20,16 +20,34 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
subtleStyle = lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "#9ca0b0", Dark: "#565f89"})
|
||||
specialStyle = lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "#43BF6D", Dark: "#73F59F"})
|
||||
warnStyle = lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "#F0E442", Dark: "#F0E442"})
|
||||
dangerStyle = lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "#F25D94", Dark: "#F25D94"})
|
||||
titleStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#7D56F4")).Bold(true)
|
||||
|
||||
activeTab = lipgloss.NewStyle().Border(lipgloss.NormalBorder(), false, false, true, false).BorderForeground(lipgloss.Color("#7D56F4")).Foreground(lipgloss.Color("#7D56F4")).Bold(true).Padding(0, 1)
|
||||
inactiveTab = lipgloss.NewStyle().Padding(0, 1).Foreground(lipgloss.AdaptiveColor{Light: "#AAA", Dark: "#555"})
|
||||
subtleStyle lipgloss.Style
|
||||
specialStyle lipgloss.Style
|
||||
warnStyle lipgloss.Style
|
||||
dangerStyle lipgloss.Style
|
||||
titleStyle lipgloss.Style
|
||||
activeTab lipgloss.Style
|
||||
inactiveTab lipgloss.Style
|
||||
)
|
||||
|
||||
func applyTheme(t Theme) {
|
||||
subtleStyle = lipgloss.NewStyle().Foreground(t.Subtle)
|
||||
specialStyle = lipgloss.NewStyle().Foreground(t.Success)
|
||||
warnStyle = lipgloss.NewStyle().Foreground(t.Warning)
|
||||
dangerStyle = lipgloss.NewStyle().Foreground(t.Danger)
|
||||
titleStyle = lipgloss.NewStyle().Foreground(t.Accent).Bold(true)
|
||||
activeTab = lipgloss.NewStyle().Border(lipgloss.NormalBorder(), false, false, true, false).BorderForeground(t.Accent).Foreground(t.Accent).Bold(true).Padding(0, 1)
|
||||
inactiveTab = lipgloss.NewStyle().Padding(0, 1).Foreground(t.Muted)
|
||||
|
||||
tableHeaderStyle = lipgloss.NewStyle().Foreground(t.Accent).Bold(true).Padding(0, 1)
|
||||
tableCellStyle = lipgloss.NewStyle().Padding(0, 1)
|
||||
tableSelectedStyle = lipgloss.NewStyle().Padding(0, 1).Bold(true).Foreground(t.SelectedFg).Background(t.SelectedBg)
|
||||
tableBorderStyle = lipgloss.NewStyle().Foreground(t.Border)
|
||||
tableZebraStyle = lipgloss.NewStyle().Padding(0, 1).Background(t.ZebraBg)
|
||||
|
||||
siteGroupStyle = lipgloss.NewStyle().Padding(0, 1).Bold(true).Foreground(t.Accent)
|
||||
maintStyle = lipgloss.NewStyle().Foreground(t.Purple)
|
||||
}
|
||||
|
||||
var pulseFrames = []string{"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"}
|
||||
|
||||
const (
|
||||
@@ -84,6 +102,8 @@ type Model struct {
|
||||
collapsed map[int]bool
|
||||
store store.Store
|
||||
engine *monitor.Engine
|
||||
theme Theme
|
||||
themeIndex int
|
||||
|
||||
// harmonica animation state
|
||||
pulseSpring harmonica.Spring
|
||||
@@ -107,6 +127,19 @@ func InitialModel(isAdmin bool, s store.Store, eng *monitor.Engine) Model {
|
||||
z := zone.New()
|
||||
spring := harmonica.NewSpring(harmonica.FPS(10), 6.0, 0.4)
|
||||
collapsed := loadCollapsed(s)
|
||||
|
||||
themeName, _ := s.GetPreference("theme")
|
||||
theme := themeByName(themeName)
|
||||
themeIdx := 0
|
||||
for i, t := range themes {
|
||||
if t.Name == theme.Name {
|
||||
themeIdx = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
applyTheme(theme)
|
||||
|
||||
return Model{
|
||||
state: stateDashboard,
|
||||
logViewport: vpLogs,
|
||||
@@ -117,6 +150,8 @@ func InitialModel(isAdmin bool, s store.Store, eng *monitor.Engine) Model {
|
||||
zones: z,
|
||||
pulseSpring: spring,
|
||||
collapsed: collapsed,
|
||||
theme: theme,
|
||||
themeIndex: themeIdx,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -458,6 +493,11 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
m.refreshData()
|
||||
}
|
||||
}
|
||||
case "T":
|
||||
m.themeIndex = (m.themeIndex + 1) % len(themes)
|
||||
m.theme = themes[m.themeIndex]
|
||||
applyTheme(m.theme)
|
||||
_ = m.store.SetPreference("theme", m.theme.Name)
|
||||
case "d", "backspace":
|
||||
if m.currentTab == 0 && len(m.sites) > 0 {
|
||||
m.deleteID = m.sites[m.cursor].ID
|
||||
@@ -723,7 +763,7 @@ func (m Model) View() string {
|
||||
hint := subtleStyle.Render("[y] Confirm [n] Cancel")
|
||||
box := lipgloss.NewStyle().
|
||||
Border(lipgloss.RoundedBorder()).
|
||||
BorderForeground(lipgloss.Color("#F25D94")).
|
||||
BorderForeground(m.theme.Danger).
|
||||
Padding(1, 3).
|
||||
Render(msg + "\n\n" + hint)
|
||||
return lipgloss.NewStyle().Padding(2, 4).Render(box)
|
||||
@@ -875,19 +915,19 @@ func (m Model) viewDashboard() string {
|
||||
|
||||
var footer string
|
||||
if m.filterMode {
|
||||
cursor := lipgloss.NewStyle().Foreground(lipgloss.Color("#7D56F4")).Render("│")
|
||||
cursor := lipgloss.NewStyle().Foreground(m.theme.Accent).Render("│")
|
||||
footer = "\n" + titleStyle.Render("/") + " " + m.filterText + cursor + " " + subtleStyle.Render("[Enter]Apply [Esc]Clear")
|
||||
} else {
|
||||
var keys string
|
||||
switch m.currentTab {
|
||||
case 0:
|
||||
keys = "[/]Filter [n]New [e]Edit [i]Info [d]Del [p]Pause [Tab]Switch [q]Quit"
|
||||
keys = "[/]Filter [n]New [e]Edit [i]Info [d]Del [p]Pause [T]Theme [Tab]Switch [q]Quit"
|
||||
case 4:
|
||||
keys = "[n]New [x]End [d]Del [Tab]Switch [q]Quit"
|
||||
keys = "[n]New [x]End [d]Del [T]Theme [Tab]Switch [q]Quit"
|
||||
case 5:
|
||||
keys = "[n]Add [d]Revoke [Tab]Switch [q]Quit"
|
||||
keys = "[n]Add [d]Revoke [T]Theme [Tab]Switch [q]Quit"
|
||||
default:
|
||||
keys = "[Tab]Switch [q]Quit"
|
||||
keys = "[T]Theme [Tab]Switch [q]Quit"
|
||||
}
|
||||
footer = "\n" + statusLine + " " + subtleStyle.Render(keys)
|
||||
if m.filterText != "" && m.currentTab == 0 {
|
||||
|
||||
Reference in New Issue
Block a user