Switch the theme live
Widgets copy their colors when they are created, so they now get role tokens (one unique RGB value per role) instead of colors, and a wrapper around the tcell screen replaces the tokens with the current theme's colors in SetContent. Changing the theme in the Config view swaps that table; the next draw recolors everything. Poster colors that equal a token are nudged off it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019reK4DUGYqDUjniWYJvwUW
This commit is contained in:
+3
-1
@@ -3,11 +3,13 @@
|
|||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
- **Dark and light themes:** fixed colors that look the same in every terminal (`ui.theme`: `auto`,
|
- **Dark and light themes:** fixed colors that look the same in every terminal (`ui.theme`: `auto`,
|
||||||
`dark` or `light`, also in the Config view; applies at the next start). Before, the panes used
|
`dark` or `light`, also in the Config view, where a change recolors everything at once). Before, the panes used
|
||||||
color 0 of the terminal's palette, which color themes often set to a grey that is not their
|
color 0 of the terminal's palette, which color themes often set to a grey that is not their
|
||||||
background (e.g. Catppuccin), so the program looked washed out, and on light themes it was a dark
|
background (e.g. Catppuccin), so the program looked washed out, and on light themes it was a dark
|
||||||
grey slab with white text. `auto` asks the terminal for its background color and picks light or
|
grey slab with white text. `auto` asks the terminal for its background color and picks light or
|
||||||
dark to match; the Log says which and why.
|
dark to match; the Log says which and why.
|
||||||
|
- **Space presses buttons** (Save, Revert, the buttons of dialogs), as the help says; before only
|
||||||
|
Enter did, except for Test.
|
||||||
- **Smooth scrolling on huge terminals:** while an arrow key is held, the poster waits until the
|
- **Smooth scrolling on huge terminals:** while an arrow key is held, the poster waits until the
|
||||||
selection rests (120 ms). Every poster rewrites all of its cells, which on a terminal zoomed far
|
selection rests (120 ms). Every poster rewrites all of its cells, which on a terminal zoomed far
|
||||||
out was more output per step than the terminal could draw, so moving through a list lagged.
|
out was more output per step than the terminal could draw, so moving through a list lagged.
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ A terminal UI to search for movies and series with [Radarr](https://radarr.video
|
|||||||
- **Config (F6)**: edit everything in the config file from inside the program, in one list you move
|
- **Config (F6)**: edit everything in the config file from inside the program, in one list you move
|
||||||
through with the arrow keys: service addresses and API keys, the add-form defaults, refresh interval, posters, theme, notifications. *Test*
|
through with the arrow keys: service addresses and API keys, the add-form defaults, refresh interval, posters, theme, notifications. *Test*
|
||||||
checks the values you typed before saving; *Save* writes the file (permissions kept, `0600` for a
|
checks the values you typed before saving; *Save* writes the file (permissions kept, `0600` for a
|
||||||
new file) and applies the changes at once (the theme at the next start). Values that come from `ARR_TUI_*`
|
new file) and applies the changes at once, no restart. Values that come from `ARR_TUI_*`
|
||||||
environment variables are greyed out and are never written to the file.
|
environment variables are greyed out and are never written to the file.
|
||||||
- **Log (F7)**: actions report through popups that fade away; the log keeps the history.
|
- **Log (F7)**: actions report through popups that fade away; the log keeps the history.
|
||||||
- **Download notifications**: a popup (and a log entry) when a SABnzbd download finishes or fails,
|
- **Download notifications**: a popup (and a log entry) when a SABnzbd download finishes or fails,
|
||||||
@@ -39,8 +39,8 @@ A terminal UI to search for movies and series with [Radarr](https://radarr.video
|
|||||||
`"bell": true` also rings the terminal bell.
|
`"bell": true` also rings the terminal bell.
|
||||||
- **Themes**: fixed dark and light color sets that look the same in every terminal, whatever its
|
- **Themes**: fixed dark and light color sets that look the same in every terminal, whatever its
|
||||||
color theme. `"ui": {"theme": ...}` is `auto` (default: light when the terminal reports a light
|
color theme. `"ui": {"theme": ...}` is `auto` (default: light when the terminal reports a light
|
||||||
background, else dark), `dark` or `light`; it can also be set in the Config view and applies at the
|
background, else dark), `dark` or `light`; it can also be switched in the Config view, which
|
||||||
next start.
|
recolors everything at once.
|
||||||
- Connection indicators for all three services in the top right; `F9` shows every key.
|
- Connection indicators for all three services in the top right; `F9` shows every key.
|
||||||
|
|
||||||
Requires Radarr/Sonarr v3+ API (Radarr 5/6, Sonarr 4) and SABnzbd 4+. Only Usenet releases are
|
Requires Radarr/Sonarr v3+ API (Radarr 5/6, Sonarr 4) and SABnzbd 4+. Only Usenet releases are
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ Layout sketch (Library view; the other views follow the same frame):
|
|||||||
|
|
||||||
Popups for action feedback appear in the bottom right and fade away; the Log view (F7) keeps the history.
|
Popups for action feedback appear in the bottom right and fade away; the Log view (F7) keeps the history.
|
||||||
|
|
||||||
Colors: two fixed themes, dark (near-black background, like k9s) and light, chosen with `ui.theme`: `auto` (default) asks the terminal for its background color at startup (OSC 11, in the same round trip as the graphics query) and takes light for a background with luminance above 0.5, dark otherwise or without an answer. The terminal's palette is not used: color themes often make color 0 ("black", tview's default background) a grey that differs from their background (Catppuccin Mocha: `#45475a` on `#1e1e2e`; Latte: `#5c5f77` on `#eff1f5`), which looked washed out, and on light themes dark grey slabs carried white text. A theme sets tview's default styles (before any widget is created; widgets copy them, so a changed theme applies at the next start) and overrides the color names used in texts in tcell's table (`[yellow]` is the theme's yellow, `white` its text and `black` its background, so the active tab `[black:white]` follows too). Every theme color is an entry 16-255 of the 256-color table, so terminals without 24-bit color show it exactly, and never one of the first 16 entries that color themes change. The Log says which theme is used and why.
|
Colors: two fixed themes, dark (near-black background, like k9s) and light, chosen with `ui.theme`: `auto` (default) asks the terminal for its background color at startup (OSC 11, in the same round trip as the graphics query) and takes light for a background with luminance above 0.5, dark otherwise or without an answer. The terminal's palette is not used: color themes often make color 0 ("black", tview's default background) a grey that differs from their background (Catppuccin Mocha: `#45475a` on `#1e1e2e`; Latte: `#5c5f77` on `#eff1f5`), which looked washed out, and on light themes dark grey slabs carried white text. Themes switch while the program runs: widgets copy tview's default styles when they are created, so instead of colors they get role tokens, one unique RGB value per role (background, text, border, field, yellow, ...; `0xa517nn`), set as tview's defaults before any widget exists and as the color names used in texts in tcell's table (`[yellow]` is the yellow role, `white` text and `black` background, so the active tab `[black:white]` follows too). The tcell screen is wrapped (`themedScreen`), and every `SetContent` replaces role tokens with the current theme's colors; tview clears and redraws the whole screen each frame, so switching the theme is a change of that table and one draw. Other colors pass unchanged: poster cells (a poster color that equals a token is nudged by one step) and the image-id colors of real-image placeholders. Every theme color is an entry 16-255 of the 256-color table, so terminals without 24-bit color show it exactly, and never one of the first 16 entries that color themes change. The Log says which theme is used and why.
|
||||||
|
|
||||||
tview primitives: `Pages` (views and stacked overlays), `Flex` (layout), `InputField` (search), `List`/`Table` (results, queues, library), `TextView` (details, log), `Form` (add dialog), `Modal` (confirmations). Popups are drawn by a wrapper around the root primitive.
|
tview primitives: `Pages` (views and stacked overlays), `Flex` (layout), `InputField` (search), `List`/`Table` (results, queues, library), `TextView` (details, log), `Form` (add dialog), `Modal` (confirmations). Popups are drawn by a wrapper around the root primitive.
|
||||||
|
|
||||||
|
|||||||
+26
-1
@@ -30,6 +30,8 @@ type view struct {
|
|||||||
// App is the top-level arr-tui application.
|
// App is the top-level arr-tui application.
|
||||||
type App struct {
|
type App struct {
|
||||||
app *tview.Application
|
app *tview.Application
|
||||||
|
theme theme // the colors in use
|
||||||
|
themed *themedScreen // draws with them; nil until Run
|
||||||
tabs *tview.TextView
|
tabs *tview.TextView
|
||||||
pages *tview.Pages
|
pages *tview.Pages
|
||||||
status *tview.TextView
|
status *tview.TextView
|
||||||
@@ -103,7 +105,7 @@ func New(cfg *config.Config) *App {
|
|||||||
// Ask the terminal before the screen takes it over, and set the colors
|
// Ask the terminal before the screen takes it over, and set the colors
|
||||||
// before any widget copies them.
|
// before any widget copies them.
|
||||||
th, themeNote := chooseTheme(cfg.UI.Theme, terminalAnswers())
|
th, themeNote := chooseTheme(cfg.UI.Theme, terminalAnswers())
|
||||||
th.apply()
|
useTokens()
|
||||||
a := &App{
|
a := &App{
|
||||||
app: tview.NewApplication(),
|
app: tview.NewApplication(),
|
||||||
tabs: tview.NewTextView().SetDynamicColors(true),
|
tabs: tview.NewTextView().SetDynamicColors(true),
|
||||||
@@ -140,6 +142,7 @@ func New(cfg *config.Config) *App {
|
|||||||
if a.covers {
|
if a.covers {
|
||||||
a.logs.add(levelInfo, coverNote)
|
a.logs.add(levelInfo, coverNote)
|
||||||
}
|
}
|
||||||
|
a.theme = th
|
||||||
a.logs.add(levelInfo, themeNote)
|
a.logs.add(levelInfo, themeNote)
|
||||||
sabClient := sabnzbd.NewClient(cfg.SABnzbd.URL, cfg.SABnzbd.APIKey)
|
sabClient := sabnzbd.NewClient(cfg.SABnzbd.URL, cfg.SABnzbd.APIKey)
|
||||||
a.sab = sabClient
|
a.sab = sabClient
|
||||||
@@ -394,12 +397,34 @@ func (a *App) Run() error {
|
|||||||
a.gfx.deleteAll()
|
a.gfx.deleteAll()
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
// The screen draws through the theme (see themedScreen).
|
||||||
|
s, err := tcell.NewScreen()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
a.themed = newThemedScreen(s, a.theme)
|
||||||
|
a.app.SetScreen(a.themed) // initializes it
|
||||||
|
if a.themed.initErr != nil {
|
||||||
|
return a.themed.initErr
|
||||||
|
}
|
||||||
|
a.themed.DisableMouse()
|
||||||
|
a.themed.DisablePaste()
|
||||||
|
|
||||||
go a.watchServices(stop)
|
go a.watchServices(stop)
|
||||||
a.setNotifyDownloads(a.notifyDone)
|
a.setNotifyDownloads(a.notifyDone)
|
||||||
defer a.setNotifyDownloads(false)
|
defer a.setNotifyDownloads(false)
|
||||||
return a.app.Run()
|
return a.app.Run()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// setTheme switches the colors while the program runs.
|
||||||
|
func (a *App) setTheme(t theme, note string) {
|
||||||
|
a.theme = t
|
||||||
|
if a.themed != nil {
|
||||||
|
a.themed.setTheme(t)
|
||||||
|
}
|
||||||
|
a.logs.add(levelInfo, note)
|
||||||
|
}
|
||||||
|
|
||||||
// setNotifyDownloads starts or stops the background watch for finished downloads.
|
// setNotifyDownloads starts or stops the background watch for finished downloads.
|
||||||
func (a *App) setNotifyDownloads(on bool) {
|
func (a *App) setNotifyDownloads(on bool) {
|
||||||
a.notifyDone = on
|
a.notifyDone = on
|
||||||
|
|||||||
@@ -35,6 +35,9 @@ func (a *App) applyConfig(c *config.Config) {
|
|||||||
a.downloads.setInterval(time.Duration(c.UI.RefreshSeconds) * time.Second)
|
a.downloads.setInterval(time.Duration(c.UI.RefreshSeconds) * time.Second)
|
||||||
}
|
}
|
||||||
a.bell = c.UI.Bell
|
a.bell = c.UI.Bell
|
||||||
|
if old.UI.Theme != c.UI.Theme {
|
||||||
|
a.setTheme(chooseTheme(c.UI.Theme, terminalAnswers()))
|
||||||
|
}
|
||||||
if c.UI.ShouldNotifyDownloads() != a.notifyDone {
|
if c.UI.ShouldNotifyDownloads() != a.notifyDone {
|
||||||
a.setNotifyDownloads(c.UI.ShouldNotifyDownloads())
|
a.setNotifyDownloads(c.UI.ShouldNotifyDownloads())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -458,8 +458,8 @@ func (c *coverView) build(cols, rows int) []styledCell {
|
|||||||
}
|
}
|
||||||
ch, fg, bg := st.bestCell(px)
|
ch, fg, bg := st.bestCell(px)
|
||||||
cells = append(cells, styledCell{ch,
|
cells = append(cells, styledCell{ch,
|
||||||
tcell.NewRGBColor(int32(fg.R), int32(fg.G), int32(fg.B)),
|
notToken(tcell.NewRGBColor(int32(fg.R), int32(fg.G), int32(fg.B))),
|
||||||
tcell.NewRGBColor(int32(bg.R), int32(bg.G), int32(bg.B))})
|
notToken(tcell.NewRGBColor(int32(bg.R), int32(bg.G), int32(bg.B)))})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return cells
|
return cells
|
||||||
|
|||||||
+1
-1
@@ -71,7 +71,7 @@ var helpSections = []helpSection{
|
|||||||
{"Tab", "jump between the fields and Save / Revert"},
|
{"Tab", "jump between the fields and Save / Revert"},
|
||||||
{"Space", "toggle a checkbox; press a button"},
|
{"Space", "toggle a checkbox; press a button"},
|
||||||
{"Test", "(button below the SABnzbd settings) checks the three services with the values typed in"},
|
{"Test", "(button below the SABnzbd settings) checks the three services with the values typed in"},
|
||||||
{"Save, Revert", "(at the bottom) Save writes the file and applies everything at once (the theme at the next start); Revert discards edits"},
|
{"Save, Revert", "(at the bottom) Save writes the file and applies everything at once; Revert discards edits"},
|
||||||
}},
|
}},
|
||||||
{title: "Log (F7)", note: "Popups fade away by themselves; everything they said is kept here.", items: []helpItem{
|
{title: "Log (F7)", note: "Popups fade away by themselves; everything they said is kept here.", items: []helpItem{
|
||||||
{"↑↓, PgUp, PgDn", "scroll"},
|
{"↑↓, PgUp, PgDn", "scroll"},
|
||||||
|
|||||||
@@ -144,7 +144,7 @@ func (r *toastRoot) Draw(screen tcell.Screen) {
|
|||||||
if t.expires.Sub(now) < toastFade {
|
if t.expires.Sub(now) < toastFade {
|
||||||
fg = named("gray") // fade out
|
fg = named("gray") // fade out
|
||||||
}
|
}
|
||||||
style := tcell.StyleDefault.Background(current.toast).Foreground(fg)
|
style := tcell.StyleDefault.Background(tokens.toast).Foreground(fg)
|
||||||
for cx := x; cx < x+width; cx++ {
|
for cx := x; cx < x+width; cx++ {
|
||||||
screen.SetContent(cx, y, ' ', nil, style)
|
screen.SetContent(cx, y, ' ', nil, style)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -108,8 +108,8 @@ func boolSetting(old *bool, value, def bool) *bool {
|
|||||||
return &value
|
return &value
|
||||||
}
|
}
|
||||||
|
|
||||||
// settingsView edits the configuration file's settings. Everything except
|
// settingsView edits the configuration file's settings. Everything applies
|
||||||
// the theme applies right after saving; the theme at the next start.
|
// right after saving.
|
||||||
type settingsView struct {
|
type settingsView struct {
|
||||||
app *App
|
app *App
|
||||||
root *tview.Flex
|
root *tview.Flex
|
||||||
@@ -184,7 +184,7 @@ func newSettingsView(app *App) *settingsView {
|
|||||||
f.AddFormItem(v.covers)
|
f.AddFormItem(v.covers)
|
||||||
v.style = tview.NewDropDown().SetLabel("Poster style").SetOptions(coverStyles, func(string, int) { v.edited() })
|
v.style = tview.NewDropDown().SetLabel("Poster style").SetOptions(coverStyles, func(string, int) { v.edited() })
|
||||||
f.AddFormItem(v.style)
|
f.AddFormItem(v.style)
|
||||||
v.theme = tview.NewDropDown().SetLabel("Theme (at the next start)").SetOptions(themes, func(string, int) { v.edited() })
|
v.theme = tview.NewDropDown().SetLabel("Theme").SetOptions(themes, func(string, int) { v.edited() })
|
||||||
f.AddFormItem(v.theme)
|
f.AddFormItem(v.theme)
|
||||||
v.notify = tview.NewCheckbox().SetLabel("Notify about finished downloads").SetChangedFunc(func(bool) { v.edited() })
|
v.notify = tview.NewCheckbox().SetLabel("Notify about finished downloads").SetChangedFunc(func(bool) { v.edited() })
|
||||||
f.AddFormItem(v.notify)
|
f.AddFormItem(v.notify)
|
||||||
@@ -347,7 +347,7 @@ func (v *settingsView) describe() {
|
|||||||
} else {
|
} else {
|
||||||
b.WriteString("Config file: " + tview.Escape(v.app.cfgPath) + "\n")
|
b.WriteString("Config file: " + tview.Escape(v.app.cfgPath) + "\n")
|
||||||
}
|
}
|
||||||
b.WriteString("Changes take effect when you save; only the theme waits for the next start.\n")
|
b.WriteString("Changes take effect when you save; nothing needs a restart.\n")
|
||||||
if len(v.overridden) > 0 {
|
if len(v.overridden) > 0 {
|
||||||
var names []string
|
var names []string
|
||||||
for _, env := range v.overridden {
|
for _, env := range v.overridden {
|
||||||
@@ -437,13 +437,9 @@ func (v *settingsView) save() {
|
|||||||
v.report("[red]Saved, but not applied: " + tview.Escape(err.Error()) + "[-]")
|
v.report("[red]Saved, but not applied: " + tview.Escape(err.Error()) + "[-]")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
themeChanged := effective.UI.Theme != v.app.cfg.UI.Theme
|
|
||||||
v.app.cfgFile = &raw
|
v.app.cfgFile = &raw
|
||||||
v.app.applyConfig(effective)
|
v.app.applyConfig(effective)
|
||||||
v.dirty = false
|
v.dirty = false
|
||||||
v.report("[green]Saved to " + tview.Escape(v.app.cfgPath) + " and applied.[-]")
|
v.report("[green]Saved to " + tview.Escape(v.app.cfgPath) + " and applied.[-]")
|
||||||
if themeChanged {
|
|
||||||
v.report("The new theme is used from the next start.")
|
|
||||||
}
|
|
||||||
v.app.Notify(levelSuccess, "Settings saved")
|
v.app.Notify(levelSuccess, "Settings saved")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,29 +51,65 @@ func TestChooseTheme(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestThemeApply(t *testing.T) {
|
func TestUseTokens(t *testing.T) {
|
||||||
oldStyles, oldCurrent := tview.Styles, current
|
oldStyles := tview.Styles
|
||||||
oldNames := map[string]tcell.Color{}
|
oldNames := map[string]tcell.Color{}
|
||||||
for k, v := range tcell.ColorNames {
|
for k, v := range tcell.ColorNames {
|
||||||
oldNames[k] = v
|
oldNames[k] = v
|
||||||
}
|
}
|
||||||
defer func() {
|
defer func() {
|
||||||
tview.Styles, current = oldStyles, oldCurrent
|
tview.Styles = oldStyles
|
||||||
for k, v := range oldNames {
|
for k, v := range oldNames {
|
||||||
tcell.ColorNames[k] = v
|
tcell.ColorNames[k] = v
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
lightTheme.apply()
|
useTokens()
|
||||||
if tview.Styles.PrimitiveBackgroundColor != lightTheme.bg || tview.Styles.PrimaryTextColor != lightTheme.text {
|
if tview.Styles.PrimitiveBackgroundColor != tokens.bg || tview.Styles.PrimaryTextColor != tokens.text {
|
||||||
t.Error("tview styles not set")
|
t.Error("tview styles not set")
|
||||||
}
|
}
|
||||||
// Color names in texts mean the theme's colors.
|
// Color names in texts mean the roles.
|
||||||
if named("yellow") != lightTheme.yellow || tcell.GetColor("black") != lightTheme.bg || tcell.GetColor("white") != lightTheme.text {
|
if named("yellow") != tokens.yellow || tcell.GetColor("black") != tokens.bg || tcell.GetColor("white") != tokens.text {
|
||||||
t.Error("color names not set")
|
t.Error("color names not set")
|
||||||
}
|
}
|
||||||
if current.name != "light" {
|
}
|
||||||
t.Error("current theme not set")
|
|
||||||
|
func TestThemedScreenSwitchesLive(t *testing.T) {
|
||||||
|
sim := tcell.NewSimulationScreen("")
|
||||||
|
if err := sim.Init(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer sim.Fini()
|
||||||
|
sim.SetSize(4, 1)
|
||||||
|
s := newThemedScreen(sim, darkTheme)
|
||||||
|
poster := tcell.NewRGBColor(10, 20, 30)
|
||||||
|
|
||||||
|
draw := func() {
|
||||||
|
s.SetContent(0, 0, 'a', nil, tcell.StyleDefault.Foreground(tokens.yellow).Background(tokens.bg))
|
||||||
|
s.SetContent(1, 0, 'b', nil, tcell.StyleDefault.Foreground(poster).Background(poster))
|
||||||
|
}
|
||||||
|
check := func(th theme) {
|
||||||
|
_, _, st, _ := sim.GetContent(0, 0)
|
||||||
|
if fg, bg, _ := st.Decompose(); fg != th.yellow || bg != th.bg {
|
||||||
|
t.Errorf("%s: role colors drawn as %v on %v", th.name, fg, bg)
|
||||||
|
}
|
||||||
|
_, _, st, _ = sim.GetContent(1, 0)
|
||||||
|
if fg, bg, _ := st.Decompose(); fg != poster || bg != poster {
|
||||||
|
t.Errorf("%s: other colors changed to %v on %v", th.name, fg, bg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
draw()
|
||||||
|
check(darkTheme)
|
||||||
|
s.setTheme(lightTheme)
|
||||||
|
draw()
|
||||||
|
check(lightTheme)
|
||||||
|
|
||||||
|
// A poster color that equals a role color is nudged off it.
|
||||||
|
if c := notToken(tokens.red); c == tokens.red {
|
||||||
|
t.Error("role color in a picture not nudged")
|
||||||
|
}
|
||||||
|
if notToken(poster) != poster {
|
||||||
|
t.Error("ordinary color changed")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+82
-11
@@ -46,9 +46,6 @@ var (
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// current is the theme in use.
|
|
||||||
var current = darkTheme
|
|
||||||
|
|
||||||
// chooseTheme resolves the ui.theme setting: "dark", "light", or "auto" (the
|
// chooseTheme resolves the ui.theme setting: "dark", "light", or "auto" (the
|
||||||
// default), which follows the background color the terminal reported and is
|
// default), which follows the background color the terminal reported and is
|
||||||
// dark when it did not say. The second value explains the choice for the log.
|
// dark when it did not say. The second value explains the choice for the log.
|
||||||
@@ -69,13 +66,34 @@ func chooseTheme(setting string, info terminalInfo) (theme, string) {
|
|||||||
return darkTheme, "Theme: dark (auto: the terminal's background " + hex + " is dark)"
|
return darkTheme, "Theme: dark (auto: the terminal's background " + hex + " is dark)"
|
||||||
}
|
}
|
||||||
|
|
||||||
// apply makes t the colors of everything created afterwards: tview's default
|
// colors lists the theme's colors in a fixed order of roles.
|
||||||
// styles (widgets copy them when they are created, so this must run before any
|
func (t theme) colors() []tcell.Color {
|
||||||
// is) and the color names used in the texts, which tview looks up in tcell's
|
return []tcell.Color{t.bg, t.text, t.border, t.field, t.fieldMuted, t.list, t.pressed, t.toast,
|
||||||
// table: "[yellow]" means this theme's yellow, "white" its text color and
|
t.gray, t.yellow, t.green, t.red, t.blue, t.purple}
|
||||||
// "black" its background (the active tab is "[black:white]").
|
}
|
||||||
func (t theme) apply() {
|
|
||||||
current = t
|
// tokens is the theme the widgets are built with: every role has a color of
|
||||||
|
// its own that no theme uses, and themedScreen replaces it with the color of
|
||||||
|
// the current theme while drawing. That is how the theme changes while the
|
||||||
|
// program runs: widgets copy their colors when they are created, but what they
|
||||||
|
// copy is a role, not a color.
|
||||||
|
var tokens = func() theme {
|
||||||
|
var c [14]tcell.Color
|
||||||
|
for i := range c {
|
||||||
|
c[i] = tcell.NewRGBColor(0xa5, 0x17, int32(i))
|
||||||
|
}
|
||||||
|
return theme{name: "tokens", bg: c[0], text: c[1], border: c[2], field: c[3], fieldMuted: c[4], list: c[5], pressed: c[6], toast: c[7],
|
||||||
|
gray: c[8], yellow: c[9], green: c[10], red: c[11], blue: c[12], purple: c[13]}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// useTokens makes the role colors the defaults of everything created
|
||||||
|
// afterwards: tview's default styles (widgets copy them when they are created,
|
||||||
|
// so this must run before any is) and the color names used in the texts,
|
||||||
|
// which tview looks up in tcell's table: "[yellow]" is the theme's yellow,
|
||||||
|
// "white" its text color and "black" its background (the active tab is
|
||||||
|
// "[black:white]").
|
||||||
|
func useTokens() {
|
||||||
|
t := tokens
|
||||||
st := &tview.Styles
|
st := &tview.Styles
|
||||||
st.PrimitiveBackgroundColor = t.bg
|
st.PrimitiveBackgroundColor = t.bg
|
||||||
st.ContrastBackgroundColor = t.field
|
st.ContrastBackgroundColor = t.field
|
||||||
@@ -97,7 +115,60 @@ func (t theme) apply() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// named returns the theme's color for a name used in the texts.
|
// themedScreen draws with the current theme: it replaces the role colors of
|
||||||
|
// everything set on the screen by the theme's colors. Other colors (posters,
|
||||||
|
// the image ids of real-image posters) pass unchanged.
|
||||||
|
type themedScreen struct {
|
||||||
|
tcell.Screen
|
||||||
|
theme theme
|
||||||
|
colors map[tcell.Color]tcell.Color
|
||||||
|
initErr error
|
||||||
|
}
|
||||||
|
|
||||||
|
func newThemedScreen(s tcell.Screen, t theme) *themedScreen {
|
||||||
|
ts := &themedScreen{Screen: s}
|
||||||
|
ts.setTheme(t)
|
||||||
|
return ts
|
||||||
|
}
|
||||||
|
|
||||||
|
// setTheme switches the colors; the next draw shows them.
|
||||||
|
func (s *themedScreen) setTheme(t theme) {
|
||||||
|
s.theme = t
|
||||||
|
s.colors = map[tcell.Color]tcell.Color{}
|
||||||
|
actual := t.colors()
|
||||||
|
for i, tok := range tokens.colors() {
|
||||||
|
s.colors[tok] = actual[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *themedScreen) SetContent(x, y int, primary rune, combining []rune, style tcell.Style) {
|
||||||
|
fg, bg, _ := style.Decompose()
|
||||||
|
if c, ok := s.colors[fg]; ok {
|
||||||
|
style = style.Foreground(c)
|
||||||
|
}
|
||||||
|
if c, ok := s.colors[bg]; ok {
|
||||||
|
style = style.Background(c)
|
||||||
|
}
|
||||||
|
s.Screen.SetContent(x, y, primary, combining, style)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init remembers the error, which tview's SetScreen drops.
|
||||||
|
func (s *themedScreen) Init() error {
|
||||||
|
s.initErr = s.Screen.Init()
|
||||||
|
return s.initErr
|
||||||
|
}
|
||||||
|
|
||||||
|
// notToken nudges a picture's color that happens to equal a role color, so
|
||||||
|
// themedScreen does not replace it.
|
||||||
|
func notToken(c tcell.Color) tcell.Color {
|
||||||
|
r, g, b := c.RGB()
|
||||||
|
if r == 0xa5 && g == 0x17 && b < int32(len(tokens.colors())) {
|
||||||
|
return tcell.NewRGBColor(r, g+1, b)
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// named returns the role color for a name used in the texts.
|
||||||
func named(name string) tcell.Color { return tcell.GetColor(name) }
|
func named(name string) tcell.Color { return tcell.GetColor(name) }
|
||||||
|
|
||||||
func hexColor(c color.RGBA) string {
|
func hexColor(c color.RGBA) string {
|
||||||
|
|||||||
Reference in New Issue
Block a user