From fe1e21e2ece8774b928c942c5b52dd498402b670 Mon Sep 17 00:00:00 2001 From: relent0r Date: Fri, 23 Oct 2026 07:38:12 +1300 Subject: [PATCH 1/3] Add LINUX file browser option logic and add StandaloneFileBrowserLinux implementation --- .../StandaloneFileBrowser.cs | 2 + .../StandaloneFileBrowserLinux.cs | 173 ++++++++++++++++++ .../StandaloneFileBrowserLinux.cs.meta | 11 ++ 3 files changed, 186 insertions(+) create mode 100644 Assets/StandaloneFileBrowser/StandaloneFileBrowserLinux.cs create mode 100644 Assets/StandaloneFileBrowser/StandaloneFileBrowserLinux.cs.meta diff --git a/Assets/StandaloneFileBrowser/StandaloneFileBrowser.cs b/Assets/StandaloneFileBrowser/StandaloneFileBrowser.cs index 71144f17..9a637a97 100644 --- a/Assets/StandaloneFileBrowser/StandaloneFileBrowser.cs +++ b/Assets/StandaloneFileBrowser/StandaloneFileBrowser.cs @@ -17,6 +17,8 @@ static StandaloneFileBrowser() { _platformWrapper = new StandaloneFileBrowserMac(); #elif UNITY_STANDALONE_WIN _platformWrapper = new StandaloneFileBrowserWindows(); +#elif UNITY_STANDALONE_LINUX + _platformWrapper = new StandaloneFileBrowserLinux(); #elif UNITY_EDITOR _platformWrapper = new StandaloneFileBrowserEditor(); #endif diff --git a/Assets/StandaloneFileBrowser/StandaloneFileBrowserLinux.cs b/Assets/StandaloneFileBrowser/StandaloneFileBrowserLinux.cs new file mode 100644 index 00000000..43677fd5 --- /dev/null +++ b/Assets/StandaloneFileBrowser/StandaloneFileBrowserLinux.cs @@ -0,0 +1,173 @@ +#if UNITY_STANDALONE_LINUX + +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Text; +using UnityEngine; + +namespace SFB { + public class StandaloneFileBrowserLinux : IStandaloneFileBrowser { + private const char MultiSelectSeparator = (char)28; + + public string[] OpenFilePanel(string title, string directory, ExtensionFilter[] extensions, bool multiselect) { + var zenityArgs = new List { "--file-selection" }; + AddOption(zenityArgs, "--title", title); + AddInitialDirectory(zenityArgs, directory); + AddZenityFilters(zenityArgs, extensions); + if (multiselect) { + zenityArgs.Add("--multiple"); + AddOption(zenityArgs, "--separator", MultiSelectSeparator.ToString()); + } + + var kdialogArgs = new List { "--title", title, "--getopenfilename" }; + if (multiselect) + kdialogArgs.AddRange(new[] { "--multiple", "--separate-output" }); + kdialogArgs.Add(string.IsNullOrEmpty(directory) ? Environment.CurrentDirectory : directory); + kdialogArgs.Add(GetKdialogFilter(extensions)); + + return ShowDialog(zenityArgs, kdialogArgs, multiselect); + } + + public string[] OpenFolderPanel(string title, string directory, bool multiselect) { + var zenityArgs = new List { "--file-selection", "--directory" }; + AddOption(zenityArgs, "--title", title); + AddInitialDirectory(zenityArgs, directory); + if (multiselect) { + zenityArgs.Add("--multiple"); + AddOption(zenityArgs, "--separator", MultiSelectSeparator.ToString()); + } + + var kdialogArgs = new List { "--title", title, "--getexistingdirectory", + string.IsNullOrEmpty(directory) ? Environment.CurrentDirectory : directory }; + + return ShowDialog(zenityArgs, kdialogArgs, multiselect); + } + + public string SaveFilePanel(string title, string directory, string defaultName, ExtensionFilter[] extensions) { + var initialPath = string.IsNullOrEmpty(directory) + ? defaultName + : Path.Combine(directory, defaultName ?? string.Empty); + + var zenityArgs = new List { "--file-selection", "--save", "--confirm-overwrite" }; + AddOption(zenityArgs, "--title", title); + if (!string.IsNullOrEmpty(initialPath)) + AddOption(zenityArgs, "--filename", initialPath); + AddZenityFilters(zenityArgs, extensions); + + var kdialogArgs = new List { "--title", title, "--getsavefilename", + string.IsNullOrEmpty(initialPath) ? Environment.CurrentDirectory : initialPath, + GetKdialogFilter(extensions) }; + + var paths = ShowDialog(zenityArgs, kdialogArgs, false); + return paths.Length == 0 ? string.Empty : AddDefaultExtension(paths[0], extensions); + } + + private static string[] ShowDialog(List zenityArgs, List kdialogArgs, bool multiselect) { + string output; + if (TryRunDialog("zenity", zenityArgs, out output)) + return SplitPaths(output, MultiSelectSeparator); + + if (TryRunDialog("kdialog", kdialogArgs, out output)) + return SplitPaths(output, multiselect ? '\n' : '\0'); + + UnityEngine.Debug.LogError("No Linux file dialog was found. Install zenity or kdialog to open files and folders."); + return new string[0]; + } + + private static bool TryRunDialog(string executable, List arguments, out string output) { + try { + using (var process = new Process()) { + process.StartInfo = new ProcessStartInfo { + FileName = executable, + Arguments = BuildArguments(arguments), + UseShellExecute = false, + RedirectStandardOutput = true, + CreateNoWindow = true + }; + process.Start(); + output = process.StandardOutput.ReadToEnd(); + process.WaitForExit(); + return true; + } + } + catch (Win32Exception) { + output = string.Empty; + return false; + } + } + + private static void AddInitialDirectory(List arguments, string directory) { + if (string.IsNullOrEmpty(directory)) + return; + + var initialDirectory = directory.EndsWith("/", StringComparison.Ordinal) + ? directory + : directory + "/"; + AddOption(arguments, "--filename", initialDirectory); + } + + private static void AddZenityFilters(List arguments, ExtensionFilter[] extensions) { + if (extensions == null) + return; + + foreach (var filter in extensions) { + var patterns = GetPatterns(filter.Extensions); + if (patterns.Length > 0) { + AddOption(arguments, "--file-filter", + string.IsNullOrEmpty(filter.Name) ? patterns : filter.Name + " | " + patterns); + } + } + } + + private static string GetKdialogFilter(ExtensionFilter[] extensions) { + if (extensions == null) + return string.Empty; + + return string.Join("\n", extensions + .Where(filter => filter.Extensions != null && filter.Extensions.Length > 0) + .Select(filter => GetPatterns(filter.Extensions) + "|" + filter.Name)); + } + + private static string GetPatterns(string[] extensions) { + if (extensions == null) + return string.Empty; + + return string.Join(" ", extensions.Select(extension => "*." + extension)); + } + + private static string AddDefaultExtension(string path, ExtensionFilter[] extensions) { + if (extensions == null || extensions.Length == 0 || extensions[0].Extensions == null + || extensions[0].Extensions.Length == 0 || Path.HasExtension(path)) + return path; + + return path + "." + extensions[0].Extensions[0]; + } + + private static void AddOption(List arguments, string option, string value) { + arguments.Add(option); + arguments.Add(value ?? string.Empty); + } + + private static string BuildArguments(List arguments) { + return string.Join(" ", arguments.Select(QuoteArgument)); + } + + private static string QuoteArgument(string argument) { + return "\"" + argument.Replace("\"", "\\\"") + "\""; + } + + private static string[] SplitPaths(string output, char separator) { + if (string.IsNullOrEmpty(output)) + return new string[0]; + + return output.TrimEnd('\r', '\n') + .Split(new[] { separator }, StringSplitOptions.RemoveEmptyEntries); + } + } +} + +#endif \ No newline at end of file diff --git a/Assets/StandaloneFileBrowser/StandaloneFileBrowserLinux.cs.meta b/Assets/StandaloneFileBrowser/StandaloneFileBrowserLinux.cs.meta new file mode 100644 index 00000000..a88f8734 --- /dev/null +++ b/Assets/StandaloneFileBrowser/StandaloneFileBrowserLinux.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: cd15be82a9444f3caf380382b408a52a +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: \ No newline at end of file From 0493cce41ae939357416fca50206d2863ea4d506 Mon Sep 17 00:00:00 2001 From: relent0r Date: Fri, 23 Oct 2026 08:01:40 +1300 Subject: [PATCH 2/3] add description --- Assets/StandaloneFileBrowser/StandaloneFileBrowserLinux.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Assets/StandaloneFileBrowser/StandaloneFileBrowserLinux.cs b/Assets/StandaloneFileBrowser/StandaloneFileBrowserLinux.cs index 43677fd5..12ef8d37 100644 --- a/Assets/StandaloneFileBrowser/StandaloneFileBrowserLinux.cs +++ b/Assets/StandaloneFileBrowser/StandaloneFileBrowserLinux.cs @@ -10,6 +10,13 @@ using UnityEngine; namespace SFB { + // Native Linux implementation of IStandaloneFileBrowser for Unity. + // Intent: Binds Unity file panel calls to native Linux desktop file pickers. + // Execution; Fallback: Attempts to execute zenity first; + // if unavailable, gracefully falls back to kdialog. + // Uses an ASCII record separator ((char)28) for Zenity multi-selection to safely handle spaces and special characters in filenames. + // Normalizes directory paths with trailing slashes for Zenity compatibility. + public class StandaloneFileBrowserLinux : IStandaloneFileBrowser { private const char MultiSelectSeparator = (char)28; From 1085a6deffdf21dc16e38987f329983fe131903a Mon Sep 17 00:00:00 2001 From: relent0r Date: Sat, 24 Oct 2026 08:18:47 +1300 Subject: [PATCH 3/3] add required license property --- Assets/StandaloneFileBrowser/StandaloneFileBrowserLinux.cs.meta | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Assets/StandaloneFileBrowser/StandaloneFileBrowserLinux.cs.meta b/Assets/StandaloneFileBrowser/StandaloneFileBrowserLinux.cs.meta index a88f8734..c2e0ec75 100644 --- a/Assets/StandaloneFileBrowser/StandaloneFileBrowserLinux.cs.meta +++ b/Assets/StandaloneFileBrowser/StandaloneFileBrowserLinux.cs.meta @@ -1,5 +1,7 @@ fileFormatVersion: 2 guid: cd15be82a9444f3caf380382b408a52a +timeCreated: 1792869188 +licenseType: Pro MonoImporter: externalObjects: {} serializedVersion: 2