diff --git a/internal/config/config.go b/internal/config/config.go index e654250..c3b3f40 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -12,7 +12,8 @@ const appName = "luxtools-client" // Config stores client configuration loaded from disk. type Config struct { - PathMap map[string]string `json:"path_map"` + PathMap map[string]string `json:"path_map"` + OpenLocationCommand string `json:"open_location_command,omitempty"` } // ConfigPath returns the full path to the config.json file. diff --git a/internal/openfolder/custom.go b/internal/openfolder/custom.go new file mode 100644 index 0000000..5dbe28c --- /dev/null +++ b/internal/openfolder/custom.go @@ -0,0 +1,72 @@ +package openfolder + +import ( + "errors" + "os/exec" + "strings" +) + +// OpenLocationCustom executes a user-defined command string, substituting %1 +// with the given path. The command string is split into executable + arguments +// using shell-like quoting rules (double quotes are respected). +// +// Example command: doublecmd.exe -C -T -P L -L "%1" +func OpenLocationCustom(command string, path string) error { + expanded := strings.ReplaceAll(command, "%1", path) + + args, err := splitCommand(expanded) + if err != nil { + return err + } + if len(args) == 0 { + return errors.New("open_location_command: empty command after expansion") + } + + return exec.Command(args[0], args[1:]...).Start() +} + +// splitCommand splits a command string into tokens, respecting double-quoted +// segments. Quotes are removed from the resulting tokens. Backslash escaping +// of a double quote (\") inside a quoted segment is supported. +func splitCommand(s string) ([]string, error) { + var tokens []string + var current strings.Builder + inQuote := false + hasToken := false + + for i := 0; i < len(s); i++ { + ch := s[i] + + switch { + case ch == '"': + inQuote = !inQuote + hasToken = true // even empty quotes produce a token part + + case ch == '\\' && inQuote && i+1 < len(s) && s[i+1] == '"': + // escaped quote inside a quoted segment + current.WriteByte('"') + i++ // skip the next quote + + case (ch == ' ' || ch == '\t') && !inQuote: + if hasToken { + tokens = append(tokens, current.String()) + current.Reset() + hasToken = false + } + + default: + current.WriteByte(ch) + hasToken = true + } + } + + if inQuote { + return nil, errors.New("open_location_command: unterminated double quote") + } + + if hasToken { + tokens = append(tokens, current.String()) + } + + return tokens, nil +} diff --git a/internal/web/templates.go b/internal/web/templates.go index d0a2b62..0a6e54c 100644 --- a/internal/web/templates.go +++ b/internal/web/templates.go @@ -60,7 +60,7 @@ var settingsTemplate = template.Must(template.New("settings").Parse(`

Path Aliases

-

Define aliases like PROJECTS -> /mnt/projects. Use in /open as PROJECTS>my/repo.

+

Define aliases like PROJECTS -> /mnt/projects. Use in /open as PROJECTS>my/repo.

@@ -70,7 +70,19 @@ var settingsTemplate = template.Must(template.New("settings").Parse(`
- +
+ +

Open Location Command

+

+ Optionally override the default file manager. Use %1 for the resolved path.
+ Example: doublecmd.exe -C -T -P L -L "%1" +

+
+ +
+ +
+
@@ -148,6 +160,7 @@ var settingsTemplate = template.Must(template.New("settings").Parse(`