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 @@