From 03c1f3095af847a2177f791a36c1864fef01825a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Paul=20B=C3=BCtow?= <1224732+snonux@users.noreply.github.com> Date: Sat, 21 Jun 2025 22:53:35 +0300 Subject: make colors configurable and add theme hotkeys --- internal/atable/table.go | 19 ++++----- internal/ui/table.go | 92 ++++++++++++++++++++++------------------ internal/ui/theme.go | 106 +++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 167 insertions(+), 50 deletions(-) create mode 100644 internal/ui/theme.go (limited to 'internal') diff --git a/internal/atable/table.go b/internal/atable/table.go index 528276f..c22d479 100644 --- a/internal/atable/table.go +++ b/internal/atable/table.go @@ -118,17 +118,19 @@ func DefaultKeyMap() KeyMap { // Styles contains style definitions for this list component. By default, these // values are generated by DefaultStyles. type Styles struct { - Header lipgloss.Style - Cell lipgloss.Style - Selected lipgloss.Style + Header lipgloss.Style + Cell lipgloss.Style + Selected lipgloss.Style + Highlight lipgloss.Style } // DefaultStyles returns a set of default style definitions for this table. func DefaultStyles() Styles { return Styles{ - Selected: lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("212")), - Header: lipgloss.NewStyle().Bold(true).Padding(0, 1), - Cell: lipgloss.NewStyle().Padding(0, 1), + Selected: lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("212")), + Header: lipgloss.NewStyle().Bold(true).Padding(0, 1), + Cell: lipgloss.NewStyle().Padding(0, 1), + Highlight: lipgloss.NewStyle().Background(lipgloss.Color("57")).Foreground(lipgloss.Color("0")), } } @@ -479,10 +481,7 @@ func (m *Model) renderRow(r int) string { highlightRow := r == m.cursor rowStyle := m.styles.Cell if highlightRow { - rowStyle = rowStyle. - Background(lipgloss.Color("57")). - Foreground(lipgloss.Color("0")). - Bold(false) + rowStyle = m.styles.Highlight } s := make([]string, 0, len(m.cols)) diff --git a/internal/ui/table.go b/internal/ui/table.go index affb148..f5e614a 100644 --- a/internal/ui/table.go +++ b/internal/ui/table.go @@ -85,6 +85,9 @@ type Model struct { total int inProgress int due int + + theme Theme + defaultTheme Theme } // editDoneMsg is emitted when the external editor process finishes. @@ -110,6 +113,9 @@ func New(filters []string) (Model, error) { m.searchInput = textinput.New() m.searchInput.Prompt = "search: " + m.defaultTheme = DefaultTheme() + m.theme = m.defaultTheme + if err := m.reload(); err != nil { return Model{}, err } @@ -135,11 +141,12 @@ func (m *Model) newTable(rows []atable.Row) (atable.Model, atable.Styles) { atable.WithShowHeaders(false), ) styles := atable.DefaultStyles() - styles.Header = styles.Header.Foreground(lipgloss.Color("205")) - styles.Selected = styles.Selected.Foreground(lipgloss.Color("229")).Background(lipgloss.Color("57")) styles.Cell = styles.Cell.Padding(0, 1) t.SetStyles(styles) - return t, styles + m.tbl = t + m.tblStyles = styles + m.applyTheme() + return m.tbl, m.tblStyles } func (m *Model) reload() error { @@ -505,17 +512,15 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } } case "t": - if row := m.tbl.SelectedRow(); row != nil { - idStr := ansi.Strip(row[0]) - if id, err := strconv.Atoi(idStr); err == nil { - m.tagsID = id - m.tagsEditing = true - m.tagsInput.SetValue("") - m.tagsInput.Focus() - m.updateTableHeight() - return m, nil - } - } + m.theme = RandomTheme() + m.applyTheme() + m.reload() + return m, nil + case "T": + m.theme = m.defaultTheme + m.applyTheme() + m.reload() + return m, nil case "/", "?": m.searching = true m.searchInput.SetValue("") @@ -698,8 +703,8 @@ func (m Model) View() string { func (m Model) statusLine() string { status := fmt.Sprintf("Total:%d InProgress:%d Due:%d | press H for help", m.total, m.inProgress, m.due) return lipgloss.NewStyle(). - Foreground(lipgloss.Color("229")). - Background(lipgloss.Color("57")). + Foreground(lipgloss.Color(m.theme.StatusFG)). + Background(lipgloss.Color(m.theme.StatusBG)). Width(m.tbl.Width()). Render(status) } @@ -707,8 +712,8 @@ func (m Model) statusLine() string { func (m Model) topStatusLine() string { line := fmt.Sprintf("Task Samurai %s", internal.Version) return lipgloss.NewStyle(). - Foreground(lipgloss.Color("229")). - Background(lipgloss.Color("57")). + Foreground(lipgloss.Color(m.theme.StatusFG)). + Background(lipgloss.Color(m.theme.StatusBG)). Width(m.tbl.Width()). Render(line) } @@ -716,7 +721,7 @@ func (m Model) topStatusLine() string { func (m Model) taskToRow(t task.Task) atable.Row { style := lipgloss.NewStyle() if t.Start != "" { - style = style.Background(lipgloss.Color("6")) + style = style.Background(lipgloss.Color(m.theme.StartBG)) } age := "" @@ -740,10 +745,10 @@ func (m Model) taskToRow(t task.Task) atable.Row { return atable.Row{ style.Render(strconv.Itoa(t.ID)), - formatPriority(t.Priority, m.priWidth), + m.formatPriority(t.Priority, m.priWidth), style.Render(age), style.Render(urg), - formatDue(t.Due, m.dueWidth), + m.formatDue(t.Due, m.dueWidth), style.Render(tags), style.Render(annStr), style.Render(t.Description), @@ -753,7 +758,7 @@ func (m Model) taskToRow(t task.Task) atable.Row { // formatDue returns a formatted due date string. Dates due today or tomorrow // are returned as "today" or "tomorrow" respectively. Past due dates are // highlighted in red. -func formatDue(s string, width int) string { +func (m Model) formatDue(s string, width int) string { if s == "" { return "" } @@ -776,20 +781,20 @@ func formatDue(s string, width int) string { } style := lipgloss.NewStyle().Width(width) if days < 0 { - style = style.Background(lipgloss.Color("1")) + style = style.Background(lipgloss.Color(m.theme.OverdueBG)) } return style.Render(val) } -func formatPriority(p string, width int) string { +func (m Model) formatPriority(p string, width int) string { style := lipgloss.NewStyle().Width(width) switch p { case "L": - style = style.Background(lipgloss.Color("10")) + style = style.Background(lipgloss.Color(m.theme.PrioLowBG)) case "M": - style = style.Background(lipgloss.Color("12")) + style = style.Background(lipgloss.Color(m.theme.PrioMedBG)) case "H": - style = style.Background(lipgloss.Color("9")) + style = style.Background(lipgloss.Color(m.theme.PrioHighBG)) default: return p } @@ -809,19 +814,19 @@ func (m Model) priorityView() string { } style := lipgloss.NewStyle() if i == m.priorityIndex { - style = style.Foreground(lipgloss.Color("229")).Background(lipgloss.Color("57")) + style = style.Foreground(lipgloss.Color(m.theme.SelectedFG)).Background(lipgloss.Color(m.theme.SelectedBG)) } parts = append(parts, style.Render(label)) } return "priority: " + strings.Join(parts, " ") } -func highlightCell(base lipgloss.Style, re *regexp.Regexp, raw string) string { +func (m Model) highlightCell(base lipgloss.Style, re *regexp.Regexp, raw string) string { if re == nil || !re.MatchString(raw) { return base.Render(raw) } - highlight := lipgloss.NewStyle().Background(lipgloss.Color("226")).Foreground(lipgloss.Color("21")) + highlight := lipgloss.NewStyle().Background(lipgloss.Color(m.theme.SearchBG)).Foreground(lipgloss.Color(m.theme.SearchFG)) var b strings.Builder last := 0 for _, loc := range re.FindAllStringIndex(raw, -1) { @@ -837,9 +842,9 @@ func highlightCell(base lipgloss.Style, re *regexp.Regexp, raw string) string { return b.String() } -func highlightCellMatch(base lipgloss.Style, re *regexp.Regexp, raw, display string) string { +func (m Model) highlightCellMatch(base lipgloss.Style, re *regexp.Regexp, raw, display string) string { if re != nil && re.MatchString(raw) { - highlight := lipgloss.NewStyle().Background(lipgloss.Color("226")).Foreground(lipgloss.Color("21")) + highlight := lipgloss.NewStyle().Background(lipgloss.Color(m.theme.SearchBG)).Foreground(lipgloss.Color(m.theme.SearchFG)) return highlight.Copy().Inherit(base).Render(display) } return base.Render(display) @@ -848,7 +853,7 @@ func highlightCellMatch(base lipgloss.Style, re *regexp.Regexp, raw, display str func (m Model) taskToRowSearch(t task.Task, re *regexp.Regexp, styles atable.Styles, selectedCol int) atable.Row { rowStyle := lipgloss.NewStyle() if t.Start != "" { - rowStyle = rowStyle.Background(lipgloss.Color("6")) + rowStyle = rowStyle.Background(lipgloss.Color(m.theme.StartBG)) } age := "" @@ -876,19 +881,19 @@ func (m Model) taskToRowSearch(t task.Task, re *regexp.Regexp, styles atable.Sty } idStr := getStyle(0).Render(strconv.Itoa(t.ID)) - priStr := formatPriority(t.Priority, m.priWidth) + priStr := m.formatPriority(t.Priority, m.priWidth) ageStr := getStyle(2).Render(age) - dueStr := formatDue(t.Due, m.dueWidth) + dueStr := m.formatDue(t.Due, m.dueWidth) urgStr := getStyle(3).Render(urg) - tagStr := highlightCell(getStyle(5), re, tags) + tagStr := m.highlightCell(getStyle(5), re, tags) annRaw := strings.Join(anns, "; ") annCount := "" if n := len(anns); n > 0 { annCount = strconv.FormatInt(int64(n), 16) } - annStr := highlightCellMatch(getStyle(6), re, annRaw, annCount) - descStr := highlightCell(getStyle(7), re, t.Description) + annStr := m.highlightCellMatch(getStyle(6), re, annRaw, annCount) + descStr := m.highlightCell(getStyle(7), re, t.Description) return atable.Row{ idStr, @@ -914,7 +919,7 @@ func (m Model) expandedCellView() string { case 0: val = strconv.Itoa(t.ID) case 1: - val = ansi.Strip(formatPriority(t.Priority, m.priWidth)) + val = ansi.Strip(m.formatPriority(t.Priority, m.priWidth)) case 2: if ts, err := time.Parse("20060102T150405Z", t.Entry); err == nil { days := int(time.Since(ts).Hours() / 24) @@ -923,7 +928,7 @@ func (m Model) expandedCellView() string { case 3: val = fmt.Sprintf("%.1f", t.Urgency) case 4: - val = ansi.Strip(formatDue(t.Due, m.dueWidth)) + val = ansi.Strip(m.formatDue(t.Due, m.dueWidth)) case 5: val = strings.Join(t.Tags, " ") case 6: @@ -1074,3 +1079,10 @@ func (m *Model) applyColumns() { } m.tbl.SetColumns(cols) } + +func (m *Model) applyTheme() { + m.tblStyles.Header = m.tblStyles.Header.Foreground(lipgloss.Color(m.theme.HeaderFG)) + m.tblStyles.Selected = m.tblStyles.Selected.Foreground(lipgloss.Color(m.theme.SelectedFG)).Background(lipgloss.Color(m.theme.SelectedBG)) + m.tblStyles.Highlight = m.tblStyles.Highlight.Background(lipgloss.Color(m.theme.RowBG)).Foreground(lipgloss.Color(m.theme.RowFG)) + m.tbl.SetStyles(m.tblStyles) +} diff --git a/internal/ui/theme.go b/internal/ui/theme.go new file mode 100644 index 0000000..9d81aab --- /dev/null +++ b/internal/ui/theme.go @@ -0,0 +1,106 @@ +package ui + +import ( + "math/rand" + "strconv" +) + +// Theme holds color configuration for the UI. +type Theme struct { + HeaderFG string + SelectedFG string + SelectedBG string + RowFG string + RowBG string + StatusFG string + StatusBG string + StartBG string + OverdueBG string + PrioLowBG string + PrioMedBG string + PrioHighBG string + SearchFG string + SearchBG string +} + +// DefaultTheme returns the color theme used by Task Samurai. +func DefaultTheme() Theme { + return Theme{ + HeaderFG: "205", + SelectedFG: "229", + SelectedBG: "57", + RowFG: "0", + RowBG: "57", + StatusFG: "229", + StatusBG: "57", + StartBG: "6", + OverdueBG: "1", + PrioLowBG: "10", + PrioMedBG: "12", + PrioHighBG: "9", + SearchFG: "21", + SearchBG: "226", + } +} + +func RandomTheme() Theme { + th := Theme{ + HeaderFG: randColor(), + SelectedBG: randColor(), + RowBG: randColor(), + StatusBG: randColor(), + StartBG: randColor(), + OverdueBG: randColor(), + PrioLowBG: randColor(), + PrioMedBG: randColor(), + PrioHighBG: randColor(), + SearchBG: randColor(), + } + th.SelectedFG = contrastColor(th.SelectedBG) + th.RowFG = contrastColor(th.RowBG) + th.StatusFG = contrastColor(th.StatusBG) + th.SearchFG = contrastColor(th.SearchBG) + return th +} + +func randColor() string { + return strconv.Itoa(rand.Intn(256)) +} + +func contrastColor(bg string) string { + i, err := strconv.Atoi(bg) + if err != nil { + return "0" + } + if brightness(i) > 128 { + return "0" + } + return "15" +} + +func brightness(i int) float64 { + r, g, b := xtermRGB(i) + return 0.299*float64(r) + 0.587*float64(g) + 0.114*float64(b) +} + +func xtermRGB(i int) (int, int, int) { + if i < 16 { + var table = [16][3]int{ + {0, 0, 0}, {205, 0, 0}, {0, 205, 0}, {205, 205, 0}, + {0, 0, 238}, {205, 0, 205}, {0, 205, 205}, {229, 229, 229}, + {127, 127, 127}, {255, 0, 0}, {0, 255, 0}, {255, 255, 0}, + {92, 92, 255}, {255, 0, 255}, {0, 255, 255}, {255, 255, 255}, + } + rgb := table[i] + return rgb[0], rgb[1], rgb[2] + } + if i >= 16 && i <= 231 { + i -= 16 + r := (i / 36) * 51 + g := (i % 36 / 6) * 51 + b := (i % 6) * 51 + return r, g, b + } + v := (i-232)*10 + 8 + return v, v, v +} -- cgit v1.2.3