fix: open native directory picker for new instances

This commit is contained in:
QiuSW
2026-07-22 16:06:39 +08:00
parent 6f08c9c9d0
commit dbbb5c0e98
12 changed files with 289 additions and 14 deletions
@@ -0,0 +1,9 @@
package files
import "context"
// DirectoryPicker opens a platform directory chooser. The implementation must
// block only in its own goroutine; UI callers receive the result asynchronously.
type DirectoryPicker interface {
ChooseDirectory(context.Context) (string, error)
}
@@ -0,0 +1,18 @@
//go:build !windows
package files
import (
"context"
"errors"
)
var ErrSelectionCanceled = context.Canceled
type unsupportedDirectoryPicker struct{}
func NewDirectoryPicker() DirectoryPicker { return unsupportedDirectoryPicker{} }
func (unsupportedDirectoryPicker) ChooseDirectory(context.Context) (string, error) {
return "", errors.New("directory picker is only supported on Windows")
}
@@ -0,0 +1,62 @@
//go:build windows
package files
import (
"context"
"fmt"
"os/exec"
"path/filepath"
"strings"
)
var ErrSelectionCanceled = context.Canceled
type commandRunner interface {
Run(context.Context, string, ...string) ([]byte, error)
}
type execRunner struct{}
func (execRunner) Run(ctx context.Context, name string, args ...string) ([]byte, error) {
return exec.CommandContext(ctx, name, args...).Output()
}
type windowsDirectoryPicker struct{ runner commandRunner }
func NewDirectoryPicker() DirectoryPicker { return windowsDirectoryPicker{runner: execRunner{}} }
func newDirectoryPickerWithRunner(runner commandRunner) windowsDirectoryPicker {
return windowsDirectoryPicker{runner: runner}
}
func (p windowsDirectoryPicker) ChooseDirectory(ctx context.Context) (string, error) {
if err := ctx.Err(); err != nil {
return "", err
}
output, err := p.runner.Run(ctx, "powershell.exe",
"-NoProfile", "-NonInteractive", "-STA", "-WindowStyle", "Hidden", "-Command", folderDialogScript)
if err != nil {
if ctxErr := ctx.Err(); ctxErr != nil {
return "", ctxErr
}
return "", fmt.Errorf("open directory picker: %w", err)
}
selected := strings.TrimSpace(string(output))
if selected == "" {
return "", ErrSelectionCanceled
}
if !filepath.IsAbs(selected) {
return "", fmt.Errorf("directory picker returned a non-absolute path")
}
return filepath.Clean(selected), nil
}
const folderDialogScript = `[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new($false)
Add-Type -AssemblyName System.Windows.Forms
$dialog = New-Object System.Windows.Forms.FolderBrowserDialog
$dialog.Description = '选择浏览器 User Data Dir'
$dialog.ShowNewFolderButton = $true
if ($dialog.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK) {
[Console]::Out.Write($dialog.SelectedPath)
}`
@@ -0,0 +1,45 @@
//go:build windows
package files
import (
"context"
"errors"
"strings"
"testing"
)
type fakeRunner struct {
name string
args []string
out []byte
err error
}
func (f *fakeRunner) Run(_ context.Context, name string, args ...string) ([]byte, error) {
f.name = name
f.args = append([]string(nil), args...)
return f.out, f.err
}
func TestWindowsDirectoryPickerReturnsCleanAbsolutePath(t *testing.T) {
runner := &fakeRunner{out: []byte("C:\\profiles\\demo\\\r\n")}
picker := newDirectoryPickerWithRunner(runner)
got, err := picker.ChooseDirectory(context.Background())
if err != nil {
t.Fatal(err)
}
if got != `C:\profiles\demo` {
t.Fatalf("path = %q", got)
}
if runner.name != "powershell.exe" || !strings.Contains(strings.Join(runner.args, " "), "-STA") {
t.Fatalf("unexpected picker command: %s %v", runner.name, runner.args)
}
}
func TestWindowsDirectoryPickerTreatsEmptySelectionAsCanceled(t *testing.T) {
picker := newDirectoryPickerWithRunner(&fakeRunner{})
if _, err := picker.ChooseDirectory(context.Background()); !errors.Is(err, ErrSelectionCanceled) {
t.Fatalf("error = %v", err)
}
}