mirror of
https://github.com/wailsapp/wails.git
synced 2026-03-14 14:45:49 +01:00
OpenFileManager for opening with the native file manager and optional file selection support (#3937)
OpenFileManager for opening with the native file manager and optional file selection support Closes #3197 Co-authored-by: Krzysztofz01 <krzysztof.zon2001@gmail.com>
This commit is contained in:
parent
8599d7befa
commit
d03f4ce54a
4 changed files with 233 additions and 11 deletions
|
|
@ -19,7 +19,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
|
||||
|
||||
|
|
|
|||
|
|
@ -1,24 +1,169 @@
|
|||
package fileexplorer
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"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)
|
||||
}
|
||||
|
||||
return cmd.Run()
|
||||
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")
|
||||
}
|
||||
|
||||
args = append(args, path)
|
||||
return "open", args, nil
|
||||
}
|
||||
|
||||
var linuxExplorerBinArgs explorerBinArgs = func(path string, selectFile bool) (string, []string, error) {
|
||||
// Map of field codes to their replacements
|
||||
var fieldCodes = map[string]string{
|
||||
"%d": "",
|
||||
"%D": "",
|
||||
"%n": "",
|
||||
"%N": "",
|
||||
"%v": "",
|
||||
"%m": "",
|
||||
"%f": path,
|
||||
"%F": path,
|
||||
"%u": pathToURI(path),
|
||||
"%U": pathToURI(path),
|
||||
}
|
||||
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 {
|
||||
return linuxFallbackExplorerBinArgs(path, selectFile)
|
||||
}
|
||||
|
||||
desktopFile, err := findDesktopFile(strings.TrimSpace((buf.String())))
|
||||
if err != nil {
|
||||
return linuxFallbackExplorerBinArgs(path, selectFile)
|
||||
}
|
||||
|
||||
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()
|
||||
for fieldCode, replacement := range fieldCodes {
|
||||
exec = strings.ReplaceAll(exec, fieldCode, replacement)
|
||||
}
|
||||
args := strings.Fields(exec)
|
||||
if !strings.Contains(strings.Join(args, " "), path) {
|
||||
args = append(args, path)
|
||||
}
|
||||
|
||||
return args[0], args[1:], nil
|
||||
}
|
||||
|
||||
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://" + url.PathEscape(absPath)
|
||||
}
|
||||
|
||||
func findDesktopFile(xdgFileName string) (string, error) {
|
||||
paths := []string{
|
||||
filepath.Join(os.Getenv("XDG_DATA_HOME"), "applications"),
|
||||
filepath.Join(os.Getenv("HOME"), ".local", "share", "applications"),
|
||||
"/usr/share/applications",
|
||||
}
|
||||
|
||||
for _, path := range paths {
|
||||
desktopFile := filepath.Join(path, xdgFileName)
|
||||
if _, err := os.Stat(desktopFile); err == nil {
|
||||
return desktopFile, nil
|
||||
}
|
||||
}
|
||||
err := fmt.Errorf("desktop file not found: %s", xdgFileName)
|
||||
return "", err
|
||||
}
|
||||
|
|
|
|||
75
v3/internal/fileexplorer/fileexplorer_test.go
Normal file
75
v3/internal/fileexplorer/fileexplorer_test.go
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
package fileexplorer_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/wailsapp/wails/v3/internal/fileexplorer"
|
||||
)
|
||||
|
||||
func TestFileExplorer(t *testing.T) {
|
||||
// TestFileExplorer verifies that the OpenFileManager function correctly handles:
|
||||
// - Opening files in the native file manager across different platforms
|
||||
// - Selecting files when the selectFile parameter is true
|
||||
// - Various error conditions like non-existent paths
|
||||
tempDir := t.TempDir() // Create a temporary directory for tests
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
path string
|
||||
selectFile bool
|
||||
expectedErr error
|
||||
}{
|
||||
{"Open Existing File", tempDir, false, nil},
|
||||
{"Select Existing File", tempDir, true, nil},
|
||||
{"Non-Existent Path", "/path/does/not/exist", false, fmt.Errorf("failed to access the specified path: /path/does/not/exist")},
|
||||
{"Path with Special Characters", filepath.Join(tempDir, "test space.txt"), true, nil},
|
||||
{"No Permission Path", "/root/test.txt", false, fmt.Errorf("failed to open the file explorer: /root/test.txt")},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
t.Run("Windows", func(t *testing.T) {
|
||||
runPlatformTest(t, "windows")
|
||||
})
|
||||
t.Run("Linux", func(t *testing.T) {
|
||||
runPlatformTest(t, "linux")
|
||||
})
|
||||
t.Run("Darwin", func(t *testing.T) {
|
||||
runPlatformTest(t, "darwin")
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func runPlatformTest(t *testing.T, platform string) {
|
||||
if runtime.GOOS != platform {
|
||||
t.Skipf("Skipping test on non-%s platform", strings.ToTitle(platform))
|
||||
}
|
||||
|
||||
testFile := filepath.Join(t.TempDir(), "test.txt")
|
||||
if err := os.WriteFile(testFile, []byte("Test file contents"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
selectFile bool
|
||||
}{
|
||||
{"OpenFile", false},
|
||||
{"SelectFile", true},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
err := fileexplorer.OpenFileManager(testFile, test.selectFile)
|
||||
if err != nil {
|
||||
t.Errorf("OpenFileManager(%s, %v) error = %v", testFile, test.selectFile, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -5,7 +5,6 @@ import (
|
|||
"embed"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/wailsapp/wails/v3/internal/fileexplorer"
|
||||
"io"
|
||||
"log"
|
||||
"log/slog"
|
||||
|
|
@ -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"
|
||||
|
|
@ -1046,8 +1047,9 @@ func (a *App) Paths(selector Paths) []string {
|
|||
return pathdirs[selector]
|
||||
}
|
||||
|
||||
func (a *App) OpenDirectory(path string) error {
|
||||
// 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.Open(path)
|
||||
return fileexplorer.OpenFileManager(path, selectFile)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue