commit 2840c9a37869d4458368bad236d924f4a6d46359 Author: JiXieShi Date: Mon Sep 8 13:59:24 2025 +0800 UP diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..9f6107c --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,91 @@ +cmake_minimum_required(VERSION 3.31) +project(CLI_Manager) + +set(CMAKE_CXX_STANDARD 20) +#set(CMAKE_CXX_STANDARD_REQUIRED ON) + +set(IMGUI_DIR $ENV{LIB_HOME}/imgui-docking) +set(GLFW_DIR $ENV{LIB_HOME}/glfw) + +set(IMGUI_BACKENDS "glfw_opengl") +#set(IMGUI_BACKENDS "win32_dx11") + +#set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -static") + +set(CMAKE_EXE_LINKER_FLAGS "-static-libgcc -static-libstdc++") + +add_definitions(-DUNICODE -D_UNICODE) + + +# add header path +include_directories( + ${IMGUI_DIR} + ${IMGUI_DIR}/backends + ${IMGUI_DIR}/misc/cpp + app/inc +) + +# set common source +file(GLOB SRC + ${IMGUI_DIR}/*.cpp + ${IMGUI_DIR}/misc/cpp/*.cpp + app/src/*.* + main.cpp +) + + +if(IMGUI_BACKENDS STREQUAL "glfw_opengl") + add_definitions(-DIMGUI_IMPL_OPENGL_LOADER_GL3W) + + include_directories( + ${GLFW_DIR}/include + ${IMGUI_DIR}/examples/libs/gl3w # for GL/gl3w.h + ) + + file(GLOB PLATFORM_SRC + ${IMGUI_DIR}/examples/libs/gl3w/GL/gl3w.* + ${IMGUI_DIR}/backends/imgui_impl_glfw.* + ${IMGUI_DIR}/backends/imgui_impl_opengl3.* + ) + + link_directories( + ${GLFW_DIR}/build/src + ) +endif() + +# 链接库和选项 +if(IMGUI_BACKENDS STREQUAL "win32_dx11") + add_definitions(-DUSE_WIN32_BACKEND) + file(GLOB PLATFORM_SRC + ${IMGUI_DIR}/backends/imgui_impl_win32.* + ${IMGUI_DIR}/backends/imgui_impl_dx11.* + ) +endif() + +# generate binary +add_executable(${PROJECT_NAME} WIN32 ${SRC} ${PLATFORM_SRC}) + +if(IMGUI_BACKENDS STREQUAL "glfw_opengl") + target_link_libraries(${PROJECT_NAME} + glfw3.a + opengl32 + ) +endif() +# 链接库和选项 +if(IMGUI_BACKENDS STREQUAL "win32_dx11") + target_link_libraries(${PROJECT_NAME} + d3d11 + dxgi + user32 + gdi32 + ole32 + dwmapi + d3dcompiler + ) +endif() + +# 设置链接选项 +#set_target_properties(${PROJECT_NAME} PROPERTIES +# LINK_FLAGS "-static -Wl,-subsystem,windows" +#) +# diff --git a/app/inc/AppState.h b/app/inc/AppState.h new file mode 100644 index 0000000..bcfa38f --- /dev/null +++ b/app/inc/AppState.h @@ -0,0 +1,51 @@ +#ifndef APP_STATE_H +#define APP_STATE_H + +#include "CLIProcess.h" +#include +#include +#include + +class AppState { +public: + AppState(); + ~AppState() = default; + + void LoadSettings(); + void SaveSettings(); + void ApplySettings(); + + bool show_main_window; + bool auto_start; + CLIProcess cli_process; + char command_input[256]{}; + char send_command[256]{}; + bool auto_scroll_logs; + int max_log_lines; + char web_url[256]{}; + + // 停止命令相关配置 + char stop_command[256]{}; + int stop_timeout_ms; + bool use_stop_command; + + // 环境变量相关配置 + std::map environment_variables; + bool use_custom_environment; + + // 新增:输出编码相关配置 + OutputEncoding output_encoding; + + bool settings_dirty; + +private: + // 环境变量序列化辅助函数 + std::string SerializeEnvironmentVariables() const; + void DeserializeEnvironmentVariables(const std::string& serialized); + + // 新增:编码序列化辅助函数 + std::string SerializeOutputEncoding() const; + void DeserializeOutputEncoding(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 new file mode 100644 index 0000000..da5e5ea --- /dev/null +++ b/app/inc/CLIProcess.h @@ -0,0 +1,92 @@ +#ifndef CLIPROCESS_H +#define CLIPROCESS_H + +#include +#include +#include +#include +#include +#include + +// 新增:输出编码枚举 +enum class OutputEncoding { + UTF8 = 0, + GBK, + GB2312, + BIG5, + SHIFT_JIS, + AUTO_DETECT +}; + +class CLIProcess { +public: + CLIProcess(); + ~CLIProcess(); + + 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 Start(const std::string& command); + void Stop(); + void Restart(const std::string& command); + + void ClearLogs(); + void AddLog(const std::string& log); + const std::vector& GetLogs() const; + + bool SendCommand(const std::string& command); + void CopyLogsToClipboard() const; + + bool IsRunning() const; + + // 环境变量管理接口 + const std::map& GetEnvironmentVariables() const; + void AddEnvironmentVariable(const std::string& key, const std::string& value); + void RemoveEnvironmentVariable(const std::string& key); + void ClearEnvironmentVariables(); + + // 新增:编码相关接口 + OutputEncoding GetOutputEncoding() const; + static std::string GetEncodingName(OutputEncoding encoding); + static std::vector> GetSupportedEncodings(); + +private: + void ReadOutput(); + void CloseProcessHandles(); + void CleanupResources(); + + // 新增:编码转换相关方法 + std::string ConvertToUTF8(const std::string& input, OutputEncoding encoding); + std::string DetectAndConvertToUTF8(const std::string& input); + UINT GetCodePageFromEncoding(OutputEncoding encoding); + bool IsValidUTF8(const std::string& str); + + PROCESS_INFORMATION pi_{}; + HANDLE hReadPipe_{}; + HANDLE hWritePipe_{}; + HANDLE hReadPipe_stdin_{}; + HANDLE hWritePipe_stdin_; + + mutable std::mutex logs_mutex_; + std::vector logs_; + int max_log_lines_; + + std::thread output_thread_; + + // 停止命令相关 + std::mutex stop_mutex_; + std::string stop_command_; + int stop_timeout_ms_; + + // 环境变量相关 + mutable std::mutex env_mutex_; + std::map environment_variables_; + + // 新增:编码相关 + mutable std::mutex encoding_mutex_; + OutputEncoding output_encoding_; +}; + +#endif // CLIPROCESS_H \ No newline at end of file diff --git a/app/inc/Manager.h b/app/inc/Manager.h new file mode 100644 index 0000000..fa6c5ee --- /dev/null +++ b/app/inc/Manager.h @@ -0,0 +1,97 @@ +#pragma once + +#include "imgui.h" +#include "AppState.h" +#include "TrayIcon.h" + +#ifdef USE_WIN32_BACKEND +#include +#include +#include "imgui_impl_win32.h" +#include "imgui_impl_dx11.h" +#else +#define GLFW_EXPOSE_NATIVE_WIN32 +#include +#include +#include "imgui_impl_glfw.h" +#include "imgui_impl_opengl3.h" +#include +#endif + +#include + +class Manager { +public: + Manager(); + ~Manager(); + + bool Initialize(); + void Run(); + void Shutdown(); + + void OnTrayShowWindow(); + void OnTrayExit(); + + AppState m_app_state; +private: + // UI渲染 + void RenderUI(); + void RenderMenuBar(); + void RenderMainContent(); + void RenderSettingsMenu(); + void RenderStopCommandSettings(); + void RenderEnvironmentVariablesSettings(); + void RenderOutputEncodingSettings(); // 新增:输出编码设置UI + + // 事件处理 + void HandleMessages(); + bool ShouldExit() const; + void ShowMainWindow(); + void HideMainWindow(); + + // 平台相关初始化 +#ifdef USE_WIN32_BACKEND + bool InitializeWin32(); + bool InitializeDirectX11(); + void CleanupWin32(); + void CleanupDirectX11(); + static LRESULT WINAPI WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam); + + HWND m_hwnd = nullptr; + WNDCLASSEX m_wc = {}; + ID3D11Device* m_pd3dDevice = nullptr; + ID3D11DeviceContext* m_pd3dDeviceContext = nullptr; + IDXGISwapChain* m_pSwapChain = nullptr; + ID3D11RenderTargetView* m_mainRenderTargetView = nullptr; +#else + bool InitializeGLFW(); + void CleanupGLFW(); + static void GlfwErrorCallback(int error, const char* description); + + GLFWwindow* m_window = nullptr; + const char* m_glsl_version = nullptr; +#endif + + // 托盘相关 + bool InitializeTray(); + void CleanupTray(); + static HWND CreateHiddenWindow(); + + std::unique_ptr m_tray; + HWND m_tray_hwnd = nullptr; + + // 控制标志 + bool m_should_exit = false; + bool m_initialized = false; + + // DPI缩放因子 + float m_dpi_scale = 1.0f; + + // 环境变量UI状态 + char env_key_input_[256] = {}; + char env_value_input_[512] = {}; + bool show_env_settings_ = false; + + // 新增:编码设置UI状态 + bool show_encoding_settings_ = false; +}; \ No newline at end of file diff --git a/app/inc/TrayIcon.h b/app/inc/TrayIcon.h new file mode 100644 index 0000000..736da02 --- /dev/null +++ b/app/inc/TrayIcon.h @@ -0,0 +1,42 @@ +#pragma once +#include +#include +#include +#include + +class TrayIcon { +public: + // 回调函数类型定义 + using ShowWindowCallback = std::function; + using ExitCallback = std::function; + + TrayIcon(HWND hwnd, HICON icon); + ~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(); + void ShowContextMenu() const; + + HWND m_hwnd; + HICON m_icon; + NOTIFYICONDATA m_nid{}; + std::wstring m_web_url; + bool m_visible; + HMENU m_menu; + + // 回调函数 + ShowWindowCallback m_show_window_callback; + ExitCallback m_exit_callback; +}; \ No newline at end of file diff --git a/app/inc/Units.h b/app/inc/Units.h new file mode 100644 index 0000000..7c6900d --- /dev/null +++ b/app/inc/Units.h @@ -0,0 +1,11 @@ +#ifndef UNITS_H +#define UNITS_H +#include + +std::wstring StringToWide(const std::string& str); +std::string WideToString(const std::wstring& wstr); +void SetAutoStart(bool enable); +bool IsAutoStartEnabled(); + + +#endif //UNITS_H diff --git a/app/src/AppState.cpp b/app/src/AppState.cpp new file mode 100644 index 0000000..7a780c6 --- /dev/null +++ b/app/src/AppState.cpp @@ -0,0 +1,188 @@ +#include "AppState.h" +#include +#include +#include + +AppState::AppState() : + show_main_window(true), + auto_start(false), + auto_scroll_logs(true), + max_log_lines(1000), + stop_timeout_ms(5000), + use_stop_command(false), + use_custom_environment(false), + output_encoding(OutputEncoding::AUTO_DETECT), // 新增:默认自动检测编码 + settings_dirty(false) { + strcpy_s(command_input, "cmd.exe"); + strcpy_s(web_url, "http://localhost:8080"); + strcpy_s(stop_command, "exit"); + memset(send_command, 0, sizeof(send_command)); +} + +std::string AppState::SerializeEnvironmentVariables() const { + std::ostringstream oss; + bool first = true; + for (const auto& pair : environment_variables) { + if (!first) { + oss << "|"; + } + oss << pair.first << "=" << pair.second; + first = false; + } + return oss.str(); +} + +void AppState::DeserializeEnvironmentVariables(const std::string& serialized) { + environment_variables.clear(); + if (serialized.empty()) return; + + std::istringstream iss(serialized); + std::string pair; + + while (std::getline(iss, pair, '|')) { + size_t equalPos = pair.find('='); + if (equalPos != std::string::npos && equalPos > 0) { + std::string key = pair.substr(0, equalPos); + std::string value = pair.substr(equalPos + 1); + environment_variables[key] = value; + } + } +} + +// 新增:序列化输出编码 +std::string AppState::SerializeOutputEncoding() const { + return std::to_string(static_cast(output_encoding)); +} + +// 新增:反序列化输出编码 +void AppState::DeserializeOutputEncoding(const std::string& serialized) { + if (serialized.empty()) { + output_encoding = OutputEncoding::AUTO_DETECT; + return; + } + + try { + int encodingValue = std::stoi(serialized); + if (encodingValue >= 0 && encodingValue <= static_cast(OutputEncoding::AUTO_DETECT)) { + output_encoding = static_cast(encodingValue); + } else { + output_encoding = OutputEncoding::AUTO_DETECT; + } + } catch (const std::exception&) { + output_encoding = OutputEncoding::AUTO_DETECT; + } +} + +void AppState::LoadSettings() { + std::ifstream file("climanager_settings.ini"); + if (!file.is_open()) return; + + std::string line; + std::string section; + + while (std::getline(file, line)) { + if (!line.empty() && line[line.size() - 1] == '\r') { + line.erase(line.size() - 1); + } + + if (line.empty()) continue; + + if (line[0] == '[' && line[line.size() - 1] == ']') { + section = line.substr(1, line.size() - 2); + } + else if (section == "Settings") { + size_t pos = line.find('='); + if (pos != std::string::npos) { + std::string key = line.substr(0, pos); + std::string value = line.substr(pos + 1); + + if (key == "CommandInput") { + strncpy_s(command_input, value.c_str(), sizeof(command_input) - 1); + } + else if (key == "MaxLogLines") { + max_log_lines = std::stoi(value); + max_log_lines = std::max(100, std::min(max_log_lines, 10000)); + } + else if (key == "AutoScrollLogs") { + auto_scroll_logs = (value == "1"); + } + else if (key == "AutoStart") { + auto_start = (value == "1"); + } + else if (key == "WebUrl") { + strncpy_s(web_url, value.c_str(), sizeof(web_url) - 1); + } + else if (key == "StopCommand") { + strncpy_s(stop_command, value.c_str(), sizeof(stop_command) - 1); + } + else if (key == "StopTimeoutMs") { + stop_timeout_ms = std::stoi(value); + stop_timeout_ms = std::max(1000, std::min(stop_timeout_ms, 60000)); + } + else if (key == "UseStopCommand") { + use_stop_command = (value == "1"); + } + else if (key == "UseCustomEnvironment") { + use_custom_environment = (value == "1"); + } + else if (key == "EnvironmentVariables") { + DeserializeEnvironmentVariables(value); + } + // 新增:输出编码配置的加载 + else if (key == "OutputEncoding") { + DeserializeOutputEncoding(value); + } + } + } + } + file.close(); +} + +void AppState::SaveSettings() { + std::ofstream file("climanager_settings.ini"); + if (!file.is_open()) return; + + file << "[Settings]\n"; + file << "CommandInput=" << command_input << "\n"; + file << "MaxLogLines=" << max_log_lines << "\n"; + file << "AutoScrollLogs=" << (auto_scroll_logs ? "1" : "0") << "\n"; + file << "AutoStart=" << (auto_start ? "1" : "0") << "\n"; + file << "WebUrl=" << web_url << "\n"; + + // 停止命令相关配置的保存 + file << "StopCommand=" << stop_command << "\n"; + file << "StopTimeoutMs=" << stop_timeout_ms << "\n"; + file << "UseStopCommand=" << (use_stop_command ? "1" : "0") << "\n"; + + // 环境变量相关配置的保存 + file << "UseCustomEnvironment=" << (use_custom_environment ? "1" : "0") << "\n"; + file << "EnvironmentVariables=" << SerializeEnvironmentVariables() << "\n"; + + // 新增:输出编码配置的保存 + file << "OutputEncoding=" << SerializeOutputEncoding() << "\n"; + + file.close(); + + settings_dirty = false; +} + +void AppState::ApplySettings() { + cli_process.SetMaxLogLines(max_log_lines); + + // 应用停止命令设置 + if (use_stop_command && strlen(stop_command) > 0) { + cli_process.SetStopCommand(stop_command, stop_timeout_ms); + } else { + cli_process.SetStopCommand("", 0); + } + + // 应用环境变量设置 + if (use_custom_environment) { + cli_process.SetEnvironmentVariables(environment_variables); + } else { + cli_process.SetEnvironmentVariables({}); + } + + // 新增:应用输出编码设置 + cli_process.SetOutputEncoding(output_encoding); +} \ No newline at end of file diff --git a/app/src/CLIProcess.cpp b/app/src/CLIProcess.cpp new file mode 100644 index 0000000..0e0bcea --- /dev/null +++ b/app/src/CLIProcess.cpp @@ -0,0 +1,603 @@ +#include "CLIProcess.h" +#include +#include + +#include "Units.h" + +CLIProcess::CLIProcess() { + ZeroMemory(&pi_, sizeof(pi_)); + max_log_lines_ = 1000; + hWritePipe_stdin_ = nullptr; + stop_timeout_ms_ = 5000; + output_encoding_ = OutputEncoding::AUTO_DETECT; // 新增:默认自动检测编码 +} + +CLIProcess::~CLIProcess() { + Stop(); +} + +// 新增:设置输出编码 +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(OutputEncoding encoding) { + switch (encoding) { + case OutputEncoding::UTF8: return "UTF-8"; + 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 "未知"; + } +} + +// 新增:获取支持的编码列表 +std::vector> CLIProcess::GetSupportedEncodings() { + return { + {OutputEncoding::AUTO_DETECT, "自动检测"}, + {OutputEncoding::UTF8, "UTF-8"}, + {OutputEncoding::GBK, "GBK (简体中文)"}, + {OutputEncoding::GB2312, "GB2312 (简体中文)"}, + {OutputEncoding::BIG5, "Big5 (繁体中文)"}, + {OutputEncoding::SHIFT_JIS, "Shift-JIS (日文)"} + }; +} + +// 新增:根据编码获取代码页 +UINT CLIProcess::GetCodePageFromEncoding(OutputEncoding encoding) { + switch (encoding) { + case OutputEncoding::UTF8: return CP_UTF8; + case OutputEncoding::GBK: return 936; + case OutputEncoding::GB2312: return 20936; + case OutputEncoding::BIG5: return 950; + case OutputEncoding::SHIFT_JIS: return 932; + default: return CP_ACP; // 系统默认代码页 + } +} + +// 新增:检查是否为有效的UTF-8 +bool CLIProcess::IsValidUTF8(const std::string& str) { + const unsigned char* bytes = reinterpret_cast(str.c_str()); + size_t len = str.length(); + + for (size_t i = 0; i < len; ) { + if (bytes[i] <= 0x7F) { + // ASCII字符 + i++; + } else if ((bytes[i] & 0xE0) == 0xC0) { + // 2字节UTF-8序列 + if (i + 1 >= len || (bytes[i + 1] & 0xC0) != 0x80) return false; + i += 2; + } else if ((bytes[i] & 0xF0) == 0xE0) { + // 3字节UTF-8序列 + if (i + 2 >= len || (bytes[i + 1] & 0xC0) != 0x80 || (bytes[i + 2] & 0xC0) != 0x80) return false; + i += 3; + } else if ((bytes[i] & 0xF8) == 0xF0) { + // 4字节UTF-8序列 + if (i + 3 >= len || (bytes[i + 1] & 0xC0) != 0x80 || (bytes[i + 2] & 0xC0) != 0x80 || (bytes[i + 3] & 0xC0) != 0x80) return false; + i += 4; + } else { + return false; + } + } + return true; +} + +// 新增:转换到UTF-8 +std::string CLIProcess::ConvertToUTF8(const std::string& input, OutputEncoding encoding) { + if (input.empty()) return input; + + // 如果已经是UTF-8编码,直接返回 + if (encoding == OutputEncoding::UTF8) { + return input; + } + + UINT codePage = GetCodePageFromEncoding(encoding); + + // 先转换为宽字符 + int wideSize = MultiByteToWideChar(codePage, 0, input.c_str(), -1, nullptr, 0); + if (wideSize <= 0) { + // 转换失败,返回原始字符串 + return input; + } + + std::vector wideStr(wideSize); + if (MultiByteToWideChar(codePage, 0, input.c_str(), -1, wideStr.data(), wideSize) <= 0) { + return input; + } + + // 再从宽字符转换为UTF-8 + int utf8Size = WideCharToMultiByte(CP_UTF8, 0, wideStr.data(), -1, nullptr, 0, nullptr, nullptr); + if (utf8Size <= 0) { + return input; + } + + std::vector utf8Str(utf8Size); + if (WideCharToMultiByte(CP_UTF8, 0, wideStr.data(), -1, utf8Str.data(), utf8Size, nullptr, nullptr) <= 0) { + return input; + } + + return std::string(utf8Str.data()); +} + +// 新增:自动检测并转换到UTF-8 +std::string CLIProcess::DetectAndConvertToUTF8(const std::string& input) { + if (input.empty()) return input; + + // 首先检查是否已经是有效的UTF-8 + if (IsValidUTF8(input)) { + return input; + } + + // 尝试不同的编码进行转换 + std::vector encodingsToTry = { + OutputEncoding::GBK, + OutputEncoding::GB2312, + OutputEncoding::BIG5, + OutputEncoding::SHIFT_JIS + }; + + for (OutputEncoding encoding : encodingsToTry) { + std::string converted = ConvertToUTF8(input, encoding); + if (converted != input && IsValidUTF8(converted)) { + // 转换成功且结果是有效的UTF-8 + return converted; + } + } + + // 如果所有编码都失败,尝试使用系统默认代码页 + return ConvertToUTF8(input, OutputEncoding::GBK); // 默认使用GBK +} +void CLIProcess::SetStopCommand(const std::string& command, int timeout_ms) { + std::lock_guard lock(stop_mutex_); + stop_command_ = command; + stop_timeout_ms_ = (timeout_ms > 0) ? timeout_ms : 5000; + + if (!command.empty()) { + AddLog("已设置停止命令: " + command + " (超时: " + std::to_string(timeout_ms) + "ms)"); + } else { + } +} + +void CLIProcess::SetMaxLogLines(int max_lines) { + std::lock_guard lock(logs_mutex_); + max_log_lines_ = max_lines; + if (logs_.size() > max_log_lines_) { + logs_.erase(logs_.begin(), logs_.end() - max_log_lines_); + } +} + +void CLIProcess::SetEnvironmentVariables(const std::map& env_vars) { + std::lock_guard lock(env_mutex_); + environment_variables_.clear(); + + // 验证所有环境变量 + for (const auto& pair : env_vars) { + if (pair.first.empty()) { + AddLog("警告: 跳过空的环境变量名"); + continue; + } + + if (pair.first.find('=') != std::string::npos || pair.first.find('\0') != std::string::npos) { + AddLog("警告: 跳过包含无效字符的环境变量: " + pair.first); + continue; + } + + environment_variables_[pair.first] = pair.second; + } + + if (!environment_variables_.empty()) { + // AddLog("已设置 " + std::to_string(environment_variables_.size()) + " 个有效环境变量"); + for (const auto& pair : environment_variables_) { + AddLog(" " + pair.first + "=" + pair.second); + } + } else { + // AddLog("已清空所有自定义环境变量"); + } +} + +const std::map& CLIProcess::GetEnvironmentVariables() const { + std::lock_guard lock(env_mutex_); + return environment_variables_; +} + +void CLIProcess::AddEnvironmentVariable(const std::string& key, const std::string& value) { + std::lock_guard lock(env_mutex_); + + // 验证环境变量名 + if (key.empty()) { + AddLog("错误: 环境变量名不能为空"); + return; + } + + // 检查是否包含无效字符 + if (key.find('=') != std::string::npos || key.find('\0') != std::string::npos) { + AddLog("错误: 环境变量名包含无效字符: " + key); + return; + } + + environment_variables_[key] = value; + // AddLog("添加环境变量: " + key + "=" + value); +} +void CLIProcess::RemoveEnvironmentVariable(const std::string& key) { + std::lock_guard lock(env_mutex_); + auto it = environment_variables_.find(key); + if (it != environment_variables_.end()) { + environment_variables_.erase(it); + // AddLog("移除环境变量: " + key); + } +} + +void CLIProcess::ClearEnvironmentVariables() { + std::lock_guard lock(env_mutex_); + environment_variables_.clear(); + // AddLog("已清空所有自定义环境变量"); +} + +void CLIProcess::Start(const std::string& command) { + if (IsRunning()) return; + Stop(); + + SECURITY_ATTRIBUTES sa; + sa.nLength = sizeof(SECURITY_ATTRIBUTES); + sa.bInheritHandle = TRUE; + sa.lpSecurityDescriptor = nullptr; + + if (!CreatePipe(&hReadPipe_, &hWritePipe_, &sa, 0)) { + AddLog("创建输出管道失败"); + return; + } + + if (!CreatePipe(&hReadPipe_stdin_, &hWritePipe_stdin_, &sa, 0)) { + AddLog("创建输入管道失败"); + CloseHandle(hReadPipe_); + CloseHandle(hWritePipe_); + 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_)); + + // 转换命令为宽字符 + std::wstring wcmd = StringToWide(command); + + // CreateProcess需要可修改的字符串 + std::vector cmdBuffer(wcmd.begin(), wcmd.end()); + cmdBuffer.push_back(L'\0'); + + // 使用Windows API设置环境变量 + std::vector> originalEnvVars; + bool envVarsSet = false; + + if (!environment_variables_.empty()) { + envVarsSet = true; + + 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 (SetEnvironmentVariableA(pair.first.c_str(), pair.second.c_str())) { + // AddLog("设置环境变量: " + pair.first + "=" + pair.second); + } else { + AddLog("设置环境变量失败: " + pair.first + " (错误代码: " + std::to_string(GetLastError()) + ")"); + } + } + } + + // AddLog("环境变量设置完成,数量: " + std::to_string(environment_variables_.size())); + } else { + AddLog("未设置自定义环境变量,使用默认环境"); + } + + 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 + ); + + // 恢复原始环境变量 + 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; + } +} + +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_; + } + } + + 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); + CloseProcessHandles(); + AddLog("进程已强制终止"); + } + + // 关闭管道和线程 + CleanupResources(); +} + +// 新增:关闭进程句柄的辅助函数 +void CLIProcess::CloseProcessHandles() { + if (pi_.hProcess) { + CloseHandle(pi_.hProcess); + pi_.hProcess = nullptr; + } + if (pi_.hThread) { + CloseHandle(pi_.hThread); + pi_.hThread = nullptr; + } +} + +// 新增:清理资源的辅助函数 +void CLIProcess::CleanupResources() { + // 关闭输入管道写入端(通知进程停止) + if (hWritePipe_stdin_) { + CloseHandle(hWritePipe_stdin_); + hWritePipe_stdin_ = nullptr; + } + + // 等待输出线程结束 + if (output_thread_.joinable()) { + output_thread_.join(); + } + + // 关闭输出管道读取端 + if (hReadPipe_) { + CloseHandle(hReadPipe_); + hReadPipe_ = nullptr; + } + + // 确保所有句柄都已关闭 + if (hWritePipe_) { + CloseHandle(hWritePipe_); + hWritePipe_ = nullptr; + } + + if (hReadPipe_stdin_) { + CloseHandle(hReadPipe_stdin_); + hReadPipe_stdin_ = nullptr; + } +} + +void CLIProcess::Restart(const std::string& command) { + Stop(); + Start(command); +} + +void CLIProcess::ClearLogs() { + std::lock_guard lock(logs_mutex_); + logs_.clear(); +} + +void CLIProcess::AddLog(const std::string& log) { + std::lock_guard lock(logs_mutex_); + logs_.push_back(log); + if (logs_.size() > max_log_lines_) { + logs_.erase(logs_.begin(), logs_.begin() + (logs_.size() - max_log_lines_)); + } +} + +const std::vector& CLIProcess::GetLogs() const { + std::lock_guard lock(logs_mutex_); + return logs_; +} + +bool CLIProcess::SendCommand(const std::string& command) { + if (!IsRunning() || !hWritePipe_stdin_) { + return false; + } + + DWORD bytesWritten; + std::string fullCommand = command + "\n"; + + if (WriteFile(hWritePipe_stdin_, fullCommand.c_str(), + static_cast(fullCommand.length()), &bytesWritten, nullptr)) { + AddLog("> " + command); + return true; + } + return false; +} + +void CLIProcess::CopyLogsToClipboard() const { + std::lock_guard lock(logs_mutex_); + if (logs_.empty()) return; + + // 构建完整的日志字符串(使用\r\n确保跨平台兼容) + std::wstring allLogs; + for (const auto& log : logs_) { + allLogs += StringToWide(log) ; + allLogs.resize(allLogs.size() - sizeof(wchar_t)); + allLogs += L"\n"; + } + + if (OpenClipboard(nullptr)) { + EmptyClipboard(); + + // 计算正确的内存大小(包括终止空字符) + const size_t dataSize = (allLogs.length() + 1) * sizeof(wchar_t); + HGLOBAL hMem = GlobalAlloc(GMEM_MOVEABLE, dataSize); + + if (hMem) { + wchar_t* pMem = static_cast(GlobalLock(hMem)); + if (pMem) { + // 安全复制宽字符串数据 + wcscpy_s(pMem, allLogs.length() + 1, allLogs.c_str()); + GlobalUnlock(hMem); + SetClipboardData(CF_UNICODETEXT, hMem); + } else { + GlobalFree(hMem); // 锁定失败时释放内存 + } + } + CloseClipboard(); + } +} + +bool CLIProcess::IsRunning() const { + return pi_.hProcess != nullptr; +} + +void CLIProcess::ReadOutput() { + constexpr int BUFFER_SIZE = 4096; + char buffer[BUFFER_SIZE]; + DWORD bytesRead; + std::string partialLine; + + while (true) { + if (!ReadFile(hReadPipe_, buffer, BUFFER_SIZE - 1, &bytesRead, nullptr) || bytesRead == 0) { + break; + } + + buffer[bytesRead] = '\0'; + std::string output(buffer); + + // 新增:根据设置的编码转换输出 + OutputEncoding currentEncoding; + { + std::lock_guard lock(encoding_mutex_); + currentEncoding = output_encoding_; + } + + std::string convertedOutput; + if (currentEncoding == OutputEncoding::AUTO_DETECT) { + convertedOutput = DetectAndConvertToUTF8(output); + } else { + convertedOutput = ConvertToUTF8(output, currentEncoding); + } + + size_t start = 0; + size_t end = convertedOutput.find('\n'); + + while (end != std::string::npos) { + std::string line = partialLine + convertedOutput.substr(start, end - start); + partialLine.clear(); + + if (!line.empty() && line.back() == '\r') { + line.pop_back(); + } + + if (!line.empty()) { + AddLog(line); + } + + start = end + 1; + end = convertedOutput.find('\n', start); + } + + if (start < convertedOutput.size()) { + partialLine = convertedOutput.substr(start); + } + } +} diff --git a/app/src/Manager.cpp b/app/src/Manager.cpp new file mode 100644 index 0000000..80718cd --- /dev/null +++ b/app/src/Manager.cpp @@ -0,0 +1,838 @@ +#include "Manager.h" +#include +#include + +#include "Units.h" + + +Manager::Manager() = default; + +Manager::~Manager() { + Shutdown(); +} + +bool Manager::Initialize() { + if (m_initialized) return true; + +#ifdef USE_WIN32_BACKEND + if (!InitializeWin32()) return false; + if (!InitializeDirectX11()) return false; +#else + if (!InitializeGLFW()) return false; +#endif + + // 初始化ImGui + IMGUI_CHECKVERSION(); + ImGui::CreateContext(); + ImGuiIO& io = ImGui::GetIO(); + io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; + io.ConfigFlags |= ImGuiConfigFlags_DockingEnable; + io.ConfigViewportsNoAutoMerge = true; + io.IniFilename = "imgui.ini"; + + ImGui::StyleColorsDark(); + ImGuiStyle& style = ImGui::GetStyle(); + if (io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable) { + style.WindowRounding = 0.0f; + style.Colors[ImGuiCol_WindowBg].w = 1.0f; + } + + // 设置样式 + style.WindowPadding = ImVec2(15, 15); + style.FramePadding = ImVec2(5, 5); + style.ItemSpacing = ImVec2(10, 8); + style.ItemInnerSpacing = ImVec2(8, 6); + 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); +#else + ImGui_ImplGlfw_InitForOpenGL(m_window, true); + ImGui_ImplOpenGL3_Init(m_glsl_version); +#endif + + // 加载中文字体 + ImFont* font = io.Fonts->AddFontFromFileTTF( + "C:/Windows/Fonts/msyh.ttc", + 18.0f, + nullptr, + io.Fonts->GetGlyphRangesChineseFull() + ); + IM_ASSERT(font != nullptr); + + // 初始化托盘 + if (!InitializeTray()) return false; + + // 初始化应用状态 + m_app_state.LoadSettings(); + m_app_state.auto_start = IsAutoStartEnabled(); + m_app_state.ApplySettings(); + m_tray->UpdateWebUrl(StringToWide(m_app_state.web_url)); + + // 如果开启了开机自启动且有启动命令,则自动启动子进程 + if (m_app_state.auto_start && strlen(m_app_state.command_input) > 0) { + m_app_state.cli_process.Start(m_app_state.command_input); + } + + m_initialized = true; + return true; +} + +void Manager::Run() { + if (!m_initialized) return; + + while (!ShouldExit()) { + HandleMessages(); + + if (m_should_exit) break; + + if (m_app_state.settings_dirty) { + m_app_state.SaveSettings(); + } + + if (m_app_state.show_main_window) { +#ifdef USE_WIN32_BACKEND + // Win32 渲染循环 + ImGui_ImplDX11_NewFrame(); + ImGui_ImplWin32_NewFrame(); +#else + // GLFW 渲染循环 + if (glfwWindowShouldClose(m_window)) { + HideMainWindow(); + continue; + } + + ImGui_ImplOpenGL3_NewFrame(); + ImGui_ImplGlfw_NewFrame(); +#endif + + ImGui::NewFrame(); + RenderUI(); + ImGui::Render(); + +#ifdef USE_WIN32_BACKEND + float clearColor[4] = {0.1f, 0.1f, 0.1f, 1.0f}; + m_pd3dDeviceContext->OMSetRenderTargets(1, &m_mainRenderTargetView, nullptr); + m_pd3dDeviceContext->ClearRenderTargetView(m_mainRenderTargetView, clearColor); + ImGui_ImplDX11_RenderDrawData(ImGui::GetDrawData()); + m_pSwapChain->Present(1, 0); +#else + int display_w, display_h; + glfwGetFramebufferSize(m_window, &display_w, &display_h); + glViewport(0, 0, display_w, display_h); + glClearColor(0.1f, 0.1f, 0.1f, 1.00f); + glClear(GL_COLOR_BUFFER_BIT); + ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData()); + glfwSwapBuffers(m_window); +#endif + } else { +#ifdef USE_WIN32_BACKEND + WaitMessage(); +#else + glfwWaitEvents(); +#endif + } + } + + if (m_app_state.settings_dirty) { + m_app_state.SaveSettings(); + } +} + +void Manager::RenderUI() { +#ifdef USE_WIN32_BACKEND + RECT rect; + GetClientRect(m_hwnd, &rect); + int display_w = rect.right - rect.left; + int display_h = rect.bottom - rect.top; +#else + int display_w, display_h; + glfwGetFramebufferSize(m_window, &display_w, &display_h); +#endif + + ImGui::SetNextWindowPos(ImVec2(0, 0)); + ImGui::SetNextWindowSize(ImVec2(display_w, display_h)); + + ImGui::Begin("CLI程序管理工具", &m_app_state.show_main_window, + ImGuiWindowFlags_NoTitleBar | + ImGuiWindowFlags_NoResize | + ImGuiWindowFlags_NoMove | + ImGuiWindowFlags_NoCollapse | + ImGuiWindowFlags_MenuBar); + + RenderMenuBar(); + RenderMainContent(); + + ImGui::End(); +} + +void Manager::RenderMenuBar() { + if (ImGui::BeginMenuBar()) { + if (ImGui::BeginMenu("设置")) { + RenderSettingsMenu(); + ImGui::EndMenu(); + } + ImGui::EndMenuBar(); + } +} + +void Manager::RenderSettingsMenu() { + if (ImGui::MenuItem("开机自启动", nullptr, m_app_state.auto_start)) { + m_app_state.auto_start = !m_app_state.auto_start; + SetAutoStart(m_app_state.auto_start); + m_app_state.settings_dirty = true; + } + + ImGui::Separator(); + ImGui::Text("日志设置"); + if (ImGui::InputInt("最大日志行数", &m_app_state.max_log_lines, 100, 500)) { + m_app_state.max_log_lines = std::max(100, std::min(m_app_state.max_log_lines, 10000)); + m_app_state.cli_process.SetMaxLogLines(m_app_state.max_log_lines); + m_app_state.settings_dirty = true; + } + + ImGui::Separator(); + ImGui::Text("Web设置"); + if (ImGui::InputText("Web地址", m_app_state.web_url, IM_ARRAYSIZE(m_app_state.web_url))) { + m_tray->UpdateWebUrl(StringToWide(m_app_state.web_url)); + m_app_state.settings_dirty = true; + } + + RenderStopCommandSettings(); + RenderEnvironmentVariablesSettings(); + RenderOutputEncodingSettings(); // 新增:渲染编码设置 +} + + +void Manager::RenderStopCommandSettings() { + ImGui::Separator(); + ImGui::Text("停止命令设置"); + + if (ImGui::Checkbox("启用优雅停止命令", &m_app_state.use_stop_command)) { + m_app_state.settings_dirty = true; + m_app_state.ApplySettings(); + } + + if (m_app_state.use_stop_command) { + if (ImGui::InputText("停止命令", m_app_state.stop_command, IM_ARRAYSIZE(m_app_state.stop_command))) { + m_app_state.settings_dirty = true; + m_app_state.ApplySettings(); + } + + if (ImGui::InputInt("超时时间(毫秒)", &m_app_state.stop_timeout_ms, 1000, 5000)) { + m_app_state.stop_timeout_ms = std::max(1000, std::min(m_app_state.stop_timeout_ms, 60000)); + m_app_state.settings_dirty = true; + m_app_state.ApplySettings(); + } + + 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("说明:禁用时将直接强制终止程序。"); + } +} + +void Manager::RenderEnvironmentVariablesSettings() { + ImGui::Separator(); + ImGui::Text("环境变量设置"); + + if (ImGui::Checkbox("使用自定义环境变量", &m_app_state.use_custom_environment)) { + m_app_state.settings_dirty = true; + m_app_state.ApplySettings(); + } + + if (m_app_state.use_custom_environment) { + ImGui::Indent(); + + // 添加新环境变量 + ImGui::Text("添加环境变量:"); + ImGui::SetNextItemWidth(200.0f); + ImGui::InputText("变量名", env_key_input_, IM_ARRAYSIZE(env_key_input_)); + ImGui::SameLine(); + ImGui::SetNextItemWidth(300.0f); + ImGui::InputText("变量值", env_value_input_, IM_ARRAYSIZE(env_value_input_)); + ImGui::SameLine(); + + if (ImGui::Button("添加") && strlen(env_key_input_) > 0) { + m_app_state.environment_variables[env_key_input_] = env_value_input_; + m_app_state.cli_process.AddEnvironmentVariable(env_key_input_, env_value_input_); + memset(env_key_input_, 0, sizeof(env_key_input_)); + memset(env_value_input_, 0, sizeof(env_value_input_)); + m_app_state.settings_dirty = true; + } + + ImGui::Spacing(); + + // 显示当前环境变量列表 + if (!m_app_state.environment_variables.empty()) { + ImGui::Text("当前环境变量 (%d个):", static_cast(m_app_state.environment_variables.size())); + + if (ImGui::BeginChild("EnvVarsList", ImVec2(0, 150), true)) { + std::vector keysToRemove; + + for (const auto& pair : m_app_state.environment_variables) { + ImGui::PushID(pair.first.c_str()); + + // 显示环境变量 + ImGui::Text("%s = %s", pair.first.c_str(), pair.second.c_str()); + ImGui::SameLine(); + + // 删除按钮 + if (ImGui::SmallButton("删除")) { + keysToRemove.push_back(pair.first); + } + + ImGui::PopID(); + } + + // 删除标记的环境变量 + for (const auto& key : keysToRemove) { + m_app_state.environment_variables.erase(key); + m_app_state.cli_process.RemoveEnvironmentVariable(key); + m_app_state.settings_dirty = true; + } + } + ImGui::EndChild(); + + // 清空所有环境变量按钮 + if (ImGui::Button("清空所有环境变量")) { + m_app_state.environment_variables.clear(); + m_app_state.cli_process.ClearEnvironmentVariables(); + m_app_state.settings_dirty = true; + } + } else { + ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "暂无自定义环境变量"); + } + + ImGui::Spacing(); + ImGui::TextWrapped("说明:启用后,CLI程序将使用这些自定义环境变量。这些变量会与系统环境变量合并,同名变量会被覆盖。"); + + ImGui::Unindent(); + } else { + ImGui::BeginDisabled(true); + ImGui::InputText("变量名", env_key_input_, IM_ARRAYSIZE(env_key_input_)); + ImGui::SameLine(); + ImGui::InputText("变量值", env_value_input_, IM_ARRAYSIZE(env_value_input_)); + ImGui::SameLine(); + ImGui::Button("添加"); + ImGui::EndDisabled(); + ImGui::TextWrapped("说明:禁用时将使用系统默认环境变量启动程序。"); + } +} + +void Manager::RenderOutputEncodingSettings() { + ImGui::Separator(); + ImGui::Text("输出编码设置"); + + // 获取支持的编码列表 + auto supportedEncodings = CLIProcess::GetSupportedEncodings(); + + // 当前选择的编码索引 + int currentEncodingIndex = static_cast(m_app_state.output_encoding); + + // 创建编码名称数组用于Combo + std::vector encodingNames; + for (const auto& encoding : supportedEncodings) { + encodingNames.push_back(encoding.second.c_str()); + } + + if (ImGui::Combo("输出编码", ¤tEncodingIndex, encodingNames.data(), static_cast(encodingNames.size()))) { + if (currentEncodingIndex >= 0 && currentEncodingIndex < static_cast(supportedEncodings.size())) { + m_app_state.output_encoding = supportedEncodings[currentEncodingIndex].first; + m_app_state.cli_process.SetOutputEncoding(m_app_state.output_encoding); + m_app_state.settings_dirty = true; + } + } + + // 显示当前编码信息 + ImGui::SameLine(); + ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 1.0f), "当前: %s", + CLIProcess::GetEncodingName(m_app_state.output_encoding).c_str()); + + // 编码说明 + ImGui::Spacing(); + ImGui::TextWrapped("说明:"); + ImGui::BulletText("自动检测:程序会尝试自动识别输出编码并转换为UTF-8显示"); + ImGui::BulletText("UTF-8:适用于现代程序和国际化应用"); + 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; + + // 启动命令输入 + ImGui::SetNextItemWidth(inputWidth); + if (ImGui::InputText("启动命令", m_app_state.command_input, IM_ARRAYSIZE(m_app_state.command_input))) { + m_app_state.settings_dirty = true; + } + + // 控制按钮 + ImGui::BeginGroup(); + if (ImGui::Button("启动", ImVec2(buttonWidth, buttonHeight))) { + m_app_state.cli_process.Start(m_app_state.command_input); + } + ImGui::SameLine(); + if (ImGui::Button("停止", ImVec2(buttonWidth, buttonHeight))) { + m_app_state.cli_process.Stop(); + } + ImGui::SameLine(); + if (ImGui::Button("重启", ImVec2(buttonWidth, buttonHeight))) { + m_app_state.cli_process.Restart(m_app_state.command_input); + } + ImGui::SameLine(); + if (ImGui::Button("清理日志", ImVec2(100.0f * m_dpi_scale, buttonHeight))) { + m_app_state.cli_process.ClearLogs(); + } + ImGui::EndGroup(); + + ImGui::Text("状态: %s", m_app_state.cli_process.IsRunning() ? "运行中" : "已停止"); + + ImGui::Separator(); + ImGui::Text("发送命令到CLI程序"); + + // 命令发送 + ImGui::BeginGroup(); + ImGui::SetNextItemWidth(inputWidth); + bool sendCommandPressed = ImGui::InputText("##命令输入", m_app_state.send_command, IM_ARRAYSIZE(m_app_state.send_command), + ImGuiInputTextFlags_EnterReturnsTrue); + ImGui::SameLine(); + if (ImGui::Button("发送", ImVec2(buttonWidth, 0)) || sendCommandPressed) { + if (m_app_state.cli_process.IsRunning() && strlen(m_app_state.send_command) > 0) { + m_app_state.cli_process.SendCommand(m_app_state.send_command); + memset(m_app_state.send_command, 0, sizeof(m_app_state.send_command)); + } + } + ImGui::EndGroup(); + + ImGui::Separator(); + + // 日志控制 + ImGui::BeginGroup(); + ImGui::Text("程序日志"); + + float logControlButtonWidth = 100.0f * m_dpi_scale; + ImGui::SameLine(); + ImGui::SameLine(ImGui::GetContentRegionAvail().x - (350.0f * m_dpi_scale)); + if (ImGui::Button("复制日志", ImVec2(logControlButtonWidth, 0))) { + m_app_state.cli_process.CopyLogsToClipboard(); + } + + ImGui::SameLine(); + ImGui::Checkbox("自动滚动", &m_app_state.auto_scroll_logs); + + ImGui::SameLine(); + ImGui::Text("行数: %d/%d", + static_cast(m_app_state.cli_process.GetLogs().size()), + m_app_state.max_log_lines); + ImGui::EndGroup(); + + float logHeight = ImGui::GetContentRegionAvail().y - ImGui::GetStyle().ItemSpacing.y; + ImGui::BeginChild("Logs", ImVec2(0, logHeight), true, ImGuiWindowFlags_HorizontalScrollbar); + + const auto& logs = m_app_state.cli_process.GetLogs(); + for (const auto& log : logs) { + ImGui::TextUnformatted(log.c_str()); + } + + if (m_app_state.auto_scroll_logs && ImGui::GetScrollY() >= ImGui::GetScrollMaxY()) { + ImGui::SetScrollHereY(1.0f); + } + + ImGui::EndChild(); +} + +void Manager::OnTrayShowWindow() { + m_app_state.show_main_window = true; + ShowMainWindow(); +#ifdef USE_WIN32_BACKEND + SetForegroundWindow(m_hwnd); +#else + glfwRestoreWindow(m_window); + glfwFocusWindow(m_window); +#endif +} + +void Manager::OnTrayExit() { + m_should_exit = true; + PostQuitMessage(0); +} + +void Manager::ShowMainWindow() { +#ifdef USE_WIN32_BACKEND + ShowWindow(m_hwnd, SW_RESTORE); + SetForegroundWindow(m_hwnd); +#else + glfwShowWindow(m_window); + glfwRestoreWindow(m_window); + glfwFocusWindow(m_window); +#endif + m_app_state.show_main_window = true; +} + +void Manager::HideMainWindow() { +#ifdef USE_WIN32_BACKEND + ShowWindow(m_hwnd, SW_HIDE); +#else + glfwHideWindow(m_window); +#endif + m_app_state.show_main_window = false; +} + +#ifdef USE_WIN32_BACKEND +extern IMGUI_IMPL_API LRESULT ImGui_ImplWin32_WndProcHandler(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam); +LRESULT WINAPI Manager::WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) { + Manager* manager = nullptr; + if (msg == WM_NCCREATE) { + CREATESTRUCT* cs = reinterpret_cast(lParam); + manager = reinterpret_cast(cs->lpCreateParams); + SetWindowLongPtr(hWnd, GWLP_USERDATA, reinterpret_cast(manager)); + } else { + manager = reinterpret_cast(GetWindowLongPtr(hWnd, GWLP_USERDATA)); + } + + if (manager && ImGui_ImplWin32_WndProcHandler(hWnd, msg, wParam, lParam)) + return true; + + switch (msg) { + case WM_CLOSE: + // 主窗口关闭时隐藏到托盘 + if (manager) { + manager->HideMainWindow(); + } + return 0; + case WM_DESTROY: + PostQuitMessage(0); + return 0; + case WM_SIZE: + if (wParam == SIZE_MINIMIZED && manager) { + // 最小化时隐藏到托盘 + manager->HideMainWindow(); + } + break; + } + return DefWindowProc(hWnd, msg, wParam, lParam); +} +#endif + +bool Manager::InitializeTray() { + m_tray_hwnd = CreateHiddenWindow(); + if (!m_tray_hwnd) { + return false; + } + + HICON trayIcon = LoadIcon(NULL, IDI_APPLICATION); + m_tray = std::make_unique(m_tray_hwnd, trayIcon); + + // 设置回调函数 + m_tray->SetShowWindowCallback([this]() { + OnTrayShowWindow(); + }); + + m_tray->SetExitCallback([this]() { + OnTrayExit(); + }); + + m_tray->Show(); + + // 设置托盘窗口的用户数据,指向TrayIcon实例 + SetWindowLongPtr(m_tray_hwnd, GWLP_USERDATA, reinterpret_cast(m_tray.get())); + + return true; +} + +HWND Manager::CreateHiddenWindow() { + WNDCLASSEX wc = {0}; + wc.cbSize = sizeof(WNDCLASSEX); + wc.lpfnWndProc = TrayIcon::WindowProc; // 使用TrayIcon的窗口过程 + wc.hInstance = GetModuleHandle(NULL); + wc.lpszClassName = L"CLIManagerTrayWindow"; + + if (!RegisterClassEx(&wc)) { + return NULL; + } + + return CreateWindowEx( + 0, + wc.lpszClassName, + L"CLI Manager Tray Window", + 0, + 0, 0, 0, 0, + NULL, NULL, + wc.hInstance, + NULL + ); +} + +void Manager::HandleMessages() { +#ifdef USE_WIN32_BACKEND + MSG msg; + while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) { + if (msg.message == WM_QUIT) { + m_should_exit = true; + } + else if (msg.message == WM_CLOSE) { + // 主窗口关闭时隐藏到托盘,而不是退出 + if (msg.hwnd == m_hwnd) { + HideMainWindow(); + continue; + } else { + m_should_exit = true; + } + } + + TranslateMessage(&msg); + DispatchMessage(&msg); + } +#else + // GLFW后端的消息处理 + MSG msg; + while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) { + if (msg.message == WM_QUIT) { + m_should_exit = true; + } + + TranslateMessage(&msg); + DispatchMessage(&msg); + } + + glfwPollEvents(); + if (glfwWindowShouldClose(m_window)) { + HideMainWindow(); + glfwSetWindowShouldClose(m_window, GLFW_FALSE); + } +#endif +} + +bool Manager::ShouldExit() const { + return m_should_exit; +} + +#ifdef USE_WIN32_BACKEND +bool Manager::InitializeWin32() { + m_wc = {}; + m_wc.cbSize = sizeof(WNDCLASSEX); + m_wc.style = CS_HREDRAW | CS_VREDRAW; + m_wc.lpfnWndProc = WndProc; + m_wc.hInstance = GetModuleHandle(NULL); + m_wc.hCursor = LoadCursor(NULL, IDC_ARROW); + m_wc.lpszClassName = L"CLIManagerWin32Class"; + + if (!RegisterClassEx(&m_wc)) { + return false; + } + + m_hwnd = CreateWindowEx( + 0, + m_wc.lpszClassName, + L"CLI程序管理工具", + WS_OVERLAPPEDWINDOW, + CW_USEDEFAULT, + CW_USEDEFAULT, + 1280, + 800, + NULL, + NULL, + m_wc.hInstance, + this // 将 this 指针传递给窗口创建数据 + ); + + if (!m_hwnd) { + return false; + } + + ShowWindow(m_hwnd, SW_SHOWDEFAULT); + UpdateWindow(m_hwnd); + + return true; +} + +bool Manager::InitializeDirectX11() { + DXGI_SWAP_CHAIN_DESC sd = {}; + sd.BufferCount = 2; + sd.BufferDesc.Width = 0; // 自动适配窗口大小 + sd.BufferDesc.Height = 0; + sd.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM; + sd.BufferDesc.RefreshRate.Numerator = 60; + sd.BufferDesc.RefreshRate.Denominator = 1; + sd.Flags = DXGI_SWAP_CHAIN_FLAG_ALLOW_MODE_SWITCH; + sd.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT; + sd.OutputWindow = m_hwnd; + sd.SampleDesc.Count = 1; + sd.SampleDesc.Quality = 0; + sd.Windowed = TRUE; + sd.SwapEffect = DXGI_SWAP_EFFECT_DISCARD; + + UINT createDeviceFlags = 0; +#ifdef _DEBUG + createDeviceFlags |= D3D11_CREATE_DEVICE_DEBUG; +#endif + + D3D_FEATURE_LEVEL featureLevel; + const D3D_FEATURE_LEVEL featureLevelArray[2] = { + D3D_FEATURE_LEVEL_11_0, + D3D_FEATURE_LEVEL_10_0, + }; + + if (D3D11CreateDeviceAndSwapChain( + NULL, D3D_DRIVER_TYPE_HARDWARE, NULL, createDeviceFlags, + featureLevelArray, 2, D3D11_SDK_VERSION, &sd, &m_pSwapChain, + &m_pd3dDevice, &featureLevel, &m_pd3dDeviceContext) != S_OK) { + return false; + } + + ID3D11Texture2D* pBackBuffer; + if (m_pSwapChain->GetBuffer(0, IID_PPV_ARGS(&pBackBuffer)) != S_OK) { + return false; + } + + if (m_pd3dDevice->CreateRenderTargetView(pBackBuffer, NULL, &m_mainRenderTargetView) != S_OK) { + pBackBuffer->Release(); + return false; + } + + pBackBuffer->Release(); + + return true; +} + +void Manager::CleanupWin32() { + if (m_hwnd) { + DestroyWindow(m_hwnd); + m_hwnd = nullptr; + } + UnregisterClass(m_wc.lpszClassName, m_wc.hInstance); +} + +void Manager::CleanupDirectX11() { + if (m_mainRenderTargetView) { + m_mainRenderTargetView->Release(); + m_mainRenderTargetView = nullptr; + } + if (m_pSwapChain) { + m_pSwapChain->Release(); + m_pSwapChain = nullptr; + } + if (m_pd3dDeviceContext) { + m_pd3dDeviceContext->Release(); + m_pd3dDeviceContext = nullptr; + } + if (m_pd3dDevice) { + m_pd3dDevice->Release(); + m_pd3dDevice = nullptr; + } +} +#else +bool Manager::InitializeGLFW() { + glfwSetErrorCallback(GlfwErrorCallback); + if (!glfwInit()) + return false; + +#if defined(IMGUI_IMPL_OPENGL_ES2) + m_glsl_version = "#version 100"; + glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 2); + glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 0); + glfwWindowHint(GLFW_CLIENT_API, GLFW_OPENGL_ES_API); +#elif defined(IMGUI_IMPL_OPENGL_ES3) + m_glsl_version = "#version 300 es"; + glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); + glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 0); + glfwWindowHint(GLFW_CLIENT_API, GLFW_OPENGL_ES_API); +#elif defined(__APPLE__) + m_glsl_version = "#version 150"; + glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); + glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 2); + glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); + glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); +#else + m_glsl_version = "#version 130"; + glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); + glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 0); +#endif + + int screenWidth, screenHeight; + GLFWmonitor* primaryMonitor = glfwGetPrimaryMonitor(); + const GLFWvidmode* mode = glfwGetVideoMode(primaryMonitor); + screenWidth = mode->width; + screenHeight = mode->height; + + int windowWidth = static_cast(screenWidth * 0.8); + int windowHeight = static_cast(screenHeight * 0.8); + + m_window = glfwCreateWindow(windowWidth, windowHeight, "CLI程序管理工具", nullptr, nullptr); + if (!m_window) + return false; + + glfwSetWindowPos(m_window, + (screenWidth - windowWidth) / 2, + (screenHeight - windowHeight) / 2); + + glfwMakeContextCurrent(m_window); + glfwSwapInterval(1); + + return true; +} + +void Manager::CleanupGLFW() { + if (m_window) { + glfwDestroyWindow(m_window); + m_window = nullptr; + } + glfwTerminate(); +} + +void Manager::GlfwErrorCallback(int error, const char* description) { + fprintf(stderr, "GLFW Error %d: %s\n", error, description); +} +#endif + +void Manager::CleanupTray() { + m_tray.reset(); + if (m_tray_hwnd) { + DestroyWindow(m_tray_hwnd); + m_tray_hwnd = nullptr; + } + UnregisterClass(L"CLIManagerTrayWindow", GetModuleHandle(nullptr)); +} + +void Manager::Shutdown() { + if (!m_initialized) return; + + if (m_app_state.settings_dirty) { + m_app_state.SaveSettings(); + } + + CleanupTray(); + +#ifdef USE_WIN32_BACKEND + ImGui_ImplDX11_Shutdown(); + ImGui_ImplWin32_Shutdown(); + ImGui::DestroyContext(); + CleanupDirectX11(); + CleanupWin32(); +#else + ImGui_ImplOpenGL3_Shutdown(); + ImGui_ImplGlfw_Shutdown(); + ImGui::DestroyContext(); + CleanupGLFW(); +#endif + + m_initialized = false; +} \ No newline at end of file diff --git a/app/src/TrayIcon.cpp b/app/src/TrayIcon.cpp new file mode 100644 index 0000000..31134ce --- /dev/null +++ b/app/src/TrayIcon.cpp @@ -0,0 +1,134 @@ +#include "TrayIcon.h" +#include "Units.h" + +TrayIcon::TrayIcon(HWND hwnd, HICON icon) + : m_hwnd(hwnd), m_icon(icon), m_visible(false), m_menu(nullptr) { + + ZeroMemory(&m_nid, sizeof(m_nid)); + m_nid.cbSize = sizeof(m_nid); + m_nid.hWnd = m_hwnd; + m_nid.uID = 1; + m_nid.uFlags = NIF_ICON | NIF_TIP | NIF_MESSAGE; + m_nid.uCallbackMessage = WM_APP + 1; + m_nid.hIcon = m_icon; + wcscpy_s(m_nid.szTip, L"CLI程序管理工具"); + m_web_url = L"http://localhost:8080"; // 默认URL + + CreateMenu(); +} + +TrayIcon::~TrayIcon() { + Hide(); + DestroyMenu(); +} + +void TrayIcon::Show() { + if (!m_visible) { + Shell_NotifyIcon(NIM_ADD, &m_nid); + m_visible = true; + } +} + +void TrayIcon::Hide() { + if (m_visible) { + Shell_NotifyIcon(NIM_DELETE, &m_nid); + m_visible = false; + } +} + +void TrayIcon::UpdateWebUrl(const std::wstring& url) { + m_web_url = url; + // 重新创建菜单以更新Web URL显示 + DestroyMenu(); + CreateMenu(); +} + +void TrayIcon::SetShowWindowCallback(const ShowWindowCallback &callback) { + m_show_window_callback = callback; +} + +void TrayIcon::SetExitCallback(const ExitCallback &callback) { + m_exit_callback = callback; +} + +void TrayIcon::CreateMenu() { + if (m_menu) { + DestroyMenu(); + } + + m_menu = CreatePopupMenu(); + AppendMenu(m_menu, MF_STRING, 1001, L"显示主窗口"); + AppendMenu(m_menu, MF_SEPARATOR, 0, NULL); + + // 添加Web地址菜单项(如果有设置) + if (!m_web_url.empty() && m_web_url != L"") { + std::wstring webText = L"打开Web页面: " + m_web_url; + AppendMenu(m_menu, MF_STRING, 1002, webText.c_str()); + AppendMenu(m_menu, MF_SEPARATOR, 0, NULL); + } + + AppendMenu(m_menu, MF_STRING, 1003, L"退出"); +} + +void TrayIcon::DestroyMenu() { + if (m_menu) { + ::DestroyMenu(m_menu); + m_menu = nullptr; + } +} + +void TrayIcon::ShowContextMenu() const { + if (!m_menu) return; + + POINT pt; + GetCursorPos(&pt); + SetForegroundWindow(m_hwnd); + TrackPopupMenu(m_menu, TPM_RIGHTBUTTON, pt.x, pt.y, 0, m_hwnd, NULL); +} + +LRESULT CALLBACK TrayIcon::WindowProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) { + auto* tray = reinterpret_cast(GetWindowLongPtr(hwnd, GWLP_USERDATA)); + + switch (msg) { + case WM_APP + 1: // 托盘图标消息 + switch (LOWORD(lParam)) { + case WM_LBUTTONDBLCLK: + if (tray && tray->m_show_window_callback) { + tray->m_show_window_callback(); + } + break; + case WM_RBUTTONUP: + if (tray) { + tray->ShowContextMenu(); + } + break; + } + break; + + case WM_COMMAND: + if (tray) { + switch (LOWORD(wParam)) { + case 1001: // 显示主窗口 + if (tray->m_show_window_callback) { + tray->m_show_window_callback(); + } + break; + case 1002: // 打开Web页面 + if (!tray->m_web_url.empty()) { + ShellExecute(NULL, L"open", tray->m_web_url.c_str(), NULL, NULL, SW_SHOWNORMAL); + } + break; + case 1003: // 退出 + if (tray->m_exit_callback) { + tray->m_exit_callback(); + } + break; + } + } + break; + + default: + return DefWindowProc(hwnd, msg, wParam, lParam); + } + return 0; +} \ No newline at end of file diff --git a/app/src/Units.cpp b/app/src/Units.cpp new file mode 100644 index 0000000..c6c357f --- /dev/null +++ b/app/src/Units.cpp @@ -0,0 +1,55 @@ +#include "Units.h" +#include +#include +#include +#include +#include +std::wstring StringToWide(const std::string& str) { + if (str.empty()) return L""; + int size = MultiByteToWideChar(CP_UTF8, 0, str.c_str(), -1, nullptr, 0); + std::wstring wstr(size, 0); + MultiByteToWideChar(CP_UTF8, 0, str.c_str(), -1, &wstr[0], size); + return wstr; +} + +std::string WideToString(const std::wstring& wstr) { + if (wstr.empty()) return ""; + int size = WideCharToMultiByte(CP_UTF8, 0, wstr.c_str(), -1, nullptr, 0, nullptr, nullptr); + std::string str(size, 0); + WideCharToMultiByte(CP_UTF8, 0, wstr.c_str(), -1, &str[0], size, nullptr, nullptr); + return str; +} + +void SetAutoStart(bool enable) { + HKEY hKey; + LPCWSTR path = L"Software\\Microsoft\\Windows\\CurrentVersion\\Run"; + + if (RegOpenKeyEx(HKEY_CURRENT_USER, path, 0, KEY_WRITE, &hKey) != ERROR_SUCCESS) + return; + + WCHAR exePath[MAX_PATH]; + GetModuleFileName(NULL, exePath, MAX_PATH); + + if (enable) { + RegSetValueEx(hKey, L"CLIManager", 0, REG_SZ, + (BYTE*)exePath, (wcslen(exePath) + 1) * sizeof(WCHAR)); + } + else { + RegDeleteValue(hKey, L"CLIManager"); + } + RegCloseKey(hKey); +} + +bool IsAutoStartEnabled() { + HKEY hKey; + LPCWSTR path = L"Software\\Microsoft\\Windows\\CurrentVersion\\Run"; + + if (RegOpenKeyEx(HKEY_CURRENT_USER, path, 0, KEY_READ, &hKey) != ERROR_SUCCESS) + return false; + + DWORD type, size = 0; + bool exists = (RegQueryValueEx(hKey, L"CLIManager", NULL, &type, NULL, &size) == ERROR_SUCCESS); + + RegCloseKey(hKey); + return exists; +} \ No newline at end of file diff --git a/img/img1.png b/img/img1.png new file mode 100644 index 0000000..2354556 Binary files /dev/null and b/img/img1.png differ diff --git a/img/img2.png b/img/img2.png new file mode 100644 index 0000000..45b607e Binary files /dev/null and b/img/img2.png differ diff --git a/img/img3.png b/img/img3.png new file mode 100644 index 0000000..d380b50 Binary files /dev/null and b/img/img3.png differ diff --git a/main.cpp b/main.cpp new file mode 100644 index 0000000..a292e06 --- /dev/null +++ b/main.cpp @@ -0,0 +1,11 @@ +#include "Manager.h" + +int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE, LPSTR, int) { + Manager manager; + if (!manager.Initialize()) { + return 1; + } + manager.Run(); + manager.Shutdown(); + return 0; +} \ No newline at end of file diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..900abcf --- /dev/null +++ b/readme.md @@ -0,0 +1,11 @@ +# 通用Cli Manager工具 + +基于IMGUI实现的支持开机自启动、环境变量、自动编码识别转换、托盘持久化 + +## 程序效果 + +![设置界面](./img/img1.png) + +![主界面](./img/img2.png) + +![托盘显示](./img/img3.png) \ No newline at end of file