diff --git a/authoverlay.go b/authoverlay.go index 34f47d0..a912748 100644 --- a/authoverlay.go +++ b/authoverlay.go @@ -66,7 +66,7 @@ func (a *AuthOverlay) GotInput() { return } a.account = acc - openURL(a.app.Config.Media, acc.AuthURI) + openURL(a.app.Config.Media, a.app.Config.OpenPattern, acc.AuthURI) a.Input.SetText("") a.authStep = authCodeStep a.Draw() diff --git a/config.example.ini b/config.example.ini index 8114f18..5a83ae7 100644 --- a/config.example.ini +++ b/config.example.ini @@ -85,6 +85,43 @@ audio-single=true # default=xdg-open link-viewer=xdg-open +[open-custom] +# This sections allows you to set up to five custom programs to upen URLs with. +# If the url points to an image, you can set c1-name to img and c1-use to imv. +# The name will show up in the UI, so keep it short so all five fits. +# +# c1-name=img +# c1-use=imv +# +# c2-name= +# c2-use= +# +# c3-name= +# c3-use= +# +# c4-name= +# c4-use= +# +# c5-name= +# c5-use= + +[open-pattern] +# Here you can set your own glob patterns for opening matching URLs in the +# program you want them to open up in. +# You could for example open Youtube videos in your video player instead of +# your default browser. +# +# You must name the keys foo-pattern and foo-use, where use is the program +# that will open up the URL. To see the syntax for glob pattern you can follow +# this URL https://github.com/gobwas/glob#syntax +# +# Example for youtube.com and youtu.be to open up in mpv instead of the browser +# +# y1-pattern=*youtube.com/watch* +# y1-use=mpv +# y2-pattern=*youtu.be/* +# y2-use=mpv + [style] # All styles can be represented in their HEX value like #ffffff or # with their name, so in this case white. diff --git a/config.go b/config.go index 5f92878..85cf657 100644 --- a/config.go +++ b/config.go @@ -1,18 +1,22 @@ package main import ( + "fmt" "os" "strings" "github.com/gdamore/tcell/v2" + "github.com/gobwas/glob" "github.com/kyoh86/xdg" "gopkg.in/ini.v1" ) type Config struct { - General GeneralConfig - Style StyleConfig - Media MediaConfig + General GeneralConfig + Style StyleConfig + Media MediaConfig + OpenPattern OpenPatternConfig + OpenCustom OpenCustomConfig } type GeneralConfig struct { @@ -63,6 +67,28 @@ type MediaConfig struct { LinkArgs []string } +type Pattern struct { + Pattern string + Open string + Compiled glob.Glob + Program string + Args []string +} + +type OpenPatternConfig struct { + Patterns []Pattern +} + +type OpenCustom struct { + Index int + Name string + Program string + Args []string +} +type OpenCustomConfig struct { + OpenCustoms []OpenCustom +} + func parseColor(input string, def string, xrdb map[string]string) tcell.Color { if input == "" { return tcell.GetColor(def) @@ -239,6 +265,80 @@ func parseMedia(cfg *ini.File) MediaConfig { return media } +func ParseOpenPattern(cfg *ini.File) OpenPatternConfig { + om := OpenPatternConfig{} + + keys := cfg.Section("open-pattern").KeyStrings() + pairs := make(map[string]Pattern) + for _, s := range keys { + parts := strings.Split(s, "-") + if len(parts) < 2 { + panic(fmt.Sprintf("Invalid key %s in config. Must end in -pattern or -use", s)) + } + last := parts[len(parts)-1] + if last != "pattern" && last != "use" { + panic(fmt.Sprintf("Invalid key %s in config. Must end in -pattern or -use", s)) + } + + name := strings.Join(parts[:len(parts)-1], "-") + if _, ok := pairs[name]; !ok { + pairs[name] = Pattern{} + } + if last == "pattern" { + tmp := pairs[name] + tmp.Pattern = cfg.Section("open-pattern").Key(s).MustString("") + pairs[name] = tmp + } + if last == "use" { + tmp := pairs[name] + tmp.Open = cfg.Section("open-pattern").Key(s).MustString("") + pairs[name] = tmp + } + } + + for key := range pairs { + if pairs[key].Pattern == "" { + panic(fmt.Sprintf("Invalid value for key %s in config. Can't be empty", key+"-pattern")) + } + if pairs[key].Open == "" { + panic(fmt.Sprintf("Invalid value for key %s in config. Can't be empty", key+"-use")) + } + + compiled, err := glob.Compile(pairs[key].Pattern) + if err != nil { + panic(fmt.Sprintf("Couldn't compile pattern for key %s in config. Error: %v", key+"-pattern", err)) + } + tmp := pairs[key] + tmp.Compiled = compiled + comp := strings.Fields(tmp.Open) + tmp.Program = comp[0] + tmp.Args = comp[1:] + om.Patterns = append(om.Patterns, tmp) + } + + return om +} + +func ParseCustom(cfg *ini.File) OpenCustomConfig { + oc := OpenCustomConfig{} + + for i := 1; i < 6; i++ { + name := cfg.Section("open-custom").Key(fmt.Sprintf("c%d-name", i)).MustString("") + use := cfg.Section("open-custom").Key(fmt.Sprintf("c%d-use", i)).MustString("") + if use == "" { + continue + } + comp := strings.Fields(use) + c := OpenCustom{} + c.Index = i + c.Name = name + c.Program = comp[0] + c.Args = comp[1:] + oc.OpenCustoms = append(oc.OpenCustoms, c) + } + return oc +} + func ParseConfig(filepath string) (Config, error) { cfg, err := ini.LoadSources(ini.LoadOptions{ SpaceBeforeInlineComment: true, @@ -250,6 +350,9 @@ func ParseConfig(filepath string) (Config, error) { conf.General = parseGeneral(cfg) conf.Media = parseMedia(cfg) conf.Style = parseStyle(cfg) + conf.OpenPattern = ParseOpenPattern(cfg) + conf.OpenCustom = ParseCustom(cfg) + return conf, nil } @@ -358,6 +461,43 @@ audio-single=true # default=xdg-open link-viewer=xdg-open +[open-custom] +# This sections allows you to set up to five custom programs to upen URLs with. +# If the url points to an image, you can set c1-name to img and c1-use to imv. +# The name will show up in the UI, so keep it short so all five fits. +# +# c1-name=img +# c1-use=imv +# +# c2-name= +# c2-use= +# +# c3-name= +# c3-use= +# +# c4-name= +# c4-use= +# +# c5-name= +# c5-use= + +[open-pattern] +# Here you can set your own glob patterns for opening matching URLs in the +# program you want them to open up in. +# You could for example open Youtube videos in your video player instead of +# your default browser. +# +# You must name the keys foo-pattern and foo-use, where use is the program +# that will open up the URL. To see the syntax for glob pattern you can follow +# this URL https://github.com/gobwas/glob#syntax +# +# Example for youtube.com and youtu.be to open up in mpv instead of the browser +# +# y1-pattern=*youtube.com/watch* +# y1-use=mpv +# y2-pattern=*youtu.be/* +# y2-use=mpv + [style] # All styles can be represented in their HEX value like #ffffff or # with their name, so in this case white. diff --git a/go.mod b/go.mod index f82395b..6141f94 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,9 @@ module github.com/RasmusLindroth/tut go 1.14 require ( + github.com/atotto/clipboard v0.1.4 github.com/gdamore/tcell/v2 v2.3.11 + github.com/gobwas/glob v0.2.3 github.com/icza/gox v0.0.0-20200702115100-7dc3510ae515 github.com/kyoh86/xdg v1.2.0 github.com/mattn/go-mastodon v0.0.5-0.20210629151305-d39c10ba5e94 diff --git a/go.sum b/go.sum index bc93847..425021a 100644 --- a/go.sum +++ b/go.sum @@ -2,6 +2,8 @@ github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/PuerkitoBio/goquery v1.5.0/go.mod h1:qD2PgZ9lccMbQlc7eEOjaeRlFQON7xY8kdmcsrnKqMg= github.com/andybalholm/cascadia v1.0.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y= +github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= +github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= github.com/chris-ramon/douceur v0.2.0 h1:IDMEdxlEUUBYBKE4z/mJnFyVXox+MjuEVDJNN27glkU= @@ -14,6 +16,8 @@ github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo github.com/gdamore/tcell/v2 v2.3.3/go.mod h1:cTTuF84Dlj/RqmaCIV5p4w8uG1zWdk0SF6oBpwHp4fU= github.com/gdamore/tcell/v2 v2.3.11 h1:ECO6WqHGbKZ3HrSL7bG/zArMCmLaNr5vcjjMVnLHpzc= github.com/gdamore/tcell/v2 v2.3.11/go.mod h1:cTTuF84Dlj/RqmaCIV5p4w8uG1zWdk0SF6oBpwHp4fU= +github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= +github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY= diff --git a/linkoverlay.go b/linkoverlay.go index 349e5bf..8a49d1b 100644 --- a/linkoverlay.go +++ b/linkoverlay.go @@ -2,6 +2,8 @@ package main import ( "fmt" + "strconv" + "strings" "github.com/gdamore/tcell/v2" "github.com/mattn/go-mastodon" @@ -25,7 +27,13 @@ func NewLinkOverlay(app *App) *LinkOverlay { l.List.ShowSecondaryText(false) l.List.SetHighlightFullLine(true) l.Flex.SetDrawFunc(app.Config.ClearContent) - l.TextBottom.SetText(ColorKey(app.Config.Style, "", "O", "pen")) + var items []string + items = append(items, ColorKey(app.Config.Style, "", "O", "pen")) + items = append(items, ColorKey(app.Config.Style, "", "Y", "ank")) + for _, cust := range app.Config.OpenCustom.OpenCustoms { + items = append(items, ColorKey(app.Config.Style, "", fmt.Sprintf("%d", cust.Index), cust.Name)) + } + l.TextBottom.SetText(strings.Join(items, " ")) return l } @@ -85,7 +93,7 @@ func (l *LinkOverlay) Open() { return } if index < len(l.urls) { - openURL(l.app.Config.Media, l.urls[index].URL) + openURL(l.app.Config.Media, l.app.Config.OpenPattern, l.urls[index].URL) return } mIndex := index - len(l.urls) @@ -110,6 +118,48 @@ func (l *LinkOverlay) Open() { } } +func (l *LinkOverlay) CopyToClipboard() { + text := l.GetURL() + if text != "" { + e := copyToClipboard(text) + if e == false { + l.app.UI.CmdBar.ShowError("Couldn't copy to clipboard.") + } + } +} + +func (l *LinkOverlay) GetURL() string { + index := l.List.GetCurrentItem() + total := len(l.urls) + len(l.mentions) + len(l.tags) + if total == 0 || index >= total { + return "" + } + if index < len(l.urls) { + return l.urls[index].URL + } + mIndex := index - len(l.urls) + if mIndex < len(l.mentions) { + return l.mentions[mIndex].URL + } + tIndex := index - len(l.mentions) - len(l.urls) + if tIndex < len(l.tags) { + return l.tags[tIndex].URL + } + return "" +} + +func (l *LinkOverlay) OpenCustom(index int) { + url := l.GetURL() + customs := l.app.Config.OpenCustom.OpenCustoms + for _, c := range customs { + if c.Index != index { + continue + } + openCustom(c.Program, c.Args, url) + return + } +} + func (l *LinkOverlay) InputHandler(event *tcell.EventKey) { if event.Key() == tcell.KeyRune { switch event.Rune() { @@ -119,6 +169,12 @@ func (l *LinkOverlay) InputHandler(event *tcell.EventKey) { l.Prev() case 'o', 'O': l.Open() + case 'y', 'Y': + l.CopyToClipboard() + case '1', '2', '3', '4', '5': + s := string(event.Rune()) + i, _ := strconv.Atoi(s) + l.OpenCustom(i) case 'q', 'Q': l.app.UI.SetFocus(LeftPaneFocus) } diff --git a/util.go b/util.go index d09305c..801b50c 100644 --- a/util.go +++ b/util.go @@ -13,6 +13,7 @@ import ( "strings" "time" + "github.com/atotto/clipboard" "github.com/gdamore/tcell/v2" "github.com/icza/gox/timex" "github.com/mattn/go-mastodon" @@ -103,7 +104,27 @@ func openEditor(app *tview.Application, content string) (string, error) { return strings.TrimSpace(string(text)), nil } -func openURL(conf MediaConfig, url string) { +func copyToClipboard(text string) bool { + if clipboard.Unsupported { + return false + } + clipboard.WriteAll(text) + return true +} + +func openCustom(program string, args []string, url string) { + args = append(args, url) + exec.Command(program, args...).Start() +} + +func openURL(conf MediaConfig, pc OpenPatternConfig, url string) { + for _, m := range pc.Patterns { + if m.Compiled.Match(url) { + args := append(m.Args, url) + exec.Command(m.Program, args...).Start() + return + } + } args := append(conf.LinkArgs, url) exec.Command(conf.LinkViewer, args...).Start() }