tray: --import-profile merges a profile document into the user config

RioJoy.Tray --import-profile <profile.json> merges a single-profile
document (a repo's profiles/*.json) into %APPDATA%\RIOJoy\config.json
via ConfigStore.ImportProfile: same-name profiles are replaced in place
(FindProfile's case-insensitive convention), everything else appended,
all other config content preserved. Refuses (exit 3) while a tray
instance is running - a live tray rewrites the config from memory and
would silently discard the import. Documents that a Name must be stated
in the file itself: RioProfile defaults Name to 'Unnamed', so the import
checks the raw JSON, not the deserialized object. This is the profile
install story for bench machines and pods (previously: hand-paste into
the Profiles array).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Cyd
2026-07-30 09:33:25 -05:00
co-authored by Claude Fable 5
parent 6967e66837
commit 3512c89dca
4 changed files with 176 additions and 3 deletions
+49 -2
View File
@@ -1,3 +1,5 @@
using RioJoy.Core.Profiles;
namespace RioJoy.Tray;
internal static class Program
@@ -12,18 +14,63 @@ internal static class Program
/// Entry point. RIOJoy runs as a background tray application with no main
/// window: an ApplicationContext owns the NotifyIcon and the runtime, so the
/// message loop stays alive while the only UI is the tray icon and its menu.
///
/// <para><c>--import-profile &lt;profile.json&gt;</c> merges a single-profile
/// document (a repo's <c>profiles/*.json</c>) into this user's config and
/// exits without starting the tray. Output goes to stdout/stderr, which a
/// GUI-subsystem exe only delivers when redirected — check the exit code
/// (0 ok, 1 failed, 2 usage, 3 tray running) when scripting it.</para>
/// </summary>
[STAThread]
private static void Main()
private static int Main(string[] args)
{
if (args.Length >= 1 && string.Equals(args[0], "--import-profile", StringComparison.OrdinalIgnoreCase))
return ImportProfile(args);
using var instance = new Mutex(initiallyOwned: true, SingleInstanceMutex, out bool createdNew);
if (!createdNew)
return; // another RIOJoy is already running in this session
return 0; // another RIOJoy is already running in this session
// net48 has no source-generated ApplicationConfiguration.Initialize();
// do the equivalent setup directly.
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new TrayApplicationContext());
return 0;
}
private static int ImportProfile(string[] args)
{
if (args.Length != 2)
{
Console.Error.WriteLine("usage: RioJoy.Tray --import-profile <profile.json>");
return 2;
}
// A running tray holds the config in memory and rewrites it on its own
// saves, which would silently discard this import — refuse instead.
// (OpenExisting + catch rather than TryOpenExisting: net40 lacks the Try form.)
try
{
using Mutex running = Mutex.OpenExisting(SingleInstanceMutex);
Console.Error.WriteLine("RIOJoy is running in this session - quit it from the tray menu, import, then relaunch.");
return 3;
}
catch (WaitHandleCannotBeOpenedException)
{
// not running — proceed
}
try
{
ProfileImportResult result = ConfigStore.ImportProfile(TrayApplicationContext.ConfigPath, args[1]);
Console.WriteLine($"{(result.Replaced ? "Replaced" : "Added")} profile '{result.Name}' in {TrayApplicationContext.ConfigPath}");
return 0;
}
catch (Exception ex)
{
Console.Error.WriteLine($"import failed: {ex.Message}");
return 1;
}
}
}
+2 -1
View File
@@ -17,7 +17,8 @@ namespace RioJoy.Tray;
/// </summary>
internal sealed class TrayApplicationContext : ApplicationContext
{
private static readonly string ConfigPath =
// Internal so Program's --import-profile writes the same store the tray reads.
internal static readonly string ConfigPath =
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "RIOJoy", "config.json");
private static readonly TimeSpan PollInterval = TimeSpan.FromSeconds(1);