机械师 2025-09-08 13:59:24 +08:00
commit 2840c9a378
16 changed files with 2224 additions and 0 deletions

91
CMakeLists.txt Normal file
View File

@ -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"
#)
#

51
app/inc/AppState.h Normal file
View File

@ -0,0 +1,51 @@
#ifndef APP_STATE_H
#define APP_STATE_H
#include "CLIProcess.h"
#include <string>
#include <map>
#include <imgui.h>
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<std::string, std::string> 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

92
app/inc/CLIProcess.h Normal file
View File

@ -0,0 +1,92 @@
#ifndef CLIPROCESS_H
#define CLIPROCESS_H
#include <string>
#include <vector>
#include <mutex>
#include <thread>
#include <map>
#include <windows.h>
// 新增:输出编码枚举
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<std::string, std::string>& 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<std::string>& GetLogs() const;
bool SendCommand(const std::string& command);
void CopyLogsToClipboard() const;
bool IsRunning() const;
// 环境变量管理接口
const std::map<std::string, std::string>& 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<std::pair<OutputEncoding, std::string>> 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<std::string> 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<std::string, std::string> environment_variables_;
// 新增:编码相关
mutable std::mutex encoding_mutex_;
OutputEncoding output_encoding_;
};
#endif // CLIPROCESS_H

97
app/inc/Manager.h Normal file
View File

@ -0,0 +1,97 @@
#pragma once
#include "imgui.h"
#include "AppState.h"
#include "TrayIcon.h"
#ifdef USE_WIN32_BACKEND
#include <d3d11.h>
#include <windows.h>
#include "imgui_impl_win32.h"
#include "imgui_impl_dx11.h"
#else
#define GLFW_EXPOSE_NATIVE_WIN32
#include <GLFW/glfw3.h>
#include <GLFW/glfw3native.h>
#include "imgui_impl_glfw.h"
#include "imgui_impl_opengl3.h"
#include <windows.h>
#endif
#include <memory>
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<TrayIcon> 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;
};

42
app/inc/TrayIcon.h Normal file
View File

@ -0,0 +1,42 @@
#pragma once
#include <windows.h>
#include <shellapi.h>
#include <string>
#include <functional>
class TrayIcon {
public:
// 回调函数类型定义
using ShowWindowCallback = std::function<void()>;
using ExitCallback = std::function<void()>;
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;
};

11
app/inc/Units.h Normal file
View File

@ -0,0 +1,11 @@
#ifndef UNITS_H
#define UNITS_H
#include <string>
std::wstring StringToWide(const std::string& str);
std::string WideToString(const std::wstring& wstr);
void SetAutoStart(bool enable);
bool IsAutoStartEnabled();
#endif //UNITS_H

188
app/src/AppState.cpp Normal file
View File

@ -0,0 +1,188 @@
#include "AppState.h"
#include <fstream>
#include <algorithm>
#include <sstream>
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<int>(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<int>(OutputEncoding::AUTO_DETECT)) {
output_encoding = static_cast<OutputEncoding>(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);
}

603
app/src/CLIProcess.cpp Normal file
View File

@ -0,0 +1,603 @@
#include "CLIProcess.h"
#include <algorithm>
#include <cstdio>
#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<std::mutex> lock(encoding_mutex_);
output_encoding_ = encoding;
AddLog("输出编码已设置为: " + GetEncodingName(encoding));
}
// 新增:获取输出编码
OutputEncoding CLIProcess::GetOutputEncoding() const {
std::lock_guard<std::mutex> 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<std::pair<OutputEncoding, std::string>> 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<const unsigned char*>(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<wchar_t> 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<char> 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<OutputEncoding> 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<std::mutex> 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<std::mutex> 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<std::string, std::string>& env_vars) {
std::lock_guard<std::mutex> 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<std::string, std::string>& CLIProcess::GetEnvironmentVariables() const {
std::lock_guard<std::mutex> lock(env_mutex_);
return environment_variables_;
}
void CLIProcess::AddEnvironmentVariable(const std::string& key, const std::string& value) {
std::lock_guard<std::mutex> 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<std::mutex> 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<std::mutex> 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<wchar_t> cmdBuffer(wcmd.begin(), wcmd.end());
cmdBuffer.push_back(L'\0');
// 使用Windows API设置环境变量
std::vector<std::pair<std::string, std::string>> 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<char> 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<std::mutex> 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<std::mutex> lock(logs_mutex_);
logs_.clear();
}
void CLIProcess::AddLog(const std::string& log) {
std::lock_guard<std::mutex> 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<std::string>& CLIProcess::GetLogs() const {
std::lock_guard<std::mutex> 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<DWORD>(fullCommand.length()), &bytesWritten, nullptr)) {
AddLog("> " + command);
return true;
}
return false;
}
void CLIProcess::CopyLogsToClipboard() const {
std::lock_guard<std::mutex> 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<wchar_t*>(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<std::mutex> 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);
}
}
}

838
app/src/Manager.cpp Normal file
View File

@ -0,0 +1,838 @@
#include "Manager.h"
#include <cstdio>
#include <algorithm>
#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<int>(m_app_state.environment_variables.size()));
if (ImGui::BeginChild("EnvVarsList", ImVec2(0, 150), true)) {
std::vector<std::string> 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<int>(m_app_state.output_encoding);
// 创建编码名称数组用于Combo
std::vector<const char*> encodingNames;
for (const auto& encoding : supportedEncodings) {
encodingNames.push_back(encoding.second.c_str());
}
if (ImGui::Combo("输出编码", &currentEncodingIndex, encodingNames.data(), static_cast<int>(encodingNames.size()))) {
if (currentEncodingIndex >= 0 && currentEncodingIndex < static_cast<int>(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<int>(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<CREATESTRUCT*>(lParam);
manager = reinterpret_cast<Manager*>(cs->lpCreateParams);
SetWindowLongPtr(hWnd, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(manager));
} else {
manager = reinterpret_cast<Manager*>(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<TrayIcon>(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<LONG_PTR>(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<int>(screenWidth * 0.8);
int windowHeight = static_cast<int>(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;
}

134
app/src/TrayIcon.cpp Normal file
View File

@ -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<TrayIcon*>(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;
}

55
app/src/Units.cpp Normal file
View File

@ -0,0 +1,55 @@
#include "Units.h"
#include <string>
#include <vector>
#include <mutex>
#include <thread>
#include <windows.h>
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;
}

BIN
img/img1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 KiB

BIN
img/img2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

BIN
img/img3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

11
main.cpp Normal file
View File

@ -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;
}

11
readme.md Normal file
View File

@ -0,0 +1,11 @@
# 通用Cli Manager工具
基于IMGUI实现的支持开机自启动、环境变量、自动编码识别转换、托盘持久化
## 程序效果
![设置界面](./img/img1.png)
![主界面](./img/img2.png)
![托盘显示](./img/img3.png)