From 7b6c87f2b0b7bf189ecc5c473464c811278fe237 Mon Sep 17 00:00:00 2001 From: JiXieShi Date: Mon, 8 Sep 2025 16:51:20 +0800 Subject: [PATCH] =?UTF-8?q?UP=20=E5=8E=86=E5=8F=B2=E5=91=BD=E4=BB=A4?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CMakeLists.txt | 8 +- app/inc/AppState.h | 19 +- app/inc/CLIProcess.h | 50 ++++- app/inc/Manager.h | 22 +- app/inc/TrayIcon.h | 46 ++++- app/src/AppState.cpp | 102 +++++++++- app/src/CLIProcess.cpp | 443 +++++++++++++++++++++++++---------------- app/src/Manager.cpp | 205 ++++++++++++++++--- app/src/TrayIcon.cpp | 96 ++++++++- 9 files changed, 751 insertions(+), 240 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 9f6107c..5cadc32 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -85,7 +85,7 @@ if(IMGUI_BACKENDS STREQUAL "win32_dx11") endif() # 设置链接选项 -#set_target_properties(${PROJECT_NAME} PROPERTIES -# LINK_FLAGS "-static -Wl,-subsystem,windows" -#) -# +set_target_properties(${PROJECT_NAME} PROPERTIES + LINK_FLAGS "-static -static-libgcc -static-libstdc++ -Wl,-Bstatic -lpthread -Wl,-subsystem,windows" +) + diff --git a/app/inc/AppState.h b/app/inc/AppState.h index bcfa38f..7d17b8e 100644 --- a/app/inc/AppState.h +++ b/app/inc/AppState.h @@ -4,6 +4,7 @@ #include "CLIProcess.h" #include #include +#include #include class AppState { @@ -15,6 +16,12 @@ public: void SaveSettings(); void ApplySettings(); + // 新增:启动命令历史记录管理 + void AddCommandToHistory(const std::string& command); + void RemoveCommandFromHistory(int index); + void ClearCommandHistory(); + const std::vector& GetCommandHistory() const { return command_history; } + bool show_main_window; bool auto_start; CLIProcess cli_process; @@ -33,9 +40,13 @@ public: std::map environment_variables; bool use_custom_environment; - // 新增:输出编码相关配置 + // 输出编码相关配置 OutputEncoding output_encoding; + // 新增:启动命令历史记录 + std::vector command_history; + int max_command_history; + bool settings_dirty; private: @@ -43,9 +54,13 @@ private: std::string SerializeEnvironmentVariables() const; void DeserializeEnvironmentVariables(const std::string& serialized); - // 新增:编码序列化辅助函数 + // 编码序列化辅助函数 std::string SerializeOutputEncoding() const; void DeserializeOutputEncoding(const std::string& serialized); + + // 新增:命令历史记录序列化辅助函数 + std::string SerializeCommandHistory() const; + void DeserializeCommandHistory(const std::string& serialized); }; #endif // APP_STATE_H \ No newline at end of file diff --git a/app/inc/CLIProcess.h b/app/inc/CLIProcess.h index d80ea2f..f73ef53 100644 --- a/app/inc/CLIProcess.h +++ b/app/inc/CLIProcess.h @@ -6,16 +6,33 @@ #include #include #include -#include -// 新增:输出编码枚举 +#ifdef _WIN32 +#include +#else +#include +#include +#include +#include +#include +#endif + +// 输出编码枚举 enum class OutputEncoding { - AUTO_DETECT =0, + AUTO_DETECT = 0, UTF8, +#ifdef _WIN32 GBK, GB2312, BIG5, SHIFT_JIS, +#else + // Unix/Linux 常见编码 + ISO_8859_1, + GB18030, + BIG5, + EUC_JP, +#endif }; class CLIProcess { @@ -26,7 +43,7 @@ public: void SetMaxLogLines(int max_lines); void SetStopCommand(const std::string& command, int timeout_ms = 5000); void SetEnvironmentVariables(const std::map& env_vars); - void SetOutputEncoding(OutputEncoding encoding); // 新增:设置输出编码 + void SetOutputEncoding(OutputEncoding encoding); void Start(const std::string& command); void Stop(); @@ -47,7 +64,7 @@ public: void RemoveEnvironmentVariable(const std::string& key); void ClearEnvironmentVariables(); - // 新增:编码相关接口 + // 编码相关接口 OutputEncoding GetOutputEncoding() const; static std::string GetEncodingName(OutputEncoding encoding); static std::vector> GetSupportedEncodings(); @@ -57,9 +74,11 @@ private: void CloseProcessHandles(); void CleanupResources(); - // 新增:编码转换相关方法 - std::string ConvertToUTF8(const std::string& input, OutputEncoding encoding); - std::string DetectAndConvertToUTF8(const std::string& input); + // 编码转换相关方法 + static std::string ConvertToUTF8(std::string& input, OutputEncoding encoding); + std::string DetectAndConvertToUTF8(std::string& input); + +#ifdef _WIN32 static UINT GetCodePageFromEncoding(OutputEncoding encoding); static bool IsValidUTF8(const std::string& str); @@ -68,6 +87,17 @@ private: HANDLE hWritePipe_{}; HANDLE hReadPipe_stdin_{}; HANDLE hWritePipe_stdin_; +#else + // Unix/Linux 进程管理 + pid_t process_pid_; + int pipe_stdout_[2]; + int pipe_stdin_[2]; + bool process_running_; + + // Unix 编码转换辅助函数 + std::string ConvertUnixEncoding(const std::string& input, const std::string& from_encoding); + static std::string GetUnixEncodingName(OutputEncoding encoding); +#endif mutable std::mutex logs_mutex_; std::vector logs_; @@ -76,7 +106,7 @@ private: std::thread output_thread_; // 停止命令相关 - std::mutex stop_mutex_; + mutable std::mutex stop_mutex_; std::string stop_command_; int stop_timeout_ms_; @@ -84,7 +114,7 @@ private: mutable std::mutex env_mutex_; std::map environment_variables_; - // 新增:编码相关 + // 编码相关 mutable std::mutex encoding_mutex_; OutputEncoding output_encoding_; }; diff --git a/app/inc/Manager.h b/app/inc/Manager.h index fa6c5ee..50234e8 100644 --- a/app/inc/Manager.h +++ b/app/inc/Manager.h @@ -10,12 +10,20 @@ #include "imgui_impl_win32.h" #include "imgui_impl_dx11.h" #else +#ifdef _WIN32 #define GLFW_EXPOSE_NATIVE_WIN32 #include #include +#include +#elif __APPLE__ +#define GLFW_EXPOSE_NATIVE_COCOA +#include +#include +#else +#include +#endif #include "imgui_impl_glfw.h" #include "imgui_impl_opengl3.h" -#include #endif #include @@ -33,6 +41,8 @@ public: void OnTrayExit(); AppState m_app_state; + + private: // UI渲染 void RenderUI(); @@ -41,7 +51,7 @@ private: void RenderSettingsMenu(); void RenderStopCommandSettings(); void RenderEnvironmentVariablesSettings(); - void RenderOutputEncodingSettings(); // 新增:输出编码设置UI + void RenderOutputEncodingSettings(); // 事件处理 void HandleMessages(); @@ -75,10 +85,12 @@ private: // 托盘相关 bool InitializeTray(); void CleanupTray(); +#ifdef _WIN32 static HWND CreateHiddenWindow(); + HWND m_tray_hwnd = nullptr; +#endif std::unique_ptr m_tray; - HWND m_tray_hwnd = nullptr; // 控制标志 bool m_should_exit = false; @@ -92,6 +104,8 @@ private: char env_value_input_[512] = {}; bool show_env_settings_ = false; - // 新增:编码设置UI状态 + // 编码设置UI状态 bool show_encoding_settings_ = false; + // 历史命令UI状态 + bool show_command_history_; }; \ No newline at end of file diff --git a/app/inc/TrayIcon.h b/app/inc/TrayIcon.h index 736da02..abe0c5c 100644 --- a/app/inc/TrayIcon.h +++ b/app/inc/TrayIcon.h @@ -1,6 +1,10 @@ #pragma once + +#ifdef _WIN32 #include #include +#endif + #include #include @@ -10,33 +14,63 @@ public: using ShowWindowCallback = std::function; using ExitCallback = std::function; +#ifdef _WIN32 TrayIcon(HWND hwnd, HICON icon); + void UpdateWebUrl(const std::wstring& url); + // 静态窗口过程 + static LRESULT CALLBACK WindowProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam); +#else + TrayIcon(void* app_delegate, void* icon); + void UpdateWebUrl(const std::string& url); + void OnMacMenuAction(int action); +#endif + ~TrayIcon(); void Show(); void Hide(); - void UpdateWebUrl(const std::wstring& url); // 设置回调函数 void SetShowWindowCallback(const ShowWindowCallback &callback); void SetExitCallback(const ExitCallback &callback); - // 静态窗口过程 - static LRESULT CALLBACK WindowProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam); - private: void CreateMenu(); void DestroyMenu(); + +#ifdef _WIN32 void ShowContextMenu() const; HWND m_hwnd; HICON m_icon; NOTIFYICONDATA m_nid{}; std::wstring m_web_url; - bool m_visible; HMENU m_menu; +#else + void ShowMacTrayIcon(); + void HideMacTrayIcon(); + void CreateMacMenu(); + void DestroyMacMenu(); + + void* m_app_delegate; + void* m_icon; + std::string m_web_url; +#endif + + bool m_visible; // 回调函数 ShowWindowCallback m_show_window_callback; ExitCallback m_exit_callback; -}; \ No newline at end of file +}; + +#ifdef __cplusplus +extern "C" { +#endif + + // C 接口函数声明,供 Objective-C 调用 + void TrayIconMenuCallback(void* tray_instance, int action); + +#ifdef __cplusplus +} +#endif \ No newline at end of file diff --git a/app/src/AppState.cpp b/app/src/AppState.cpp index 7a780c6..6ec8c0d 100644 --- a/app/src/AppState.cpp +++ b/app/src/AppState.cpp @@ -11,7 +11,8 @@ AppState::AppState() : stop_timeout_ms(5000), use_stop_command(false), use_custom_environment(false), - output_encoding(OutputEncoding::AUTO_DETECT), // 新增:默认自动检测编码 + output_encoding(OutputEncoding::AUTO_DETECT), + max_command_history(20), // 新增:最大历史记录数量 settings_dirty(false) { strcpy_s(command_input, "cmd.exe"); strcpy_s(web_url, "http://localhost:8080"); @@ -19,6 +20,90 @@ AppState::AppState() : memset(send_command, 0, sizeof(send_command)); } +// 新增:添加命令到历史记录 +void AppState::AddCommandToHistory(const std::string& command) { + if (command.empty()) return; + + // 移除重复的命令 + auto it = std::find(command_history.begin(), command_history.end(), command); + if (it != command_history.end()) { + command_history.erase(it); + } + + // 添加到开头 + command_history.insert(command_history.begin(), command); + + // 限制历史记录数量 + if (command_history.size() > static_cast(max_command_history)) { + command_history.resize(max_command_history); + } + + settings_dirty = true; +} + +// 新增:从历史记录中移除命令 +void AppState::RemoveCommandFromHistory(int index) { + if (index >= 0 && index < static_cast(command_history.size())) { + command_history.erase(command_history.begin() + index); + settings_dirty = true; + } +} + +// 新增:清空命令历史记录 +void AppState::ClearCommandHistory() { + command_history.clear(); + settings_dirty = true; +} + +// 新增:序列化命令历史记录 +std::string AppState::SerializeCommandHistory() const { + std::ostringstream oss; + bool first = true; + for (const auto& command : command_history) { + if (!first) { + oss << "|"; + } + // 转义分隔符 + std::string escaped_command = command; + size_t pos = 0; + while ((pos = escaped_command.find("|", pos)) != std::string::npos) { + escaped_command.replace(pos, 1, "\\|"); + pos += 2; + } + oss << escaped_command; + first = false; + } + return oss.str(); +} + +// 新增:反序列化命令历史记录 +void AppState::DeserializeCommandHistory(const std::string& serialized) { + command_history.clear(); + if (serialized.empty()) return; + + std::istringstream iss(serialized); + std::string command; + std::string current; + + for (size_t i = 0; i < serialized.length(); ++i) { + if (serialized[i] == '\\' && i + 1 < serialized.length() && serialized[i + 1] == '|') { + current += '|'; + ++i; // 跳过下一个字符 + } else if (serialized[i] == '|') { + if (!current.empty()) { + command_history.push_back(current); + current.clear(); + } + } else { + current += serialized[i]; + } + } + + if (!current.empty()) { + command_history.push_back(current); + } +} + std::string AppState::SerializeEnvironmentVariables() const { std::ostringstream oss; bool first = true; @@ -128,10 +213,17 @@ void AppState::LoadSettings() { else if (key == "EnvironmentVariables") { DeserializeEnvironmentVariables(value); } - // 新增:输出编码配置的加载 else if (key == "OutputEncoding") { DeserializeOutputEncoding(value); } + // 新增:命令历史记录配置的加载 + else if (key == "CommandHistory") { + DeserializeCommandHistory(value); + } + else if (key == "MaxCommandHistory") { + max_command_history = std::stoi(value); + max_command_history = std::max(5, std::min(max_command_history, 100)); + } } } } @@ -158,9 +250,13 @@ void AppState::SaveSettings() { file << "UseCustomEnvironment=" << (use_custom_environment ? "1" : "0") << "\n"; file << "EnvironmentVariables=" << SerializeEnvironmentVariables() << "\n"; - // 新增:输出编码配置的保存 + // 输出编码配置的保存 file << "OutputEncoding=" << SerializeOutputEncoding() << "\n"; + // 新增:命令历史记录配置的保存 + file << "CommandHistory=" << SerializeCommandHistory() << "\n"; + file << "MaxCommandHistory=" << max_command_history << "\n"; + file.close(); settings_dirty = false; diff --git a/app/src/CLIProcess.cpp b/app/src/CLIProcess.cpp index bd53c8c..bad25e3 100644 --- a/app/src/CLIProcess.cpp +++ b/app/src/CLIProcess.cpp @@ -2,59 +2,88 @@ #include #include +#ifdef _WIN32 #include "Units.h" +#else +#include +#include +#include +#include +extern char **environ; +#endif CLIProcess::CLIProcess() { +#ifdef _WIN32 ZeroMemory(&pi_, sizeof(pi_)); - max_log_lines_ = 1000; hWritePipe_stdin_ = nullptr; +#else + process_pid_ = -1; + pipe_stdout_[0] = pipe_stdout_[1] = -1; + pipe_stdin_[0] = pipe_stdin_[1] = -1; + process__running_ = false; +#endif + max_log_lines_ = 1000; stop_timeout_ms_ = 5000; - output_encoding_ = OutputEncoding::AUTO_DETECT; // 新增:默认自动检测编码 + output_encoding_ = OutputEncoding::AUTO_DETECT; } CLIProcess::~CLIProcess() { Stop(); + CleanupResources(); } - -// 新增:设置输出编码 +// 设置输出编码 void CLIProcess::SetOutputEncoding(OutputEncoding encoding) { std::lock_guard lock(encoding_mutex_); output_encoding_ = encoding; AddLog("输出编码已设置为: " + GetEncodingName(encoding)); } -// 新增:获取输出编码 +// 获取输出编码 OutputEncoding CLIProcess::GetOutputEncoding() const { std::lock_guard lock(encoding_mutex_); return output_encoding_; } -// 新增:获取编码名称 +// 获取编码名称 std::string CLIProcess::GetEncodingName(const OutputEncoding encoding) { switch (encoding) { + case OutputEncoding::AUTO_DETECT: return "自动检测"; case OutputEncoding::UTF8: return "UTF-8"; +#ifdef _WIN32 case OutputEncoding::GBK: return "GBK"; case OutputEncoding::GB2312: return "GB2312"; - case OutputEncoding::BIG5: return "Big5"; - case OutputEncoding::SHIFT_JIS: return "Shift-JIS"; - case OutputEncoding::AUTO_DETECT: return "自动检测"; - default: return "未知"; + case OutputEncoding::BIG5: return "BIG5"; + case OutputEncoding::SHIFT_JIS: return "Shift_JIS"; +#else + case OutputEncoding::ISO_8859_1: return "ISO-8859-1"; + case OutputEncoding::GB18030: return "GB18030"; + case OutputEncoding::BIG5: return "BIG5"; + case OutputEncoding::EUC_JP: return "EUC-JP"; +#endif + default: return "未知编码"; } } -// 新增:获取支持的编码列表 +// 获取支持的编码列表 std::vector> CLIProcess::GetSupportedEncodings() { return { {OutputEncoding::AUTO_DETECT, "自动检测"}, {OutputEncoding::UTF8, "UTF-8"}, +#ifdef _WIN32 {OutputEncoding::GBK, "GBK (简体中文)"}, {OutputEncoding::GB2312, "GB2312 (简体中文)"}, {OutputEncoding::BIG5, "Big5 (繁体中文)"}, - {OutputEncoding::SHIFT_JIS, "Shift-JIS (日文)"} + {OutputEncoding::SHIFT_JIS, "Shift-JIS (日文)"}, +#else + {OutputEncoding::ISO_8859_1, "ISO-8859-1"}, + {OutputEncoding::GB18030, "GB18030"}, + {OutputEncoding::BIG5, "BIG5"}, + {OutputEncoding::EUC_JP, "EUC-JP"}, +#endif }; } -// 新增:根据编码获取代码页 +// 根据编码获取代码页 UINT CLIProcess::GetCodePageFromEncoding(const OutputEncoding encoding) { switch (encoding) { case OutputEncoding::GBK: return 936; @@ -65,7 +94,7 @@ UINT CLIProcess::GetCodePageFromEncoding(const OutputEncoding encoding) { } } -// 新增:检查是否为有效的UTF-8 +// 检查是否为有效的UTF-8 bool CLIProcess::IsValidUTF8(const std::string& str) { const auto* bytes = reinterpret_cast(str.c_str()); size_t len = str.length(); @@ -93,8 +122,8 @@ bool CLIProcess::IsValidUTF8(const std::string& str) { return true; } -// 新增:转换到UTF-8 -std::string CLIProcess::ConvertToUTF8(const std::string& input, OutputEncoding encoding) { +// 转换到UTF-8 +std::string CLIProcess::ConvertToUTF8(std::string& input, const OutputEncoding encoding) { if (input.empty()) return input; // 如果已经是UTF-8编码,直接返回 @@ -130,8 +159,8 @@ std::string CLIProcess::ConvertToUTF8(const std::string& input, OutputEncoding e return std::string(utf8Str.data()); } -// 新增:自动检测并转换到UTF-8 -std::string CLIProcess::DetectAndConvertToUTF8(const std::string& input) { +// 自动检测并转换到UTF-8 +std::string CLIProcess::DetectAndConvertToUTF8(std::string& input) { if (input.empty()) return input; // 首先检查是否已经是有效的UTF-8 @@ -140,7 +169,7 @@ std::string CLIProcess::DetectAndConvertToUTF8(const std::string& input) { } // 尝试不同的编码进行转换 - std::vector encodingsToTry = { + const std::vector encodingsToTry = { OutputEncoding::GBK, OutputEncoding::GB2312, OutputEncoding::BIG5, @@ -245,190 +274,185 @@ void CLIProcess::ClearEnvironmentVariables() { } void CLIProcess::Start(const std::string& command) { - if (IsRunning()) return; Stop(); +#ifdef _WIN32 + SECURITY_ATTRIBUTES saAttr; + saAttr.nLength = sizeof(SECURITY_ATTRIBUTES); + saAttr.bInheritHandle = TRUE; + saAttr.lpSecurityDescriptor = nullptr; - SECURITY_ATTRIBUTES sa; - sa.nLength = sizeof(SECURITY_ATTRIBUTES); - sa.bInheritHandle = TRUE; - sa.lpSecurityDescriptor = nullptr; + HANDLE hReadTmp = nullptr; + HANDLE hWriteTmp = nullptr; - if (!CreatePipe(&hReadPipe_, &hWritePipe_, &sa, 0)) { - AddLog("创建输出管道失败"); + if (!CreatePipe(&hReadTmp, &hWriteTmp, &saAttr, 0)) { + return; + } + if (!SetHandleInformation(hReadTmp, HANDLE_FLAG_INHERIT, 0)) { + CloseHandle(hReadTmp); + CloseHandle(hWriteTmp); return; } - if (!CreatePipe(&hReadPipe_stdin_, &hWritePipe_stdin_, &sa, 0)) { - AddLog("创建输入管道失败"); - CloseHandle(hReadPipe_); - CloseHandle(hWritePipe_); + HANDLE hReadTmp_stdin = nullptr; + HANDLE hWriteTmp_stdin = nullptr; + if (!CreatePipe(&hReadTmp_stdin, &hWriteTmp_stdin, &saAttr, 0)) { + CloseHandle(hReadTmp); + CloseHandle(hWriteTmp); + return; + } + if (!SetHandleInformation(hWriteTmp_stdin, HANDLE_FLAG_INHERIT, 0)) { + CloseHandle(hReadTmp); + CloseHandle(hWriteTmp); + CloseHandle(hReadTmp_stdin); + CloseHandle(hWriteTmp_stdin); return; } - STARTUPINFO si; - ZeroMemory(&si, sizeof(si)); - si.cb = sizeof(si); - si.dwFlags = STARTF_USESTDHANDLES | STARTF_USESHOWWINDOW; - si.hStdOutput = hWritePipe_; - si.hStdError = hWritePipe_; - si.hStdInput = hReadPipe_stdin_; - si.wShowWindow = SW_HIDE; - ZeroMemory(&pi_, sizeof(pi_)); + STARTUPINFOA siStartInfo; + ZeroMemory(&siStartInfo, sizeof(STARTUPINFOA)); + siStartInfo.cb = sizeof(STARTUPINFOA); + siStartInfo.hStdError = hWriteTmp; + siStartInfo.hStdOutput = hWriteTmp; + siStartInfo.hStdInput = hReadTmp_stdin; + siStartInfo.dwFlags |= STARTF_USESTDHANDLES; - // 转换命令为宽字符 - std::wstring wcmd = StringToWide(command); + PROCESS_INFORMATION piProcInfo; + ZeroMemory(&piProcInfo, sizeof(PROCESS_INFORMATION)); - // CreateProcess需要可修改的字符串 - std::vector cmdBuffer(wcmd.begin(), wcmd.end()); - cmdBuffer.push_back(L'\0'); + // Prepare environment block + std::string env_block; + { + std::lock_guard lock(env_mutex_); + for (const auto& kv : environment_variables_) { + env_block += kv.first + "=" + kv.second + '\0'; + } + env_block += '\0'; + } - // 使用Windows API设置环境变量 - std::vector> originalEnvVars; - bool envVarsSet = false; + BOOL bSuccess = CreateProcessA( + nullptr, + const_cast(command.c_str()), + nullptr, + nullptr, + TRUE, + CREATE_NO_WINDOW, + env_block.empty() ? nullptr : (LPVOID)env_block.data(), + nullptr, + &siStartInfo, + &piProcInfo); - if (!environment_variables_.empty()) { - envVarsSet = true; + CloseHandle(hWriteTmp); + CloseHandle(hReadTmp_stdin); - for (const auto& pair : environment_variables_) { - if (!pair.first.empty()) { - // 保存原始值(如果存在) - DWORD bufferSize = GetEnvironmentVariableA(pair.first.c_str(), nullptr, 0); - if (bufferSize > 0) { - // 变量存在,保存原始值 - std::vector buffer(bufferSize); - if (GetEnvironmentVariableA(pair.first.c_str(), buffer.data(), bufferSize) > 0) { - originalEnvVars.emplace_back(pair.first, std::string(buffer.data())); - } else { - originalEnvVars.emplace_back(pair.first, ""); - } - } else { - // 变量不存在,标记为新变量(使用空字符串表示原来不存在) - originalEnvVars.emplace_back(pair.first, ""); - } + if (!bSuccess) { + CloseHandle(hReadTmp); + CloseHandle(hWriteTmp_stdin); + return; + } - // 设置新的环境变量值 - if (SetEnvironmentVariableA(pair.first.c_str(), pair.second.c_str())) { - // AddLog("设置环境变量: " + pair.first + "=" + pair.second); - } else { - AddLog("设置环境变量失败: " + pair.first + " (错误代码: " + std::to_string(GetLastError()) + ")"); - } + CloseProcessHandles(); + + pi_ = piProcInfo; + hReadPipe_ = hReadTmp; + hWritePipe_stdin_ = hWriteTmp_stdin; + + // Start output reading thread + output_thread_ = std::thread(&CLIProcess::ReadOutput, this); + +#else + // Unix/Linux implementation using posix_spawn + int pipe_out[2]; + int pipe_in[2]; + + if (pipe(pipe_out) < 0 || pipe(pipe_in) < 0) { + return; + } + + pid_t pid = fork(); + if (pid == 0) { + // child process + close(pipe_out[0]); + dup2(pipe_out[1], STDOUT_FILENO); + dup2(pipe_out[1], STDERR_FILENO); + close(pipe_out[1]); + + close(pipe_in[1]); + dup2(pipe_in[0], STDIN_FILENO); + close(pipe_in[0]); + + // Prepare environment variables + if (!environment_variables_.empty()) { + for (const auto& kv : environment_variables_) { + setenv(kv.first.c_str(), kv.second.c_str(), 1); } } - // AddLog("环境变量设置完成,数量: " + std::to_string(environment_variables_.size())); - } else { - AddLog("未设置自定义环境变量,使用默认环境"); + execl("/bin/sh", "sh", "-c", command.c_str(), (char*)nullptr); + _exit(127); } + else if (pid > 0) { + // parent process + close(pipe_out[1]); + close(pipe_in[0]); - BOOL result = CreateProcess( - nullptr, // lpApplicationName - cmdBuffer.data(), // lpCommandLine - nullptr, // lpProcessAttributes - nullptr, // lpThreadAttributes - TRUE, // bInheritHandles - CREATE_NO_WINDOW, // dwCreationFlags - nullptr, // lpEnvironment (使用nullptr让子进程继承当前环境) - nullptr, // lpCurrentDirectory - &si, // lpStartupInfo - &pi_ // lpProcessInformation - ); + process_pid_ = pid; + pipe_stdout_[0] = pipe_out[0]; + pipe_stdout_[1] = pipe_out[1]; // closed already in parent, but keep for safety + pipe_stdin_[0] = pipe_in[0]; // closed already in parent, but keep for safety + pipe_stdin_[1] = pipe_in[1]; - // 恢复原始环境变量 - if (envVarsSet) { - for (const auto& pair : originalEnvVars) { - if (pair.second.empty()) { - // 原来不存在,删除变量 - SetEnvironmentVariableA(pair.first.c_str(), nullptr); - } else { - // 恢复原始值 - SetEnvironmentVariableA(pair.first.c_str(), pair.second.c_str()); - } - } - } - - if (result) { - AddLog("进程已启动: " + command); - - CloseHandle(hWritePipe_); - CloseHandle(hReadPipe_stdin_); - hWritePipe_ = nullptr; - hReadPipe_stdin_ = nullptr; - - output_thread_ = std::thread([this]() { - ReadOutput(); - }); - } - else { - DWORD err = GetLastError(); - - // 获取详细的错误信息 - LPWSTR messageBuffer = nullptr; - size_t size = FormatMessageW( - FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, - nullptr, err, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), - (LPWSTR)&messageBuffer, 0, nullptr); - - std::string errorMsg = "CreateProcess 失败 (错误代码: " + std::to_string(err) + ")"; - if (messageBuffer) { - std::wstring wErrorMsg(messageBuffer); - errorMsg += " - " + WideToString(wErrorMsg); - LocalFree(messageBuffer); - } - - AddLog(errorMsg); - - // 清理资源 - CloseHandle(hReadPipe_); - CloseHandle(hWritePipe_); - CloseHandle(hReadPipe_stdin_); - CloseHandle(hWritePipe_stdin_); - hReadPipe_ = hWritePipe_ = hReadPipe_stdin_ = hWritePipe_stdin_ = nullptr; + process_running_ = true; + + // Start output reading thread + output_thread_ = std::thread(&CLIProcess::ReadOutput, this); } +#endif } void CLIProcess::Stop() { - bool useStopCommand = false; - std::string stopCmd; - int timeout = stop_timeout_ms_; - - // 检查是否设置了停止命令 - { - std::lock_guard lock(stop_mutex_); - if (!stop_command_.empty() && IsRunning()) { - useStopCommand = true; - stopCmd = stop_command_; +#ifdef _WIN32 + std::lock_guard lock(stop_mutex_); + if (pi_.hProcess != nullptr) { + if (!stop_command_.empty()) { + SendCommand(stop_command_); + // Wait for process to exit within timeout + WaitForSingleObject(pi_.hProcess, stop_timeout_ms_); } - } - - if (useStopCommand) { - AddLog("尝试发送停止命令: " + stopCmd); - if (SendCommand(stopCmd)) { - // 等待进程正常退出 - DWORD waitResult = WaitForSingleObject(pi_.hProcess, timeout); - if (waitResult == WAIT_OBJECT_0) { - // 进程已正常退出 - CloseProcessHandles(); - AddLog("进程已通过停止命令正常退出"); - return; - } - AddLog("停止命令超时,将强制终止进程"); - } else { - AddLog("发送停止命令失败,将强制终止进程"); - } - } - - // 强制终止进程 - if (pi_.hProcess) { TerminateProcess(pi_.hProcess, 0); + WaitForSingleObject(pi_.hProcess, INFINITE); CloseProcessHandles(); - AddLog("进程已强制终止"); } +#else + if (process_running_) { + std::lock_guard lock(stop_mutex_); + if (!stop_command_.empty()) { + SendCommand(stop_command_); + // Wait for termination or timeout + int status = 0; + for (int i = 0; i < stop_timeout_ms_/100; ++i) { + pid_t result = waitpid(process_pid_, &status, WNOHANG); + if (result == process_pid_) { + process_running_ = false; + break; + } + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + } + } + if (process_running_) { + kill(process_pid_, SIGKILL); + waitpid(process_pid_, nullptr, 0); + process_running_ = false; + } + CloseProcessHandles(); + } +#endif - // 关闭管道和线程 - CleanupResources(); + if (output_thread_.joinable()) { + output_thread_.join(); + } } -// 新增:关闭进程句柄的辅助函数 +// 关闭进程句柄的辅助函数 void CLIProcess::CloseProcessHandles() { if (pi_.hProcess) { CloseHandle(pi_.hProcess); @@ -440,7 +464,7 @@ void CLIProcess::CloseProcessHandles() { } } -// 新增:清理资源的辅助函数 +// 清理资源的辅助函数 void CLIProcess::CleanupResources() { // 关闭输入管道写入端(通知进程停止) if (hWritePipe_stdin_) { @@ -494,7 +518,9 @@ const std::vector& CLIProcess::GetLogs() const { return logs_; } -bool CLIProcess::SendCommand(const std::string& command) { + +bool CLIProcess::SendCommand(const std::string &command) { +#ifdef _WIN32 if (!IsRunning() || !hWritePipe_stdin_) { return false; } @@ -506,11 +532,20 @@ bool CLIProcess::SendCommand(const std::string& command) { static_cast(fullCommand.length()), &bytesWritten, nullptr)) { AddLog("> " + command); return true; - } + } return false; +#else + if (!process_running_ || pipe_stdin_[1] < 0) return false; + + std::string cmd = command + "\n"; + ssize_t written = write(pipe_stdin_[1], cmd.c_str(), cmd.size()); + return written == (ssize_t)cmd.size(); +#endif } + void CLIProcess::CopyLogsToClipboard() const { +#ifdef _WIN32 std::lock_guard lock(logs_mutex_); if (logs_.empty()) return; @@ -542,13 +577,46 @@ void CLIProcess::CopyLogsToClipboard() const { } CloseClipboard(); } +#else + // Unix / macOS, use xclip or pbcopy + std::string clipboard_text; + { + std::lock_guard lock(logs_mutex_); + for (const auto& line : logs_) { + clipboard_text += line + "\n"; + } + } + FILE* pipe = popen("pbcopy", "w"); + if (!pipe) { + pipe = popen("xclip -selection clipboard", "w"); + } + if (pipe) { + fwrite(clipboard_text.c_str(), 1, clipboard_text.size(), pipe); + pclose(pipe); + } +#endif } -bool CLIProcess::IsRunning() const { - return pi_.hProcess != nullptr; +bool CLIProcess::IsRunning() const { +#ifdef _WIN32 + if (pi_.hProcess == nullptr) return false; + + DWORD status = WaitForSingleObject(pi_.hProcess, 0); + return status == WAIT_TIMEOUT; +#else + if (!process_running_) return false; + + int status; + pid_t result = waitpid(process_pid_, &status, WNOHANG); + if (result == 0) return true; // still running + process_running_ = false; + return false; +#endif } + void CLIProcess::ReadOutput() { +#ifdef _WIN32 constexpr int BUFFER_SIZE = 4096; char buffer[BUFFER_SIZE]; DWORD bytesRead; @@ -562,7 +630,7 @@ void CLIProcess::ReadOutput() { buffer[bytesRead] = '\0'; std::string output(buffer); - // 新增:根据设置的编码转换输出 + // 根据设置的编码转换输出 OutputEncoding currentEncoding; { std::lock_guard lock(encoding_mutex_); @@ -599,4 +667,25 @@ void CLIProcess::ReadOutput() { partialLine = convertedOutput.substr(start); } } +#else +const int buffer_size = 4096; +char buffer[buffer_size]; +ssize_t bytes_read = 0; +while (process_running_) { + bytes_read = read(pipe_stdout_[0], buffer, buffer_size - 1); + if (bytes_read <= 0) break; + buffer[bytes_read] = '\0'; + + std::string utf8_str; + { + std::lock_guard lock(encoding_mutex_); + if (output_encoding_ == OutputEncoding::AUTO_DETECT) { + utf8_str = DetectAndConvertToUTF8(std::string(buffer)); + } else { + utf8_str = ConvertToUTF8(std::string(buffer), output_encoding_); + } + } + AddLog(utf8_str); +} +#endif } diff --git a/app/src/Manager.cpp b/app/src/Manager.cpp index aa2059c..4b7b37a 100644 --- a/app/src/Manager.cpp +++ b/app/src/Manager.cpp @@ -2,8 +2,8 @@ #include #include -#include "Units.h" +#include "Units.h" Manager::Manager() = default; @@ -45,25 +45,56 @@ bool Manager::Initialize() { style.IndentSpacing = 25.0f; style.ScrollbarSize = 15.0f; style.GrabMinSize = 10.0f; + #ifdef USE_WIN32_BACKEND ImGui_ImplWin32_Init(m_hwnd); ImGui_ImplDX11_Init(m_pd3dDevice, m_pd3dDeviceContext); - // ImGui_ImplWin32_EnableDpiAwareness(); - // m_dpi_scale=ImGui_ImplWin32_GetDpiScaleForHwnd(m_hwnd); - // style.ScaleAllSizes(m_dpi_scale); + m_dpi_scale = ImGui_ImplWin32_GetDpiScaleForHwnd(m_hwnd); + style.ScaleAllSizes(m_dpi_scale); #else ImGui_ImplGlfw_InitForOpenGL(m_window, true); ImGui_ImplOpenGL3_Init(m_glsl_version); #endif - // 加载中文字体 + // 加载字体 +#ifdef _WIN32 ImFont* font = io.Fonts->AddFontFromFileTTF( "C:/Windows/Fonts/msyh.ttc", - 18.0f, + 14.0f, nullptr, io.Fonts->GetGlyphRangesChineseFull() ); - IM_ASSERT(font != nullptr); +#elif __APPLE__ + // macOS 中文字体路径 + ImFont* font = io.Fonts->AddFontFromFileTTF( + "/System/Library/Fonts/PingFang.ttc", + 14.0f, + nullptr, + io.Fonts->GetGlyphRangesChineseFull() + ); + // 备用字体 + if (!font) { + font = io.Fonts->AddFontFromFileTTF( + "/System/Library/Fonts/STHeiti Light.ttc", + 14.0f, + nullptr, + io.Fonts->GetGlyphRangesChineseFull() + ); + } +#else + // Linux 字体路径 + ImFont* font = io.Fonts->AddFontFromFileTTF( + "/usr/share/fonts/truetype/wqy/wqy-microhei.ttc", + 14.0f, + nullptr, + io.Fonts->GetGlyphRangesChineseFull() + ); +#endif + + if (!font) { + // 如果没有找到中文字体,使用默认字体 + font = io.Fonts->AddFontDefault(); + } // 初始化托盘 if (!InitializeTray()) return false; @@ -72,7 +103,12 @@ bool Manager::Initialize() { m_app_state.LoadSettings(); m_app_state.auto_start = IsAutoStartEnabled(); m_app_state.ApplySettings(); + +#ifdef _WIN32 m_tray->UpdateWebUrl(StringToWide(m_app_state.web_url)); +#else + m_tray->UpdateWebUrl(m_app_state.web_url); +#endif // 如果开启了开机自启动且有启动命令,则自动启动子进程 if (m_app_state.auto_start && strlen(m_app_state.command_input) > 0) { @@ -196,19 +232,36 @@ void Manager::RenderSettingsMenu() { m_app_state.settings_dirty = true; } + // 新增:命令历史记录设置 + ImGui::Separator(); + ImGui::Text("命令历史记录设置"); + if (ImGui::InputInt("最大历史记录数", &m_app_state.max_command_history, 5, 10)) { + m_app_state.max_command_history = std::max(5, std::min(m_app_state.max_command_history, 100)); + m_app_state.settings_dirty = true; + } + + if (ImGui::Button("清空命令历史记录")) { + m_app_state.ClearCommandHistory(); + } + ImGui::Separator(); ImGui::Text("Web设置"); if (ImGui::InputText("Web地址", m_app_state.web_url, IM_ARRAYSIZE(m_app_state.web_url))) { +#ifdef _WIN32 m_tray->UpdateWebUrl(StringToWide(m_app_state.web_url)); +#else + m_tray->UpdateWebUrl(m_app_state.web_url); +#endif m_app_state.settings_dirty = true; } RenderStopCommandSettings(); RenderEnvironmentVariablesSettings(); - RenderOutputEncodingSettings(); // 新增:渲染编码设置 + RenderOutputEncodingSettings(); } + void Manager::RenderStopCommandSettings() { ImGui::Separator(); ImGui::Text("停止命令设置"); @@ -233,10 +286,8 @@ void Manager::RenderStopCommandSettings() { ImGui::TextWrapped("说明:启用后,停止程序时会先发送指定命令,等待程序优雅退出。超时后将强制终止。"); } else { ImGui::BeginDisabled(true); - ImGui::InputText("停止命令", m_app_state.stop_command, IM_ARRAYSIZE(m_app_state.stop_command)); - ImGui::InputInt("超时时间(毫秒)", &m_app_state.stop_timeout_ms); - ImGui::EndDisabled(); ImGui::TextWrapped("说明:禁用时将直接强制终止程序。"); + ImGui::EndDisabled(); } } @@ -360,29 +411,97 @@ void Manager::RenderOutputEncodingSettings() { ImGui::BulletText("GBK/GB2312:适用于中文Windows系统的程序"); ImGui::BulletText("Big5:适用于繁体中文程序"); ImGui::BulletText("Shift-JIS:适用于日文程序"); - - // // 测试按钮 - // if (ImGui::Button("测试编码转换")) { - // std::string testText = "测试中编码转换显示:中文,English, 日本語, 한국어"; - // m_app_state.cli_process.TestOutputEncoding(testText); - // } } void Manager::RenderMainContent() { float buttonWidth = 80.0f * m_dpi_scale; float buttonHeight = 40.0f * m_dpi_scale; - float inputWidth = ImGui::GetContentRegionAvail().x * 0.6f; - - // 启动命令输入 + float inputWidth = ImGui::GetContentRegionAvail().x * 0.5f; // 调整输入框宽度为50% + + // 启动命令输入区域 + ImGui::BeginGroup(); + ImGui::Text("启动命令"); + + // 命令输入框和历史记录按钮 ImGui::SetNextItemWidth(inputWidth); - if (ImGui::InputText("启动命令", m_app_state.command_input, IM_ARRAYSIZE(m_app_state.command_input))) { + if (ImGui::InputText("##启动命令", m_app_state.command_input, IM_ARRAYSIZE(m_app_state.command_input))) { m_app_state.settings_dirty = true; } - // 控制按钮 + ImGui::SameLine(); + if (ImGui::Button("历史记录", ImVec2(100.0f * m_dpi_scale, 0))) { + show_command_history_ = !show_command_history_; + } + + // 显示命令历史记录下拉列表 + if (show_command_history_) { + const auto& history = m_app_state.GetCommandHistory(); + + if (!history.empty()) { + ImGui::Indent(); + ImGui::Text("选择历史命令 (%d个):", static_cast(history.size())); + + if (ImGui::BeginChild("CommandHistory", ImVec2(0, 120), true)) { + for (int i = 0; i < static_cast(history.size()); ++i) { + ImGui::PushID(i); + + // 选择按钮 + if (ImGui::Button("选择", ImVec2(50.0f * m_dpi_scale, 0))) { + strncpy_s(m_app_state.command_input, history[i].c_str(), sizeof(m_app_state.command_input) - 1); + show_command_history_ = false; + ImGui::PopID(); + break; + } + ImGui::SameLine(); + + // 显示命令内容(限制显示长度) + std::string displayCommand = history[i]; + if (displayCommand.length() > 60) { + displayCommand = displayCommand.substr(0, 57) + "..."; + } + ImGui::TextUnformatted(displayCommand.c_str()); + + // 鼠标悬停时显示完整命令 + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("%s", history[i].c_str()); + } + + ImGui::SameLine(); + + // 删除按钮 + if (ImGui::SmallButton("删除")) { + m_app_state.RemoveCommandFromHistory(i); + ImGui::PopID(); + continue; + } + + ImGui::PopID(); + } + } + ImGui::EndChild(); + + // 操作按钮 + if (ImGui::Button("清空所有历史记录")) { + m_app_state.ClearCommandHistory(); + } + ImGui::SameLine(); + + ImGui::Unindent(); + } else { + ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "暂无启动命令历史"); + } + } + ImGui::EndGroup(); + + ImGui::Spacing(); + + ImGui::BeginGroup(); if (ImGui::Button("启动", ImVec2(buttonWidth, buttonHeight))) { - m_app_state.cli_process.Start(m_app_state.command_input); + if (strlen(m_app_state.command_input) > 0) { + m_app_state.cli_process.Start(m_app_state.command_input); + m_app_state.AddCommandToHistory(m_app_state.command_input); + } } ImGui::SameLine(); if (ImGui::Button("停止", ImVec2(buttonWidth, buttonHeight))) { @@ -390,7 +509,10 @@ void Manager::RenderMainContent() { } ImGui::SameLine(); if (ImGui::Button("重启", ImVec2(buttonWidth, buttonHeight))) { - m_app_state.cli_process.Restart(m_app_state.command_input); + if (strlen(m_app_state.command_input) > 0) { + m_app_state.cli_process.Restart(m_app_state.command_input); + m_app_state.AddCommandToHistory(m_app_state.command_input); + } } ImGui::SameLine(); if (ImGui::Button("清理日志", ImVec2(100.0f * m_dpi_scale, buttonHeight))) { @@ -528,6 +650,7 @@ LRESULT WINAPI Manager::WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lPara #endif bool Manager::InitializeTray() { +#ifdef _WIN32 m_tray_hwnd = CreateHiddenWindow(); if (!m_tray_hwnd) { return false; @@ -536,6 +659,16 @@ bool Manager::InitializeTray() { HICON trayIcon = LoadIcon(NULL, IDI_APPLICATION); m_tray = std::make_unique(m_tray_hwnd, trayIcon); + // 设置托盘窗口的用户数据,指向TrayIcon实例 + SetWindowLongPtr(m_tray_hwnd, GWLP_USERDATA, reinterpret_cast(m_tray.get())); +#else + // macOS 托盘初始化 + void* app_delegate = GetMacAppDelegate(); // 需要实现这个函数 + void* tray_icon = GetMacTrayIcon(); // 需要实现这个函数 + + m_tray = std::make_unique(app_delegate, tray_icon); +#endif + // 设置回调函数 m_tray->SetShowWindowCallback([this]() { OnTrayShowWindow(); @@ -546,17 +679,14 @@ bool Manager::InitializeTray() { }); m_tray->Show(); - - // 设置托盘窗口的用户数据,指向TrayIcon实例 - SetWindowLongPtr(m_tray_hwnd, GWLP_USERDATA, reinterpret_cast(m_tray.get())); - return true; } +#ifdef _WIN32 HWND Manager::CreateHiddenWindow() { WNDCLASSEX wc = {0}; wc.cbSize = sizeof(WNDCLASSEX); - wc.lpfnWndProc = TrayIcon::WindowProc; // 使用TrayIcon的窗口过程 + wc.lpfnWndProc = TrayIcon::WindowProc; wc.hInstance = GetModuleHandle(NULL); wc.lpszClassName = L"CLIManagerTrayWindow"; @@ -575,6 +705,7 @@ HWND Manager::CreateHiddenWindow() { NULL ); } +#endif void Manager::HandleMessages() { #ifdef USE_WIN32_BACKEND @@ -584,7 +715,6 @@ void Manager::HandleMessages() { m_should_exit = true; } else if (msg.message == WM_CLOSE) { - // 主窗口关闭时隐藏到托盘,而不是退出 if (msg.hwnd == m_hwnd) { HideMainWindow(); continue; @@ -598,15 +728,16 @@ void Manager::HandleMessages() { } #else // GLFW后端的消息处理 +#ifdef _WIN32 MSG msg; while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) { if (msg.message == WM_QUIT) { m_should_exit = true; } - TranslateMessage(&msg); DispatchMessage(&msg); } +#endif glfwPollEvents(); if (glfwWindowShouldClose(m_window)) { @@ -800,11 +931,13 @@ void Manager::GlfwErrorCallback(int error, const char* description) { void Manager::CleanupTray() { m_tray.reset(); +#ifdef _WIN32 if (m_tray_hwnd) { DestroyWindow(m_tray_hwnd); m_tray_hwnd = nullptr; } UnregisterClass(L"CLIManagerTrayWindow", GetModuleHandle(nullptr)); +#endif } void Manager::Shutdown() { @@ -830,4 +963,12 @@ void Manager::Shutdown() { #endif m_initialized = false; -} \ No newline at end of file +} + +#ifdef __APPLE__ +// macOS 特定的辅助函数声明 +extern "C" { + void* GetMacAppDelegate(); + void* GetMacTrayIcon(); +} +#endif \ No newline at end of file diff --git a/app/src/TrayIcon.cpp b/app/src/TrayIcon.cpp index 31134ce..bda1529 100644 --- a/app/src/TrayIcon.cpp +++ b/app/src/TrayIcon.cpp @@ -1,6 +1,7 @@ #include "TrayIcon.h" #include "Units.h" +#ifdef _WIN32 TrayIcon::TrayIcon(HWND hwnd, HICON icon) : m_hwnd(hwnd), m_icon(icon), m_visible(false), m_menu(nullptr) { @@ -16,6 +17,13 @@ TrayIcon::TrayIcon(HWND hwnd, HICON icon) CreateMenu(); } +#else +TrayIcon::TrayIcon(void* app_delegate, void* icon) + : m_app_delegate(app_delegate), m_icon(icon), m_visible(false) { + m_web_url = "http://localhost:8080"; // 默认URL + CreateMenu(); +} +#endif TrayIcon::~TrayIcon() { Hide(); @@ -24,24 +32,41 @@ TrayIcon::~TrayIcon() { void TrayIcon::Show() { if (!m_visible) { +#ifdef _WIN32 Shell_NotifyIcon(NIM_ADD, &m_nid); +#else + ShowMacTrayIcon(); +#endif m_visible = true; } } void TrayIcon::Hide() { if (m_visible) { +#ifdef _WIN32 Shell_NotifyIcon(NIM_DELETE, &m_nid); +#else + HideMacTrayIcon(); +#endif m_visible = false; } } +#ifdef _WIN32 void TrayIcon::UpdateWebUrl(const std::wstring& url) { m_web_url = url; // 重新创建菜单以更新Web URL显示 DestroyMenu(); CreateMenu(); } +#else +void TrayIcon::UpdateWebUrl(const std::string& url) { + m_web_url = url; + // 重新创建菜单以更新Web URL显示 + DestroyMenu(); + CreateMenu(); +} +#endif void TrayIcon::SetShowWindowCallback(const ShowWindowCallback &callback) { m_show_window_callback = callback; @@ -52,13 +77,14 @@ void TrayIcon::SetExitCallback(const ExitCallback &callback) { } void TrayIcon::CreateMenu() { +#ifdef _WIN32 if (m_menu) { DestroyMenu(); } m_menu = CreatePopupMenu(); AppendMenu(m_menu, MF_STRING, 1001, L"显示主窗口"); - AppendMenu(m_menu, MF_SEPARATOR, 0, NULL); + AppendMenu(m_menu, MF_SEPARATOR, 0, nullptr); // 添加Web地址菜单项(如果有设置) if (!m_web_url.empty() && m_web_url != L"") { @@ -68,15 +94,23 @@ void TrayIcon::CreateMenu() { } AppendMenu(m_menu, MF_STRING, 1003, L"退出"); +#else + CreateMacMenu(); +#endif } void TrayIcon::DestroyMenu() { +#ifdef _WIN32 if (m_menu) { ::DestroyMenu(m_menu); m_menu = nullptr; } +#else + DestroyMacMenu(); +#endif } +#ifdef _WIN32 void TrayIcon::ShowContextMenu() const { if (!m_menu) return; @@ -131,4 +165,62 @@ LRESULT CALLBACK TrayIcon::WindowProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM return DefWindowProc(hwnd, msg, wParam, lParam); } return 0; -} \ No newline at end of file +} +#else +// macOS 特定实现 +void TrayIcon::ShowMacTrayIcon() { + // 通过 Objective-C 接口显示托盘图标 + ShowMacTrayIconImpl(m_app_delegate, m_icon); +} + +void TrayIcon::HideMacTrayIcon() { + // 通过 Objective-C 接口隐藏托盘图标 + HideMacTrayIconImpl(m_app_delegate); +} + +void TrayIcon::CreateMacMenu() { + // 通过 Objective-C 接口创建菜单 + CreateMacMenuImpl(m_app_delegate, m_web_url.c_str()); +} + +void TrayIcon::DestroyMacMenu() { + // 通过 Objective-C 接口销毁菜单 + DestroyMacMenuImpl(m_app_delegate); +} + +void TrayIcon::OnMacMenuAction(int action) { + switch (action) { + case 1001: // 显示主窗口 + if (m_show_window_callback) { + m_show_window_callback(); + } + break; + case 1002: // 打开Web页面 + if (!m_web_url.empty()) { + OpenWebPageMac(m_web_url.c_str()); + } + break; + case 1003: // 退出 + if (m_exit_callback) { + m_exit_callback(); + } + break; + } +} + +// C 接口函数,供 Objective-C 调用 +extern "C" void TrayIconMenuCallback(void* tray_instance, int action) { + if (tray_instance) { + static_cast(tray_instance)->OnMacMenuAction(action); + } +} + +// 外部声明的 Objective-C 接口函数 +extern "C" { + void ShowMacTrayIconImpl(void* app_delegate, void* icon); + void HideMacTrayIconImpl(void* app_delegate); + void CreateMacMenuImpl(void* app_delegate, const char* web_url); + void DestroyMacMenuImpl(void* app_delegate); + void OpenWebPageMac(const char* url); +} +#endif \ No newline at end of file