Initial version of improved file opening
MODIFY
config/config.go
config/config.go
@@ -33,6 +33,8 @@ const (
// component and transfer the typed character into it as well.
FocusMessageInputOnTypeInList = 2
+ // NoColor will render all usernames using the PrimaryTextColor defined in
+ // the theme that's currently loaded.
NoColor UserColor = "none"
// SingleColor causes cordless to take the color specified in the theme
SingleColor UserColor = "single"
@@ -47,33 +49,8 @@ const (
type UserColor string
var (
- //Current is the currently loaded configuration. The values here are the
- //defaults which can / will be overwritten by loading the config file.
- Current = &Config{
- Autocomplete: true,
- Times: HourMinuteAndSeconds,
- UserColors: SingleColor,
- ShowUserContainer: true,
- UseFixedLayout: false,
- FixedSizeLeft: 12,
- FixedSizeRight: 12,
- FocusChannelAfterGuildSelection: true,
- FocusMessageInputAfterChannelSelection: true,
- OnTypeInListBehaviour: SearchOnTypeInList,
- MouseEnabled: true,
- ShortenLinks: false,
- ShortenWithExtension: false,
- ShortenerPort: 63212,
- DesktopNotifications: true,
- DesktopNotificationsUserInactivityThreshold: 10,
- DesktopNotificationsForLoadedChannel: true,
- ShowPlaceholderForBlockedMessages: true,
- DontShowUpdateNotificationFor: "",
- ShowUpdateNotifications: true,
- IndicateChannelAccessRestriction: false,
- ShowBottomBar: true,
- ImageViewer: "feh",
- }
+ //Current is the currently loaded configuration.
+ Current = createDefaultConfig()
)
//Config contains all possible configuration for the application.
@@ -163,11 +140,14 @@ type Config struct {
// bottom of cordless or not.
ShowBottomBar bool
- // The image viewer to open when the user uses the "view attached images"
- // shortcut. This program will be passed a list of 1 or more image links
- // as if it were called from the command line, so the selected program
- // must be capable of opening image links.
- ImageViewer string
+ // FileHandlers allow registering specific file-handers for certain
+ FileOpenHandlers map[string]string
+ // FileOpenSaveFilesPermanently decides whether opened files are saved
+ // in the system cache (temporary) or in the user specified path.
+ FileOpenSaveFilesPermanently bool
+ // FileOpenSaveFolder defines the folder where opened files are downloaded
+ // to, if FileOpenSaveFilesPermanently is set to true.
+ FileOpenSaveFolder string
}
// Account has a name and a token. The name is just for the users recognition.
@@ -181,6 +161,39 @@ var cachedConfigDir string
var cachedConfigFile string
var cachedScriptDir string
+func createDefaultConfig() *Config {
+ // The values here are the defaults which can / will be overwritten
+ // by loading the config file. This can also be used to reset the
+ // active set of settings during runtime.
+ return &Config{
+ Autocomplete: true,
+ Times: HourMinuteAndSeconds,
+ UserColors: SingleColor,
+ ShowUserContainer: true,
+ UseFixedLayout: false,
+ FixedSizeLeft: 12,
+ FixedSizeRight: 12,
+ FocusChannelAfterGuildSelection: true,
+ FocusMessageInputAfterChannelSelection: true,
+ OnTypeInListBehaviour: SearchOnTypeInList,
+ MouseEnabled: true,
+ ShortenLinks: false,
+ ShortenWithExtension: false,
+ ShortenerPort: 63212,
+ DesktopNotifications: true,
+ DesktopNotificationsUserInactivityThreshold: 10,
+ DesktopNotificationsForLoadedChannel: true,
+ ShowPlaceholderForBlockedMessages: true,
+ DontShowUpdateNotificationFor: "",
+ ShowUpdateNotifications: true,
+ IndicateChannelAccessRestriction: false,
+ ShowBottomBar: true,
+ FileOpenHandlers: make(map[string]string),
+ FileOpenSaveFilesPermanently: false,
+ FileOpenSaveFolder: "~/Downloads",
+ }
+}
+
// SetConfigFile sets the config file path cache to the
// entered value
func SetConfigFile(configFilePath string) error {

MODIFY
go.mod
go.mod
@@ -23,6 +23,7 @@ require (
github.com/rivo/uniseg v0.1.0
github.com/robertkrimen/otto v0.0.0-20180617131154-15f95af6e78d
github.com/sergi/go-diff v1.1.0
+ github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966
github.com/tadvi/systray v0.0.0-20190226123456-11a2b8fa57af // indirect
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 // indirect
golang.org/x/sys v0.0.0-20200622214017-ed371f2e16b4 // indirect

MODIFY
go.sum
go.sum
github.com/Bios-Marcel/discordemojimap v1.0.1 h1:b3UYPO7+h1+ciStkwU/KQCerOmpUNPHsBf4a7EjMdys=
github.com/Bios-Marcel/discordemojimap v1.0.1/go.mod h1:AoHIpUwf3EVCAAUmk+keXjb9khyZcFnW84/rhJd4IkU=
-github.com/Bios-Marcel/discordgo v0.21.2-0.20200626182914-8d68ec3bb0a2 h1:r7/SdRpAiIf4ebut1UzM/hVsN/AqLms2c/vpijbue74=
-github.com/Bios-Marcel/discordgo v0.21.2-0.20200626182914-8d68ec3bb0a2/go.mod h1:bLnfQU0j/SejmPozgW5GepmKvd8CrbMIml2I0IZENVE=
github.com/Bios-Marcel/discordgo v0.21.2-0.20200810175449-28b4ad667cd5 h1:9Rp5fAYhne7jA3w5iJYvuMAaG3fq2RkE7cC5h+RCnZY=
github.com/Bios-Marcel/discordgo v0.21.2-0.20200810175449-28b4ad667cd5/go.mod h1:bLnfQU0j/SejmPozgW5GepmKvd8CrbMIml2I0IZENVE=
github.com/Bios-Marcel/goclipimg v0.0.0-20191117180634-d0f7b06fbe82 h1:gspJ6CW9bhboosSISmuX2iq03pUsYHzlJN0s+z4fz4E=
@@ -97,6 +95,8 @@ 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/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
+github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 h1:JIAuq3EEf9cgbU6AtGPK4CTG3Zf6CKMNqf0MHTggAUA=
+github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=

MODIFY
ui/window.go
ui/window.go
@@ -4,14 +4,18 @@ import (
"bytes"
"fmt"
"log"
+ "os"
"os/exec"
+ "path/filepath"
"strings"
"time"
"unicode"
"github.com/mattn/go-runewidth"
"github.com/mdp/qrterminal/v3"
+ "github.com/skratchdot/open-golang/open"
+ "github.com/Bios-Marcel/cordless/util/files"
"github.com/Bios-Marcel/cordless/util/fuzzy"
"github.com/Bios-Marcel/cordless/util/text"
"github.com/Bios-Marcel/cordless/version"
@@ -366,11 +370,53 @@ func NewWindow(doRestart chan bool, app *tview.Application, session *discordgo.S
for _, file := range message.Attachments {
links = append(links, file.URL)
}
- if len(links) > 0 {
- cmd := exec.Command(config.Current.ImageViewer, links...)
- err := cmd.Start()
- if err != nil {
- window.ShowErrorDialog(err.Error())
+
+ var targetFolder string
+
+ if config.Current.FileOpenSaveFilesPermanently {
+ absolutePath, pathError := files.ToAbsolutePath(config.Current.FileOpenSaveFolder)
+ if pathError == nil {
+ targetFolder = absolutePath
+ }
+ }
+
+ if targetFolder == "" {
+ cacheDir, osError := os.UserCacheDir()
+ if osError == nil {
+ targetFolder = cacheDir
+ }
+ }
+
+ for _, link := range links {
+ targetFile := filepath.Join(targetFolder, filepath.Base(link))
+ downloadError := files.DownloadFile(targetFile, link)
+ if downloadError != nil {
+ window.ShowCustomErrorDialog("Couldn't open file", downloadError.Error())
+ continue
+ }
+
+ extension := strings.TrimPrefix(filepath.Ext(targetFile), ".")
+ handler, handlerSet := config.Current.FileOpenHandlers[extension]
+ if handlerSet {
+ handlerTrimmed := strings.TrimSpace(handler)
+ //Empty means to not open files with the given extension.
+ if handlerTrimmed == "" {
+ log.Printf("skip opening link %s, as the extension %s has been disabled.\n", link, extension)
+ continue
+ }
+
+ commandParts := commands.ParseCommand(strings.ReplaceAll(handlerTrimmed, "{$file}", targetFile))
+ command := exec.Command(commandParts[0], commandParts[1:]...)
+ startError := command.Start()
+ if startError != nil {
+ window.ShowCustomErrorDialog("Couldn't open file", startError.Error())
+ }
+ } else {
+ log.Println("Attempting to open file: " + targetFile)
+ openError := open.Run(targetFile)
+ if openError != nil {
+ window.ShowCustomErrorDialog("Couldn't open file", openError.Error())
+ }
}
}
@@ -2516,6 +2562,12 @@ func (window *Window) ShowErrorDialog(text string) {
window.ShowDialog(config.GetTheme().ErrorColor, "An error occurred - "+text, func(_ string) {}, "Okay")
}
+// ShowCustomErrorDialog shows a simple error dialog with a custom title
+// and text. The button says "Okay" and only closes the dialog.
+func (window *Window) ShowCustomErrorDialog(title, text string) {
+ window.ShowDialog(config.GetTheme().ErrorColor, title+" - "+text, func(_ string) {}, "Okay")
+}
+
func (window *Window) editMessage(channelID, messageID, messageEdited string) {
go func() {
window.app.QueueUpdateDraw(func() {

MODIFY
util/files/files.go
util/files/files.go
package files
import (
+ "io"
+ "net/http"
"net/url"
+ "os"
"os/user"
"path/filepath"
"strings"
@@ -41,3 +44,22 @@ func ToAbsolutePath(input string) (string, error) {
return resolvedPath, nil
}
+
+// DownloadFile will download a url to a local file. It's efficient because it will
+// write as it downloads and not load the whole file into memory.
+func DownloadFile(filepath string, url string) error {
+ response, httpError := http.Get(url)
+ if httpError != nil {
+ return httpError
+ }
+ defer response.Body.Close()
+
+ outputFile, fileError := os.Create(filepath)
+ if fileError != nil {
+ return fileError
+ }
+ defer outputFile.Close()
+
+ _, writeError := io.Copy(outputFile, response.Body)
+ return writeError
+}

Initial version of improved file opening