From 920f1d8eae06b669e926cdb2efed74798f437f6e Mon Sep 17 00:00:00 2001 From: markmybytes Date: Sat, 10 Oct 2026 22:35:31 +0800 Subject: [PATCH] feat: start driver file dialog at the driver's folder The driver's file dialog opened wherever Windows last had been, so editing an existing driver meant hunting for the file again from an unrelated folder. SelectExecutable starts at the configured path's folder, then drivers/, then the app root, and always returns a path relative to the app root so exported configs stay portable. Each rung is used only if it exists on disk. Wails rejects a DefaultDirectory that is not a directory, so without those checks a stale configured path would fail the dialog outright instead of falling through to the next rung. defaultDir holds no driver knowledge; the caller names the fallback folder. A driver with no type yet skips that rung rather than resolving to the drivers root, which would look like a valid category folder. No file-type filter is applied, so scripts stay selectable. Note that exec.Command cannot run a .ps1 or .msi even though ExecutableExists reports it as valid, so such a path fails at run time rather than at selection time. SelectFile and SelectFolder keep their existing signatures. --- app.go | 63 ++++++++++++++++++++---- app_test.go | 51 +++++++++++++++++++ frontend/src/__tests__/setup.ts | 1 + frontend/src/components/DriverEditor.vue | 4 +- frontend/wailsjs/go/main/App.d.ts | 2 + frontend/wailsjs/go/main/App.js | 4 ++ 6 files changed, 114 insertions(+), 11 deletions(-) diff --git a/app.go b/app.go index 3e7593e..627ddf8 100644 --- a/app.go +++ b/app.go @@ -38,15 +38,7 @@ func (a *App) SelectFolder(relative bool) (string, error) { if path, err := wails_runtime.OpenDirectoryDialog(a.ctx, wails_runtime.OpenDialogOptions{}); err != nil || path == "" { return "", errcode.New("errAppDialogOpen") } else if relative { - exePath, err := os.Executable() - if err != nil { - return "", errcode.New("errAppExecLookup") - } - rel, err := filepath.Rel(filepath.Dir(exePath), path) - if err != nil { - return "", errcode.New("errAppExecLookup") - } - return rel, nil + return relToRoot(path) } else { return path, nil } @@ -70,6 +62,59 @@ func (a *App) SelectFile(relative bool) (string, error) { } } +// SelectExecutable opens a file dialog for a driver executable, starting from +// the configured path's folder, then drivers/, then the app root. The +// result is always relative to the app root so exported configs stay portable. +func (a *App) SelectExecutable(currentPath, driverType string) (string, error) { + // Fall back to the driver's category folder, e.g. drivers/network. A driver + // with no type yet has no category folder, so skip that rung entirely — + // an empty driverType would otherwise resolve to the drivers root. + fallbackDir := "" + if driverType != "" { + fallbackDir = filepath.Join(dirDir, driverType) + } + + if path, err := wails_runtime.OpenFileDialog(a.ctx, wails_runtime.OpenDialogOptions{ + DefaultDirectory: defaultDir(currentPath, fallbackDir), + }); err != nil || path == "" { + return "", errcode.New("errAppDialogOpen") + } else { + return relToRoot(path) + } +} + +// relToRoot converts an absolute path to one relative to the app root, so the +// config stays portable across install locations. +func relToRoot(path string) (string, error) { + exePath, err := os.Executable() + if err != nil { + return "", errcode.New("errAppExecLookup") + } + rel, err := filepath.Rel(filepath.Dir(exePath), path) + if err != nil { + return "", errcode.New("errAppExecLookup") + } + return rel, nil +} + +// defaultDir picks the folder a file dialog should open in: the configured +// path's folder, then fallbackDir, then the app root. Each rung is used only +// if it exists, so a stale path falls through instead of erroring. +func defaultDir(currentPath, fallbackDir string) string { + if currentPath != "" { + d := filepath.Dir(filepath.Join(dirRoot, filepath.FromSlash(currentPath))) + if info, err := os.Stat(d); err == nil && info.IsDir() { + return d + } + } + if fallbackDir != "" { + if info, err := os.Stat(fallbackDir); err == nil && info.IsDir() { + return fallbackDir + } + } + return dirRoot +} + func (a App) PathExists(path string) bool { _, err := os.Stat(path) return err == nil diff --git a/app_test.go b/app_test.go index 65a7c4b..756489c 100644 --- a/app_test.go +++ b/app_test.go @@ -90,3 +90,54 @@ func TestApp_PathExists_NonExistent(t *testing.T) { t.Errorf("PathExists(%q) = true, want false for non-existent path", path) } } + +func TestDefaultDir(t *testing.T) { + origRoot, origDir := dirRoot, dirDir + t.Cleanup(func() { dirRoot, dirDir = origRoot, origDir }) + + tests := []struct { + name string + currentPath string + fallback string // subdir of drivers to fall back to; "" skips that rung + want string // "fallback", "root", or a slash path relative to root + }{ + {name: "currentPath folder wins over fallback", currentPath: "drivers/network/foo.exe", fallback: "display", want: "drivers/network"}, + {name: "no currentPath uses fallback", fallback: "display", want: "fallback"}, + {name: "stale currentPath falls back", currentPath: "drivers/gone/foo.exe", fallback: "display", want: "fallback"}, + {name: "stale currentPath and stale fallback return root", currentPath: "drivers/gone/foo.exe", fallback: "absent", want: "root"}, + {name: "no fallback rung returns root", currentPath: "drivers/gone/foo.exe", want: "root"}, + {name: "nothing configured returns root", want: "root"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + root := t.TempDir() + dirRoot = root + dirDir = filepath.Join(root, "drivers") + for _, sub := range []string{"network", "display"} { + if err := os.MkdirAll(filepath.Join(dirDir, sub), os.ModePerm); err != nil { + t.Fatal(err) + } + } + + fallback := "" + if tt.fallback != "" { + fallback = filepath.Join(dirDir, tt.fallback) + } + + var want string + switch tt.want { + case "fallback": + want = fallback + case "root": + want = dirRoot + default: + want = filepath.Join(dirRoot, filepath.FromSlash(tt.want)) + } + + if got := defaultDir(tt.currentPath, fallback); got != want { + t.Errorf("defaultDir(%q, %q) = %q, want %q", tt.currentPath, fallback, got, want) + } + }) + } +} diff --git a/frontend/src/__tests__/setup.ts b/frontend/src/__tests__/setup.ts index 74fa51f..3d1df83 100644 --- a/frontend/src/__tests__/setup.ts +++ b/frontend/src/__tests__/setup.ts @@ -15,6 +15,7 @@ vi.mock('@/wailsjs/go/main/App', () => ({ ExecutableExists: vi.fn().mockResolvedValue(true), SelectFolder: vi.fn().mockResolvedValue('C:/selected'), SelectFile: vi.fn().mockResolvedValue('C:/selected/file.exe'), + SelectExecutable: vi.fn().mockResolvedValue('C:/drivers/network/setup.exe'), SetContext: vi.fn().mockResolvedValue(undefined), Update: vi.fn().mockResolvedValue(undefined), WebView2Version: vi.fn().mockResolvedValue('110.0.1'), diff --git a/frontend/src/components/DriverEditor.vue b/frontend/src/components/DriverEditor.vue index 7ee6863..d505b2c 100644 --- a/frontend/src/components/DriverEditor.vue +++ b/frontend/src/components/DriverEditor.vue @@ -1,7 +1,7 @@