Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 54 additions & 9 deletions app.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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/<type>, 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
Expand Down
51 changes: 51 additions & 0 deletions app_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
})
}
}
1 change: 1 addition & 0 deletions frontend/src/__tests__/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/components/DriverEditor.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<script setup lang="ts">
import ChipInput from '@/components/ChipInput.vue'
import DriverSelector from '@/components/DriverSelector.vue'
import { ExecutableExists, SelectFile } from '@/wailsjs/go/main/App'
import { ExecutableExists, SelectExecutable } from '@/wailsjs/go/main/App'
import { storage } from '@/wailsjs/go/models'
import { computed, onUnmounted, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
Expand Down Expand Up @@ -252,7 +252,7 @@ function handleRemove() {
type="button"
class="inline-flex shrink-0 items-center justify-center gap-1 rounded-lg border border-gray-200 bg-gray-50/80 px-3.5 py-2 text-xs font-bold text-gray-700 transition-colors hover:border-half-baked-300 hover:bg-white xl:text-sm"
@click="
SelectFile(true).then(path => {
SelectExecutable(driver.path, driver.type).then(path => {
if (path) driver.path = path
})
"
Expand Down
2 changes: 2 additions & 0 deletions frontend/wailsjs/go/main/App.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ export function ExecutableExists(arg1:string):Promise<boolean>;

export function PathExists(arg1:string):Promise<boolean>;

export function SelectExecutable(arg1:string,arg2:string):Promise<string>;

export function SelectFile(arg1:boolean):Promise<string>;

export function SelectFolder(arg1:boolean):Promise<string>;
Expand Down
4 changes: 4 additions & 0 deletions frontend/wailsjs/go/main/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ export function PathExists(arg1) {
return window['go']['main']['App']['PathExists'](arg1);
}

export function SelectExecutable(arg1, arg2) {
return window['go']['main']['App']['SelectExecutable'](arg1, arg2);
}

export function SelectFile(arg1) {
return window['go']['main']['App']['SelectFile'](arg1);
}
Expand Down
Loading