1
0
mirror of https://github.com/makew0rld/amfora.git synced 2024-06-11 19:00:43 +00:00

Merge changes from master

This commit is contained in:
mntn 2021-12-07 21:49:11 -05:00
commit e9fecaca25
41 changed files with 744 additions and 214 deletions

View File

@ -24,6 +24,6 @@ jobs:
uses: golangci/golangci-lint-action@v2
with:
# Required: the version of golangci-lint is required and must be specified without patch version: we always use the latest patch version.
version: v1.35
version: v1.43
# Optional: show only new issues if it's a pull request. The default value is `false`.
only-new-issues: true

View File

@ -16,7 +16,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v2
with:
go-version: 1.16
go-version: 1.17
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v2
with:

View File

@ -19,7 +19,7 @@ jobs:
strategy:
fail-fast: false
matrix:
go-version: ['1.14', '1.15', '1.16']
go-version: ['1.15', '1.16', '1.17']
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:

View File

@ -19,15 +19,13 @@ linters:
- goerr113
- gofmt
- goimports
- golint
- revive
- goprintffuncname
- interfacer
- lll
- maligned
- misspell
- nolintlint
- prealloc
- scopelint
- exportloopref
- unconvert
- unparam

View File

@ -14,21 +14,39 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `bind_beginning` and `bind_end` keybindings
- Display gemtext from stdin (#205, #242)
- Specifying `default` in the theme config uses the terminal's default background color, including transparency (#244, #245)
- Redirects occur automatically if it only adds a trailing slash (#271)
- Non-gemini links are underlined by default to help color blind users (#189)
- Text and element colors of default theme change to be black on terminals with light backgrounds (#181)
- Support paths with spaces in `[url-handlers]` config settings (#214)
- Display info modal when opening URL with custom application
- Files can be opened by relative path on the commandline (#231, #257)
- Support keybindings that use <kbd>Shift</kbd> (#269)
### Changed
- Favicon support removed (#199)
- Bookmarks are stored using XML in the XBEL format, old bookmarks are transferred (#68)
- Text no longer disappears under the left margin when scrolling (regression from v1.8.0) (#197)
- Text no longer disappears under the left margin when scrolling (regression in v1.8.0) (#197)
- Default search engine changed to geminispace.info from gus.guru
- The user's terminal theme colors are used by default (#181)
- By default, non-gemini URI schemes are opened in the default application. This requires a config change for previous users, see the [wiki](https://github.com/makeworld-the-better-one/amfora/wiki/Handling-Other-URL-Schemes) (#207)
- Windows uses paths set by `XDG` variables over `APPDATA` if they are set (#255)
- Treat status codes like 22 as equivalent to 20 as per the latest spec (#266)
- Show minimal loading page instead of `about:newtab` when loading a URL in a new tab (#272)
## Removed
- Favicon support (#199)
- The default Amfora theme, get it back [here](https://github.com/makeworld-the-better-one/amfora/blob/master/contrib/themes/amfora.toml)
### Fixed
- Help text is now the same color as `regular_text` in the theme config
- Non-ASCII (multibyte) characters can now be used as keybindings (#198, #200)
- Possible subscription update race condition on startup
- Plaintext documents are escaped properly (regression from v1.8.0)
- Plaintext documents are escaped properly (regression in v1.8.0)
- Help page scrollbar color matches what's in the theme config
- Regression where lists would not appear if `bullets = false` (#234, #235)
- Support multiple bookmarks with the same name
- Cert change message grammar: "an security" -> "a security" (#274)
- Display an error modal for status codes that can't be handled
- Prevent user from getting trapped in the help menu when keybindings are pressed (#241, #261)
## [1.8.0] - 2021-02-17

View File

@ -6,7 +6,6 @@
## Upstream Bugs
- Bookmark keys aren't deleted, just set to `""`
- Waiting on [this viper PR](https://github.com/spf13/viper/pull/519) to be merged
- [cview.Styles not being used](https://code.rocketnine.space/tslocum/cview/issues/47) - issue is circumvented in Amfora
- [ANSI conversion is messed up](https://code.rocketnine.space/tslocum/cview/issues/48)
- [WordWrap is broken in some cases](https://code.rocketnine.space/tslocum/cview/issues/27) - close #156 if this is fixed
- [Prevent panic when reformatting](https://code.rocketnine.space/tslocum/cview/issues/50) - can't reliably reproduce or debug

View File

@ -13,7 +13,7 @@
###### Recording of v1.0.0
Amfora aims to be the best looking [Gemini](https://gemini.circumlunar.space/) client with the most features... all in the terminal. It does not support Gopher or other non-Web protocols - check out [Bombadillo](http://bombadillo.colorfield.space/) for that.
Amfora aims to be the best looking [Gemini](https://geminiquickst.art/) client with the most features... all in the terminal. It does not support Gopher or other non-Web protocols - check out [Bombadillo](http://bombadillo.colorfield.space/) for that.
It also aims to be completely cross platform, with full Windows support. If you're on Windows, I would not recommend using the default terminal software. Use [Windows Terminal](https://www.microsoft.com/en-us/p/windows-terminal/9n0dx20hk701) instead, and make sure it [works with UTF-8](https://akr.am/blog/posts/using-utf-8-in-the-windows-terminal). Note that some of the application colors might not display correctly on Windows, but all functionality will still work.
@ -80,7 +80,7 @@ This section is for advanced users who want to install the latest (possibly unst
<summary>Click to expand</summary>
**Requirements:**
- Go 1.14 or later
- Go 1.15 or later
- GNU Make
Please note the Makefile does not intend to support Windows, and so there may be issues.
@ -163,6 +163,8 @@ Amfora ❤️ open source!
- [progressbar](https://github.com/schollz/progressbar)
- [go-humanize](https://github.com/dustin/go-humanize)
- [gofeed](https://github.com/mmcdole/gofeed)
- [clipboard](https://github.com/atotto/clipboard)
- [termenv](https://github.com/muesli/termenv)
## License
This project is licensed under the GPL v3.0. See the [LICENSE](./LICENSE) file for details.

View File

@ -23,3 +23,5 @@ Thank you to the following contributors, who have helped make Amfora great. FOSS
* Anas Mohamed (@amohamed11)
* David Jimenez (@dvejmz)
* Michael McDonagh (@m-mcdonagh)
* mooff (@awfulcooking)
* Josias (@justjosias)

View File

@ -4,12 +4,14 @@ import (
"fmt"
"io"
"os"
"path/filepath"
"strings"
"github.com/makeworld-the-better-one/amfora/bookmarks"
"github.com/makeworld-the-better-one/amfora/client"
"github.com/makeworld-the-better-one/amfora/config"
"github.com/makeworld-the-better-one/amfora/display"
"github.com/makeworld-the-better-one/amfora/logger"
"github.com/makeworld-the-better-one/amfora/subscriptions"
)
@ -20,10 +22,15 @@ var (
)
func main() {
// err := logger.Init()
// if err != nil {
// panic(err)
// }
log, err := logger.GetLogger()
if err != nil {
panic(err)
}
debugModeEnabled := os.Getenv("AMFORA_DEBUG") == "1"
if debugModeEnabled {
log.Println("Debug mode enabled")
}
if len(os.Args) > 1 {
if os.Args[1] == "--version" || os.Args[1] == "-v" {
@ -42,7 +49,7 @@ func main() {
}
}
err := config.Init()
err = config.Init()
if err != nil {
fmt.Fprintf(os.Stderr, "Config error: %v\n", err)
os.Exit(1)
@ -67,11 +74,29 @@ func main() {
// Initialize Amfora's settings
display.Init(version, commit, builtBy)
display.NewTab()
// Load a URL, file, or render from stdin
if len(os.Args[1:]) > 0 {
display.URL(os.Args[1])
url := os.Args[1]
if !strings.Contains(url, "://") || strings.HasPrefix(url, "../") || strings.HasPrefix(url, "./") {
fileName := url
if _, err := os.Stat(fileName); err == nil {
if !strings.HasPrefix(fileName, "/") {
cwd, err := os.Getwd()
if err != nil {
fmt.Fprintf(os.Stderr, "error getting working directory path: %v\n", err)
os.Exit(1)
}
fileName = filepath.Join(cwd, fileName)
}
url = "file://" + fileName
}
}
display.NewTabWithURL(url)
} else if !isStdinEmpty() {
renderFromStdin()
} else {
display.NewTab()
}
// Start

View File

@ -61,7 +61,6 @@ func Init() error {
err = os.Remove(config.OldBkmkPath)
if err != nil {
//nolint:goerr113
return fmt.Errorf(
"couldn't delete old bookmarks file (%s), you must delete it yourself to prevent duplicate bookmarks: %w",
config.OldBkmkPath,

View File

@ -62,7 +62,6 @@ func loadTofuEntry(domain string, port string) (string, time.Time, error) {
return id, expiry, nil
}
//nolint:errcheck
// certID returns a generic string representing a cert or domain.
func certID(cert *x509.Certificate) string {
h := sha256.New()
@ -73,7 +72,7 @@ func certID(cert *x509.Certificate) string {
// origCertID uses cert.Raw, which was used in v1.0.0 of the app.
func origCertID(cert *x509.Certificate) string {
h := sha256.New()
h.Write(cert.Raw) //nolint:errcheck
h.Write(cert.Raw)
return fmt.Sprintf("%X", h.Sum(nil))
}

View File

@ -15,6 +15,7 @@ import (
"github.com/gdamore/tcell/v2"
"github.com/makeworld-the-better-one/amfora/cache"
homedir "github.com/mitchellh/go-homedir"
"github.com/muesli/termenv"
"github.com/rkoesters/xdg/basedir"
"github.com/rkoesters/xdg/userdirs"
"github.com/spf13/viper"
@ -59,9 +60,16 @@ var MediaHandlers = make(map[string]MediaHandler)
// Defaults to ScrollBarAuto on an invalid value
var ScrollBar cview.ScrollBarVisibility
// Whether the user's terminal is dark or light
// Defaults to dark, but is determined in Init()
// Used to prevent white text on a white background with the default theme
var hasDarkTerminalBackground bool
func Init() error {
// *** Set paths ***
// Windows uses paths under APPDATA, Unix systems use XDG paths
// Windows systems use XDG paths if variables are defined, see #255
home, err := homedir.Dir()
if err != nil {
@ -78,10 +86,10 @@ func Init() error {
}
// Store config directory and file paths
if runtime.GOOS == "windows" {
if runtime.GOOS == "windows" && os.Getenv("XDG_CONFIG_HOME") == "" {
configDir = amforaAppData
} else {
// Unix / POSIX system
// Unix / POSIX system, or Windows with XDG_CONFIG_HOME defined
configDir = filepath.Join(basedir.ConfigHome, "amfora")
}
configPath = filepath.Join(configDir, "config.toml")
@ -94,7 +102,7 @@ func Init() error {
}
// Store TOFU db directory and file paths
if runtime.GOOS == "windows" {
if runtime.GOOS == "windows" && os.Getenv("XDG_CACHE_HOME") == "" {
// Windows just stores it in APPDATA along with other stuff
tofuDBDir = amforaAppData
} else {
@ -104,7 +112,7 @@ func Init() error {
tofuDBPath = filepath.Join(tofuDBDir, "tofu.toml")
// Store bookmarks dir and path
if runtime.GOOS == "windows" {
if runtime.GOOS == "windows" && os.Getenv("XDG_DATA_HOME") == "" {
// Windows just keeps it in APPDATA along with other Amfora files
bkmkDir = amforaAppData
} else {
@ -115,18 +123,12 @@ func Init() error {
BkmkPath = filepath.Join(bkmkDir, "bookmarks.xml")
// Feeds dir and path
if runtime.GOOS == "windows" {
if runtime.GOOS == "windows" && os.Getenv("XDG_DATA_HOME") == "" {
// In APPDATA beside other Amfora files
subscriptionDir = amforaAppData
} else {
// XDG data dir on POSIX systems
xdg_data, ok := os.LookupEnv("XDG_DATA_HOME")
if ok && strings.TrimSpace(xdg_data) != "" {
subscriptionDir = filepath.Join(xdg_data, "amfora")
} else {
// Default to ~/.local/share/amfora
subscriptionDir = filepath.Join(home, ".local", "share", "amfora")
}
subscriptionDir = filepath.Join(basedir.DataHome, "amfora")
}
SubscriptionPath = filepath.Join(subscriptionDir, "subscriptions.json")
@ -203,6 +205,7 @@ func Init() error {
viper.SetDefault("a-general.page_max_size", 2097152)
viper.SetDefault("a-general.page_max_time", 10)
viper.SetDefault("a-general.scrollbar", "auto")
viper.SetDefault("a-general.underline", true)
viper.SetDefault("commands.command1", "")
viper.SetDefault("commands.command2", "")
viper.SetDefault("commands.command3", "")
@ -281,7 +284,7 @@ func Init() error {
viper.SetDefault("keybindings.bind_beginning", []string{"Home", "g"})
viper.SetDefault("keybindings.bind_end", []string{"End", "G"})
viper.SetDefault("keybindings.shift_numbers", "")
viper.SetDefault("url-handlers.other", "off")
viper.SetDefault("url-handlers.other", "default")
viper.SetDefault("cache.max_size", 0)
viper.SetDefault("cache.max_pages", 20)
viper.SetDefault("cache.timeout", 1800)
@ -398,7 +401,15 @@ func Init() error {
}
if viper.GetBool("a-general.color") {
cview.Styles.PrimitiveBackgroundColor = GetColor("bg")
} // Otherwise it's black by default
} else {
// No colors allowed, set background to black instead of default
themeMu.Lock()
theme["bg"] = tcell.ColorBlack
cview.Styles.PrimitiveBackgroundColor = tcell.ColorBlack
themeMu.Unlock()
}
hasDarkTerminalBackground = termenv.HasDarkBackground()
// Parse HTTP command
HTTPCommand = viper.GetStringSlice("a-general.http")

View File

@ -3,6 +3,17 @@ 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.
# You can edit this file to set your own configuration for Amfora.
# When Amfora updates, defaults may change, but this file on your drive will not.
# You can always get the latest defaults on GitHub.
# https://github.com/makeworld-the-better-one/amfora/blob/master/default-config.toml
# Please also check out the Amfora Wiki for more help
# https://github.com/makeworld-the-better-one/amfora/wiki
# gemini://makeworld.space/amfora-wiki/
# All URL values may omit the scheme and/or port, as well as the beginning double slash
# Valid URL examples:
@ -26,7 +37,7 @@ auto_redirect = false
# If a command is set, than the URL will be added (in quotes) to the end of the command.
# A space will be prepended to the URL.
#
# The best to define a command is using a string array.
# The best way to define a command is using a string array.
# Examples:
# http = ['firefox']
# http = ['custom-browser', '--flag', '--option=2']
@ -74,6 +85,10 @@ page_max_time = 10
# "auto" means the scrollbar only appears when the page is longer than the window.
scrollbar = "auto"
# Underline non-gemini URLs
# This is done to help color blind users
underline = true
[auth]
# Authentication settings
@ -216,19 +231,30 @@ scrollbar = "auto"
[url-handlers]
# Allows setting the commands to run for various URL schemes.
# E.g. to open FTP URLs with FileZilla set the following key:
# ftp = 'filezilla'
# You can set any scheme to "off" or "" to disable handling it, or
# ftp = ['filezilla']
# You can set any scheme to 'off' or '' to disable handling it, or
# just leave the key unset.
#
# DO NOT use this for setting the HTTP command.
# Use the http setting in the "a-general" section above.
#
# NOTE: These settings are overrided by the ones in the proxies section.
#
# The best way to define a command is using a string array.
# Examples:
# magnet = ['transmission']
# foo = ['custom-browser', '--flag', '--option=2']
# tel = ['/path/with spaces/in it/telephone']
#
# Note the use of single quotes, so that backslashes will not be escaped.
# Using just a string will also work, but it is deprecated, and will degrade if
# you use paths with spaces.
# This is a special key that defines the handler for all URL schemes for which
# no handler is defined.
other = 'off'
# It uses the special value 'default', which will try and use the default
# application on your computer for opening this kind of URI.
other = 'default'
# [[mediatype-handlers]] section
@ -353,6 +379,8 @@ entries_per_page = 20
# Colors can be set using a W3C color name, or a hex value such as "#ffffff".
# Setting a background to "default" keeps the terminal default
# If your terminal has transparency, set any background to "default" to keep it transparent
# The key "bg" is already set to "default", but this can be used on other backgrounds,
# like for modals.
# 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

View File

@ -101,7 +101,7 @@ var tcellKeys map[string]tcell.Key
// a string in the format used by the configuration file. Support
// function for GetKeyBinding(), used to make the help panel helpful.
func keyBindingToString(kb keyBinding) (string, bool) {
var prefix string = ""
var prefix string
if kb.mod&tcell.ModAlt == tcell.ModAlt {
prefix = "Alt-"
@ -124,7 +124,7 @@ func keyBindingToString(kb keyBinding) (string, bool) {
// Used by the help panel so bindable keys display with their
// bound values rather than hardcoded defaults.
func GetKeyBinding(cmd Command) string {
var s string = ""
var s string
for kb, c := range bindings {
if c == cmd {
t, ok := keyBindingToString(kb)
@ -143,14 +143,19 @@ func GetKeyBinding(cmd Command) string {
// Parse a single keybinding string and add it to the binding map
func parseBinding(cmd Command, binding string) {
var k tcell.Key
var m tcell.ModMask = 0
var r rune = 0
var m tcell.ModMask
var r rune
if strings.HasPrefix(binding, "Alt-") {
m = tcell.ModAlt
binding = binding[4:]
}
if strings.HasPrefix(binding, "Shift-") {
m += tcell.ModShift
binding = binding[6:]
}
if len([]rune(binding)) == 1 {
k = tcell.KeyRune
r = []rune(binding)[0]

View File

@ -8,97 +8,164 @@ import (
)
// Functions to allow themeing configuration.
// UI element colors are mapped to a string key, such as "error" or "tab_bg"
// UI element tcell.Colors are mapped to a string key, such as "error" or "tab_bg"
// These are the same keys used in the config file.
// Special color with no real color value
// Used for a default foreground color
// White is the terminal background is black, black if the terminal background is white
// Converted to a real color in this file before being sent out to other modules
const ColorFg = tcell.ColorSpecial | 2
// The same as ColorFg, but inverted
const ColorBg = tcell.ColorSpecial | 3
var themeMu = sync.RWMutex{}
var theme = map[string]tcell.Color{
// Default values below
// Map these for special uses in code
"ColorBg": ColorBg,
"ColorFg": ColorFg,
"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,
"scrollbar": tcell.ColorWhite,
// Default values below
// Only the 16 Xterm system tcell.Colors are used, because those are the tcell.Colors overrided
// by the user's default terminal theme
// Used for cview.Styles.PrimitiveBackgroundColor
// Set to tcell.ColorDefault because that allows transparent terminals to work
// The rest of this theme assumes that the background is equivalent to black, but
// white colors switched to black later if the background is determined to be white.
//
// Also, this is set to tcell.ColorBlack in config.go if colors are disabled in the config.
"bg": tcell.ColorDefault,
"tab_num": tcell.ColorTeal,
"tab_divider": ColorFg,
"bottombar_label": tcell.ColorTeal,
"bottombar_text": ColorBg,
"bottombar_bg": ColorFg,
"scrollbar": ColorFg,
// Modals
"btn_bg": tcell.ColorNavy, // All modal buttons
"btn_text": tcell.ColorWhite,
"btn_bg": tcell.ColorTeal, // All modal buttons
"btn_text": tcell.ColorWhite, // White instead of ColorFg because background is known to be Teal
"dl_choice_modal_bg": tcell.ColorPurple,
"dl_choice_modal_bg": tcell.ColorOlive,
"dl_choice_modal_text": tcell.ColorWhite,
"dl_modal_bg": tcell.Color130, // xterm:DarkOrange3, #af5f00
"dl_modal_bg": tcell.ColorOlive,
"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_bg": tcell.ColorTeal,
"yesno_modal_text": tcell.ColorWhite,
"tofu_modal_bg": tcell.ColorMaroon,
"tofu_modal_text": tcell.ColorWhite,
"subscription_modal_bg": tcell.Color61, // xterm:SlateBlue3, #5f5faf
"subscription_modal_bg": tcell.ColorTeal,
"subscription_modal_text": tcell.ColorWhite,
"input_modal_bg": tcell.ColorGreen,
"input_modal_text": tcell.ColorWhite,
"input_modal_field_bg": tcell.ColorBlue,
"input_modal_field_bg": tcell.ColorNavy,
"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_bg": tcell.ColorNavy,
"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
"amfora_link": tcell.ColorBlue,
"foreign_link": tcell.ColorPurple,
"link_number": tcell.ColorSilver,
"regular_text": tcell.ColorWhite,
"quote_text": tcell.ColorWhite,
"preformatted_text": tcell.Color229, // xterm:Wheat1, #ffffaf
"list_text": tcell.ColorWhite,
"regular_text": ColorFg,
"quote_text": ColorFg,
"preformatted_text": ColorFg,
"list_text": ColorFg,
}
func SetColor(key string, color tcell.Color) {
themeMu.Lock()
theme[key] = color
// Use truecolor because this is only called with user-set tcell.Colors
// Which should be represented exactly
theme[key] = color.TrueColor()
themeMu.Unlock()
}
// GetColor will return tcell.ColorBlack if there is no color for the provided key.
// GetColor will return tcell.ColorBlack if there is no tcell.Color for the provided key.
func GetColor(key string) tcell.Color {
themeMu.RLock()
defer themeMu.RUnlock()
return theme[key].TrueColor()
color := theme[key]
if color == ColorFg {
if hasDarkTerminalBackground {
return tcell.ColorWhite
}
return tcell.ColorBlack
}
if color == ColorBg {
if hasDarkTerminalBackground {
return tcell.ColorBlack
}
return tcell.ColorWhite
}
return color
}
// 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()
color := theme[key].TrueColor()
// colorToString converts a color to a string for use in a cview tag
func colorToString(color tcell.Color) string {
if color == tcell.ColorDefault {
return "-"
}
if color == ColorFg {
if hasDarkTerminalBackground {
return "white"
}
return "black"
}
if color == ColorBg {
if hasDarkTerminalBackground {
return "black"
}
return "white"
}
if color&tcell.ColorIsRGB == 0 {
// tcell.Color is not RGB/TrueColor, it's a tcell.Color from the default terminal
// theme as set above
// Return a tcell.Color name instead of a hex code, so that cview doesn't use TrueColor
return ColorToColorName[color]
}
// Color set by user, must be respected exactly so hex code is used
return fmt.Sprintf("#%06x", color.Hex())
}
// GetContrastingColor returns ColorBlack if color is brighter than gray
// otherwise returns ColorWhite if color is dimmer than gray
// if color is ColorDefault (undefined luminance) this returns ColorDefault
// GetColorString returns a string that can be used in a cview tcell.Color tag,
// for the given theme key.
// It will return "#000000" if there is no tcell.Color for the provided key.
func GetColorString(key string) string {
themeMu.RLock()
defer themeMu.RUnlock()
return colorToString(theme[key])
}
// GetContrastingColor returns tcell.ColorBlack if tcell.Color is brighter than gray
// otherwise returns tcell.ColorWhite if tcell.Color is dimmer than gray
// if tcell.Color is tcell.ColorDefault (undefined luminance) this returns tcell.ColorDefault
func GetContrastingColor(color tcell.Color) tcell.Color {
if color == tcell.ColorDefault {
// color should never be tcell.ColorDefault
// tcell.Color should never be tcell.ColorDefault
// only config keys which end in bg are allowed to be set to default
// and the only way the argument of this function is set to ColorDefault
// and the only way the argument of this function is set to tcell.ColorDefault
// is if both the text and bg of an element in the UI are set to default
return tcell.ColorDefault
}
@ -128,6 +195,149 @@ func GetTextColor(key, bg string) tcell.Color {
// This happens on focus of a UI element which has a bg of default, in which case
// It return tcell.ColorBlack or tcell.ColorWhite, depending on which is more readable
func GetTextColorString(key, bg string) string {
color := GetTextColor(key, bg)
return fmt.Sprintf("#%06x", color.Hex())
return colorToString(GetTextColor(key, bg))
}
// Inverted version of a tcell map
// https://github.com/gdamore/tcell/blob/v2.3.3/color.go#L845
var ColorToColorName = map[tcell.Color]string{
tcell.ColorBlack: "black",
tcell.ColorMaroon: "maroon",
tcell.ColorGreen: "green",
tcell.ColorOlive: "olive",
tcell.ColorNavy: "navy",
tcell.ColorPurple: "purple",
tcell.ColorTeal: "teal",
tcell.ColorSilver: "silver",
tcell.ColorGray: "gray",
tcell.ColorRed: "red",
tcell.ColorLime: "lime",
tcell.ColorYellow: "yellow",
tcell.ColorBlue: "blue",
tcell.ColorFuchsia: "fuchsia",
tcell.ColorAqua: "aqua",
tcell.ColorWhite: "white",
tcell.ColorAliceBlue: "aliceblue",
tcell.ColorAntiqueWhite: "antiquewhite",
tcell.ColorAquaMarine: "aquamarine",
tcell.ColorAzure: "azure",
tcell.ColorBeige: "beige",
tcell.ColorBisque: "bisque",
tcell.ColorBlanchedAlmond: "blanchedalmond",
tcell.ColorBlueViolet: "blueviolet",
tcell.ColorBrown: "brown",
tcell.ColorBurlyWood: "burlywood",
tcell.ColorCadetBlue: "cadetblue",
tcell.ColorChartreuse: "chartreuse",
tcell.ColorChocolate: "chocolate",
tcell.ColorCoral: "coral",
tcell.ColorCornflowerBlue: "cornflowerblue",
tcell.ColorCornsilk: "cornsilk",
tcell.ColorCrimson: "crimson",
tcell.ColorDarkBlue: "darkblue",
tcell.ColorDarkCyan: "darkcyan",
tcell.ColorDarkGoldenrod: "darkgoldenrod",
tcell.ColorDarkGray: "darkgray",
tcell.ColorDarkGreen: "darkgreen",
tcell.ColorDarkKhaki: "darkkhaki",
tcell.ColorDarkMagenta: "darkmagenta",
tcell.ColorDarkOliveGreen: "darkolivegreen",
tcell.ColorDarkOrange: "darkorange",
tcell.ColorDarkOrchid: "darkorchid",
tcell.ColorDarkRed: "darkred",
tcell.ColorDarkSalmon: "darksalmon",
tcell.ColorDarkSeaGreen: "darkseagreen",
tcell.ColorDarkSlateBlue: "darkslateblue",
tcell.ColorDarkSlateGray: "darkslategray",
tcell.ColorDarkTurquoise: "darkturquoise",
tcell.ColorDarkViolet: "darkviolet",
tcell.ColorDeepPink: "deeppink",
tcell.ColorDeepSkyBlue: "deepskyblue",
tcell.ColorDimGray: "dimgray",
tcell.ColorDodgerBlue: "dodgerblue",
tcell.ColorFireBrick: "firebrick",
tcell.ColorFloralWhite: "floralwhite",
tcell.ColorForestGreen: "forestgreen",
tcell.ColorGainsboro: "gainsboro",
tcell.ColorGhostWhite: "ghostwhite",
tcell.ColorGold: "gold",
tcell.ColorGoldenrod: "goldenrod",
tcell.ColorGreenYellow: "greenyellow",
tcell.ColorHoneydew: "honeydew",
tcell.ColorHotPink: "hotpink",
tcell.ColorIndianRed: "indianred",
tcell.ColorIndigo: "indigo",
tcell.ColorIvory: "ivory",
tcell.ColorKhaki: "khaki",
tcell.ColorLavender: "lavender",
tcell.ColorLavenderBlush: "lavenderblush",
tcell.ColorLawnGreen: "lawngreen",
tcell.ColorLemonChiffon: "lemonchiffon",
tcell.ColorLightBlue: "lightblue",
tcell.ColorLightCoral: "lightcoral",
tcell.ColorLightCyan: "lightcyan",
tcell.ColorLightGoldenrodYellow: "lightgoldenrodyellow",
tcell.ColorLightGray: "lightgray",
tcell.ColorLightGreen: "lightgreen",
tcell.ColorLightPink: "lightpink",
tcell.ColorLightSalmon: "lightsalmon",
tcell.ColorLightSeaGreen: "lightseagreen",
tcell.ColorLightSkyBlue: "lightskyblue",
tcell.ColorLightSlateGray: "lightslategray",
tcell.ColorLightSteelBlue: "lightsteelblue",
tcell.ColorLightYellow: "lightyellow",
tcell.ColorLimeGreen: "limegreen",
tcell.ColorLinen: "linen",
tcell.ColorMediumAquamarine: "mediumaquamarine",
tcell.ColorMediumBlue: "mediumblue",
tcell.ColorMediumOrchid: "mediumorchid",
tcell.ColorMediumPurple: "mediumpurple",
tcell.ColorMediumSeaGreen: "mediumseagreen",
tcell.ColorMediumSlateBlue: "mediumslateblue",
tcell.ColorMediumSpringGreen: "mediumspringgreen",
tcell.ColorMediumTurquoise: "mediumturquoise",
tcell.ColorMediumVioletRed: "mediumvioletred",
tcell.ColorMidnightBlue: "midnightblue",
tcell.ColorMintCream: "mintcream",
tcell.ColorMistyRose: "mistyrose",
tcell.ColorMoccasin: "moccasin",
tcell.ColorNavajoWhite: "navajowhite",
tcell.ColorOldLace: "oldlace",
tcell.ColorOliveDrab: "olivedrab",
tcell.ColorOrange: "orange",
tcell.ColorOrangeRed: "orangered",
tcell.ColorOrchid: "orchid",
tcell.ColorPaleGoldenrod: "palegoldenrod",
tcell.ColorPaleGreen: "palegreen",
tcell.ColorPaleTurquoise: "paleturquoise",
tcell.ColorPaleVioletRed: "palevioletred",
tcell.ColorPapayaWhip: "papayawhip",
tcell.ColorPeachPuff: "peachpuff",
tcell.ColorPeru: "peru",
tcell.ColorPink: "pink",
tcell.ColorPlum: "plum",
tcell.ColorPowderBlue: "powderblue",
tcell.ColorRebeccaPurple: "rebeccapurple",
tcell.ColorRosyBrown: "rosybrown",
tcell.ColorRoyalBlue: "royalblue",
tcell.ColorSaddleBrown: "saddlebrown",
tcell.ColorSalmon: "salmon",
tcell.ColorSandyBrown: "sandybrown",
tcell.ColorSeaGreen: "seagreen",
tcell.ColorSeashell: "seashell",
tcell.ColorSienna: "sienna",
tcell.ColorSkyblue: "skyblue",
tcell.ColorSlateBlue: "slateblue",
tcell.ColorSlateGray: "slategray",
tcell.ColorSnow: "snow",
tcell.ColorSpringGreen: "springgreen",
tcell.ColorSteelBlue: "steelblue",
tcell.ColorTan: "tan",
tcell.ColorThistle: "thistle",
tcell.ColorTomato: "tomato",
tcell.ColorTurquoise: "turquoise",
tcell.ColorViolet: "violet",
tcell.ColorWheat: "wheat",
tcell.ColorWhiteSmoke: "whitesmoke",
tcell.ColorYellowGreen: "yellowgreen",
}

View File

@ -2,6 +2,14 @@
You can use these themes by replacing the `[theme]` section of your [config](https://github.com/makeworld-the-better-one/amfora/wiki/Configuration) with their contents. Some themes won't display properly on terminals that do not have truecolor support.
## Amfora
This is the original Amfora theme we all know and love. From v1.9.0 and onwards, the user's terminal theme is used by default. Use this theme to restore the original Amfora look.
<a href="https://raw.githubusercontent.com/makeworld-the-better-one/amfora/master/demo-large.gif">
<img src="../../demo-large.gif" alt="Demo GIF" width="80%">
</a>
## Nord
Contributed by **[@lokesh-krishna](https://github.com/lokesh-krishna)**.

View File

@ -0,0 +1,50 @@
[theme]
# Only the 256 xterm colors are used, so truecolor support is not needed
bg = "black"
tab_num = "#008787"
tab_divider = "white"
bottombar_label = "#008787"
bottombar_text = "black"
bottombar_bg = "white"
scrollbar = "white"
btn_bg = "#000080"
btn_text = "white"
dl_choice_modal_bg = "#800080"
dl_choice_modal_text = "white"
dl_modal_bg = "#af5f00"
dl_modal_text = "white"
info_modal_bg = "#808080"
info_modal_text = "white"
error_modal_bg = "#800000"
error_modal_text = "white"
yesno_modal_bg = "#800080"
yesno_modal_text = "white"
tofu_modal_bg = "#800000"
tofu_modal_text = "white"
subscription_modal_bg = "#5f5faf"
subscription_modal_text = "white"
input_modal_bg = "#008000"
input_modal_text = "white"
input_modal_field_bg = "#0000ff"
input_modal_field_text = "white"
bkmk_modal_bg = "#008080"
bkmk_modal_text = "white"
bkmk_modal_label = "#ffff00"
bkmk_modal_field_bg = "#0000ff"
bkmk_modal_field_text = "white"
hdg_1 = "#ff0000"
hdg_2 = "#00ff00"
hdg_3 = "#ff00ff"
amfora_link = "#0087ff"
foreign_link = "#8700d7"
link_number = "#c0c0c0"
regular_text = "white"
quote_text = "white"
preformatted_text = "#ffffaf"
list_text = "white"

View File

@ -1,5 +1,16 @@
# This is the default config file.
# It also shows all the default values, if you don't create the file.
# You can edit this file to set your own configuration for Amfora.
# When Amfora updates, defaults may change, but this file on your drive will not.
# You can always get the latest defaults on GitHub.
# https://github.com/makeworld-the-better-one/amfora/blob/master/default-config.toml
# Please also check out the Amfora Wiki for more help
# https://github.com/makeworld-the-better-one/amfora/wiki
# gemini://makeworld.space/amfora-wiki/
# All URL values may omit the scheme and/or port, as well as the beginning double slash
# Valid URL examples:
@ -23,7 +34,7 @@ auto_redirect = false
# If a command is set, than the URL will be added (in quotes) to the end of the command.
# A space will be prepended to the URL.
#
# The best to define a command is using a string array.
# The best way to define a command is using a string array.
# Examples:
# http = ['firefox']
# http = ['custom-browser', '--flag', '--option=2']
@ -71,6 +82,10 @@ page_max_time = 10
# "auto" means the scrollbar only appears when the page is longer than the window.
scrollbar = "auto"
# Underline non-gemini URLs
# This is done to help color blind users
underline = true
[auth]
# Authentication settings
@ -213,19 +228,30 @@ scrollbar = "auto"
[url-handlers]
# Allows setting the commands to run for various URL schemes.
# E.g. to open FTP URLs with FileZilla set the following key:
# ftp = 'filezilla'
# You can set any scheme to "off" or "" to disable handling it, or
# ftp = ['filezilla']
# You can set any scheme to 'off' or '' to disable handling it, or
# just leave the key unset.
#
# DO NOT use this for setting the HTTP command.
# Use the http setting in the "a-general" section above.
#
# NOTE: These settings are overrided by the ones in the proxies section.
#
# The best way to define a command is using a string array.
# Examples:
# magnet = ['transmission']
# foo = ['custom-browser', '--flag', '--option=2']
# tel = ['/path/with spaces/in it/telephone']
#
# Note the use of single quotes, so that backslashes will not be escaped.
# Using just a string will also work, but it is deprecated, and will degrade if
# you use paths with spaces.
# This is a special key that defines the handler for all URL schemes for which
# no handler is defined.
other = 'off'
# It uses the special value 'default', which will try and use the default
# application on your computer for opening this kind of URI.
other = 'default'
# [[mediatype-handlers]] section
@ -350,6 +376,8 @@ entries_per_page = 20
# Colors can be set using a W3C color name, or a hex value such as "#ffffff".
# Setting a background to "default" keeps the terminal default
# If your terminal has transparency, set any background to "default" to keep it transparent
# The key "bg" is already set to "default", but this can be used on other backgrounds,
# like for modals.
# 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

View File

@ -29,7 +29,7 @@ var bkmkCh = make(chan bkmkAction)
var bkmkModalText string // The current text of the input field in the modal
func bkmkInit() {
panels.AddPanel("bkmk", bkmkModal, false, false)
panels.AddPanel(PanelBookmarks, bkmkModal, false, false)
m := bkmkModal
if viper.GetBool("a-general.color") {
@ -111,13 +111,13 @@ func openBkmkModal(name string, exists bool) (string, bkmkAction) {
bkmkModalText = text
})
panels.ShowPanel("bkmk")
panels.SendToFront("bkmk")
panels.ShowPanel(PanelBookmarks)
panels.SendToFront(PanelBookmarks)
App.SetFocus(bkmkModal)
App.Draw()
action := <-bkmkCh
panels.HidePanel("bkmk")
panels.HidePanel(PanelBookmarks)
App.SetFocus(tabs[curTab].view)
App.Draw()

View File

@ -87,7 +87,7 @@ func Init(version, commit, builtBy string) {
}(tabs[curTab])
})
panels.AddPanel("browser", browser, true, true)
panels.AddPanel(PanelBrowser, browser, true, true)
helpInit()
@ -96,8 +96,6 @@ func Init(version, commit, builtBy string) {
layout.AddItem(bottomBar, 1, 1, false)
if viper.GetBool("a-general.color") {
layout.SetBackgroundColor(config.GetColor("bg"))
bottomBar.SetBackgroundColor(config.GetColor("bottombar_bg"))
bottomBar.SetLabelColor(config.GetColor("bottombar_label"))
bottomBar.SetFieldBackgroundColor(config.GetColor("bottombar_bg"))
@ -106,7 +104,7 @@ func Init(version, commit, builtBy string) {
browser.SetTabBackgroundColor(config.GetColor("bg"))
browser.SetTabBackgroundColorFocused(config.GetColor("tab_num"))
browser.SetTabTextColor(config.GetColor("tab_num"))
browser.SetTabTextColorFocused(config.GetTextColor("bg", "tab_num"))
browser.SetTabTextColorFocused(config.GetColor("ColorBg"))
browser.SetTabSwitcherDivider(
"",
fmt.Sprintf("[%s:%s]|[-]", config.GetColorString("tab_divider"), config.GetColorString("bg")),
@ -189,7 +187,6 @@ func Init(version, commit, builtBy string) {
if i <= len(tabs[tab].page.Links) && i > 0 {
// Open new tab and load link
oldTab := tab
NewTab()
// Resolve and follow link manually
prevParsed, _ := url.Parse(tabs[oldTab].page.URL)
nextParsed, err := url.Parse(tabs[oldTab].page.Links[i-1])
@ -198,7 +195,7 @@ func Init(version, commit, builtBy string) {
reset()
return
}
URL(prevParsed.ResolveReference(nextParsed).String())
NewTabWithURL(prevParsed.ResolveReference(nextParsed).String())
return
}
} else {
@ -279,9 +276,16 @@ func Init(version, commit, builtBy string) {
// It's focused on a modal right now, nothing should interrupt
return event
}
_, ok = App.GetFocus().(*cview.Table)
if ok {
frontPanelName, _ := panels.GetFrontPanel()
if frontPanelName == PanelHelp {
// It's focused on help right now
if config.TranslateKeyEvent(event) == config.CmdQuit {
// Allow quit key to work, but nothing else
Stop()
return nil
}
// Pass everything else directly, inhibiting other keybindings
// like for editing the URL
return event
}
@ -330,8 +334,7 @@ func Init(version, commit, builtBy string) {
Error("URL Error", err.Error())
return nil
}
NewTab()
URL(next)
NewTabWithURL(next)
} else {
NewTab()
}
@ -379,6 +382,17 @@ func Stop() {
// NewTab opens a new tab and switches to it, displaying the
// the default empty content because there's no URL.
func NewTab() {
NewTabWithURL("about:newtab")
bottomBar.SetLabel("")
bottomBar.SetText("")
tabs[NumTabs()-1].saveBottomBar()
}
// NewTabWithURL opens a new tab and switches to it, displaying the
// the URL provided.
func NewTabWithURL(url string) {
// Create TextView and change curTab
// Set the TextView options, and the changed func to App.Draw()
// SetDoneFunc to do link highlighting
@ -395,8 +409,16 @@ func NewTab() {
curTab = NumTabs()
tabs = append(tabs, makeNewTab())
temp := newTabPage // Copy
setPage(tabs[curTab], &temp)
var interstitial string
if !strings.HasPrefix(url, "about:") {
interstitial = "Loading " + url + "..."
}
setPage(tabs[curTab], renderPageFromString(interstitial))
// Regardless of the starting URL, about:newtab will
// be the history root.
tabs[curTab].addToHistory("about:newtab")
tabs[curTab].history.pos = 0 // Manually set as first page
@ -408,9 +430,7 @@ func NewTab() {
browser.SetCurrentTab(strconv.Itoa(curTab))
App.SetFocus(tabs[curTab].view)
bottomBar.SetLabel("")
bottomBar.SetText("")
tabs[curTab].saveBottomBar()
URL(url)
// Draw just in case
App.Draw()
@ -531,11 +551,11 @@ func URL(u string) {
func RenderFromString(str string) {
t := tabs[curTab]
page, _ := renderPageFromString(str)
page := renderPageFromString(str)
setPage(t, page)
}
func renderPageFromString(str string) (*structs.Page, bool) {
func renderPageFromString(str string) *structs.Page {
rendered, links := renderer.RenderGemini(str, textWidth(), false)
page := &structs.Page{
Mediatype: structs.TextGemini,
@ -545,7 +565,7 @@ func renderPageFromString(str string) (*structs.Page, bool) {
TermWidth: termW,
}
return page, true
return page
}
func NumTabs() int {

View File

@ -33,8 +33,8 @@ var dlChoiceCh = make(chan string)
var dlModal = cview.NewModal()
func dlInit() {
panels.AddPanel("dl", dlModal, false, false)
panels.AddPanel("dlChoice", dlChoiceModal, false, false)
panels.AddPanel(PanelDownload, dlModal, false, false)
panels.AddPanel(PanelDownloadChoiceModal, dlChoiceModal, false, false)
dlm := dlModal
chm := dlChoiceModal
@ -96,7 +96,7 @@ func dlInit() {
frame.SetTitle(" Download ")
dlm.SetDoneFunc(func(buttonIndex int, buttonLabel string) {
if buttonLabel == "Ok" {
panels.HidePanel("dl")
panels.HidePanel(PanelDownload)
App.SetFocus(tabs[curTab].view)
App.Draw()
}
@ -141,29 +141,29 @@ func dlChoice(text, u string, resp *gemini.Response) {
choice = "Open"
} else {
dlChoiceModal.SetText(text)
panels.ShowPanel("dlChoice")
panels.SendToFront("dlChoice")
panels.ShowPanel(PanelDownloadChoiceModal)
panels.SendToFront(PanelDownloadChoiceModal)
App.SetFocus(dlChoiceModal)
App.Draw()
choice = <-dlChoiceCh
}
if choice == "Download" {
panels.HidePanel("dlChoice")
panels.HidePanel(PanelDownloadChoiceModal)
App.Draw()
downloadURL(config.DownloadsDir, u, resp)
resp.Body.Close() // Only close when the file is downloaded
return
}
if choice == "Open" {
panels.HidePanel("dlChoice")
panels.HidePanel(PanelDownloadChoiceModal)
App.Draw()
open(u, resp)
return
}
// They chose the "Cancel" button
panels.HidePanel("dlChoice")
panels.HidePanel(PanelDownloadChoiceModal)
App.SetFocus(tabs[curTab].view)
App.Draw()
}
@ -200,7 +200,7 @@ func open(u string, resp *gemini.Response) {
return
}
panels.HidePanel("dl")
panels.HidePanel(PanelDownload)
App.SetFocus(tabs[curTab].view)
App.Draw()
@ -267,15 +267,15 @@ func downloadURL(dir, u string, resp *gemini.Response) string {
// Display
dlModal.ClearButtons()
dlModal.AddButtons([]string{"Downloading..."})
panels.ShowPanel("dl")
panels.SendToFront("dl")
panels.ShowPanel(PanelDownload)
panels.SendToFront(PanelDownload)
App.SetFocus(dlModal)
App.Draw()
_, err = io.Copy(io.MultiWriter(f, bar), resp.Body)
done = true
if err != nil {
panels.HidePanel("dl")
panels.HidePanel(PanelDownload)
Error("Download Error", err.Error())
f.Close()
os.Remove(savePath) // Remove partial file

View File

@ -2,6 +2,7 @@ package display
import (
"errors"
"fmt"
"mime"
"net"
"net/url"
@ -16,6 +17,7 @@ import (
"github.com/makeworld-the-better-one/amfora/rr"
"github.com/makeworld-the-better-one/amfora/structs"
"github.com/makeworld-the-better-one/amfora/subscriptions"
"github.com/makeworld-the-better-one/amfora/sysopen"
"github.com/makeworld-the-better-one/amfora/webbrowser"
"github.com/makeworld-the-better-one/go-gemini"
"github.com/spf13/viper"
@ -46,7 +48,7 @@ func handleHTTP(u string, showInfo bool) bool {
}
// Custom command
var err error = nil
var err error
if len(config.HTTPCommand) > 1 {
err = exec.Command(config.HTTPCommand[0], append(config.HTTPCommand[1:], u)...).Start()
} else {
@ -56,6 +58,7 @@ func handleHTTP(u string, showInfo bool) bool {
Error("HTTP Error", "Error executing custom browser command: "+err.Error())
return false
}
Info("Opened with: " + config.HTTPCommand[0])
App.Draw()
return true
@ -68,21 +71,49 @@ func handleOther(u string) {
parsed, _ := url.Parse(u)
// Search for a handler for the URL scheme
handler := strings.TrimSpace(viper.GetString("url-handlers." + parsed.Scheme))
handler := viper.GetStringSlice("url-handlers." + parsed.Scheme)
if len(handler) == 0 {
handler = strings.TrimSpace(viper.GetString("url-handlers.other"))
}
switch handler {
case "", "off":
Error("URL Error", "Opening "+parsed.Scheme+" URLs is turned off.")
default:
// The config has a custom command to execute for URLs
fields := strings.Fields(handler)
err := exec.Command(fields[0], append(fields[1:], u)...).Start()
if err != nil {
Error("URL Error", "Error executing custom command: "+err.Error())
// A string and not a list of strings, use old method of parsing
// #214
handler = strings.Fields(viper.GetString("url-handlers." + parsed.Scheme))
if len(handler) == 0 {
handler = viper.GetStringSlice("url-handlers.other")
if len(handler) == 0 {
handler = strings.Fields(viper.GetString("url-handlers.other"))
}
}
}
if len(handler) == 1 {
// Maybe special key
switch strings.TrimSpace(handler[0]) {
case "", "off":
Error("URL Error", "Opening "+parsed.Scheme+" URLs is turned off.")
return
case "default":
_, err := sysopen.Open(u)
if err != nil {
Error("Application Error", err.Error())
return
}
Info("Opened in default application")
return
}
}
// Custom application command
var err error
if len(handler) > 1 {
err = exec.Command(handler[0], append(handler[1:], u)...).Start()
} else {
err = exec.Command(handler[0], u).Start()
}
if err != nil {
Error("URL Error", "Error executing custom command: "+err.Error())
}
Info("Opened with: " + handler[0])
App.Draw()
}
@ -351,12 +382,14 @@ func handleURL(t *tab, u string, numRedirects int) (string, bool) {
// Could be a non 20 status code, or a different kind of document
// Handle each status code
switch res.Status {
// Except 20, that's handled after the switch
status := gemini.CleanStatus(res.Status)
switch status {
case 10, 11:
var userInput string
var ok bool
if res.Status == 10 {
if status == 10 {
// Regular input
userInput, ok = Input(res.Meta, false)
} else {
@ -380,9 +413,10 @@ func handleURL(t *tab, u string, numRedirects int) (string, bool) {
return ret("", false)
}
redir := parsed.ResolveReference(parsedMeta).String()
justAddsSlash := (redir == u+"/")
// Prompt before redirecting to non-Gemini protocol
redirect := false
if !strings.HasPrefix(redir, "gemini") {
if !justAddsSlash && !strings.HasPrefix(redir, "gemini") {
if YesNo("Follow redirect to non-Gemini URL?\n" + redir) {
redirect = true
} else {
@ -390,9 +424,9 @@ func handleURL(t *tab, u string, numRedirects int) (string, bool) {
}
}
// Prompt before redirecting
autoRedirect := viper.GetBool("a-general.auto_redirect")
autoRedirect := justAddsSlash || viper.GetBool("a-general.auto_redirect")
if redirect || (autoRedirect && numRedirects < 5) || YesNo("Follow redirect?\n"+redir) {
if res.Status == gemini.StatusRedirectPermanent {
if status == gemini.StatusRedirectPermanent {
go cache.AddRedir(u, redir)
}
return ret(handleURL(t, redir, numRedirects+1))
@ -437,6 +471,12 @@ func handleURL(t *tab, u string, numRedirects int) (string, bool) {
case 62:
Error("Certificate Not Valid", escapeMeta(res.Meta))
return ret("", false)
default:
if !gemini.StatusInRange(status) {
// Status code not in a valid range
Error("Status Code Error", fmt.Sprintf("Out of range status code: %d", status))
return ret("", false)
}
}
// Status code 20, but not a document that can be displayed

View File

@ -59,8 +59,8 @@ var helpTable = cview.NewTextView()
// Help displays the help and keybindings.
func Help() {
helpTable.ScrollToBeginning()
panels.ShowPanel("help")
panels.SendToFront("help")
panels.ShowPanel(PanelHelp)
panels.SendToFront(PanelHelp)
App.SetFocus(helpTable)
}
@ -71,7 +71,7 @@ func helpInit() {
helpTable.SetPadding(0, 0, 1, 1)
helpTable.SetDoneFunc(func(key tcell.Key) {
if key == tcell.KeyEsc || key == tcell.KeyEnter {
panels.HidePanel("help")
panels.HidePanel(PanelHelp)
App.SetFocus(tabs[curTab].view)
App.Draw()
}
@ -132,5 +132,5 @@ func helpInit() {
w.Flush()
panels.AddPanel("help", helpTable, true, false)
panels.AddPanel(PanelHelp, helpTable, true, false)
}

View File

@ -18,6 +18,7 @@ import (
var infoModal = cview.NewModal()
var errorModal = cview.NewModal()
var errorModalDone = make(chan struct{})
var inputModal = cview.NewModal()
var inputCh = make(chan string)
@ -35,10 +36,10 @@ func modalInit() {
yesNoModal.AddButtons([]string{"Yes", "No"})
panels.AddPanel("info", infoModal, false, false)
panels.AddPanel("error", errorModal, false, false)
panels.AddPanel("input", inputModal, false, false)
panels.AddPanel("yesno", yesNoModal, false, false)
panels.AddPanel(PanelInfoModal, infoModal, false, false)
panels.AddPanel(PanelErrorModal, errorModal, false, false)
panels.AddPanel(PanelInputModal, inputModal, false, false)
panels.AddPanel(PanelYesNoModal, yesNoModal, false, false)
// Color setup
if viper.GetBool("a-general.color") {
@ -141,7 +142,7 @@ func modalInit() {
frame.SetTitleAlign(cview.AlignCenter)
frame.SetTitle(" Info ")
infoModal.SetDoneFunc(func(buttonIndex int, buttonLabel string) {
panels.HidePanel("info")
panels.HidePanel(PanelInfoModal)
App.SetFocus(tabs[curTab].view)
App.Draw()
})
@ -149,9 +150,10 @@ func modalInit() {
errorModal.SetBorder(true)
errorModal.GetFrame().SetTitleAlign(cview.AlignCenter)
errorModal.SetDoneFunc(func(buttonIndex int, buttonLabel string) {
panels.HidePanel("error")
panels.HidePanel(PanelErrorModal)
App.SetFocus(tabs[curTab].view)
App.Draw()
errorModalDone <- struct{}{}
})
inputModal.SetBorder(true)
@ -196,17 +198,19 @@ func Error(title, text string) {
errorModal.GetFrame().SetTitle(title)
errorModal.SetText(text)
panels.ShowPanel("error")
panels.SendToFront("error")
panels.ShowPanel(PanelErrorModal)
panels.SendToFront(PanelErrorModal)
App.SetFocus(errorModal)
App.Draw()
<-errorModalDone
}
// Info displays some info on the screen in a modal.
func Info(s string) {
infoModal.SetText(s)
panels.ShowPanel("info")
panels.SendToFront("info")
panels.ShowPanel(PanelInfoModal)
panels.SendToFront(PanelInfoModal)
App.SetFocus(infoModal)
App.Draw()
}
@ -236,14 +240,14 @@ func Input(prompt string, sensitive bool) (string, bool) {
}
inputModal.SetText(prompt + " ")
panels.ShowPanel("input")
panels.SendToFront("input")
panels.ShowPanel(PanelInputModal)
panels.SendToFront(PanelInputModal)
App.SetFocus(inputModal)
App.Draw()
resp := <-inputCh
panels.HidePanel("input")
panels.HidePanel(PanelInputModal)
App.SetFocus(tabs[curTab].view)
App.Draw()
@ -272,13 +276,13 @@ func YesNo(prompt string) bool {
}
yesNoModal.GetFrame().SetTitle("")
yesNoModal.SetText(prompt)
panels.ShowPanel("yesno")
panels.SendToFront("yesno")
panels.ShowPanel(PanelYesNoModal)
panels.SendToFront(PanelYesNoModal)
App.SetFocus(yesNoModal)
App.Draw()
resp := <-yesNoCh
panels.HidePanel("yesno")
panels.HidePanel(PanelYesNoModal)
App.SetFocus(tabs[curTab].view)
App.Draw()
return resp
@ -305,18 +309,18 @@ func Tofu(host string, expiry time.Time) bool {
frame.SetTitle(" TOFU ")
m.SetText(
//nolint:lll
fmt.Sprintf("%s's certificate has changed, possibly indicating an security issue. The certificate would have expired %s. Are you sure you want to continue? ",
fmt.Sprintf("%s's certificate has changed, possibly indicating a security issue. The certificate would have expired %s. Are you sure you want to continue? ",
host,
humanize.Time(expiry),
),
)
panels.ShowPanel("yesno")
panels.SendToFront("yesno")
panels.ShowPanel(PanelYesNoModal)
panels.SendToFront(PanelYesNoModal)
App.SetFocus(yesNoModal)
App.Draw()
resp := <-yesNoCh
panels.HidePanel("yesno")
panels.HidePanel(PanelYesNoModal)
App.SetFocus(tabs[curTab].view)
App.Draw()
return resp

14
display/panels.go Normal file
View File

@ -0,0 +1,14 @@
package display
const (
PanelBrowser = "browser"
PanelBookmarks = "bkmk"
PanelDownload = "dl"
PanelDownloadChoiceModal = "dlChoice"
PanelHelp = "help"
PanelYesNoModal = "yesno"
PanelInfoModal = "info"
PanelErrorModal = "error"
PanelInputModal = "input"
)

View File

@ -134,6 +134,9 @@ func goURL(t *tab, u string) {
final, displayed := handleURL(t, u, 0)
if displayed {
t.addToHistory(final)
} else if t.page.URL == "" {
// The tab is showing interstitial or no content. Let's go to about:newtab.
handleAbout(t, "about:newtab")
}
if t == tabs[curTab] {
// Display the bottomBar state that handleURL set

View File

@ -260,13 +260,13 @@ func openSubscriptionModal(validFeed, subscribed bool) bool {
}
}
panels.ShowPanel("yesno")
panels.SendToFront("yesno")
panels.ShowPanel(PanelYesNoModal)
panels.SendToFront(PanelYesNoModal)
App.SetFocus(yesNoModal)
App.Draw()
resp := <-yesNoCh
panels.HidePanel("yesno")
panels.HidePanel(PanelYesNoModal)
App.SetFocus(tabs[curTab].view)
App.Draw()
return resp

View File

@ -27,4 +27,6 @@ Thank you to the following contributors, who have helped make Amfora great. FOSS
* Anas Mohamed (@amohamed11)
* David Jimenez (@dvejmz)
* Michael McDonagh (@m-mcdonagh)
* mooff (@awfulcooking)
* Josias (@justjosias)
`)

7
go.mod
View File

@ -1,18 +1,19 @@
module github.com/makeworld-the-better-one/amfora
go 1.14
go 1.15
require (
code.rocketnine.space/tslocum/cview v1.5.6-0.20210525194531-92dca67ac283
code.rocketnine.space/tslocum/cview v1.5.6-0.20210530175404-7e8817f20bdc
github.com/atotto/clipboard v0.1.4
github.com/dustin/go-humanize v1.0.0
github.com/fsnotify/fsnotify v1.4.9 // indirect
github.com/gdamore/tcell/v2 v2.3.3
github.com/google/go-cmp v0.5.0 // indirect
github.com/makeworld-the-better-one/go-gemini v0.11.0
github.com/makeworld-the-better-one/go-gemini v0.12.1
github.com/mitchellh/go-homedir v1.1.0
github.com/mitchellh/mapstructure v1.3.1 // indirect
github.com/mmcdole/gofeed v1.1.2
github.com/muesli/termenv v0.9.0
github.com/pelletier/go-toml v1.8.0 // indirect
github.com/rkoesters/xdg v0.0.0-20181125232953-edd15b846f9b
github.com/schollz/progressbar/v3 v3.8.0

12
go.sum
View File

@ -12,8 +12,8 @@ cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2k
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
code.rocketnine.space/tslocum/cbind v0.1.5 h1:i6NkeLLNPNMS4NWNi3302Ay3zSU6MrqOT+yJskiodxE=
code.rocketnine.space/tslocum/cbind v0.1.5/go.mod h1:LtfqJTzM7qhg88nAvNhx+VnTjZ0SXBJtxBObbfBWo/M=
code.rocketnine.space/tslocum/cview v1.5.6-0.20210525194531-92dca67ac283 h1:5KBGXdQdfV09eYXOZuFTxqDujndqtRraXj+lyFcxlPk=
code.rocketnine.space/tslocum/cview v1.5.6-0.20210525194531-92dca67ac283/go.mod h1:KBRxzIsj8bfgFpnMpkGVoxsrPUvnQsRnX29XJ2yzB6M=
code.rocketnine.space/tslocum/cview v1.5.6-0.20210530175404-7e8817f20bdc h1:nAcBp7ZCWHpa8fHpynCbULDTAZgPQv28+Z+QnhnFG7E=
code.rocketnine.space/tslocum/cview v1.5.6-0.20210530175404-7e8817f20bdc/go.mod h1:KBRxzIsj8bfgFpnMpkGVoxsrPUvnQsRnX29XJ2yzB6M=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
@ -138,11 +138,13 @@ github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4=
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/makeworld-the-better-one/go-gemini v0.11.0 h1:MNGiULJFvcqls9oCy40tE897hDeKvNmEK9i5kRucgQk=
github.com/makeworld-the-better-one/go-gemini v0.11.0/go.mod h1:F+3x+R1xeYK90jMtBq+U+8Sh64r2dHleDZ/en3YgSmg=
github.com/makeworld-the-better-one/go-gemini v0.12.1 h1:cWHvCHL31Caq3Rm9elCFFoQeyrn92Kv7KummsVxCOFg=
github.com/makeworld-the-better-one/go-gemini v0.12.1/go.mod h1:F+3x+R1xeYK90jMtBq+U+8Sh64r2dHleDZ/en3YgSmg=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.13 h1:qdl+GuBjcsKKDco5BsxPJlId98mSWNKqYA+Co0SC1yA=
github.com/mattn/go-isatty v0.0.13/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
@ -171,6 +173,8 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJ
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/muesli/termenv v0.9.0 h1:wnbOaGz+LUR3jNT0zOzinPnyDaCZUQRZj9GxK8eRVl8=
github.com/muesli/termenv v0.9.0/go.mod h1:R/LzAKf+suGs4IsO95y7+7DpFHO0KABgnZqtlyx2mBw=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=

View File

@ -3,18 +3,42 @@ package logger
// For debugging
import (
"io"
"io/ioutil"
"log"
"os"
)
var Log *log.Logger
var logger *log.Logger
func Init() error {
f, err := os.Create("debug.log")
if err != nil {
return err
func GetLogger() (*log.Logger, error) {
if logger != nil {
return logger, nil
}
Log = log.New(f, "", log.LstdFlags)
Log.Println("Started Log")
return nil
var writer io.Writer
var err error
debugModeEnabled := os.Getenv("AMFORA_DEBUG") == "1"
if debugModeEnabled {
writer, err = os.Create("debug.log")
if err != nil {
return nil, err
}
} else {
// Suppress all logging output if debug mode is disabled
writer = ioutil.Discard
}
logger = log.New(writer, "", log.LstdFlags)
if !debugModeEnabled {
// Clear all flags to skip log output formatting step to increase
// performance somewhat if we're not logging anything
logger.SetFlags(0)
}
logger.Println("Started logger")
return logger, nil
}

View File

@ -159,6 +159,14 @@ func convertRegularGemini(s string, numLinks, width int, proxied bool) (string,
spacing = " "
}
// Underline non-gemini links if enabled
var linkTag string
if viper.GetBool("a-general.underline") {
linkTag = `[` + config.GetColorString("foreign_link") + `::u]`
} else {
linkTag = `[` + config.GetColorString("foreign_link") + `]`
}
// Wrap and add link text
// Wrap the link text, but add some spaces to indent the wrapped lines past the link number
// Set the style tags
@ -166,11 +174,12 @@ func convertRegularGemini(s string, numLinks, width int, proxied bool) (string,
var wrappedLink []string
if viper.GetBool("a-general.color") {
pU, err := urlPkg.Parse(url)
if !proxied && err == nil &&
(pU.Scheme == "" || pU.Scheme == "gemini" || pU.Scheme == "about") {
// A gemini link
pU, err := urlPkg.Parse(url)
if !proxied && err == nil &&
(pU.Scheme == "" || pU.Scheme == "gemini" || pU.Scheme == "about") {
// A gemini link
if viper.GetBool("a-general.color") {
// 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
@ -187,33 +196,50 @@ func convertRegularGemini(s string, numLinks, width int, proxied bool) (string,
`["` + strconv.Itoa(num-1) + `"][` + config.GetColorString("amfora_link") + `]` +
wrappedLink[0] + `[-][""]`
} else {
// Not a gemini link
// No color
wrappedLink = wrapLine(linkText, width,
strings.Repeat(" ", len(strconv.Itoa(num))+4)+ // +4 for spaces and brackets
`["`+strconv.Itoa(num-1)+`"]`,
`[""]`,
false, // Don't indent the first line, it's the one with link number
)
wrappedLink[0] = `[::b][` + strconv.Itoa(num) + "[][::-] " +
`["` + strconv.Itoa(num-1) + `"]` +
wrappedLink[0] + `[""]`
}
} else {
// Not a gemini link
if viper.GetBool("a-general.color") {
// Color
wrappedLink = wrapLine(linkText, width,
strings.Repeat(" ", indent)+
`["`+strconv.Itoa(num-1)+`"][`+config.GetColorString("foreign_link")+`]`,
`[-][""]`,
`["`+strconv.Itoa(num-1)+`"]`+linkTag,
`[-::-][""]`,
false, // Don't indent the first line, it's the one with link number
)
wrappedLink[0] = fmt.Sprintf(`[%s::b][`, config.GetColorString("link_number")) +
strconv.Itoa(num) + "[]" + "[-::-]" + spacing +
`["` + strconv.Itoa(num-1) + `"][` + config.GetColorString("foreign_link") + `]` +
wrappedLink[0] + `[-][""]`
strconv.Itoa(num) + "[][-::-]" + spacing +
`["` + strconv.Itoa(num-1) + `"]` + linkTag +
wrappedLink[0] + `[-::-][""]`
} else {
// No color
wrappedLink = wrapLine(linkText, width,
strings.Repeat(" ", indent)+
`["`+strconv.Itoa(num-1)+`"]`+linkTag,
`[::-][""]`,
false, // Don't indent the first line, it's the one with link number
)
wrappedLink[0] = `[::b][` + strconv.Itoa(num) + "[][::-]" + spacing +
`["` + strconv.Itoa(num-1) + `"]` + linkTag +
wrappedLink[0] + `[::-][""]`
}
} else {
// No colors allowed
wrappedLink = wrapLine(linkText, width,
strings.Repeat(" ", len(strconv.Itoa(num))+4)+ // +4 for spaces and brackets
`["`+strconv.Itoa(num-1)+`"]`,
`[""]`,
false, // Don't indent the first line, it's the one with link number
)
wrappedLink[0] = `[::b][` + strconv.Itoa(num) + "[][::-] " +
`["` + strconv.Itoa(num-1) + `"]` +
wrappedLink[0] + `[""]`
}
wrappedLines = append(wrappedLines, wrappedLink...)

View File

@ -251,7 +251,9 @@ func getResource(url string) (string, *gemini.Response, error) {
return url, nil, err
}
if res.Status == gemini.StatusSuccess {
status := gemini.CleanStatus(res.Status)
if status == gemini.StatusSuccess {
// No redirects
return url, res, nil
}
@ -266,8 +268,8 @@ func getResource(url string) (string, *gemini.Response, error) {
urls := make([]*urlPkg.URL, 0)
// Loop through redirects
for (res.Status == gemini.StatusRedirectPermanent || res.Status == gemini.StatusRedirectTemporary) && i < 5 {
redirs = append(redirs, res.Status)
for (status == gemini.StatusRedirectPermanent || status == gemini.StatusRedirectTemporary) && i < 5 {
redirs = append(redirs, status)
urls = append(urls, parsed)
tmp, err := parsed.Parse(res.Meta)
@ -302,7 +304,7 @@ func getResource(url string) (string, *gemini.Response, error) {
if i < 5 {
// The server stopped redirecting after <5 redirects
if res.Status == gemini.StatusSuccess {
if status == gemini.StatusSuccess {
// It ended by succeeding
for j := range redirs {

View File

@ -1,3 +1,4 @@
//go:build darwin
// +build darwin
package sysopen

View File

@ -1,3 +1,4 @@
//go:build !linux && !darwin && !windows && !freebsd && !netbsd && !openbsd
// +build !linux,!darwin,!windows,!freebsd,!netbsd,!openbsd
package sysopen
@ -7,5 +8,5 @@ import "fmt"
// Open opens `path` in default system viewer, but not on this OS.
func Open(path string) (string, error) {
return "", fmt.Errorf("unsupported OS for default system viewer. " +
"Set a catch-all [[mediatype-handlers]] command in the config")
"Set a catch-all command in the config")
}

View File

@ -1,3 +1,4 @@
//go:build linux || freebsd || netbsd || openbsd
// +build linux freebsd netbsd openbsd
//nolint:goerr113
@ -20,7 +21,7 @@ func Open(path string) (string, error) {
switch {
case xorgDisplay == "" && waylandDisplay == "":
return "", fmt.Errorf("no display server was found. " +
"You may set a default [[mediatype-handlers]] command in the config")
"You may set a default command in the config")
case xdgOpenNotFoundErr == nil:
// Use start rather than run or output in order
// to make application run in background.
@ -30,6 +31,6 @@ func Open(path string) (string, error) {
return "Opened in default system viewer", nil
default:
return "", fmt.Errorf("could not determine default system viewer. " +
"Set a catch-all [[mediatype-handlers]] command in the config")
"Set a catch-all command in the config")
}
}

View File

@ -1,3 +1,4 @@
//go:build windows && (!linux || !darwin || !freebsd || !netbsd || !openbsd)
// +build windows
// +build !linux !darwin !freebsd !netbsd !openbsd

View File

@ -1,3 +1,4 @@
//go:build darwin
// +build darwin
package webbrowser

View File

@ -1,3 +1,4 @@
//go:build !linux && !darwin && !windows && !freebsd && !netbsd && !openbsd
// +build !linux,!darwin,!windows,!freebsd,!netbsd,!openbsd
package webbrowser

View File

@ -1,3 +1,4 @@
//go:build linux || freebsd || netbsd || openbsd
// +build linux freebsd netbsd openbsd
//nolint:goerr113

View File

@ -1,3 +1,4 @@
//go:build windows && (!linux || !darwin || !freebsd || !netbsd || !openbsd)
// +build windows
// +build !linux !darwin !freebsd !netbsd !openbsd