From 56a56896c95371d7065b42c0327c3a18ae003942 Mon Sep 17 00:00:00 2001 From: makeworld Date: Tue, 28 Jul 2020 16:58:32 -0400 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Themeing=20support?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 8 ++- NOTES.md | 3 ++ README.md | 1 + cache/redir.go | 18 +++---- config/config.go | 21 ++++++++ config/default.go | 72 ++++++++++++++++++++++++- config/default.sh | 2 +- config/theme.go | 85 +++++++++++++++++++++++++++++ default-config.toml | 70 ++++++++++++++++++++++++ display/bookmarks.go | 31 +++++++---- display/display.go | 59 ++++++++++++-------- display/download.go | 48 ++++++++++------- display/help.go | 8 +-- display/modals.go | 126 ++++++++++++++++++++++++++++--------------- display/tab.go | 20 +++---- renderer/page.go | 2 +- renderer/renderer.go | 66 +++++++++++++++++------ structs/structs.go | 2 +- 18 files changed, 503 insertions(+), 139 deletions(-) create mode 100644 config/theme.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 3166ae8..0473261 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,18 +6,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added +- **Themeing** - check out [default-config.toml](./default-config.toml) for details (#46) - Tab now also enters link selecting mode, like Enter (#48) - Number keys can be pressed to navigate to links 1 through 10 (#47) - Permanent redirects are cached for the session (#22) +- `.ansi` is also supported for `text/x-ansi` files, as well as the already supported `.ans` ### Changed - Documented Ctrl-C has "Hard quit" +- Updated [cview](https://gitlab.com/tslocum/cview/) to latest commit: `cc7796c4ca44e3908f80d93e92e73694562d936a` +- The bottom bar label now uses the same color as the tabs at the top +- Tab and blue link colors were changed very slightly to be part of the 256 Xterm colors, for better terminal support ### Fixed - You can't change link selection while the page is loading - Only one request is made for each URL - `v1.3.0` accidentally made two requests each time (#50) - Using the `..` command doesn't keep the query string (#49) -- Any error that occurs when downloading a file will be displayed, and the partially download file will be deleted +- Any error that occurs when downloading a file will be displayed, and the partially downloaded file will be deleted ## [1.3.0] - 2020-07-10 @@ -34,7 +39,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Pages are rewrapped dynamically, whenever the terminal size changes (#33) - TOFU warning message mentions how long the previous cert was still valid for (#34) -- Update [cview](https://gitlab.com/tslocum/cview/) to latest commit ### Fixed - Many potential network and display race conditions eliminated diff --git a/NOTES.md b/NOTES.md index 9020111..f769c08 100644 --- a/NOTES.md +++ b/NOTES.md @@ -3,6 +3,9 @@ ## Issues - URL for each tab should not be stored as a string - in the current code there's lots of reparsing the URL - Can't go back or do other things while page is loading - need a way to stop `handleURL` +- Allow for opening a new tab while the current one is loading +- Can't leave the help window +- Can't interact with page after clicking Cancel on bkmk (and other?) modal(s) ## Upstream Bugs - Wrapping messes up on brackets diff --git a/README.md b/README.md index d9ce81e..ef2db99 100644 --- a/README.md +++ b/README.md @@ -73,6 +73,7 @@ Features in *italics* are in the master branch, but not in the latest release. - [x] Built-in search (uses GUS by default) - [x] Bookmarks - [x] Download pages and arbitrary data +- [x] *Themeing* - [ ] Search in pages with Ctrl-F - [ ] Emoji favicons - See `gemini://mozz.us/files/rfc_gemini_favicon.gmi` for details diff --git a/cache/redir.go b/cache/redir.go index 4e56fcc..0f85e46 100644 --- a/cache/redir.go +++ b/cache/redir.go @@ -7,12 +7,12 @@ import ( // Functions for caching redirects. var redirUrls = make(map[string]string) // map original URL to redirect -var redirMut = sync.RWMutex{} +var redirMu = sync.RWMutex{} // AddRedir adds a original-to-redirect pair to the cache. func AddRedir(og, redir string) { - redirMut.Lock() - defer redirMut.Unlock() + redirMu.Lock() + defer redirMu.Unlock() for k, v := range redirUrls { if og == v { @@ -32,8 +32,8 @@ func AddRedir(og, redir string) { // ClearRedirs removes all redirects from the cache. func ClearRedirs() { - redirMut.Lock() - defer redirMut.Unlock() + redirMu.Lock() + defer redirMu.Unlock() redirUrls = make(map[string]string) } @@ -41,8 +41,8 @@ func ClearRedirs() { // exists for that URL in the cache. // If one does not then the original URL is returned. func Redirect(u string) string { - redirMut.RLock() - defer redirMut.RUnlock() + redirMu.RLock() + defer redirMu.RUnlock() // A single lookup is enough, because AddRedir // removes loops and chains. @@ -54,7 +54,7 @@ func Redirect(u string) string { } func NumRedirs() int { - redirMut.RLock() - defer redirMut.RUnlock() + redirMu.RLock() + defer redirMu.RUnlock() return len(redirUrls) } diff --git a/config/config.go b/config/config.go index 8aa7ba0..a424a52 100644 --- a/config/config.go +++ b/config/config.go @@ -7,9 +7,11 @@ import ( "runtime" "strings" + "github.com/gdamore/tcell" "github.com/makeworld-the-better-one/amfora/cache" homedir "github.com/mitchellh/go-homedir" "github.com/spf13/viper" + "gitlab.com/tslocum/cview" ) var amforaAppData string // Where amfora files are stored on Windows - cached here @@ -196,5 +198,24 @@ func Init() error { cache.SetMaxSize(viper.GetInt("cache.max_size")) cache.SetMaxPages(viper.GetInt("cache.max_pages")) + // Theme + configTheme := viper.Sub("theme") + if configTheme != nil { + for k, v := range configTheme.AllSettings() { + colorStr, ok := v.(string) + if !ok { + return fmt.Errorf(`value for "%s" is not a string: %v`, k, v) + } + color := tcell.GetColor(strings.ToLower(colorStr)) + if color == tcell.ColorDefault { + return fmt.Errorf(`invalid color format for "%s": %s`, k, colorStr) + } + SetColor(k, color) + } + } + if viper.GetBool("a-general.color") { + cview.Styles.PrimitiveBackgroundColor = GetColor("bg") + } // Otherwise it's black by default + return nil } diff --git a/config/default.go b/config/default.go index 3e6bdc5..a124017 100644 --- a/config/default.go +++ b/config/default.go @@ -1,5 +1,6 @@ package config +//go:generate ./default.sh var defaultConf = []byte(`# This is the default config file. # It also shows all the default values, if you don't create the file. @@ -39,4 +40,73 @@ page_max_time = 10 # Zero values mean there is no limit max_size = 0 # Size in bytes max_pages = 30 # The maximum number of pages the cache will store -`) + +[theme] +# This section is for changing the COLORS used in Amfora. +# These colors only apply if color is enabled above. +# Colors can be set using a W3C color name, or a hex value such as #ffffff". + +# Note that not all colors will work on terminals that do not have truecolor support. +# If you want to stick to the standard 16 or 256 colors, you can get +# a list of those here: https://jonasjacek.github.io/colors/ +# Do NOT use the names from that site, just the hex codes. + +# Definitions: +# bg = background +# fg = foreground +# dl = download +# btn = button +# hdg = heading +# bkmk = bookmark +# modal = a popup window/box in the middle of the screen + +# EXAMPLES: +# hdg_1 = "green" +# hdg_2 = "#5f0000" + +# Available keys to set: + +# bg: background for pages, tab row, app in general +# tab_num: The number/highlight of the tabs at the top +# tab_divider: The color of the divider character between tab numbers: | +# bottombar_label: The color of the prompt that appears when you press space +# bottombar_text: The color of the text you type +# bottombar_bg + +# hdg_1 +# hdg_2 +# hdg_3 +# amfora_link: A link that Amfora supports viewing. For now this is only gemini:// +# foreign_link: HTTP(S), Gopher, etc +# link_number: The silver number that appears to the left of a link +# regular_text: Normal gemini text, and plaintext documents +# quote_text +# preformatted_text +# list_text + +# btn_bg: The bg color for all modal buttons +# btn_text: The text color for all modal buttons + +# dl_choice_modal_bg +# dl_choice_modal_text +# dl_modal_bg +# dl_modal_text +# info_modal_bg +# info_modal_text +# error_modal_bg +# error_modal_text +# yesno_modal_bg +# yesno_modal_text +# tofu_modal_bg +# tofu_modal_text + +# input_modal_bg +# input_modal_text +# input_modal_field_bg: The bg of the input field, where you type the text +# input_modal_field_text: The color of the text you type + +# bkmk_modal_bg +# bkmk_modal_text +# bkmk_modal_label +# bkmk_modal_field_bg +# bkmk_modal_field_text`) diff --git a/config/default.sh b/config/default.sh index df4b25f..3de772f 100755 --- a/config/default.sh +++ b/config/default.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash -head -n 1 default.go | tee default.go > /dev/null +head -n 3 default.go | tee default.go > /dev/null echo -n 'var defaultConf = []byte(`' >> default.go cat ../default-config.toml >> default.go echo '`)' >> default.go \ No newline at end of file diff --git a/config/theme.go b/config/theme.go new file mode 100644 index 0000000..6c021cc --- /dev/null +++ b/config/theme.go @@ -0,0 +1,85 @@ +package config + +import ( + "fmt" + "sync" + + "github.com/gdamore/tcell" +) + +// Functions to allow themeing configuration. +// UI element colors are mapped to a string key, such as "error" or "tab_background" +// These are the same keys used in the config file. + +var themeMu = sync.RWMutex{} +var theme = map[string]tcell.Color{ + // Default values below + + "bg": tcell.ColorBlack, // Used for cview.Styles.PrimitiveBackgroundColor + "tab_num": tcell.Color30, // xterm:Turquoise4, #008787 + "tab_divider": tcell.ColorWhite, + "bottombar_label": tcell.Color30, + "bottombar_text": tcell.ColorBlack, + "bottombar_bg": tcell.ColorWhite, + + // Modals + "btn_bg": tcell.ColorNavy, // All modal buttons + "btn_text": tcell.ColorWhite, + + "dl_choice_modal_bg": tcell.ColorPurple, + "dl_choice_modal_text": tcell.ColorWhite, + "dl_modal_bg": tcell.Color130, // xterm:DarkOrange3, #af5f00 + "dl_modal_text": tcell.ColorWhite, + "info_modal_bg": tcell.ColorGray, + "info_modal_text": tcell.ColorWhite, + "error_modal_bg": tcell.ColorMaroon, + "error_modal_text": tcell.ColorWhite, + "yesno_modal_bg": tcell.ColorPurple, + "yesno_modal_text": tcell.ColorWhite, + "tofu_modal_bg": tcell.ColorMaroon, + "tofu_modal_text": tcell.ColorWhite, + + "input_modal_bg": tcell.ColorGreen, + "input_modal_text": tcell.ColorWhite, + "input_modal_field_bg": tcell.ColorBlue, + "input_modal_field_text": tcell.ColorWhite, + + "bkmk_modal_bg": tcell.ColorTeal, + "bkmk_modal_text": tcell.ColorWhite, + "bkmk_modal_label": tcell.ColorYellow, + "bkmk_modal_field_bg": tcell.ColorBlue, + "bkmk_modal_field_text": tcell.ColorWhite, + + "hdg_1": tcell.ColorRed, + "hdg_2": tcell.ColorLime, + "hdg_3": tcell.ColorFuchsia, + "amfora_link": tcell.Color33, // xterm:DodgerBlue1, #0087ff + "foreign_link": tcell.Color92, // xterm:DarkViolet, #8700d7 + "link_number": tcell.ColorSilver, + "regular_text": tcell.ColorWhite, + "quote_text": tcell.ColorWhite, + "preformatted_text": tcell.ColorWhite, + "list_text": tcell.ColorWhite, +} + +func SetColor(key string, color tcell.Color) { + themeMu.Lock() + defer themeMu.Unlock() + theme[key] = color +} + +// GetColor will return tcell.ColorBlack if there is no color for the provided key. +func GetColor(key string) tcell.Color { + themeMu.RLock() + defer themeMu.RUnlock() + return theme[key] +} + +// GetColorString returns a string that can be used in a cview color tag, +// for the given theme key. +// It will return "#000000" if there is no color for the provided key. +func GetColorString(key string) string { + themeMu.RLock() + defer themeMu.RUnlock() + return fmt.Sprintf("#%06x", theme[key].Hex()) +} diff --git a/default-config.toml b/default-config.toml index 1be5e1b..154e82f 100644 --- a/default-config.toml +++ b/default-config.toml @@ -37,3 +37,73 @@ page_max_time = 10 # Zero values mean there is no limit max_size = 0 # Size in bytes max_pages = 30 # The maximum number of pages the cache will store + +[theme] +# This section is for changing the COLORS used in Amfora. +# These colors only apply if color is enabled above. +# Colors can be set using a W3C color name, or a hex value such as #ffffff". + +# Note that not all colors will work on terminals that do not have truecolor support. +# If you want to stick to the standard 16 or 256 colors, you can get +# a list of those here: https://jonasjacek.github.io/colors/ +# Do NOT use the names from that site, just the hex codes. + +# Definitions: +# bg = background +# fg = foreground +# dl = download +# btn = button +# hdg = heading +# bkmk = bookmark +# modal = a popup window/box in the middle of the screen + +# EXAMPLES: +# hdg_1 = "green" +# hdg_2 = "#5f0000" + +# Available keys to set: + +# bg: background for pages, tab row, app in general +# tab_num: The number/highlight of the tabs at the top +# tab_divider: The color of the divider character between tab numbers: | +# bottombar_label: The color of the prompt that appears when you press space +# bottombar_text: The color of the text you type +# bottombar_bg + +# hdg_1 +# hdg_2 +# hdg_3 +# amfora_link: A link that Amfora supports viewing. For now this is only gemini:// +# foreign_link: HTTP(S), Gopher, etc +# link_number: The silver number that appears to the left of a link +# regular_text: Normal gemini text, and plaintext documents +# quote_text +# preformatted_text +# list_text + +# btn_bg: The bg color for all modal buttons +# btn_text: The text color for all modal buttons + +# dl_choice_modal_bg +# dl_choice_modal_text +# dl_modal_bg +# dl_modal_text +# info_modal_bg +# info_modal_text +# error_modal_bg +# error_modal_text +# yesno_modal_bg +# yesno_modal_text +# tofu_modal_bg +# tofu_modal_text + +# input_modal_bg +# input_modal_text +# input_modal_field_bg: The bg of the input field, where you type the text +# input_modal_field_text: The color of the text you type + +# bkmk_modal_bg +# bkmk_modal_text +# bkmk_modal_label +# bkmk_modal_field_bg +# bkmk_modal_field_text \ No newline at end of file diff --git a/display/bookmarks.go b/display/bookmarks.go index a690cb1..d6543ed 100644 --- a/display/bookmarks.go +++ b/display/bookmarks.go @@ -7,6 +7,7 @@ import ( "github.com/gdamore/tcell" "github.com/makeworld-the-better-one/amfora/bookmarks" + "github.com/makeworld-the-better-one/amfora/config" "github.com/makeworld-the-better-one/amfora/renderer" "github.com/makeworld-the-better-one/amfora/structs" "github.com/spf13/viper" @@ -14,8 +15,7 @@ import ( ) // For adding and removing bookmarks, basically a clone of the input modal. -var bkmkModal = cview.NewModal(). - SetTextColor(tcell.ColorWhite) +var bkmkModal = cview.NewModal() // bkmkCh is for the user action var bkmkCh = make(chan int) // 1, 0, -1 for add/update, cancel, and remove @@ -23,21 +23,35 @@ var bkmkModalText string // The current text of the input field in the modal func bkmkInit() { if viper.GetBool("a-general.color") { - bkmkModal.SetBackgroundColor(tcell.ColorTeal). - SetButtonBackgroundColor(tcell.ColorNavy). - SetButtonTextColor(tcell.ColorWhite) + bkmkModal.SetBackgroundColor(config.GetColor("bkmk_modal_bg")). + SetButtonBackgroundColor(config.GetColor("btn_bg")). + SetButtonTextColor(config.GetColor("btn_text")). + SetTextColor(config.GetColor("bkmk_modal_text")) + bkmkModal.GetForm(). + SetLabelColor(config.GetColor("bkmk_modal_label")). + SetFieldBackgroundColor(config.GetColor("bkmk_modal_field_bg")). + SetFieldTextColor(config.GetColor("bkmk_modal_field_text")) + bkmkModal.GetFrame(). + SetBorderColor(config.GetColor("bkmk_modal_text")). + SetTitleColor(config.GetColor("bkmk_modal_text")) } else { bkmkModal.SetBackgroundColor(tcell.ColorBlack). SetButtonBackgroundColor(tcell.ColorWhite). - SetButtonTextColor(tcell.ColorBlack) + SetButtonTextColor(tcell.ColorBlack). + SetTextColor(tcell.ColorWhite) bkmkModal.GetForm(). SetLabelColor(tcell.ColorWhite). SetFieldBackgroundColor(tcell.ColorWhite). SetFieldTextColor(tcell.ColorBlack) + bkmkModal.GetFrame(). + SetBorderColor(tcell.ColorWhite). + SetTitleColor(tcell.ColorWhite) } bkmkModal.SetBorder(true) - bkmkModal.SetBorderColor(tcell.ColorWhite) + bkmkModal.GetFrame(). + SetTitleAlign(cview.AlignCenter). + SetTitle(" Add Bookmark ") bkmkModal.SetDoneFunc(func(buttonIndex int, buttonLabel string) { switch buttonLabel { case "Add": @@ -52,9 +66,6 @@ func bkmkInit() { //tabPages.SwitchToPage(strconv.Itoa(curTab)) - handled in bkmk() }) - bkmkModal.GetFrame().SetTitleColor(tcell.ColorWhite) - bkmkModal.GetFrame().SetTitleAlign(cview.AlignCenter) - bkmkModal.GetFrame().SetTitle(" Add Bookmark ") } // Bkmk displays the "Add a bookmark" modal. diff --git a/display/display.go b/display/display.go index abcff96..b6ea6e6 100644 --- a/display/display.go +++ b/display/display.go @@ -9,6 +9,7 @@ import ( "github.com/gdamore/tcell" "github.com/makeworld-the-better-one/amfora/cache" + "github.com/makeworld-the-better-one/amfora/config" "github.com/makeworld-the-better-one/amfora/renderer" "github.com/makeworld-the-better-one/amfora/structs" "github.com/spf13/viper" @@ -23,9 +24,7 @@ var termW int var termH int // The user input and URL display bar at the bottom -var bottomBar = cview.NewInputField(). - SetFieldBackgroundColor(tcell.ColorWhite). - SetFieldTextColor(tcell.ColorBlack) +var bottomBar = cview.NewInputField() // Viewer for the tab primitives // Pages are named as strings of tab numbers - so the textview for the first tab @@ -50,18 +49,7 @@ var tabRow = cview.NewTextView(). // Root layout var layout = cview.NewFlex(). - SetDirection(cview.FlexRow). - AddItem(tabRow, 1, 1, false). - AddItem(nil, 1, 1, false). // One line of empty space above the page - AddItem(tabPages, 0, 1, true). - // AddItem(cview.NewFlex(). // The page text in the middle is held in another flex, to center it - // SetDirection(cview.FlexColumn). - // AddItem(nil, 0, 1, false). - // AddItem(tabPages, 0, 7, true). // The text occupies 7/9 of the screen horizontally - // AddItem(nil, 0, 1, false), - // 0, 1, true). - AddItem(nil, 1, 1, false). // One line of empty space before bottomBar - AddItem(bottomBar, 1, 1, false) + SetDirection(cview.FlexRow) var renderedNewTabContent string var newTabLinks []string @@ -77,8 +65,8 @@ var App = cview.NewApplication(). // Make sure the current tab content is reformatted when the terminal size changes go func(t *tab) { - t.reformatMut.Lock() // Only one reformat job per tab - defer t.reformatMut.Unlock() + t.reformatMu.Lock() // Only one reformat job per tab + defer t.reformatMu.Unlock() // Use the current tab, but don't affect other tabs if the user switches tabs reformatPageAndSetView(t, t.page) }(tabs[curTab]) @@ -91,12 +79,29 @@ func Init() { helpInit() + layout. + AddItem(tabRow, 1, 1, false). + AddItem(nil, 1, 1, false). // One line of empty space above the page + AddItem(tabPages, 0, 1, true). + AddItem(nil, 1, 1, false). // One line of empty space before bottomBar + AddItem(bottomBar, 1, 1, false) + if viper.GetBool("a-general.color") { - bottomBar.SetLabelColor(tcell.ColorGreen) + layout.SetBackgroundColor(config.GetColor("bg")) + tabRow.SetBackgroundColor(config.GetColor("bg")) + + bottomBar.SetBackgroundColor(config.GetColor("bottombar_bg")) + bottomBar. + SetLabelColor(config.GetColor("bottombar_label")). + SetFieldBackgroundColor(config.GetColor("bottombar_bg")). + SetFieldTextColor(config.GetColor("bottombar_text")) } else { - bottomBar.SetLabelColor(tcell.ColorBlack) + bottomBar.SetBackgroundColor(tcell.ColorWhite) + bottomBar. + SetLabelColor(tcell.ColorBlack). + SetFieldBackgroundColor(tcell.ColorWhite). + SetFieldTextColor(tcell.ColorBlack) } - bottomBar.SetBackgroundColor(tcell.ColorWhite) bottomBar.SetDoneFunc(func(key tcell.Key) { tab := curTab @@ -432,7 +437,12 @@ func NewTab() { // Add tab number to the actual place where tabs are show on the screen // Tab regions are 0-indexed but text displayed on the screen starts at 1 if viper.GetBool("a-general.color") { - fmt.Fprintf(tabRow, `["%d"][darkcyan] %d [white][""]|`, curTab, curTab+1) + fmt.Fprintf(tabRow, `["%d"][%s] %d [%s][""]|`, + curTab, + config.GetColorString("tab_num"), + curTab+1, + config.GetColorString("tab_divider"), + ) } else { fmt.Fprintf(tabRow, `["%d"] %d [""]|`, curTab, curTab+1) } @@ -477,7 +487,12 @@ func CloseTab() { tabRow.Clear() if viper.GetBool("a-general.color") { for i := 0; i < NumTabs(); i++ { - fmt.Fprintf(tabRow, `["%d"][darkcyan] %d [white][""]|`, i, i+1) + fmt.Fprintf(tabRow, `["%d"][%s] %d [%s][""]|`, + i, + config.GetColorString("tab_num"), + i+1, + config.GetColorString("tab_divider"), + ) } } else { for i := 0; i < NumTabs(); i++ { diff --git a/display/download.go b/display/download.go index 3b74ec9..31e8c64 100644 --- a/display/download.go +++ b/display/download.go @@ -23,50 +23,62 @@ import ( // For choosing between download and the portal - copy of YesNo basically var dlChoiceModal = cview.NewModal(). - SetTextColor(tcell.ColorWhite). AddButtons([]string{"Download", "Open in portal", "Cancel"}) // Channel to indicate what choice they made using the button text var dlChoiceCh = make(chan string) -var dlModal = cview.NewModal(). - SetTextColor(tcell.ColorWhite) +var dlModal = cview.NewModal() func dlInit() { if viper.GetBool("a-general.color") { - dlChoiceModal.SetButtonBackgroundColor(tcell.ColorNavy). - SetButtonTextColor(tcell.ColorWhite). - SetBackgroundColor(tcell.ColorPurple) - dlModal.SetButtonBackgroundColor(tcell.ColorNavy). - SetButtonTextColor(tcell.ColorWhite). - SetBackgroundColor(tcell.Color130) // DarkOrange3, #af5f00 + dlChoiceModal.SetButtonBackgroundColor(config.GetColor("btn_bg")). + SetButtonTextColor(config.GetColor("btn_text")). + SetBackgroundColor(config.GetColor("dl_choice_modal_bg")). + SetTextColor(config.GetColor("dl_choice_modal_text")) + dlChoiceModal.GetFrame(). + SetBorderColor(config.GetColor("dl_choice_modal_text")). + SetTitleColor(config.GetColor("dl_choice_modal_text")) + + dlModal.SetButtonBackgroundColor(config.GetColor("btn_bg")). + SetButtonTextColor(config.GetColor("btn_text")). + SetBackgroundColor(config.GetColor("dl_modal_bg")). + SetTextColor(config.GetColor("dl_modal_text")) + dlModal.GetFrame(). + SetBorderColor(config.GetColor("dl_modal_text")). + SetTitleColor(config.GetColor("dl_modal_text")) } else { dlChoiceModal.SetButtonBackgroundColor(tcell.ColorWhite). SetButtonTextColor(tcell.ColorBlack). - SetBackgroundColor(tcell.ColorBlack) + SetBackgroundColor(tcell.ColorBlack). + SetTextColor(tcell.ColorWhite) + dlChoiceModal.SetBorderColor(tcell.ColorWhite) + dlChoiceModal.GetFrame().SetTitleColor(tcell.ColorWhite) + dlModal.SetButtonBackgroundColor(tcell.ColorWhite). SetButtonTextColor(tcell.ColorBlack). - SetBackgroundColor(tcell.ColorBlack) + SetBackgroundColor(tcell.ColorBlack). + SetTextColor(tcell.ColorWhite) + dlModal.GetFrame(). + SetBorderColor(tcell.ColorWhite). + SetTitleColor(tcell.ColorWhite) } dlChoiceModal.SetBorder(true) - dlChoiceModal.SetBorderColor(tcell.ColorWhite) + dlChoiceModal.GetFrame().SetTitleAlign(cview.AlignCenter) dlChoiceModal.SetDoneFunc(func(buttonIndex int, buttonLabel string) { dlChoiceCh <- buttonLabel }) - dlChoiceModal.GetFrame().SetTitleColor(tcell.ColorWhite) - dlChoiceModal.GetFrame().SetTitleAlign(cview.AlignCenter) dlModal.SetBorder(true) - dlModal.SetBorderColor(tcell.ColorWhite) + dlModal.GetFrame(). + SetTitleAlign(cview.AlignCenter). + SetTitle(" Download ") dlModal.SetDoneFunc(func(buttonIndex int, buttonLabel string) { if buttonLabel == "Ok" { tabPages.SwitchToPage(strconv.Itoa(curTab)) } }) - dlModal.GetFrame().SetTitleColor(tcell.ColorWhite) - dlModal.GetFrame().SetTitleAlign(cview.AlignCenter) - dlModal.GetFrame().SetTitle(" Download ") } // dlChoice displays the download choice modal and acts on the user's choice. diff --git a/display/help.go b/display/help.go index ce03fa2..0fb1c71 100644 --- a/display/help.go +++ b/display/help.go @@ -9,7 +9,7 @@ import ( ) var helpCells = strings.TrimSpace(` -?|Bring up this help. +?|Bring up this help. You can scroll! Esc|Leave the help Arrow keys, h/j/k/l|Scroll and move a page. Tab|Navigate to the next item in a popup. @@ -22,6 +22,7 @@ spacebar|Open bar at the bottom - type a URL, link number, search term. |You can also type two dots (..) to go up a directory in the URL. |Typing new:N will open link number N in a new tab |instead of the current one. +Numbers|Go to links 1-10 respectively. Enter, Tab|On a page this will start link highlighting. |Press Tab and Shift-Tab to pick different links. |Press Enter again to go to one, or Esc to stop. @@ -37,13 +38,14 @@ Ctrl-B|View bookmarks Ctrl-D|Add, change, or remove a bookmark for the current page. Ctrl-S|Save the current page to your downloads. q, Ctrl-Q|Quit -Ctrl-C|Hard quit. This can be used when in the middle of downloading, for example. +Ctrl-C|Hard quit. This can be used when in the middle of downloading, +|for example. `) var helpTable = cview.NewTable(). SetSelectable(false, false). SetBorders(false). - SetBordersColor(tcell.ColorGray) + SetScrollBarVisibility(cview.ScrollBarNever) // Help displays the help and keybindings. func Help() { diff --git a/display/modals.go b/display/modals.go index ff39817..5e700fa 100644 --- a/display/modals.go +++ b/display/modals.go @@ -8,6 +8,7 @@ import ( "github.com/dustin/go-humanize" "github.com/gdamore/tcell" + "github.com/makeworld-the-better-one/amfora/config" "github.com/spf13/viper" "gitlab.com/tslocum/cview" ) @@ -16,22 +17,16 @@ import ( // The bookmark modal is in bookmarks.go var infoModal = cview.NewModal(). - SetTextColor(tcell.ColorWhite). AddButtons([]string{"Ok"}) var errorModal = cview.NewModal(). - SetTextColor(tcell.ColorWhite). AddButtons([]string{"Ok"}) -var inputModal = cview.NewModal(). - SetTextColor(tcell.ColorWhite) - //AddButtons([]string{"Send", "Cancel"}) - Added in func - +var inputModal = cview.NewModal() var inputCh = make(chan string) var inputModalText string // The current text of the input field in the modal var yesNoModal = cview.NewModal(). - SetTextColor(tcell.ColorWhite). AddButtons([]string{"Yes", "No"}) // Channel to receive yesNo answer on @@ -48,29 +43,60 @@ func modalInit() { // Color setup if viper.GetBool("a-general.color") { - infoModal.SetBackgroundColor(tcell.ColorGray). - SetButtonBackgroundColor(tcell.ColorNavy). - SetButtonTextColor(tcell.ColorWhite) - errorModal.SetBackgroundColor(tcell.ColorMaroon). - SetButtonBackgroundColor(tcell.ColorNavy). - SetButtonTextColor(tcell.ColorWhite) - inputModal.SetBackgroundColor(tcell.ColorGreen). - SetButtonBackgroundColor(tcell.ColorNavy). - SetButtonTextColor(tcell.ColorWhite) - yesNoModal.SetButtonBackgroundColor(tcell.ColorNavy). - SetButtonTextColor(tcell.ColorWhite) + infoModal.SetBackgroundColor(config.GetColor("info_modal_bg")). + SetButtonBackgroundColor(config.GetColor("btn_bg")). + SetButtonTextColor(config.GetColor("btn_text")). + SetTextColor(config.GetColor("info_modal_text")) + infoModal.GetFrame(). + SetBorderColor(config.GetColor("info_modal_text")). + SetTitleColor(config.GetColor("info_modal_text")) + + errorModal.SetBackgroundColor(config.GetColor("error_modal_bg")). + SetButtonBackgroundColor(config.GetColor("btn_bg")). + SetButtonTextColor(config.GetColor("btn_text")). + SetTextColor(config.GetColor("error_modal_text")) + errorModal.GetFrame(). + SetBorderColor(config.GetColor("error_modal_text")). + SetTitleColor(config.GetColor("error_modal_text")) + + inputModal.SetBackgroundColor(config.GetColor("input_modal_bg")). + SetButtonBackgroundColor(config.GetColor("btn_bg")). + SetButtonTextColor(config.GetColor("btn_text")). + SetTextColor(config.GetColor("input_modal_text")) + inputModal.GetFrame(). + SetBorderColor(config.GetColor("input_modal_text")). + SetTitleColor(config.GetColor("input_modal_text")) + inputModal.GetForm(). + SetFieldBackgroundColor(config.GetColor("input_modal_field_bg")). + SetFieldTextColor(config.GetColor("input_modal_field_text")) + + yesNoModal.SetButtonBackgroundColor(config.GetColor("btn_bg")). + SetButtonTextColor(config.GetColor("btn_text")) } else { infoModal.SetBackgroundColor(tcell.ColorBlack). SetButtonBackgroundColor(tcell.ColorWhite). - SetButtonTextColor(tcell.ColorBlack) + SetButtonTextColor(tcell.ColorBlack). + SetTextColor(tcell.ColorWhite) + infoModal.GetFrame(). + SetBorderColor(tcell.ColorWhite). + SetTitleColor(tcell.ColorWhite) + errorModal.SetBackgroundColor(tcell.ColorBlack). SetButtonBackgroundColor(tcell.ColorWhite). - SetButtonTextColor(tcell.ColorBlack) + SetButtonTextColor(tcell.ColorBlack). + SetTextColor(tcell.ColorWhite) + errorModal.GetFrame(). + SetBorderColor(tcell.ColorWhite). + SetTitleColor(tcell.ColorWhite) + inputModal.SetBackgroundColor(tcell.ColorBlack). SetButtonBackgroundColor(tcell.ColorWhite). - SetButtonTextColor(tcell.ColorBlack) + SetButtonTextColor(tcell.ColorBlack). + SetTextColor(tcell.ColorWhite) + inputModal.GetFrame(). + SetBorderColor(tcell.ColorWhite). + SetTitleColor(tcell.ColorWhite) inputModal.GetForm(). - SetLabelColor(tcell.ColorWhite). SetFieldBackgroundColor(tcell.ColorWhite). SetFieldTextColor(tcell.ColorBlack) @@ -82,24 +108,23 @@ func modalInit() { // Modal functions that can't be added up above, because they return the wrong type infoModal.SetBorder(true) - infoModal.SetBorderColor(tcell.ColorWhite) + infoModal.GetFrame(). + SetTitleAlign(cview.AlignCenter). + SetTitle(" Info ") infoModal.SetDoneFunc(func(buttonIndex int, buttonLabel string) { tabPages.SwitchToPage(strconv.Itoa(curTab)) }) - infoModal.GetFrame().SetTitleColor(tcell.ColorWhite) - infoModal.GetFrame().SetTitleAlign(cview.AlignCenter) - infoModal.GetFrame().SetTitle(" Info ") errorModal.SetBorder(true) - errorModal.SetBorderColor(tcell.ColorWhite) + errorModal.GetFrame().SetTitleAlign(cview.AlignCenter) errorModal.SetDoneFunc(func(buttonIndex int, buttonLabel string) { tabPages.SwitchToPage(strconv.Itoa(curTab)) }) - errorModal.GetFrame().SetTitleColor(tcell.ColorWhite) - errorModal.GetFrame().SetTitleAlign(cview.AlignCenter) inputModal.SetBorder(true) - inputModal.SetBorderColor(tcell.ColorWhite) + inputModal.GetFrame(). + SetTitleAlign(cview.AlignCenter). + SetTitle(" Input ") inputModal.SetDoneFunc(func(buttonIndex int, buttonLabel string) { if buttonLabel == "Send" { inputCh <- inputModalText @@ -107,15 +132,10 @@ func modalInit() { } // Empty string indicates no input inputCh <- "" - - //tabPages.SwitchToPage(strconv.Itoa(curTab)) - handled in Input() }) - inputModal.GetFrame().SetTitleColor(tcell.ColorWhite) - inputModal.GetFrame().SetTitleAlign(cview.AlignCenter) - inputModal.GetFrame().SetTitle(" Input ") yesNoModal.SetBorder(true) - yesNoModal.SetBorderColor(tcell.ColorWhite) + yesNoModal.GetFrame().SetTitleAlign(cview.AlignCenter) yesNoModal.SetDoneFunc(func(buttonIndex int, buttonLabel string) { if buttonLabel == "Yes" { yesNoCh <- true @@ -125,8 +145,6 @@ func modalInit() { //tabPages.SwitchToPage(strconv.Itoa(curTab)) - Handled in YesNo() }) - yesNoModal.GetFrame().SetTitleColor(tcell.ColorWhite) - yesNoModal.GetFrame().SetTitleAlign(cview.AlignCenter) bkmkInit() dlInit() @@ -174,7 +192,7 @@ func Input(prompt string) (string, bool) { inputModalText = text }) - inputModal.SetText(prompt) + inputModal.SetText(prompt + " ") tabPages.ShowPage("input") tabPages.SendToFront("input") App.SetFocus(inputModal) @@ -191,9 +209,19 @@ func Input(prompt string) (string, bool) { // YesNo displays a modal asking a yes-or-no question. func YesNo(prompt string) bool { if viper.GetBool("a-general.color") { - yesNoModal.SetBackgroundColor(tcell.ColorPurple) + yesNoModal. + SetBackgroundColor(config.GetColor("yesno_modal_bg")). + SetTextColor(config.GetColor("yesno_modal_text")) + yesNoModal.GetFrame(). + SetBorderColor(config.GetColor("yesno_modal_text")). + SetTitleColor(config.GetColor("yesno_modal_text")) } else { - yesNoModal.SetBackgroundColor(tcell.ColorBlack) + yesNoModal. + SetBackgroundColor(tcell.ColorBlack). + SetTextColor(tcell.ColorWhite) + yesNoModal.GetFrame(). + SetBorderColor(tcell.ColorWhite). + SetTitleColor(tcell.ColorWhite) } yesNoModal.GetFrame().SetTitle("") yesNoModal.SetText(prompt) @@ -210,12 +238,22 @@ func YesNo(prompt string) bool { // Tofu displays the TOFU warning modal. // It returns a bool indicating whether the user wants to continue. func Tofu(host string, expiry time.Time) bool { - // Reuses yesNoModal, with error colour + // Reuses yesNoModal, with error color if viper.GetBool("a-general.color") { - yesNoModal.SetBackgroundColor(tcell.ColorMaroon) + yesNoModal. + SetBackgroundColor(config.GetColor("tofu_modal_bg")). + SetTextColor(config.GetColor("tofu_modal_text")) + yesNoModal.GetFrame(). + SetBorderColor(config.GetColor("tofu_modal_text")). + SetTitleColor(config.GetColor("tofu_modal_text")) } else { - yesNoModal.SetBackgroundColor(tcell.ColorBlack) + yesNoModal. + SetBackgroundColor(tcell.ColorBlack). + SetTextColor(tcell.ColorWhite) + yesNoModal. + SetBorderColor(tcell.ColorWhite). + SetTitleColor(tcell.ColorWhite) } yesNoModal.GetFrame().SetTitle(" TOFU ") yesNoModal.SetText( diff --git a/display/tab.go b/display/tab.go index d69495f..2848cf0 100644 --- a/display/tab.go +++ b/display/tab.go @@ -24,13 +24,13 @@ type tabHistory struct { // tab hold the information needed for each browser tab. type tab struct { - page *structs.Page - view *cview.TextView - history *tabHistory - mode tabMode - reformatMut *sync.Mutex // Mutex for reformatting, so there's only one reformat job at once - barLabel string // The bottomBar label for the tab - barText string // The bottomBar text for the tab + page *structs.Page + view *cview.TextView + history *tabHistory + mode tabMode + reformatMu *sync.Mutex // Mutex for reformatting, so there's only one reformat job at once + barLabel string // The bottomBar label for the tab + barText string // The bottomBar text for the tab } // makeNewTab initializes an tab struct with no content. @@ -45,9 +45,9 @@ func makeNewTab() *tab { SetChangedFunc(func() { App.Draw() }), - history: &tabHistory{}, - reformatMut: &sync.Mutex{}, - mode: tabModeDone, + history: &tabHistory{}, + reformatMu: &sync.Mutex{}, + mode: tabModeDone, } t.view.SetDoneFunc(func(key tcell.Key) { // Altered from: https://gitlab.com/tslocum/cview/-/blob/master/demos/textview/main.go diff --git a/renderer/page.go b/renderer/page.go index 46d939b..c1c2254 100644 --- a/renderer/page.go +++ b/renderer/page.go @@ -109,7 +109,7 @@ func MakePage(url string, res *gemini.Response, width, leftMargin int) (*structs Links: links, }, nil } else if strings.HasPrefix(mediatype, "text/") { - if mediatype == "text/x-ansi" || strings.HasSuffix(url, ".ans") { + if mediatype == "text/x-ansi" || strings.HasSuffix(url, ".ans") || strings.HasSuffix(url, ".ansi") { // ANSI return &structs.Page{ Mediatype: structs.TextAnsi, diff --git a/renderer/renderer.go b/renderer/renderer.go index 8e12918..2165d6f 100644 --- a/renderer/renderer.go +++ b/renderer/renderer.go @@ -5,10 +5,12 @@ package renderer import ( + "fmt" urlPkg "net/url" "strconv" "strings" + "github.com/makeworld-the-better-one/amfora/config" "github.com/spf13/viper" "gitlab.com/tslocum/cview" ) @@ -33,7 +35,9 @@ func RenderPlainText(s string, leftMargin int) string { var shifted string lines := strings.Split(cview.Escape(s), "\n") for i := range lines { - shifted += strings.Repeat(" ", leftMargin) + lines[i] + "\n" + shifted += strings.Repeat(" ", leftMargin) + + "[" + config.GetColorString("regular_text") + "]" + lines[i] + "[-]" + + "\n" } return shifted } @@ -70,6 +74,17 @@ func wrapLine(line string, width int, prefix, suffix string, includeFirst bool) return ret } +// tagLines splits a string into lines and adds a the given +// string to the start and another to the end. +// It is used for adding cview color tags. +func tagLines(s, start, end string) string { + lines := strings.Split(s, "\n") + for i := range lines { + lines[i] = start + lines[i] + end + } + return strings.Join(lines, "\n") +} + // convertRegularGemini converts non-preformatted blocks of text/gemini // into a cview-compatible format. // It also returns a slice of link URLs. @@ -88,14 +103,16 @@ func convertRegularGemini(s string, numLinks, width int) (string, []string) { if strings.HasPrefix(lines[i], "#") { // Headings + var tag string if viper.GetBool("a-general.color") { if strings.HasPrefix(lines[i], "###") { - wrappedLines = append(wrappedLines, wrapLine(lines[i], width, "[fuchsia::b]", "[-::-]", true)...) + tag = fmt.Sprintf("[%s::b]", config.GetColorString("hdg_3")) } else if strings.HasPrefix(lines[i], "##") { - wrappedLines = append(wrappedLines, wrapLine(lines[i], width, "[lime::b]", "[-::-]", true)...) + tag = fmt.Sprintf("[%s::b]", config.GetColorString("hdg_2")) } else if strings.HasPrefix(lines[i], "#") { - wrappedLines = append(wrappedLines, wrapLine(lines[i], width, "[red::b]", "[-::-]", true)...) + tag = fmt.Sprintf("[%s::b]", config.GetColorString("hdg_1")) } + wrappedLines = append(wrappedLines, wrapLine(lines[i], width, tag, "[-::-]", true)...) } else { // Just bold, no colors wrappedLines = append(wrappedLines, wrapLine(lines[i], width, "[::b]", "[-::-]", true)...) @@ -141,34 +158,37 @@ func convertRegularGemini(s string, numLinks, width int) (string, []string) { if err == nil && (pU.Scheme == "" || pU.Scheme == "gemini" || pU.Scheme == "about") { // A gemini link // Add the link text in blue (in a region), and a gray link number to the left of it + // Those are the default colors, anyway wrappedLink = wrapLine(linkText, width, strings.Repeat(" ", len(strconv.Itoa(numLinks+len(links)))+4)+ // +4 for spaces and brackets - `["`+strconv.Itoa(numLinks+len(links)-1)+`"][dodgerblue]`, + `["`+strconv.Itoa(numLinks+len(links)-1)+`"][`+config.GetColorString("amfora_link")+`]`, `[-][""]`, false, // Don't indent the first line, it's the one with link number ) // Add special stuff to first line, like the link number - wrappedLink[0] = `[silver::b][` + strconv.Itoa(numLinks+len(links)) + "[]" + "[-::-] " + - `["` + strconv.Itoa(numLinks+len(links)-1) + `"][dodgerblue]` + + wrappedLink[0] = fmt.Sprintf(`[%s::b][`, config.GetColorString("link_number")) + + strconv.Itoa(numLinks+len(links)) + "[]" + "[-::-] " + + `["` + strconv.Itoa(numLinks+len(links)-1) + `"][` + config.GetColorString("amfora_link") + `]` + wrappedLink[0] + `[-][""]` } else { - // Not a gemini link, use purple instead + // Not a gemini link wrappedLink = wrapLine(linkText, width, strings.Repeat(" ", len(strconv.Itoa(numLinks+len(links)))+4)+ // +4 for spaces and brackets - `["`+strconv.Itoa(numLinks+len(links)-1)+`"][#8700d7]`, + `["`+strconv.Itoa(numLinks+len(links)-1)+`"][`+config.GetColorString("foreign_link")+`]`, `[-][""]`, false, // Don't indent the first line, it's the one with link number ) - wrappedLink[0] = `[silver::b][` + strconv.Itoa(numLinks+len(links)) + "[]" + "[-::-] " + - `["` + strconv.Itoa(numLinks+len(links)-1) + `"][#8700d7]` + + wrappedLink[0] = fmt.Sprintf(`[%s::b][`, config.GetColorString("link_number")) + + strconv.Itoa(numLinks+len(links)) + "[]" + "[-::-] " + + `["` + strconv.Itoa(numLinks+len(links)-1) + `"][` + config.GetColorString("foreign_link") + `]` + wrappedLink[0] + `[-][""]` } } else { - // No colours allowed + // No colors allowed wrappedLink = wrapLine(linkText, width, strings.Repeat(" ", len(strconv.Itoa(numLinks+len(links)))+4)+ // +4 for spaces and brackets @@ -188,9 +208,12 @@ func convertRegularGemini(s string, numLinks, width int) (string, []string) { } else if strings.HasPrefix(lines[i], "* ") { if viper.GetBool("a-general.bullets") { // Wrap list item, and indent wrapped lines past the bullet - wrappedItem := wrapLine(lines[i][1:], width, " ", "", false) + wrappedItem := wrapLine(lines[i][1:], width, + fmt.Sprintf(" [%s]", config.GetColorString("list_text")), + "[-]", false) // Add bullet - wrappedItem[0] = " \u2022" + wrappedItem[0] + wrappedItem[0] = fmt.Sprintf(" [%s]\u2022", config.GetColorString("list_text")) + + wrappedItem[0] + "[-]" wrappedLines = append(wrappedLines, wrappedItem...) } // Optionally list lines could be colored here too, if color is enabled @@ -200,14 +223,19 @@ func convertRegularGemini(s string, numLinks, width int) (string, []string) { // Remove beginning quote and maybe space lines[i] = strings.TrimPrefix(lines[i], ">") lines[i] = strings.TrimPrefix(lines[i], " ") - wrappedLines = append(wrappedLines, wrapLine(lines[i], width, "> [::i]", "[::-]", true)...) + wrappedLines = append(wrappedLines, + wrapLine(lines[i], width, fmt.Sprintf("[%s::i]> ", config.GetColorString("quote_text")), + "[-::-]", true)..., + ) } else if strings.TrimSpace(lines[i]) == "" { // Just add empty line without processing wrappedLines = append(wrappedLines, "") } else { // Regular line, just wrap it - wrappedLines = append(wrappedLines, wrapLine(lines[i], width, "", "", true)...) + wrappedLines = append(wrappedLines, wrapLine(lines[i], width, + fmt.Sprintf("[%s]", config.GetColorString("regular_text")), + "[-]", true)...) } } @@ -237,7 +265,11 @@ func RenderGemini(s string, width, leftMargin int) (string, []string) { if pre { // In a preformatted block, so add the text as is // Don't add the current line with backticks - rendered += buf + rendered += tagLines( + buf, + fmt.Sprintf("[%s]", config.GetColorString("preformatted_text")), + "[-]", + ) } else { // Not preformatted, regular text ren, lks := convertRegularGemini(buf, len(links), width) diff --git a/structs/structs.go b/structs/structs.go index a33953b..a383adc 100644 --- a/structs/structs.go +++ b/structs/structs.go @@ -21,7 +21,7 @@ type Page struct { Url string Mediatype Mediatype Raw string // The raw response, as received over the network - Content string // The processed content, NOT raw. Uses cview colour tags. All link/link texts must have region tags. It will also have a left margin. + Content string // The processed content, NOT raw. Uses cview color tags. All link/link texts must have region tags. It will also have a left margin. Links []string // URLs, for each region in the content. Row int // Scroll position Column int // ditto