feat: open the logs folder from the Settings tab

Reading a log file meant copying the configured path out of Settings and
pasting it into a file manager. The Logs directory row now carries an Open
button beside Browse that reveals the folder directly.

The new src/platform/filemanager package holds the platform split — explorer
on Windows, xdg-open on Linux, an "unsupported" error elsewhere — and starts
the handler without waiting on it, since Explorer exits non-zero even after it
opens the window and blocking would stall the UI thread. A missing path, a
path that is a file, and a handler that will not start are all reported to the
user; the logs directory does not exist until the first run, so that case is
reachable.

The button opens whatever the field currently holds rather than the saved
config, so an edit can be checked before Save. Resolving a relative directory
against the application folder is the store's rule, so resolveConfiguredDir is
now exported as storage.ResolveConfiguredDir instead of being duplicated in
the UI.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
mixeme
2026-07-26 23:09:33 +03:00
parent fe13d1f34e
commit 5e09ba1d58
11 changed files with 258 additions and 5 deletions
+35
View File
@@ -0,0 +1,35 @@
package ui
import (
"path/filepath"
"testing"
)
// TestSettingsFolderPath covers the path the "Open" button beside the logs
// directory hands to the file manager: blank means nothing to open, a relative
// directory resolves against the application directory (as the store does),
// and an absolute directory is used as typed. Both directories come from
// t.TempDir so the absolute case is genuinely absolute on Windows too.
func TestSettingsFolderPath(t *testing.T) {
appDir := t.TempDir()
absolute := filepath.Join(t.TempDir(), "logs")
cases := []struct {
name string
text string
want string
}{
{name: "empty", text: "", want: ""},
{name: "whitespace only", text: " ", want: ""},
{name: "relative", text: "logs", want: filepath.Join(appDir, "logs")},
{name: "relative with spaces around it", text: " logs ", want: filepath.Join(appDir, "logs")},
{name: "absolute", text: absolute, want: absolute},
}
for _, testCase := range cases {
t.Run(testCase.name, func(t *testing.T) {
if got := settingsFolderPath(appDir, testCase.text); got != testCase.want {
t.Errorf("settingsFolderPath(%q, %q) = %q, want %q", appDir, testCase.text, got, testCase.want)
}
})
}
}