1
0
mirror of https://github.com/makew0rld/amfora.git synced 2024-06-01 18:31:08 +00:00

Merge branch 'commands' of github.com:mntn-xyz/amfora into commands

This commit is contained in:
mntn 2022-01-01 20:13:55 -05:00
commit 416f4b300c
23 changed files with 251 additions and 81 deletions

View File

@ -4,6 +4,20 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
### Added
- Syntax highlighting for preformatted text blocks with alt text (#252, #263, [wiki page](https://github.com/makeworld-the-better-one/amfora/wiki/Source-Code-Highlighting))
### Changed
- Center text automatically, removing `left_margin` from the config (#233)
- `max_width` defaults to 80 columns instead of 100 (#233)
### Fixed
- Modal can't be closed when opening non-gemini text URLs from the commandline (#283, #284)
- External programs started by Amfora remain as zombie processes (#219)
- Prevent link lines (and other types) from being wider than the `max_width` setting (#280)
## [1.9.2] - 2021-12-10
### Fixed
- Preformatted text color showing even when `color = false` (bug since v1.8.0 at least) (#278)

View File

@ -142,7 +142,8 @@ Features in *italics* are in the master branch, but not in the latest release.
- So is subscribing to a page, to know when it changes
- [x] Open non-text files in another application
- [x] Ability to stream content instead of downloading it first
- [x] Run custom commands using the current or selected URL as an argument
- [x] *Highlighting of preformatted code blocks that list a language in the alt text*
- [x] *Run custom commands using the current or selected URL as an argument*
- [ ] Stream support
- [ ] Table of contents for pages
- [ ] Search in pages with <kbd>Ctrl-F</kbd>
@ -163,6 +164,7 @@ Amfora ❤️ open source!
- [progressbar](https://github.com/schollz/progressbar)
- [go-humanize](https://github.com/dustin/go-humanize)
- [gofeed](https://github.com/mmcdole/gofeed)
- [chroma](https://github.com/alecthomas/chroma) for source code syntax highlighting
- [clipboard](https://github.com/atotto/clipboard)
- [termenv](https://github.com/muesli/termenv)

View File

@ -25,3 +25,4 @@ Thank you to the following contributors, who have helped make Amfora great. FOSS
* Michael McDonagh (@m-mcdonagh)
* mooff (@awfulcooking)
* Josias (@justjosias)
* mntn (@mntn-xyz)

View File

@ -196,10 +196,11 @@ func Init() error {
viper.SetDefault("a-general.search", "gemini://geminispace.info/search")
viper.SetDefault("a-general.color", true)
viper.SetDefault("a-general.ansi", true)
viper.SetDefault("a-general.highlight_code", true)
viper.SetDefault("a-general.highlight_style", "monokai")
viper.SetDefault("a-general.bullets", true)
viper.SetDefault("a-general.show_link", false)
viper.SetDefault("a-general.left_margin", 0.15)
viper.SetDefault("a-general.max_width", 100)
viper.SetDefault("a-general.max_width", 80)
viper.SetDefault("a-general.downloads", "")
viper.SetDefault("a-general.temp_downloads", "")
viper.SetDefault("a-general.page_max_size", 2097152)

View File

@ -58,17 +58,20 @@ color = true
# Whether ANSI color codes from the page content should be rendered
ansi = true
# Whether or not to support source code highlighting in preformatted blocks based on alt text
highlight_code = true
# Which highlighting style to use (see https://xyproto.github.io/splash/docs/)
highlight_style = "monokai"
# Whether to replace list asterisks with unicode bullets
bullets = true
# Whether to show link after link text
show_link = false
# A number from 0 to 1, indicating what percentage of the terminal width the left margin should take up.
left_margin = 0.15
# The max number of columns to wrap a page's text to. Preformatted blocks are not wrapped.
max_width = 100
max_width = 80
# 'downloads' is the path to a downloads folder.
# An empty value means the code will find the default downloads folder for your system.

View File

@ -55,17 +55,20 @@ color = true
# Whether ANSI color codes from the page content should be rendered
ansi = true
# Whether or not to support source code highlighting in preformatted blocks based on alt text
highlight_code = true
# Which highlighting style to use (see https://xyproto.github.io/splash/docs/)
highlight_style = "monokai"
# Whether to replace list asterisks with unicode bullets
bullets = true
# Whether to show link after link text
show_link = false
# A number from 0 to 1, indicating what percentage of the terminal width the left margin should take up.
left_margin = 0.15
# The max number of columns to wrap a page's text to. Preformatted blocks are not wrapped.
max_width = 100
max_width = 80
# 'downloads' is the path to a downloads folder.
# An empty value means the code will find the default downloads folder for your system.

View File

@ -15,6 +15,7 @@ import (
"github.com/makeworld-the-better-one/amfora/renderer"
"github.com/makeworld-the-better-one/amfora/structs"
"github.com/makeworld-the-better-one/go-gemini"
"github.com/muesli/termenv"
"github.com/spf13/viper"
)
@ -60,6 +61,18 @@ var App = cview.NewApplication()
func Init(version, commit, builtBy string) {
aboutInit(version, commit, builtBy)
// Detect terminal colors for syntax highlighting
switch termenv.ColorProfile() {
case termenv.TrueColor:
renderer.TermColor = "terminal16m"
case termenv.ANSI256:
renderer.TermColor = "terminal256"
case termenv.ANSI:
renderer.TermColor = "terminal16"
case termenv.Ascii:
renderer.TermColor = ""
}
App.EnableMouse(false)
App.SetRoot(layout, true)
App.SetAfterResizeFunc(func(width int, height int) {
@ -228,7 +241,7 @@ func Init(version, commit, builtBy string) {
}
if i <= len(tabs[tab].page.Links) && i > 0 {
// It's a valid link number
followLink(tabs[tab], tabs[tab].page.URL, tabs[tab].page.Links[i-1])
go followLink(tabs[tab], tabs[tab].page.URL, tabs[tab].page.Links[i-1])
return
}
// Invalid link number, don't do anything

View File

@ -191,6 +191,8 @@ func open(u string, resp *gemini.Response) {
Error("File Opening Error", "Error executing custom command: "+err.Error())
return
}
//nolint:errcheck
go proc.Wait() // Prevent zombies, see #219
Info("Opened with " + cmd[0])
return
}
@ -214,11 +216,14 @@ func open(u string, resp *gemini.Response) {
Info("Opened in default system viewer")
} else {
cmd := mediaHandler.Cmd
err := exec.Command(cmd[0], append(cmd[1:], path)...).Start()
proc := exec.Command(cmd[0], append(cmd[1:], path)...)
err := proc.Start()
if err != nil {
Error("File Opening Error", "Error executing custom command: "+err.Error())
return
}
//nolint:errcheck
go proc.Wait() // Prevent zombies, see #219
Info("Opened with " + cmd[0])
}
App.Draw()

View File

@ -48,16 +48,19 @@ func handleHTTP(u string, showInfo bool) bool {
}
// Custom command
var err error
var proc *exec.Cmd
if len(config.HTTPCommand) > 1 {
err = exec.Command(config.HTTPCommand[0], append(config.HTTPCommand[1:], u)...).Start()
proc = exec.Command(config.HTTPCommand[0], append(config.HTTPCommand[1:], u)...)
} else {
err = exec.Command(config.HTTPCommand[0], u).Start()
proc = exec.Command(config.HTTPCommand[0], u)
}
err := proc.Start()
if err != nil {
Error("HTTP Error", "Error executing custom browser command: "+err.Error())
return false
}
//nolint:errcheck
go proc.Wait() // Prevent zombies, see #219
Info("Opened with: " + config.HTTPCommand[0])
App.Draw()
@ -104,15 +107,18 @@ func handleOther(u string) {
// Custom application command
var err error
var proc *exec.Cmd
if len(handler) > 1 {
err = exec.Command(handler[0], append(handler[1:], u)...).Start()
proc = exec.Command(handler[0], append(handler[1:], u)...)
} else {
err = exec.Command(handler[0], u).Start()
proc = exec.Command(handler[0], u)
}
err := proc.Start()
if err != nil {
Error("URL Error", "Error executing custom command: "+err.Error())
}
//nolint:errcheck
go proc.Wait() // Prevent zombies, see #219
Info("Opened with: " + handler[0])
App.Draw()
}
@ -352,7 +358,7 @@ func handleURL(t *tab, u string, numRedirects int) (string, bool) {
// Disable read timeout and go back to start
res.SetReadTimeout(0) //nolint: errcheck
res.Body.(*rr.RestartReader).Restart()
go dlChoice("That page is too large. What would you like to do?", u, res)
dlChoice("That page is too large. What would you like to do?", u, res)
return ret("", false)
}
if errors.Is(err, renderer.ErrTimedOut) {
@ -360,7 +366,7 @@ func handleURL(t *tab, u string, numRedirects int) (string, bool) {
// Disable read timeout and go back to start
res.SetReadTimeout(0) //nolint: errcheck
res.Body.(*rr.RestartReader).Restart()
go dlChoice("Loading that page timed out. What would you like to do?", u, res)
dlChoice("Loading that page timed out. What would you like to do?", u, res)
return ret("", false)
}
if err != nil {
@ -493,7 +499,7 @@ func handleURL(t *tab, u string, numRedirects int) (string, bool) {
// Disable read timeout and go back to start
res.SetReadTimeout(0) //nolint: errcheck
res.Body.(*rr.RestartReader).Restart()
go dlChoice("That file could not be displayed. What would you like to do?", u, res)
dlChoice("That file could not be displayed. What would you like to do?", u, res)
}
}()
return ret("", false)
@ -503,6 +509,6 @@ func handleURL(t *tab, u string, numRedirects int) (string, bool) {
// Disable read timeout and go back to start
res.SetReadTimeout(0) //nolint: errcheck
res.Body.(*rr.RestartReader).Restart()
go dlChoice("That file could not be displayed. What would you like to do?", u, res)
dlChoice("That file could not be displayed. What would you like to do?", u, res)
return ret("", false)
}

View File

@ -16,24 +16,21 @@ import (
// The bookmark modal is in bookmarks.go
var infoModal = cview.NewModal()
var errorModal = cview.NewModal()
var errorModalDone = make(chan struct{})
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()
// Channel to receive yesNo answer on
var inputCh = make(chan string)
var yesNoCh = make(chan bool)
var inputModalText string // The current text of the input field in the modal
// Internal channel used to know when a modal has been dismissed
var modalDone = make(chan struct{})
func modalInit() {
infoModal.AddButtons([]string{"Ok"})
errorModal.AddButtons([]string{"Ok"})
yesNoModal.AddButtons([]string{"Yes", "No"})
panels.AddPanel(PanelInfoModal, infoModal, false, false)
@ -145,6 +142,7 @@ func modalInit() {
panels.HidePanel(PanelInfoModal)
App.SetFocus(tabs[curTab].view)
App.Draw()
modalDone <- struct{}{}
})
errorModal.SetBorder(true)
@ -153,7 +151,7 @@ func modalInit() {
panels.HidePanel(PanelErrorModal)
App.SetFocus(tabs[curTab].view)
App.Draw()
errorModalDone <- struct{}{}
modalDone <- struct{}{}
})
inputModal.SetBorder(true)
@ -183,7 +181,7 @@ func modalInit() {
dlInit()
}
// Error displays an error on the screen in a modal.
// Error displays an error on the screen in a modal, and blocks until dismissed by the user.
func Error(title, text string) {
if text == "" {
text = "No additional information."
@ -203,19 +201,21 @@ func Error(title, text string) {
App.SetFocus(errorModal)
App.Draw()
<-errorModalDone
<-modalDone
}
// Info displays some info on the screen in a modal.
// Info displays some info on the screen in a modal, and blocks until dismissed by the user.
func Info(s string) {
infoModal.SetText(s)
panels.ShowPanel(PanelInfoModal)
panels.SendToFront(PanelInfoModal)
App.SetFocus(infoModal)
App.Draw()
<-modalDone
}
// Input pulls up a modal that asks for input, and returns the user's input.
// Input pulls up a modal that asks for input, waits for that input, and returns it.
// It returns an bool indicating if the user chose to send input or not.
func Input(prompt string, sensitive bool) (string, bool) {
// Remove elements and re-add them - to clear input text and keep input in focus
@ -257,7 +257,7 @@ func Input(prompt string, sensitive bool) (string, bool) {
return resp, true
}
// YesNo displays a modal asking a yes-or-no question.
// YesNo displays a modal asking a yes-or-no question, waits for an answer, then returns it as a bool.
func YesNo(prompt string) bool {
if viper.GetBool("a-general.color") {
m := yesNoModal
@ -289,7 +289,7 @@ func YesNo(prompt string) bool {
}
// Tofu displays the TOFU warning modal.
// It returns a bool indicating whether the user wants to continue.
// It blocks then returns a bool indicating whether the user wants to continue.
func Tofu(host string, expiry time.Time) bool {
// Reuses yesNoModal, with error color

View File

@ -1,5 +1,8 @@
package display
// This file contains the functions that aren't part of the public API.
// The funcs are for network and displaying.
import (
"net/url"
"strconv"
@ -9,17 +12,20 @@ import (
"github.com/makeworld-the-better-one/amfora/structs"
)
// This file contains the functions that aren't part of the public API.
// The funcs are for network and displaying.
// followLink should be used when the user "clicks" a link on a page.
// Not when a URL is opened on a new tab for the first time.
// It will handle setting the bottomBar.
// followLink should be used when the user "clicks" a link on a page,
// but not when a URL is opened on a new tab for the first time.
//
// It will handle updating the bottomBar.
//
// It should be called with the `go` keyword to spawn a new goroutine if
// it would otherwise block the UI loop, such as when called from an input
// handler.
//
// It blocks until navigation is finished, and we've completed any user
// interaction related to loading the URL (such as info, error modals)
func followLink(t *tab, prev, next string) {
if strings.HasPrefix(next, "about:") {
if final, ok := handleAbout(t, next); ok {
t.addToHistory(final)
}
goURL(t, next)
return
}
@ -29,7 +35,7 @@ func followLink(t *tab, prev, next string) {
Error("URL Error", err.Error())
return
}
go goURL(t, nextURL)
goURL(t, nextURL)
return
}
// No content on current tab, so the "prev" URL is not valid.
@ -39,7 +45,7 @@ func followLink(t *tab, prev, next string) {
Error("URL Error", "Link URL could not be parsed")
return
}
go goURL(t, next)
goURL(t, next)
}
// reformatPage will take the raw page content and reformat it according to the current terminal dimensions.

View File

@ -86,7 +86,7 @@ func makeNewTab() *tab {
linkN, _ := strconv.Atoi(currentSelection[0])
tabs[tab].page.Selected = tabs[tab].page.Links[linkN]
tabs[tab].page.SelectedID = currentSelection[0]
followLink(tabs[tab], tabs[tab].page.URL, tabs[tab].page.Links[linkN])
go followLink(tabs[tab], tabs[tab].page.URL, tabs[tab].page.Links[linkN])
return
}
if len(currentSelection) == 0 && (key == tcell.KeyEnter || key == tcell.KeyTab) {
@ -156,12 +156,12 @@ func makeNewTab() *tab {
if t.hasContent() {
savePath, err := downloadPage(t.page)
if err != nil {
Error("Download Error", fmt.Sprintf("Error saving page content: %v", err))
go Error("Download Error", fmt.Sprintf("Error saving page content: %v", err))
} else {
Info(fmt.Sprintf("Page content saved to %s. ", savePath))
go Info(fmt.Sprintf("Page content saved to %s. ", savePath))
}
} else {
Info("The current page has no content, so it couldn't be downloaded.")
go Info("The current page has no content, so it couldn't be downloaded.")
}
return nil
case config.CmdBack:
@ -178,7 +178,7 @@ func makeNewTab() *tab {
currentURL := tabs[curTab].page.URL
err := clipboard.WriteAll(currentURL)
if err != nil {
Error("Copy Error", err.Error())
go Error("Copy Error", err.Error())
return nil
}
return nil
@ -193,14 +193,14 @@ func makeNewTab() *tab {
if err != nil {
err := clipboard.WriteAll(selectedURL)
if err != nil {
Error("Copy Error", err.Error())
go Error("Copy Error", err.Error())
return nil
}
return nil
}
err = clipboard.WriteAll(copiedURL.String())
if err != nil {
Error("Copy Error", err.Error())
go Error("Copy Error", err.Error())
return nil
}
return nil
@ -209,7 +209,7 @@ func makeNewTab() *tab {
if cmd >= config.CmdLink1 && cmd <= config.CmdLink0 {
if int(cmd) <= len(t.page.Links) {
// It's a valid link number
followLink(&t, t.page.URL, t.page.Links[cmd-1])
go followLink(&t, t.page.URL, t.page.Links[cmd-1])
return nil
}
}

View File

@ -29,4 +29,5 @@ Thank you to the following contributors, who have helped make Amfora great. FOSS
* Michael McDonagh (@m-mcdonagh)
* mooff (@awfulcooking)
* Josias (@justjosias)
* mntn (@mntn-xyz)
`)

View File

@ -63,7 +63,14 @@ func isValidTab(t *tab) bool {
}
func leftMargin() int {
return int(float64(termW) * viper.GetFloat64("a-general.left_margin"))
// Return the left margin size that centers the text, assuming it's the max width
// https://github.com/makeworld-the-better-one/amfora/issues/233
lm := (termW - viper.GetInt("a-general.max_width")) / 2
if lm < 0 {
return 0
}
return lm
}
func textWidth() int {
@ -73,13 +80,11 @@ func textWidth() int {
return viper.GetInt("a-general.max_width")
}
rightMargin := leftMargin()
if leftMargin() > 10 {
// 10 is the max right margin
rightMargin = 10
}
// Subtract left and right margin from total width to get text width
// Left and right margin are equal because text is automatically centered, see:
// https://github.com/makeworld-the-better-one/amfora/issues/233
max := termW - leftMargin() - rightMargin
max := termW - leftMargin()*2
if max < viper.GetInt("a-general.max_width") {
return max
}

1
go.mod
View File

@ -4,6 +4,7 @@ go 1.15
require (
code.rocketnine.space/tslocum/cview v1.5.6-0.20210530175404-7e8817f20bdc
github.com/alecthomas/chroma v0.9.2
github.com/atotto/clipboard v0.1.4
github.com/dustin/go-humanize v1.0.0
github.com/fsnotify/fsnotify v1.4.9 // indirect

20
go.sum
View File

@ -21,6 +21,15 @@ github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/PuerkitoBio/goquery v1.5.1 h1:PSPBGne8NIUWw+/7vFBV+kG2J/5MOjbzc7154OaKCSE=
github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=
github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38 h1:smF2tmSOzy2Mm+0dGI2AIUHY+w0BUc+4tn40djz7+6U=
github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38/go.mod h1:r7bzyVFMNntcxPZXK3/+KdruV1H5KSlyVY0gc+NgInI=
github.com/alecthomas/chroma v0.9.2 h1:yU1sE2+TZbLIQPMk30SolL2Hn53SR/Pv750f7qZ/XMs=
github.com/alecthomas/chroma v0.9.2/go.mod h1:eMuEnpA18XbG/WhOWtCzJHS7WqEtDAI+HxdwoW0nVSk=
github.com/alecthomas/colour v0.0.0-20160524082231-60882d9e2721 h1:JHZL0hZKJ1VENNfmXvHbgYlbUOvpzYzvy2aZU5gXVeo=
github.com/alecthomas/colour v0.0.0-20160524082231-60882d9e2721/go.mod h1:QO9JBoKquHd+jz9nshCh40fOfO+JzsoXy8qTHF68zU0=
github.com/alecthomas/kong v0.2.4/go.mod h1:kQOmtJgV+Lb4aj+I2LEn40cbtawdWJ9Y8QLq+lElKxE=
github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897 h1:p9Sln00KOTlrYkxI1zYWl1QLnEqAqEARBEYa8FQnQcY=
github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897/go.mod h1:xTS7Pm1pD1mvyM075QCDSRqH6qRLXylzS24ZTpRiSzQ=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/andybalholm/cascadia v1.1.0 h1:BuuO6sSfQNFRu1LppgbD25Hr2vLYW25JvxHs5zzsLTo=
@ -42,11 +51,15 @@ github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3Ee
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964 h1:y5HC9v93H5EPKqaS1UYVg1uYah5Xf51mBfIoWehClUQ=
github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964/go.mod h1:Xd9hchkHSWYkEqJwUGisez3G1QY8Ryz0sdWrLPMGjLk=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
github.com/dlclark/regexp2 v1.4.0 h1:F1rxgk7p4uKjwIQxBs9oAXe5CqrXlCduYEJvrF4u93E=
github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
@ -141,7 +154,9 @@ github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czP
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-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
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=
@ -183,6 +198,7 @@ github.com/pelletier/go-toml v1.8.0 h1:Keo9qb7iRJs2voHvunFtuuYFsbWeOBh8/P9v/kVMF
github.com/pelletier/go-toml v1.8.0/go.mod h1:D6yutnOGMveHEPV7VQOuvI/gXY61bv+9bAOTRnLElKs=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
@ -207,6 +223,8 @@ github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb
github.com/schollz/progressbar/v3 v3.8.0 h1:BKyefEMgFBDbo+JaeqHcm/9QdSj8qG8sUY+6UppGpnw=
github.com/schollz/progressbar/v3 v3.8.0/go.mod h1:Y9mmL2knZj3LUaBDyBEzFdPrymIr08hnlFMZmfxwbx4=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
@ -314,6 +332,8 @@ golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200413165638-669c56c373c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210223095934-7937bea0104d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210309040221-94ec62e08169/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=

View File

@ -5,6 +5,7 @@
package renderer
import (
"bytes"
"fmt"
urlPkg "net/url"
"regexp"
@ -12,13 +13,25 @@ import (
"strings"
"code.rocketnine.space/tslocum/cview"
"github.com/alecthomas/chroma/formatters"
"github.com/alecthomas/chroma/lexers"
"github.com/alecthomas/chroma/styles"
"github.com/makeworld-the-better-one/amfora/config"
"github.com/spf13/viper"
)
// Terminal color information, set during display initialization by display/display.go
var TermColor string
// Regex for identifying ANSI color codes
var ansiRegex = regexp.MustCompile(`\x1b\[[0-9;]*m`)
// Regex for identifying possible language string, based on RFC 6838 and lexers used by Chroma
var langRegex = regexp.MustCompile(`^([a-zA-Z0-9]+/)?[a-zA-Z0-9]+([a-zA-Z0-9!_\#\$\&\-\^\.\+]+)*`)
// Regex for removing trailing newline (without disturbing ANSI codes) from code formatted with Chroma
var trailingNewline = regexp.MustCompile(`(\r?\n)(?:\x1b\[[0-9;]*m)*$`)
// RenderANSI renders plain text pages containing ANSI codes.
// Practically, it is used for the text/x-ansi.
func RenderANSI(s string) string {
@ -45,6 +58,10 @@ func RenderPlainText(s string) string {
//
// Set includeFirst to true if the prefix and suffix should be applied to the first wrapped line as well
func wrapLine(line string, width int, prefix, suffix string, includeFirst bool) []string {
if width < 1 {
width = 1
}
// Anonymous function to allow recovery from potential WordWrap panic
var ret []string
func() {
@ -183,7 +200,7 @@ func convertRegularGemini(s string, numLinks, width int, proxied bool) (string,
// 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,
wrappedLink = wrapLine(linkText, width-indent,
strings.Repeat(" ", indent)+
`["`+strconv.Itoa(num-1)+`"][`+config.GetColorString("amfora_link")+`]`,
`[-][""]`,
@ -198,8 +215,8 @@ func convertRegularGemini(s string, numLinks, width int, proxied bool) (string,
} else {
// No color
wrappedLink = wrapLine(linkText, width,
strings.Repeat(" ", len(strconv.Itoa(num))+4)+ // +4 for spaces and brackets
wrappedLink = wrapLine(linkText, width-indent,
strings.Repeat(" ", indent)+ // +4 for spaces and brackets
`["`+strconv.Itoa(num-1)+`"]`,
`[""]`,
false, // Don't indent the first line, it's the one with link number
@ -215,7 +232,7 @@ func convertRegularGemini(s string, numLinks, width int, proxied bool) (string,
if viper.GetBool("a-general.color") {
// Color
wrappedLink = wrapLine(linkText, width,
wrappedLink = wrapLine(linkText, width-indent,
strings.Repeat(" ", indent)+
`["`+strconv.Itoa(num-1)+`"]`+linkTag,
`[-::-][""]`,
@ -229,7 +246,7 @@ func convertRegularGemini(s string, numLinks, width int, proxied bool) (string,
} else {
// No color
wrappedLink = wrapLine(linkText, width,
wrappedLink = wrapLine(linkText, width-indent,
strings.Repeat(" ", indent)+
`["`+strconv.Itoa(num-1)+`"]`,
`[::-][""]`,
@ -248,7 +265,8 @@ func convertRegularGemini(s string, numLinks, width int, proxied bool) (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,
wrappedItem := wrapLine(lines[i][1:],
width-4, // Subtract the 4 indent spaces
fmt.Sprintf(" [%s]", config.GetColorString("list_text")),
"[-]", false)
// Add bullet
@ -256,7 +274,8 @@ func convertRegularGemini(s string, numLinks, width int, proxied bool) (string,
wrappedItem[0] + "[-]"
wrappedLines = append(wrappedLines, wrappedItem...)
} else {
wrappedItem := wrapLine(lines[i][1:], width,
wrappedItem := wrapLine(lines[i][1:],
width-4, // Subtract the 4 indent spaces
fmt.Sprintf(" [%s]", config.GetColorString("list_text")),
"[-]", false)
// Add "*"
@ -277,7 +296,9 @@ func convertRegularGemini(s string, numLinks, width int, proxied bool) (string,
lines[i] = strings.TrimPrefix(lines[i], ">")
lines[i] = strings.TrimPrefix(lines[i], " ")
wrappedLines = append(wrappedLines,
wrapLine(lines[i], width, fmt.Sprintf("[%s::i]> ", config.GetColorString("quote_text")),
wrapLine(lines[i],
width-2, // Subtract 2 for width of prefix string
fmt.Sprintf("[%s::i]> ", config.GetColorString("quote_text")),
"[-::-]", true)...,
)
}
@ -315,11 +336,46 @@ func RenderGemini(s string, width int, proxied bool) (string, []string) {
pre := false
buf := "" // Block of regular or preformatted lines
// Language, formatter, and style for syntax highlighting
lang := ""
formatterName := TermColor
styleName := viper.GetString("a-general.highlight_style")
// processPre is for rendering preformatted blocks
processPre := func() {
syntaxHighlighted := false
// Perform syntax highlighting if language is set
if lang != "" {
style := styles.Get(styleName)
if style == nil {
style = styles.Fallback
}
formatter := formatters.Get(formatterName)
if formatter == nil {
formatter = formatters.Fallback
}
lexer := lexers.Get(lang)
if lexer == nil {
lexer = lexers.Fallback
}
// Tokenize and format the text after stripping ANSI codes, replacing buffer if there are no errors
iterator, err := lexer.Tokenise(nil, ansiRegex.ReplaceAllString(buf, ""))
if err == nil {
formattedBuffer := new(bytes.Buffer)
if formatter.Format(formattedBuffer, style, iterator) == nil {
// Strip extra newline added by Chroma and replace buffer
buf = string(trailingNewline.ReplaceAll(formattedBuffer.Bytes(), []byte{}))
}
syntaxHighlighted = true
}
}
// Support ANSI color codes in preformatted blocks - see #59
if viper.GetBool("a-general.color") && viper.GetBool("a-general.ansi") {
// This will also execute if code highlighting was successful for this block
if viper.GetBool("a-general.color") && (viper.GetBool("a-general.ansi") || syntaxHighlighted) {
buf = cview.TranslateANSI(buf)
// The TranslateANSI function will reset the colors when it encounters
// an ANSI reset code, injecting a full reset tag: [-:-:-]
@ -366,9 +422,21 @@ func RenderGemini(s string, width int, proxied bool) (string, []string) {
// Don't add the current line with backticks
processPre()
// Clear the language
lang = ""
} else {
// Not preformatted, regular text
processRegular()
if viper.GetBool("a-general.highlight_code") {
// Check for alt text indicating a language that Chroma can highlight
alt := strings.TrimSpace(strings.TrimPrefix(lines[i], "```"))
if matches := langRegex.FindStringSubmatch(alt); matches != nil {
if lexers.Get(matches[0]) != nil {
lang = matches[0]
}
}
}
}
buf = "" // Clear buffer for next block
pre = !pre

View File

@ -7,9 +7,12 @@ import "os/exec"
// Open opens `path` in default system viewer.
func Open(path string) (string, error) {
err := exec.Command("open", path).Start()
proc := exec.Command("open", path)
err := proc.Start()
if err != nil {
return "", err
}
//nolint:errcheck
go proc.Wait() // Prevent zombies, see #219
return "Opened in default system viewer", nil
}

View File

@ -25,9 +25,12 @@ func Open(path string) (string, error) {
case xdgOpenNotFoundErr == nil:
// Use start rather than run or output in order
// to make application run in background.
if err := exec.Command(xdgOpenPath, path).Start(); err != nil {
proc := exec.Command(xdgOpenPath, path)
if err := proc.Start(); err != nil {
return "", err
}
//nolint:errcheck
go proc.Wait() // Prevent zombies, see #219
return "Opened in default system viewer", nil
default:
return "", fmt.Errorf("could not determine default system viewer. " +

View File

@ -8,9 +8,12 @@ import "os/exec"
// Open opens `path` in default system vierwer.
func Open(path string) (string, error) {
err := exec.Command("rundll32", "url.dll,FileProtocolHandler", path).Start()
proc := exec.Command("rundll32", "url.dll,FileProtocolHandler", path)
err := proc.Start()
if err != nil {
return "", err
}
//nolint:errcheck
go proc.Wait() // Prevent zombies, see #219
return "Opened in default system viewer", nil
}

View File

@ -7,9 +7,12 @@ import "os/exec"
// Open opens `url` in default system browser.
func Open(url string) (string, error) {
err := exec.Command("open", url).Start()
proc := exec.Command("open", url)
err := proc.Start()
if err != nil {
return "", err
}
//nolint:errcheck
go proc.Wait() // Prevent zombies, see #219
return "Opened in system default web browser", nil
}

View File

@ -34,14 +34,20 @@ func Open(url string) (string, error) {
case xdgOpenNotFoundErr == nil: // Prefer xdg-open over $BROWSER
// Use start rather than run or output in order
// to make browser running in background.
if err := exec.Command(xdgOpenPath, url).Start(); err != nil {
proc := exec.Command(xdgOpenPath, url)
if err := proc.Start(); err != nil {
return "", err
}
//nolint:errcheck
go proc.Wait() // Prevent zombies, see #219
return "Opened in system default web browser", nil
case envBrowser != "":
if err := exec.Command(envBrowser, url).Start(); err != nil {
proc := exec.Command(envBrowser, url)
if err := proc.Start(); err != nil {
return "", err
}
//nolint:errcheck
go proc.Wait() // Prevent zombies, see #219
return "Opened in system default web browser", nil
default:
return "", fmt.Errorf("could not determine system browser")

View File

@ -8,9 +8,12 @@ import "os/exec"
// Open opens `url` in default system browser.
func Open(url string) (string, error) {
err := exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Start()
proc := exec.Command("rundll32", "url.dll,FileProtocolHandler", url)
err := proc.Start()
if err != nil {
return "", err
}
//nolint:errcheck
go proc.Wait() // Prevent zombies, see #219
return "Opened in system default web browser", nil
}