Skip to content

Commit

Permalink
OpenFileManager for opening with the native file manager and optional
Browse files Browse the repository at this point in the history
file selection support

Closes #3197
  • Loading branch information
Krzysztofz01 authored and rcalixte committed Dec 8, 2024
1 parent 2e4fce7 commit 17abf0b
Show file tree
Hide file tree
Showing 4 changed files with 189 additions and 9 deletions.
1 change: 1 addition & 0 deletions mkdocs-website/docs/en/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added

- `app.OpenDirectory(dir string)` to open the system file explorer to the directory `dir` by [@leaanthony](https://github.com/leaanthony)
- `app.OpenFileManager(path string, selectFile bool)` to open the system file manager to the path `path` with optional highlighting via `selectFile` by [@Krzysztofz01](https://github.com/Krzysztofz01) [@rcalixte](https://github.com/rcalixte)

### Fixed

Expand Down
144 changes: 137 additions & 7 deletions v3/internal/fileexplorer/fileexplorer.go
Original file line number Diff line number Diff line change
@@ -1,24 +1,154 @@
package fileexplorer

import (
"bytes"
"context"
"errors"
"fmt"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"time"

ini "gopkg.in/ini.v1"
)

func Open(path string) error {
var cmd *exec.Cmd
type explorerBinArgs func(path string, selectFile bool) (string, []string, error)

func OpenFileManager(path string, selectFile bool) error {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()

path = os.ExpandEnv(path)
path = filepath.Clean(path)
absPath, err := filepath.Abs(path)
if err != nil {
return fmt.Errorf("failed to resolve the absolute path: %w", err)
}
path = absPath
if pathInfo, err := os.Stat(path); err != nil {
return fmt.Errorf("failed to access the specified path: %w", err)
} else {
selectFile = selectFile && !pathInfo.IsDir()
}

var (
explorerBinArgs explorerBinArgs
ignoreExitCode bool = false
)

switch runtime.GOOS {
case "windows":
cmd = exec.Command("explorer", path)
explorerBinArgs = windowsExplorerBinArgs
// NOTE: Disabling the exit code check on Windows system. Workaround for explorer.exe
// exit code handling (https://github.com/microsoft/WSL/issues/6565)
ignoreExitCode = true
case "darwin":
cmd = exec.Command("open", path)
explorerBinArgs = darwinExplorerBinArgs
case "linux":
cmd = exec.Command("xdg-open", path)
explorerBinArgs = linuxExplorerBinArgs
default:
return fmt.Errorf("unsupported platform")
return errors.New("unsupported platform: " + runtime.GOOS)
}

explorerBin, explorerArgs, err := explorerBinArgs(path, selectFile)
if err != nil {
return fmt.Errorf("failed to determine the file explorer binary: %w", err)
}

cmd := exec.CommandContext(ctx, explorerBin, explorerArgs...)
cmd.Stdout = nil
cmd.Stderr = nil

if err := cmd.Run(); err != nil {
if !ignoreExitCode {
return fmt.Errorf("failed to open the file explorer: %w", err)
}
}
return nil
}

var windowsExplorerBinArgs explorerBinArgs = func(path string, selectFile bool) (string, []string, error) {
args := []string{}
if selectFile {
args = append(args, fmt.Sprintf("/select,\"%s\"", path))
} else {
args = append(args, path)
}
return "explorer.exe", args, nil
}

var darwinExplorerBinArgs explorerBinArgs = func(path string, selectFile bool) (string, []string, error) {
args := []string{}
if selectFile {
args = append(args, "-R")
}

return cmd.Run()
args = append(args, path)
return "open", args, nil
}

var linuxExplorerBinArgs explorerBinArgs = func(path string, selectFile bool) (string, []string, error) {
fileManagerQuery := exec.Command("xdg-mime", "query", "default", "inode/directory")
buf := new(bytes.Buffer)
fileManagerQuery.Stdout = buf
fileManagerQuery.Stderr = nil

if err := fileManagerQuery.Run(); err == nil {
var desktopFile string
xdgPath := strings.TrimSpace(os.Getenv("XDG_DATA_HOME"))
if xdgPath == "" {
xdgPath = filepath.Join(os.Getenv("HOME"), ".local", "share")
}
desktopFile = filepath.Join(xdgPath, "applications", strings.TrimSpace((buf.String())))

if _, err := os.Stat(desktopFile); err != nil {
desktopFile = filepath.Join("/usr/share/applications", strings.TrimSpace((buf.String())))
}

cfg, err := ini.Load(desktopFile)
if err != nil {
// Opting to fallback rather than fail
return linuxFallbackExplorerBinArgs(path, selectFile)
}

exec := cfg.Section("Desktop Entry").Key("Exec").String()
exec = strings.ReplaceAll(exec, "%f", path)
exec = strings.ReplaceAll(exec, "%F", path)
exec = strings.ReplaceAll(exec, "%u", pathToURI(path))
exec = strings.ReplaceAll(exec, "%U", pathToURI(path))
// Strip other field codes
exec = strings.NewReplacer(
"%d", "",
"%D", "",
"%n", "",
"%N", "",
"%v", "",
"%m", "",
).Replace(exec)
args := strings.Fields(exec)
if !strings.Contains(strings.Join(args, " "), path) {
args = append(args, path)
}

return args[0], args[1:], nil
} else {
return linuxFallbackExplorerBinArgs(path, selectFile)
}
}

var linuxFallbackExplorerBinArgs explorerBinArgs = func(path string, selectFile bool) (string, []string, error) {
// NOTE: The linux fallback explorer opening is not supporting file selection
path = filepath.Dir(path)
return "xdg-open", []string{path}, nil
}

func pathToURI(path string) string {
absPath, err := filepath.Abs(path)
if err != nil {
return path
}
return "file://" + absPath
}
40 changes: 40 additions & 0 deletions v3/internal/fileexplorer/fileexplorer_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package fileexplorer_test

import (
"runtime"
"testing"

"github.com/wailsapp/wails/v3/internal/fileexplorer"
)

func TestFileExplorer(t *testing.T) {
t.Run("OpenFileManager", func(t *testing.T) {
t.Run("Windows", func(t *testing.T) {
if runtime.GOOS != "windows" {
t.Skip("Skipping test on non-Windows platform")
}
err := fileexplorer.OpenFileManager("C:\\Users\\wails\\Desktop\\test.txt", false)
if err != nil {
t.Fatal(err)
}
})
t.Run("Linux", func(t *testing.T) {
if runtime.GOOS != "linux" {
t.Skip("Skipping test on non-Linux platform")
}
err := fileexplorer.OpenFileManager("/home/wails/Desktop/test.txt", false)
if err != nil {
t.Fatal(err)
}
})
t.Run("Darwin", func(t *testing.T) {
if runtime.GOOS != "darwin" {
t.Skip("Skipping test on non-Darwin platform")
}
err := fileexplorer.OpenFileManager("/Users/wails/Desktop/test.txt", false)
if err != nil {
t.Fatal(err)
}
})
})
}
13 changes: 11 additions & 2 deletions v3/pkg/application/application.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import (
"embed"
"encoding/json"
"fmt"
"github.com/wailsapp/wails/v3/internal/fileexplorer"
"io"
"log"
"log/slog"
Expand All @@ -17,6 +16,8 @@ import (
"strings"
"sync"

"github.com/wailsapp/wails/v3/internal/fileexplorer"

"github.com/wailsapp/wails/v3/internal/operatingsystem"

"github.com/pkg/browser"
Expand Down Expand Up @@ -1046,8 +1047,16 @@ func (a *App) Paths(selector Paths) []string {
return pathdirs[selector]
}

// OpenDirectory opens the system file explorer to the specified directory.
func (a *App) OpenDirectory(path string) error {
return InvokeSyncWithError(func() error {
return fileexplorer.Open(path)
return fileexplorer.OpenFileManager(path, false)
})
}

// OpenFileManager opens the file manager at the specified path, optionally selecting the file.
func (a *App) OpenFileManager(path string, selectFile bool) error {
return InvokeSyncWithError(func() error {
return fileexplorer.OpenFileManager(path, selectFile)
})
}

0 comments on commit 17abf0b

Please sign in to comment.