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:
+39
-1
@@ -1,6 +1,7 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"image/color"
|
||||
"net/url"
|
||||
"runtime"
|
||||
@@ -10,6 +11,8 @@ import (
|
||||
|
||||
"gitea.mixdep.ru/mix/gosentry/src/app"
|
||||
"gitea.mixdep.ru/mix/gosentry/src/domain"
|
||||
"gitea.mixdep.ru/mix/gosentry/src/platform/filemanager"
|
||||
"gitea.mixdep.ru/mix/gosentry/src/storage"
|
||||
|
||||
"fyne.io/fyne/v2"
|
||||
"fyne.io/fyne/v2/canvas"
|
||||
@@ -103,6 +106,13 @@ func settingsView(w fyne.Window, svc *app.Service) fyne.CanvasObject {
|
||||
logsDirBrowse := widget.NewButtonWithIcon("Browse", theme.FolderOpenIcon(), func() {
|
||||
chooseFolder(w, logsDir)
|
||||
})
|
||||
// Log files are read outside the app, so the folder gets a direct shortcut
|
||||
// beside its path instead of making the user copy the path into a file
|
||||
// manager. It reveals whatever the field currently holds, so an edit can be
|
||||
// checked before Save.
|
||||
logsDirOpen := widget.NewButtonWithIcon("Open", theme.FolderIcon(), func() {
|
||||
openFolder(w, settingsFolderPath(store.Paths.AppDir, logsDir.Text))
|
||||
})
|
||||
maxLogFiles := widget.NewEntry()
|
||||
maxLogFiles.SetText(strconv.Itoa(store.Config.MaxLogFiles))
|
||||
maxLogFiles.OnChanged = func(string) { updateSaveState() }
|
||||
@@ -259,7 +269,9 @@ func settingsView(w fyne.Window, svc *app.Service) fyne.CanvasObject {
|
||||
widget.NewLabelWithStyle("Storage", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
|
||||
settingsRow("Config JSON", widget.NewLabel(store.Paths.ConfigPath)),
|
||||
settingsRow("Jobs directory", container.NewBorder(nil, nil, nil, jobsDirBrowse, jobsDir)),
|
||||
settingsRow("Logs directory", container.NewBorder(nil, nil, nil, logsDirBrowse, logsDir)),
|
||||
// Browse stays rightmost so it lines up with the Jobs directory row
|
||||
// above it; Open sits between it and the path it opens.
|
||||
settingsRow("Logs directory", container.NewBorder(nil, nil, nil, container.NewHBox(logsDirOpen, logsDirBrowse), logsDir)),
|
||||
settingsRow("Max log files", maxLogFiles),
|
||||
settingsRow("Max log age days", maxLogAgeDays),
|
||||
),
|
||||
@@ -359,6 +371,32 @@ func chooseFolder(w fyne.Window, target *widget.Entry) {
|
||||
folderDialog.Show()
|
||||
}
|
||||
|
||||
// settingsFolderPath resolves what a directory field currently points at,
|
||||
// applying the same relative-path rule the store uses when it loads the config
|
||||
// so the folder that opens is the one the setting would use. Blank text has no
|
||||
// folder to open and yields an empty path.
|
||||
func settingsFolderPath(appDir string, text string) string {
|
||||
trimmed := strings.TrimSpace(text)
|
||||
if trimmed == "" {
|
||||
return ""
|
||||
}
|
||||
return storage.ResolveConfiguredDir(appDir, trimmed)
|
||||
}
|
||||
|
||||
// openFolder reveals dir in the desktop file manager. A folder that is not set
|
||||
// or cannot be opened (most often: it does not exist yet, because the logs
|
||||
// directory is created on the first run) is reported in a dialog rather than
|
||||
// leaving the button looking dead.
|
||||
func openFolder(w fyne.Window, dir string) {
|
||||
if dir == "" {
|
||||
dialog.ShowError(errors.New("no folder is set"), w)
|
||||
return
|
||||
}
|
||||
if err := filemanager.Open(dir); err != nil {
|
||||
dialog.ShowError(err, w)
|
||||
}
|
||||
}
|
||||
|
||||
// Theme dropdown labels. These are the human-facing captions; themeLabel and
|
||||
// themeFromLabel translate between them and the stored domain.Theme values so the
|
||||
// select never leaks the on-disk "default"/"gosentry" strings to the user.
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user