summaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
authorPaul Bütow <1224732+snonux@users.noreply.github.com>2025-06-21 22:53:35 +0300
committerPaul Bütow <1224732+snonux@users.noreply.github.com>2025-06-21 22:53:35 +0300
commit03c1f3095af847a2177f791a36c1864fef01825a (patch)
tree64f8e41d8f54dc6bbcd74e7897f4157e786d8586 /internal
parent2234198b6f603240ed81f7656449cc6a319de701 (diff)
make colors configurable and add theme hotkeys
Diffstat (limited to 'internal')
-rw-r--r--internal/atable/table.go19
-rw-r--r--internal/ui/table.go92
-rw-r--r--internal/ui/theme.go106
3 files changed, 167 insertions, 50 deletions
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
+}