mirror of
https://github.com/makeworld-the-better-one/amfora.git
synced 2024-08-17 10:40:35 +03:00
Mediatypes support (#134)
Co-authored-by: makeworld <25111343+makeworld-the-better-one@users.noreply.github.com> Co-authored-by: Stephen Robinson <stephen@drsudo.com>
This commit is contained in:
parent
39290b09c6
commit
0df5effdcf
@ -116,6 +116,8 @@ Features in *italics* are in the master branch, but not in the latest release.
|
||||
- Disabled by default, enable in config
|
||||
- [x] Proxying
|
||||
- Schemes like Gopher or HTTP can be proxied through a Gemini server
|
||||
- [x] *Configure applications to open particular mediatypes*
|
||||
- [ ] Allow piping/streaming content instead of downloading it first
|
||||
- [x] Client certificate support
|
||||
- [ ] Full client certificate UX within the client
|
||||
- Create transient and permanent certs within the client, per domain
|
||||
|
@ -38,6 +38,7 @@ var bkmkDir string
|
||||
var bkmkPath string
|
||||
|
||||
var DownloadsDir string
|
||||
var TempDownloadsDir string
|
||||
|
||||
// Subscriptions
|
||||
var subscriptionDir string
|
||||
@ -46,6 +47,13 @@ var SubscriptionPath string
|
||||
// Command for opening HTTP(S) URLs in the browser, from "a-general.http" in config.
|
||||
var HTTPCommand []string
|
||||
|
||||
type MediaHandler struct {
|
||||
Cmd []string
|
||||
NoPrompt bool
|
||||
}
|
||||
|
||||
var MediaHandlers = make(map[string]MediaHandler)
|
||||
|
||||
func Init() error {
|
||||
|
||||
// *** Set paths ***
|
||||
@ -194,6 +202,36 @@ func Init() error {
|
||||
DownloadsDir = dDir
|
||||
}
|
||||
|
||||
// Setup temporary downloads dir
|
||||
if viper.GetString("a-general.temp_downloads") == "" {
|
||||
TempDownloadsDir = filepath.Join(os.TempDir(), "amfora_temp")
|
||||
|
||||
// Make sure it exists
|
||||
err = os.MkdirAll(TempDownloadsDir, 0755)
|
||||
if err != nil {
|
||||
return fmt.Errorf("temp downloads path could not be created: %s", TempDownloadsDir)
|
||||
}
|
||||
} else {
|
||||
// Validate path
|
||||
dDir := viper.GetString("a-general.temp_downloads")
|
||||
di, err := os.Stat(dDir)
|
||||
if err == nil {
|
||||
if !di.IsDir() {
|
||||
return fmt.Errorf("temp downloads path specified is not a directory: %s", dDir)
|
||||
}
|
||||
} else if os.IsNotExist(err) {
|
||||
// Try to create path
|
||||
err = os.MkdirAll(dDir, 0755)
|
||||
if err != nil {
|
||||
return fmt.Errorf("temp downloads path could not be created: %s", dDir)
|
||||
}
|
||||
} else {
|
||||
// Some other error
|
||||
return fmt.Errorf("couldn't access temp downloads directory: %s", dDir)
|
||||
}
|
||||
TempDownloadsDir = dDir
|
||||
}
|
||||
|
||||
// *** Setup vipers ***
|
||||
|
||||
TofuStore.SetConfigFile(tofuDBPath)
|
||||
@ -228,6 +266,7 @@ func Init() error {
|
||||
viper.SetDefault("a-general.left_margin", 0.15)
|
||||
viper.SetDefault("a-general.max_width", 100)
|
||||
viper.SetDefault("a-general.downloads", "")
|
||||
viper.SetDefault("a-general.temp_downloads", "")
|
||||
viper.SetDefault("a-general.page_max_size", 2097152)
|
||||
viper.SetDefault("a-general.page_max_time", 10)
|
||||
viper.SetDefault("a-general.emoji_favicons", false)
|
||||
@ -279,5 +318,26 @@ func Init() error {
|
||||
HTTPCommand = strings.Fields(viper.GetString("a-general.http"))
|
||||
}
|
||||
|
||||
var rawMediaHandlers []struct {
|
||||
Cmd []string `mapstructure:"cmd"`
|
||||
Types []string `mapstructure:"types"`
|
||||
NoPrompt bool `mapstructure:"no_prompt"`
|
||||
}
|
||||
err = viper.UnmarshalKey("mediatype-handlers", &rawMediaHandlers)
|
||||
if err != nil {
|
||||
return fmt.Errorf("couldn't parse mediatype-handlers section in config: %w", err)
|
||||
}
|
||||
for _, rawMediaHandler := range rawMediaHandlers {
|
||||
for _, typ := range rawMediaHandler.Types {
|
||||
if _, ok := MediaHandlers[typ]; ok {
|
||||
return fmt.Errorf("multiple mediatype-handlers defined for %v", typ)
|
||||
}
|
||||
MediaHandlers[typ] = MediaHandler{
|
||||
Cmd: rawMediaHandler.Cmd,
|
||||
NoPrompt: rawMediaHandler.NoPrompt,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@ -115,6 +115,54 @@ shift_numbers = "!@#$%^&*()"
|
||||
other = 'off'
|
||||
|
||||
|
||||
# [[mediatype-handlers]]
|
||||
# Specify what applications will open certain media types.
|
||||
# By default your default application will be used to open the file when you select "Open".
|
||||
# You only need to configure this section if you want to override your default application,
|
||||
# or do special things like streaming.
|
||||
#
|
||||
# To open jpeg files with the feh command:
|
||||
# [[mediatype-handlers]]
|
||||
# cmd = ["feh"]
|
||||
# types = ["image/jpeg"]
|
||||
#
|
||||
# Each command that you specify must come under its own [[mediatype-handlers]]. You may
|
||||
# specify as many [[mediatype-handlers]] as you want to setup multiple commands.
|
||||
#
|
||||
# If the subtype is omitted then the specified command will be used for the
|
||||
# entire type:
|
||||
# [[mediatype-handlers]]
|
||||
# command = ["vlc", "--flag"]
|
||||
# types = ["audio", "video"]
|
||||
#
|
||||
# A catch-all handler can by specified with "*".
|
||||
# Note that there are already catch-all handlers in place for all OSes,
|
||||
# that open the file using your default application. This is only if you
|
||||
# want to override that.
|
||||
# [[mediatype-handlers]]
|
||||
# cmd = ["some-command"]
|
||||
# types = [
|
||||
# "application/pdf",
|
||||
# "*",
|
||||
# ]
|
||||
#
|
||||
# If you want to always open a type in its viewer without the download or open
|
||||
# prompt appearing, you can add no_prompt = true
|
||||
#
|
||||
# [[mediatype-handlers]]
|
||||
# cmd = ["feh"]
|
||||
# types = ["image"]
|
||||
# no_prompt = true
|
||||
#
|
||||
# Note: Multiple handlers cannot be defined for the same full media type, but
|
||||
# still there needs to be an order for which handlers are used. The following
|
||||
# order applies regardless of the order written in the config:
|
||||
#
|
||||
# 1. Full media type: "image/jpeg"
|
||||
# 2. Just type: "image"
|
||||
# 3. Catch-all: "*"
|
||||
|
||||
|
||||
[cache]
|
||||
# Options for page cache - which is only for text/gemini pages
|
||||
# Increase the cache size to speed up browsing at the expense of memory
|
||||
|
@ -1,6 +1,10 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
head -n 3 default.go | tee default.go > /dev/null
|
||||
cat > default.go <<-EOF
|
||||
package config
|
||||
|
||||
//go:generate ./default.sh
|
||||
EOF
|
||||
echo -n 'var defaultConf = []byte(`' >> default.go
|
||||
cat ../default-config.toml >> default.go
|
||||
echo '`)' >> default.go
|
||||
echo '`)' >> default.go
|
||||
|
@ -112,6 +112,54 @@ shift_numbers = "!@#$%^&*()"
|
||||
other = 'off'
|
||||
|
||||
|
||||
# [[mediatype-handlers]]
|
||||
# Specify what applications will open certain media types.
|
||||
# By default your default application will be used to open the file when you select "Open".
|
||||
# You only need to configure this section if you want to override your default application,
|
||||
# or do special things like streaming.
|
||||
#
|
||||
# To open jpeg files with the feh command:
|
||||
# [[mediatype-handlers]]
|
||||
# cmd = ["feh"]
|
||||
# types = ["image/jpeg"]
|
||||
#
|
||||
# Each command that you specify must come under its own [[mediatype-handlers]]. You may
|
||||
# specify as many [[mediatype-handlers]] as you want to setup multiple commands.
|
||||
#
|
||||
# If the subtype is omitted then the specified command will be used for the
|
||||
# entire type:
|
||||
# [[mediatype-handlers]]
|
||||
# command = ["vlc", "--flag"]
|
||||
# types = ["audio", "video"]
|
||||
#
|
||||
# A catch-all handler can by specified with "*".
|
||||
# Note that there are already catch-all handlers in place for all OSes,
|
||||
# that open the file using your default application. This is only if you
|
||||
# want to override that.
|
||||
# [[mediatype-handlers]]
|
||||
# cmd = ["some-command"]
|
||||
# types = [
|
||||
# "application/pdf",
|
||||
# "*",
|
||||
# ]
|
||||
#
|
||||
# If you want to always open a type in its viewer without the download or open
|
||||
# prompt appearing, you can add no_prompt = true
|
||||
#
|
||||
# [[mediatype-handlers]]
|
||||
# cmd = ["feh"]
|
||||
# types = ["image"]
|
||||
# no_prompt = true
|
||||
#
|
||||
# Note: Multiple handlers cannot be defined for the same full media type, but
|
||||
# still there needs to be an order for which handlers are used. The following
|
||||
# order applies regardless of the order written in the config:
|
||||
#
|
||||
# 1. Full media type: "image/jpeg"
|
||||
# 2. Just type: "image"
|
||||
# 3. Catch-all: "*"
|
||||
|
||||
|
||||
[cache]
|
||||
# Options for page cache - which is only for text/gemini pages
|
||||
# Increase the cache size to speed up browsing at the expense of memory
|
||||
|
@ -4,8 +4,10 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"mime"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
@ -15,15 +17,16 @@ import (
|
||||
"github.com/gdamore/tcell"
|
||||
"github.com/makeworld-the-better-one/amfora/config"
|
||||
"github.com/makeworld-the-better-one/amfora/structs"
|
||||
"github.com/makeworld-the-better-one/amfora/sysopen"
|
||||
"github.com/makeworld-the-better-one/go-gemini"
|
||||
"github.com/makeworld-the-better-one/progressbar/v3"
|
||||
"github.com/spf13/viper"
|
||||
"gitlab.com/tslocum/cview"
|
||||
)
|
||||
|
||||
// For choosing between download and the portal - copy of YesNo basically
|
||||
// For choosing between download and opening - copy of YesNo basically
|
||||
var dlChoiceModal = cview.NewModal().
|
||||
AddButtons([]string{"Download", "Open in portal", "Cancel"})
|
||||
AddButtons([]string{"Open", "Download", "Cancel"})
|
||||
|
||||
// Channel to indicate what choice they made using the button text
|
||||
var dlChoiceCh = make(chan string)
|
||||
@ -83,46 +86,62 @@ func dlInit() {
|
||||
})
|
||||
}
|
||||
|
||||
func getMediaHandler(resp *gemini.Response) config.MediaHandler {
|
||||
def := config.MediaHandler{
|
||||
Cmd: nil,
|
||||
NoPrompt: false,
|
||||
}
|
||||
|
||||
mediatype, _, err := mime.ParseMediaType(resp.Meta)
|
||||
if err != nil {
|
||||
return def
|
||||
}
|
||||
|
||||
if ret, ok := config.MediaHandlers[mediatype]; ok {
|
||||
return ret
|
||||
}
|
||||
|
||||
splitType := strings.Split(mediatype, "/")[0]
|
||||
if ret, ok := config.MediaHandlers[splitType]; ok {
|
||||
return ret
|
||||
}
|
||||
|
||||
if ret, ok := config.MediaHandlers["*"]; ok {
|
||||
return ret
|
||||
}
|
||||
|
||||
return def
|
||||
}
|
||||
|
||||
// dlChoice displays the download choice modal and acts on the user's choice.
|
||||
// It should run in a goroutine.
|
||||
func dlChoice(text, u string, resp *gemini.Response) {
|
||||
defer resp.Body.Close()
|
||||
|
||||
parsed, err := url.Parse(u)
|
||||
if err != nil {
|
||||
Error("URL Error", err.Error())
|
||||
return
|
||||
mediaHandler := getMediaHandler(resp)
|
||||
var choice string
|
||||
|
||||
if mediaHandler.NoPrompt {
|
||||
choice = "Open"
|
||||
} else {
|
||||
dlChoiceModal.SetText(text)
|
||||
tabPages.ShowPage("dlChoice")
|
||||
tabPages.SendToFront("dlChoice")
|
||||
App.SetFocus(dlChoiceModal)
|
||||
App.Draw()
|
||||
choice = <-dlChoiceCh
|
||||
}
|
||||
|
||||
dlChoiceModal.SetText(text)
|
||||
tabPages.ShowPage("dlChoice")
|
||||
tabPages.SendToFront("dlChoice")
|
||||
App.SetFocus(dlChoiceModal)
|
||||
App.Draw()
|
||||
|
||||
choice := <-dlChoiceCh
|
||||
if choice == "Download" {
|
||||
tabPages.HidePage("dlChoice")
|
||||
App.Draw()
|
||||
downloadURL(u, resp)
|
||||
downloadURL(config.DownloadsDir, u, resp)
|
||||
return
|
||||
}
|
||||
if choice == "Open in portal" {
|
||||
// Open in mozz's proxy
|
||||
portalURL := u
|
||||
if parsed.RawQuery != "" {
|
||||
// Remove query and add encoded version on the end
|
||||
query := parsed.RawQuery
|
||||
parsed.RawQuery = ""
|
||||
portalURL = parsed.String() + "%3F" + query
|
||||
}
|
||||
portalURL = strings.TrimPrefix(portalURL, "gemini://") + "?raw=1"
|
||||
ok := handleHTTP("https://portal.mozz.us/gemini/"+portalURL, false)
|
||||
if ok {
|
||||
tabPages.SwitchToPage(strconv.Itoa(curTab))
|
||||
App.SetFocus(tabs[curTab].view)
|
||||
App.Draw()
|
||||
}
|
||||
if choice == "Open" {
|
||||
tabPages.HidePage("dlChoice")
|
||||
App.Draw()
|
||||
open(u, resp)
|
||||
return
|
||||
}
|
||||
tabPages.SwitchToPage(strconv.Itoa(curTab))
|
||||
@ -130,9 +149,43 @@ func dlChoice(text, u string, resp *gemini.Response) {
|
||||
App.Draw()
|
||||
}
|
||||
|
||||
// open performs the same actions as downloadURL except it also opens the file.
|
||||
// If there is no system viewer configured for the particular mediatype, it opens it
|
||||
// with the default system viewer.
|
||||
func open(u string, resp *gemini.Response) {
|
||||
mediaHandler := getMediaHandler(resp)
|
||||
path := downloadURL(config.TempDownloadsDir, u, resp)
|
||||
if path == "" {
|
||||
return
|
||||
}
|
||||
tabPages.SwitchToPage(strconv.Itoa(curTab))
|
||||
App.SetFocus(tabs[curTab].view)
|
||||
App.Draw()
|
||||
if mediaHandler.Cmd == nil {
|
||||
// Open with system default viewer
|
||||
_, err := sysopen.Open(path)
|
||||
if err != nil {
|
||||
Error("System Viewer Error", err.Error())
|
||||
return
|
||||
}
|
||||
Info("Opened in default system viewer")
|
||||
} else {
|
||||
cmd := mediaHandler.Cmd
|
||||
err := exec.Command(cmd[0], append(cmd[1:], path)...).Start()
|
||||
if err != nil {
|
||||
Error("File Opening Error", "Error executing custom command: "+err.Error())
|
||||
return
|
||||
}
|
||||
Info("Opened with " + cmd[0])
|
||||
}
|
||||
App.SetFocus(dlModal)
|
||||
App.Draw()
|
||||
}
|
||||
|
||||
// downloadURL pulls up a modal to show download progress and saves the URL content.
|
||||
// downloadPage should be used for Page content.
|
||||
func downloadURL(u string, resp *gemini.Response) {
|
||||
// Returns location downloaded to or an empty string on error.
|
||||
func downloadURL(dir, u string, resp *gemini.Response) string {
|
||||
_, _, width, _ := dlModal.GetInnerRect()
|
||||
// Copy of progressbar.DefaultBytesSilent with custom width
|
||||
bar := progressbar.NewOptions64(
|
||||
@ -146,15 +199,15 @@ func downloadURL(u string, resp *gemini.Response) {
|
||||
)
|
||||
bar.RenderBlank() //nolint:errcheck
|
||||
|
||||
savePath, err := downloadNameFromURL(u, "")
|
||||
savePath, err := downloadNameFromURL(dir, u, "")
|
||||
if err != nil {
|
||||
Error("Download Error", "Error deciding on file name: "+err.Error())
|
||||
return
|
||||
return ""
|
||||
}
|
||||
f, err := os.OpenFile(savePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
|
||||
if err != nil {
|
||||
Error("Download Error", "Error creating download file: "+err.Error())
|
||||
return
|
||||
return ""
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
@ -184,7 +237,7 @@ func downloadURL(u string, resp *gemini.Response) {
|
||||
Error("Download Error", err.Error())
|
||||
f.Close()
|
||||
os.Remove(savePath) // Remove partial file
|
||||
return
|
||||
return ""
|
||||
}
|
||||
dlModal.SetText(fmt.Sprintf("Download complete! File saved to %s.", savePath))
|
||||
dlModal.ClearButtons()
|
||||
@ -192,6 +245,8 @@ func downloadURL(u string, resp *gemini.Response) {
|
||||
dlModal.GetForm().SetFocus(100)
|
||||
App.SetFocus(dlModal)
|
||||
App.Draw()
|
||||
|
||||
return savePath
|
||||
}
|
||||
|
||||
// downloadPage saves the passed Page to a file.
|
||||
@ -202,9 +257,9 @@ func downloadPage(p *structs.Page) (string, error) {
|
||||
var err error
|
||||
|
||||
if p.Mediatype == structs.TextGemini {
|
||||
savePath, err = downloadNameFromURL(p.URL, ".gmi")
|
||||
savePath, err = downloadNameFromURL(config.DownloadsDir, p.URL, ".gmi")
|
||||
} else {
|
||||
savePath, err = downloadNameFromURL(p.URL, ".txt")
|
||||
savePath, err = downloadNameFromURL(config.DownloadsDir, p.URL, ".txt")
|
||||
}
|
||||
if err != nil {
|
||||
return "", err
|
||||
@ -221,13 +276,13 @@ func downloadPage(p *structs.Page) (string, error) {
|
||||
// downloadNameFromURL takes a URl and returns a safe download path that will not overwrite any existing file.
|
||||
// ext is an extension that will be added if the file has no extension, and for domain only URLs.
|
||||
// It should include the dot.
|
||||
func downloadNameFromURL(u string, ext string) (string, error) {
|
||||
func downloadNameFromURL(dir, u, ext string) (string, error) {
|
||||
var name string
|
||||
var err error
|
||||
parsed, _ := url.Parse(u)
|
||||
if parsed.Path == "" || path.Base(parsed.Path) == "/" {
|
||||
// No file, just the root domain
|
||||
name, err = getSafeDownloadName(parsed.Hostname()+ext, true, 0)
|
||||
name, err = getSafeDownloadName(dir, parsed.Hostname()+ext, true, 0)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@ -238,23 +293,23 @@ func downloadNameFromURL(u string, ext string) (string, error) {
|
||||
// No extension
|
||||
name += ext
|
||||
}
|
||||
name, err = getSafeDownloadName(name, false, 0)
|
||||
name, err = getSafeDownloadName(dir, name, false, 0)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
return filepath.Join(config.DownloadsDir, name), nil
|
||||
return filepath.Join(dir, name), nil
|
||||
}
|
||||
|
||||
// getSafeDownloadName is used by downloads.go only.
|
||||
// It returns a modified name that is unique for the downloads folder.
|
||||
// It returns a modified name that is unique for the specified folder.
|
||||
// This way duplicate saved files will not overwrite each other.
|
||||
//
|
||||
// lastDot should be set to true if the number added to the name should come before
|
||||
// the last dot in the filename instead of the first.
|
||||
//
|
||||
// n should be set to 0, it is used for recursiveness.
|
||||
func getSafeDownloadName(name string, lastDot bool, n int) (string, error) {
|
||||
func getSafeDownloadName(dir, name string, lastDot bool, n int) (string, error) {
|
||||
// newName("test.txt", 3) -> "test(3).txt"
|
||||
newName := func() string {
|
||||
if n <= 0 {
|
||||
@ -271,7 +326,7 @@ func getSafeDownloadName(name string, lastDot bool, n int) (string, error) {
|
||||
return name[:idx] + "(" + strconv.Itoa(n) + ")" + name[idx:]
|
||||
}
|
||||
|
||||
d, err := os.Open(config.DownloadsDir)
|
||||
d, err := os.Open(dir)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@ -285,7 +340,7 @@ func getSafeDownloadName(name string, lastDot bool, n int) (string, error) {
|
||||
for i := range files {
|
||||
if nn == files[i] {
|
||||
d.Close()
|
||||
return getSafeDownloadName(name, lastDot, n+1)
|
||||
return getSafeDownloadName(dir, name, lastDot, n+1)
|
||||
}
|
||||
}
|
||||
d.Close()
|
||||
|
14
sysopen/open_browser_darwin.go
Normal file
14
sysopen/open_browser_darwin.go
Normal file
@ -0,0 +1,14 @@
|
||||
// +build darwin
|
||||
|
||||
package sysopen
|
||||
|
||||
import "os/exec"
|
||||
|
||||
// Open opens `path` in default system viewer.
|
||||
func Open(path string) (string, error) {
|
||||
err := exec.Command("open", path).Start()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return "Opened in default system viewer", nil
|
||||
}
|
11
sysopen/open_browser_other.go
Normal file
11
sysopen/open_browser_other.go
Normal file
@ -0,0 +1,11 @@
|
||||
// +build !linux,!darwin,!windows,!freebsd,!netbsd,!openbsd
|
||||
|
||||
package sysopen
|
||||
|
||||
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")
|
||||
}
|
35
sysopen/open_browser_unix.go
Normal file
35
sysopen/open_browser_unix.go
Normal file
@ -0,0 +1,35 @@
|
||||
// +build linux freebsd netbsd openbsd
|
||||
|
||||
//nolint:goerr113
|
||||
package sysopen
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
// Open opens `path` in default system viewer. It tries to do so using
|
||||
// xdg-open. It only works if there is a display server working.
|
||||
func Open(path string) (string, error) {
|
||||
var (
|
||||
xorgDisplay = os.Getenv("DISPLAY")
|
||||
waylandDisplay = os.Getenv("WAYLAND_DISPLAY")
|
||||
xdgOpenPath, xdgOpenNotFoundErr = exec.LookPath("xdg-open")
|
||||
)
|
||||
switch {
|
||||
case xorgDisplay == "" && waylandDisplay == "":
|
||||
return "", fmt.Errorf("no display server was found. " +
|
||||
"You may set a default [[mediatype-handlers]] command in the config")
|
||||
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 {
|
||||
return "", err
|
||||
}
|
||||
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")
|
||||
}
|
||||
}
|
15
sysopen/open_browser_windows.go
Normal file
15
sysopen/open_browser_windows.go
Normal file
@ -0,0 +1,15 @@
|
||||
// +build windows
|
||||
// +build !linux !darwin !freebsd !netbsd !openbsd
|
||||
|
||||
package sysopen
|
||||
|
||||
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()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return "Opened in default system viewer", nil
|
||||
}
|
Loading…
Reference in New Issue
Block a user