diff --git a/Core/RenderManager.cs b/Core/RenderManager.cs
index 9618f52..8200297 100644
--- a/Core/RenderManager.cs
+++ b/Core/RenderManager.cs
@@ -475,7 +475,7 @@ namespace ADOFAIRenderer.Core
var renderFPS = Configuration.RenderFPS;
var videoFPS = Configuration.VideoFPS;
- var selectedCodec = Configuration.VideoConfig?.Codec ?? Codec.H264;
+ var selectedCodec = Configuration.VideoConfig.Codec;
var ext = GetOutputExtension(selectedCodec);
var startTime = DateTime.Now;
@@ -493,7 +493,7 @@ namespace ADOFAIRenderer.Core
Height = Configuration.RenderHeight,
FPS = effectiveVideoFps,
Codec = selectedCodec,
- Mode = Configuration.VideoConfig!.RateControlMode,
+ Mode = Configuration.VideoConfig.RateControlMode,
QualityValue = Configuration.VideoConfig.QP,
TargetBitrate = Configuration.VideoConfig.Bitrate,
MaxBitrate = Configuration.VideoConfig.MaxBitrate,
@@ -754,11 +754,11 @@ namespace ADOFAIRenderer.Core
private IEnumerator StopRenderCoroutine()
{
yield return new WaitUntil(() => endSuccessful);
- var file = !string.IsNullOrEmpty(_currentOutPath) ? Path.GetFileName(_currentOutPath) : "동영상";
+ var fileString = !string.IsNullOrEmpty(_currentOutPath) ? $"'{Path.GetFileName(_currentOutPath)}'이(가)" : "동영상이";
if (!Application.isFocused)
{
string renderFolder = Path.Combine(MelonEnvironment.GameRootDirectory, "Render");
- OSNotification.Show("렌더링 완료!", $"'{file}'이(가) 성공적으로 저장되었습니다.", renderFolder);
+ OSNotification.Show("렌더링 완료!", $"{fileString} 성공적으로 저장되었습니다.", renderFolder);
}
EndRender();
}
diff --git a/Util/OSNotification.cs b/Util/OSNotification.cs
index 69543a5..a556081 100644
--- a/Util/OSNotification.cs
+++ b/Util/OSNotification.cs
@@ -1,31 +1,49 @@
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
+using System.Threading;
using MelonLoader;
namespace ADOFAIRenderer.Util
{
+ ///
+ /// Sends native desktop notifications on Windows, macOS, and Linux.
+ ///
+ /// - Windows: Uses Win32 Shell_NotifyIcon (system tray balloon) via P/Invoke.
+ /// No PowerShell, no UWP, no WinRT dependencies.
+ ///
+ ///
+ /// - macOS: Uses osascript with AppleScript's display notification.
+ ///
+ ///
+ /// - Linux: Uses notify-send (libnotify) with xdg-open for folder actions.
+ ///
+ ///
public static class OSNotification
{
///
- /// Sends a native desktop notification on Windows, macOS, or Linux.
- /// If a folder path is provided, it attempts to add an action button to open the folder on Windows and Linux.
+ /// Sends a native desktop notification.
+ /// If is provided, Windows and Linux will attempt to open
+ /// the folder when the notification is clicked/dismissed.
///
+ /// The notification title.
+ /// The notification body text.
+ /// Optional folder path to open on click (Windows balloon click / Linux action).
public static void Show(string title, string message, string? folderPath = null)
{
try
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
- ShowWindowsNotification(title, message, folderPath);
+ WindowsNotification.Send(title, message, folderPath);
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
- ShowMacNotification(title, message);
+ MacNotification.Send(title, message);
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
- ShowLinuxNotification(title, message, folderPath);
+ LinuxNotification.Send(title, message, folderPath);
}
}
catch (Exception ex)
@@ -34,105 +52,405 @@ namespace ADOFAIRenderer.Util
}
}
- private static void ShowWindowsNotification(string title, string message, string? folderPath)
+ #region Windows — Toast (primary) + Shell_NotifyIcon (fallback)
+
+ ///
+ /// Windows notification with two strategies:
+ /// 1. Toast via PowerShell — persists in Action Center.
+ /// 2. Shell_NotifyIcon balloon via P/Invoke — fallback if toast fails.
+ ///
+ private static class WindowsNotification
{
- // XML-escape the title and message to prevent XML syntax errors (e.g. from '&', '<', '>', '"', '\'')
- string xmlTitle = System.Security.SecurityElement.Escape(title)!;
- string xmlMessage = System.Security.SecurityElement.Escape(message)!;
-
- string toastXml;
- string psScript;
-
- if (!string.IsNullOrEmpty(folderPath))
+ public static void Send(string title, string message, string? folderPath)
{
- var nonNullFolder = folderPath;
- var escapedFolder = nonNullFolder.Replace("'", "''");
-
- // UWP Toast XML using protocol activation.
- // Clicking the body (launch) or the button (arguments) will invoke the custom 'adofai-renderer://' protocol.
- toastXml = $@"
-
-
-
- {xmlTitle}
- {xmlMessage}
-
-
-
-
-
-
-".Replace("\r\n", "").Replace("\n", "").Trim();
-
- // Build a PowerShell script that registers a custom protocol 'adofai-renderer' in HKCU (no admin rights needed),
- // mapping it to invoke PowerShell and open the folder, then sends the notification and exits immediately.
- psScript =
- $"$protocolPath = 'HKCU:\\Software\\Classes\\adofai-renderer';\n" +
- $"if (-not (Test-Path $protocolPath)) {{\n" +
- $" $null = New-Item -Path $protocolPath -Force;\n" +
- $" $null = New-ItemProperty -Path $protocolPath -Name 'URL Protocol' -Value '' -PropertyType String -Force;\n" +
- $"}}\n" +
- $"Set-Item -Path $protocolPath -Value 'URL:adofai-renderer Protocol' | Out-Null;\n" +
- $"$cmdPath = \"$protocolPath\\shell\\open\\command\";\n" +
- $"if (-not (Test-Path $cmdPath)) {{ $null = New-Item -Path $cmdPath -Force; }}\n" +
- $"$command = 'powershell.exe -NoProfile -NonInteractive -WindowStyle Hidden -Command \"Invoke-Item ''{escapedFolder}''\"';\n" +
- $"Set-Item -Path $cmdPath -Value $command | Out-Null;\n" +
- $"[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] | Out-Null;\n" +
- $"$xml = [Windows.Data.Xml.Dom.XmlDocument]::new();\n" +
- $"$xml.LoadXml('{toastXml}');\n" +
- $"[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier('ADOFAI Renderer').Show([Windows.UI.Notifications.ToastNotification]::new($xml))";
- }
- else
- {
- // Simple UWP Toast XML without actions.
- toastXml = $@"
-
-
-
- {xmlTitle}
- {xmlMessage}
-
-
-
-".Replace("\r\n", "").Replace("\n", "").Trim();
-
- psScript =
- $"[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] | Out-Null;\n" +
- $"$xml = [Windows.Data.Xml.Dom.XmlDocument]::new();\n" +
- $"$xml.LoadXml('{toastXml}');\n" +
- $"[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier('ADOFAI Renderer').Show([Windows.UI.Notifications.ToastNotification]::new($xml))";
+ var thread = new Thread(() =>
+ {
+ try
+ {
+ if (!TrySendToast(title, message, folderPath))
+ {
+ SendBalloon(title, message, folderPath);
+ }
+ }
+ catch (Exception ex)
+ {
+ MelonLogger.Warning($"[OSNotification] Windows 알림 실패: {ex.Message}");
+ }
+ })
+ {
+ IsBackground = true,
+ Name = "OSNotification-Win"
+ };
+ thread.Start();
}
- // Encode the script to UTF-16LE Base64 and run it via PowerShell -EncodedCommand.
- // This guarantees that any special characters (double quotes, backslashes, ampersands, etc.) in the title/message
- // do not break the command-line parsing or cause syntax errors.
- byte[] bytes = System.Text.Encoding.Unicode.GetBytes(psScript);
- string base64 = Convert.ToBase64String(bytes);
+ #region Toast via PowerShell
- RunProcess("powershell.exe", $"-NoProfile -NonInteractive -EncodedCommand {base64}");
- }
-
- private static void ShowMacNotification(string title, string message)
- {
- string escapedTitle = title.Replace("\"", "\\\"");
- string escapedMessage = message.Replace("\"", "\\\"");
-
- string args = $"-e \"display notification \\\"{escapedMessage}\\\" with title \\\"{escapedTitle}\\\"\"";
- RunProcess("osascript", args);
- }
-
- private static void ShowLinuxNotification(string title, string message, string? folderPath)
- {
- var escapedTitle = title.Replace("\"", "\\\"");
- var escapedMessage = message.Replace("\"", "\\\"");
-
- if (!string.IsNullOrEmpty(folderPath))
+ ///
+ /// Attempts to show a Windows Toast notification via PowerShell.
+ /// Returns true if the process launched successfully, false otherwise.
+ ///
+ private static bool TrySendToast(string title, string message, string? folderPath)
{
- var nonNullFolder = folderPath;
- // Linux notify-send supports action buttons since v1.2.
- // It prints the action name to standard output when clicked.
- var args = $"-a \"ADOFAI Renderer\" --action=\"open_folder=렌더 폴더 열기\" \"{escapedTitle}\" \"{escapedMessage}\"";
-
+ try
+ {
+ string xmlTitle = System.Security.SecurityElement.Escape(title) ?? title;
+ string xmlMessage = System.Security.SecurityElement.Escape(message) ?? message;
+
+ string toastXml;
+ string psScript;
+
+ if (!string.IsNullOrEmpty(folderPath))
+ {
+ var escapedFolder = folderPath.Replace("'", "''");
+
+ toastXml =
+ "" +
+ "" +
+ $"{xmlTitle}{xmlMessage}" +
+ "" +
+ "" +
+ "" +
+ "";
+
+ psScript =
+ "$protocolPath = 'HKCU:\\Software\\Classes\\adofai-renderer'\n" +
+ "$null = New-Item -Path $protocolPath -Force\n" +
+ "$null = New-ItemProperty -Path $protocolPath -Name 'URL Protocol' -Value '' -PropertyType String -Force\n" +
+ "Set-Item -Path $protocolPath -Value 'URL:adofai-renderer Protocol' | Out-Null\n" +
+ "$cmdPath = \"$protocolPath\\shell\\open\\command\"\n" +
+ "$null = New-Item -Path $cmdPath -Force\n" +
+ $"Set-Item -Path $cmdPath -Value 'explorer.exe \"{escapedFolder}\"' | Out-Null\n" +
+ "[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] | Out-Null\n" +
+ "[Windows.Data.Xml.Dom.XmlDocument, Windows.Data, ContentType = WindowsRuntime] | Out-Null\n" +
+ "$xml = [Windows.Data.Xml.Dom.XmlDocument]::new()\n" +
+ $"$xml.LoadXml('{toastXml}')\n" +
+ "[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier('ADOFAI Renderer').Show([Windows.UI.Notifications.ToastNotification]::new($xml))";
+ }
+ else
+ {
+ toastXml =
+ "" +
+ $"{xmlTitle}{xmlMessage}" +
+ "";
+
+ psScript =
+ "[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] | Out-Null\n" +
+ "[Windows.Data.Xml.Dom.XmlDocument, Windows.Data, ContentType = WindowsRuntime] | Out-Null\n" +
+ "$xml = [Windows.Data.Xml.Dom.XmlDocument]::new()\n" +
+ $"$xml.LoadXml('{toastXml}')\n" +
+ "[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier('ADOFAI Renderer').Show([Windows.UI.Notifications.ToastNotification]::new($xml))";
+ }
+
+ byte[] bytes = System.Text.Encoding.Unicode.GetBytes(psScript);
+ string base64 = Convert.ToBase64String(bytes);
+
+ var startInfo = new ProcessStartInfo
+ {
+ FileName = "powershell.exe",
+ Arguments = $"-NoProfile -NonInteractive -ExecutionPolicy Bypass -EncodedCommand {base64}",
+ CreateNoWindow = true,
+ UseShellExecute = false,
+ RedirectStandardError = true,
+ WindowStyle = ProcessWindowStyle.Hidden
+ };
+
+ using var process = new Process();
+ process.StartInfo = startInfo;
+ process.Start();
+ process.WaitForExit(5000);
+
+ // If PowerShell exited with error, treat as failure
+ if (process.ExitCode != 0)
+ {
+ string stderr = process.StandardError.ReadToEnd().Trim();
+ if (!string.IsNullOrEmpty(stderr))
+ MelonLogger.Warning($"[OSNotification] Toast 실패 (fallback 사용): {stderr}");
+ return false;
+ }
+
+ return true;
+ }
+ catch (Exception ex)
+ {
+ MelonLogger.Warning($"[OSNotification] Toast 실패 (fallback 사용): {ex.Message}");
+ return false;
+ }
+ }
+
+ #endregion
+
+ #region Balloon via Shell_NotifyIcon
+
+ // ReSharper disable InconsistentNaming
+ private const int NIM_ADD = 0x00;
+ private const int NIM_DELETE = 0x02;
+
+ private const int NIF_MESSAGE = 0x01;
+ private const int NIF_ICON = 0x02;
+ private const int NIF_TIP = 0x04;
+ private const int NIF_INFO = 0x10;
+
+ private const int NIIF_INFO = 0x01;
+
+ private const int WM_USER = 0x0400;
+ private const int WM_TRAYICON = WM_USER + 1;
+ private const int NIN_BALLOONUSERCLICK = 0x0405;
+
+ private const int WS_OVERLAPPED = 0x00000000;
+ private const uint WM_CLOSE = 0x0010;
+ private const int TRAY_ICON_UID = 29471;
+ // ReSharper restore InconsistentNaming
+
+ [DllImport("shell32.dll", CharSet = CharSet.Unicode)]
+ private static extern bool Shell_NotifyIconW(int dwMessage, ref NotifyIconDataW lpData);
+
+ [DllImport("user32.dll", CharSet = CharSet.Unicode)]
+ private static extern IntPtr CreateWindowExW(
+ int dwExStyle, string lpClassName, string lpWindowName,
+ int dwStyle, int x, int y, int nWidth, int nHeight,
+ IntPtr hWndParent, IntPtr hMenu, IntPtr hInstance, IntPtr lpParam);
+
+ [DllImport("user32.dll")]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ private static extern bool DestroyWindow(IntPtr hWnd);
+
+ [DllImport("user32.dll")]
+ private static extern IntPtr LoadIconW(IntPtr hInstance, IntPtr lpIconName);
+
+ [DllImport("user32.dll")]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ private static extern bool GetMessageW(out Msg lpMsg, IntPtr hWnd, uint wMsgFilterMin, uint wMsgFilterMax);
+
+ [DllImport("user32.dll")]
+ private static extern IntPtr TranslateMessage(ref Msg lpMsg);
+
+ [DllImport("user32.dll")]
+ private static extern IntPtr DispatchMessageW(ref Msg lpMsg);
+
+ [DllImport("user32.dll")]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ private static extern bool PostMessageW(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam);
+
+ [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
+ private struct NotifyIconDataW
+ {
+ public int cbSize;
+ public IntPtr hWnd;
+ public int uID;
+ public int uFlags;
+ public int uCallbackMessage;
+ public IntPtr hIcon;
+ [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)]
+ public string szTip;
+ public int dwState;
+ public int dwStateMask;
+ [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)]
+ public string szInfo;
+ public int uTimeoutOrVersion;
+ [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 64)]
+ public string szInfoTitle;
+ public int dwInfoFlags;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ private struct Msg
+ {
+ public IntPtr hwnd;
+ public uint message;
+ public IntPtr wParam;
+ public IntPtr lParam;
+ public uint time;
+ public int pt_x;
+ public int pt_y;
+ }
+
+ private static void SendBalloon(string title, string message, string? folderPath)
+ {
+ if (title.Length > 63) title = title.Substring(0, 63);
+ if (message.Length > 255) message = message.Substring(0, 255);
+
+ IntPtr hWnd = IntPtr.Zero;
+ var nid = new NotifyIconDataW();
+
+ try
+ {
+ var hwndMessage = new IntPtr(-3);
+ hWnd = CreateWindowExW(
+ 0, "Static", "ADOFAIRendererNotify",
+ WS_OVERLAPPED, 0, 0, 0, 0,
+ hwndMessage, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero);
+
+ if (hWnd == IntPtr.Zero)
+ {
+ MelonLogger.Warning("[OSNotification] 메시지 윈도우를 생성할 수 없습니다.");
+ return;
+ }
+
+ IntPtr hIcon = LoadIconW(IntPtr.Zero, (IntPtr)32512);
+
+ nid.cbSize = Marshal.SizeOf(typeof(NotifyIconDataW));
+ nid.hWnd = hWnd;
+ nid.uID = TRAY_ICON_UID;
+ nid.uFlags = NIF_MESSAGE | NIF_ICON | NIF_TIP | NIF_INFO;
+ nid.uCallbackMessage = WM_TRAYICON;
+ nid.hIcon = hIcon;
+ nid.szTip = "ADOFAI Renderer";
+ nid.szInfo = message;
+ nid.szInfoTitle = title;
+ nid.dwInfoFlags = NIIF_INFO;
+ nid.uTimeoutOrVersion = 4;
+
+ Shell_NotifyIconW(NIM_ADD, ref nid);
+
+ if (!string.IsNullOrEmpty(folderPath))
+ {
+ var capturedFolder = folderPath;
+ var closeTimer = new System.Threading.Timer(_ =>
+ {
+ try { PostMessageW(hWnd, WM_CLOSE, IntPtr.Zero, IntPtr.Zero); }
+ catch { /* ignored */ }
+ }, null, 12000, Timeout.Infinite);
+
+ try
+ {
+ while (GetMessageW(out var msg, IntPtr.Zero, 0, 0))
+ {
+ if (msg.message == WM_CLOSE)
+ break;
+
+ if (msg.message == WM_TRAYICON)
+ {
+ int notifyMsg = (int)(long)msg.lParam;
+ if (notifyMsg == NIN_BALLOONUSERCLICK)
+ {
+ try
+ {
+ var psi = new ProcessStartInfo();
+ psi.FileName = capturedFolder;
+ psi.UseShellExecute = true;
+ Process.Start(psi);
+ }
+ catch (Exception ex)
+ {
+ MelonLogger.Warning(
+ $"[OSNotification] 폴더를 열 수 없습니다: {ex.Message}");
+ }
+
+ break;
+ }
+ }
+
+ TranslateMessage(ref msg);
+ DispatchMessageW(ref msg);
+ }
+ }
+ finally
+ {
+ closeTimer.Dispose();
+ }
+ }
+ else
+ {
+ Thread.Sleep(500);
+ }
+ }
+ catch (Exception ex)
+ {
+ MelonLogger.Warning($"[OSNotification] Balloon 알림 실패: {ex.Message}");
+ }
+ finally
+ {
+ if (hWnd != IntPtr.Zero)
+ {
+ nid.uFlags = 0;
+ Shell_NotifyIconW(NIM_DELETE, ref nid);
+ DestroyWindow(hWnd);
+ }
+ }
+ }
+
+ #endregion
+ }
+
+ #endregion
+
+ #region macOS — osascript
+
+ ///
+ /// macOS notification using osascript -e 'display notification ...'.
+ ///
+ private static class MacNotification
+ {
+ public static void Send(string title, string message)
+ {
+ // AppleScript string escaping: backslash and double-quote
+ string escapedTitle = EscapeAppleScript(title);
+ string escapedMessage = EscapeAppleScript(message);
+
+ // Use ProcessStartInfo with ArgumentList to avoid shell-level escaping issues.
+ // Each argument is passed directly to the process without shell interpretation.
+ var script =
+ $"display notification \"{escapedMessage}\" with title \"{escapedTitle}\"";
+
+ var startInfo = new ProcessStartInfo
+ {
+ FileName = "osascript",
+ CreateNoWindow = true,
+ UseShellExecute = false,
+ WindowStyle = ProcessWindowStyle.Hidden
+ };
+ startInfo.ArgumentList.Add("-e");
+ startInfo.ArgumentList.Add(script);
+
+ RunProcess(startInfo);
+ }
+
+ private static string EscapeAppleScript(string value)
+ {
+ // In AppleScript string literals, escape backslash and double-quote
+ return value
+ .Replace("\\", "\\\\")
+ .Replace("\"", "\\\"");
+ }
+ }
+
+ #endregion
+
+ #region Linux — notify-send / gdbus
+
+ ///
+ /// Linux notification using notify-send (libnotify CLI).
+ /// Falls back to gdbus (D-Bus direct call) if notify-send is unavailable.
+ ///
+ private static class LinuxNotification
+ {
+ public static void Send(string title, string message, string? folderPath)
+ {
+ if (!string.IsNullOrEmpty(folderPath))
+ {
+ ShowWithAction(title, message, folderPath);
+ }
+ else
+ {
+ ShowSimple(title, message);
+ }
+ }
+
+ private static void ShowSimple(string title, string message)
+ {
+ // Try notify-send first
+ if (TryNotifySend(title, message))
+ return;
+
+ // Fallback to gdbus
+ TryGdbus(title, message);
+ }
+
+ private static void ShowWithAction(string title, string message, string folderPath)
+ {
+ // Run on a background thread because notify-send with --action blocks
+ // until the notification is dismissed or the action is clicked.
System.Threading.Tasks.Task.Run(() =>
{
try
@@ -140,50 +458,121 @@ namespace ADOFAIRenderer.Util
var startInfo = new ProcessStartInfo
{
FileName = "notify-send",
- Arguments = args,
CreateNoWindow = true,
UseShellExecute = false,
RedirectStandardOutput = true,
WindowStyle = ProcessWindowStyle.Hidden
};
+ startInfo.ArgumentList.Add("-a");
+ startInfo.ArgumentList.Add("ADOFAI Renderer");
+ startInfo.ArgumentList.Add("--action=open_folder=렌더 폴더 열기");
+ startInfo.ArgumentList.Add(title);
+ startInfo.ArgumentList.Add(message);
using var process = new Process();
process.StartInfo = startInfo;
process.Start();
string output = process.StandardOutput.ReadToEnd().Trim();
- // If user clicked the button, open the folder using default file manager
+ process.WaitForExit();
+
if (output == "open_folder")
{
- RunProcess("xdg-open", $"\"{nonNullFolder.Replace("\"", "\\\"")}\"");
+ var openInfo = new ProcessStartInfo
+ {
+ FileName = "xdg-open",
+ CreateNoWindow = true,
+ UseShellExecute = false,
+ WindowStyle = ProcessWindowStyle.Hidden
+ };
+ openInfo.ArgumentList.Add(folderPath);
+
+ RunProcess(openInfo);
}
}
catch (Exception ex)
{
MelonLogger.Warning($"[OSNotification] 리눅스 알림 처리 실패: {ex.Message}");
+
+ // Fallback: show without action
+ ShowSimple(title, message);
}
});
}
- else
+
+ private static bool TryNotifySend(string title, string message)
{
- var args = $"-a \"ADOFAI Renderer\" \"{escapedTitle}\" \"{escapedMessage}\"";
- RunProcess("notify-send", args);
+ try
+ {
+ var startInfo = new ProcessStartInfo
+ {
+ FileName = "notify-send",
+ CreateNoWindow = true,
+ UseShellExecute = false,
+ WindowStyle = ProcessWindowStyle.Hidden
+ };
+ startInfo.ArgumentList.Add("-a");
+ startInfo.ArgumentList.Add("ADOFAI Renderer");
+ startInfo.ArgumentList.Add(title);
+ startInfo.ArgumentList.Add(message);
+
+ RunProcess(startInfo);
+ return true;
+ }
+ catch
+ {
+ return false;
+ }
+ }
+
+ private static void TryGdbus(string title, string message)
+ {
+ try
+ {
+ // Direct D-Bus call to org.freedesktop.Notifications
+ var startInfo = new ProcessStartInfo
+ {
+ FileName = "gdbus",
+ CreateNoWindow = true,
+ UseShellExecute = false,
+ WindowStyle = ProcessWindowStyle.Hidden
+ };
+ startInfo.ArgumentList.Add("call");
+ startInfo.ArgumentList.Add("--session");
+ startInfo.ArgumentList.Add("--dest=org.freedesktop.Notifications");
+ startInfo.ArgumentList.Add("--object-path=/org/freedesktop/Notifications");
+ startInfo.ArgumentList.Add("--method=org.freedesktop.Notifications.Notify");
+ startInfo.ArgumentList.Add("ADOFAI Renderer"); // app_name
+ startInfo.ArgumentList.Add("0"); // replaces_id
+ startInfo.ArgumentList.Add(""); // app_icon
+ startInfo.ArgumentList.Add(title); // summary
+ startInfo.ArgumentList.Add(message); // body
+ startInfo.ArgumentList.Add("[]"); // actions
+ startInfo.ArgumentList.Add("{}"); // hints
+ startInfo.ArgumentList.Add("-1"); // expire_timeout
+
+ RunProcess(startInfo);
+ }
+ catch (Exception ex)
+ {
+ MelonLogger.Warning($"[OSNotification] gdbus 알림 실패: {ex.Message}");
+ }
}
}
- private static void RunProcess(string fileName, string arguments)
- {
- var startInfo = new ProcessStartInfo
- {
- FileName = fileName,
- Arguments = arguments,
- CreateNoWindow = true, // CREATE_NO_WINDOW flag
- UseShellExecute = false, // Direct execution
- WindowStyle = ProcessWindowStyle.Hidden
- };
+ #endregion
+ #region Shared utility
+
+ ///
+ /// Starts a process fire-and-forget. Does not redirect output or wait for exit.
+ ///
+ private static void RunProcess(ProcessStartInfo startInfo)
+ {
using var process = new Process();
process.StartInfo = startInfo;
process.Start();
}
+
+ #endregion
}
}