From 7c5b76b48226b76ae31fb3ee6160e5c1ceeeb290 Mon Sep 17 00:00:00 2001 From: evgeny Date: Thu, 15 Oct 2026 21:27:32 +0200 Subject: [PATCH] Add minimized Windows autostart and preserve terminal output --- tools/chatgui/CMakeLists.txt | 2 +- tools/chatgui/installer/README.txt | 16 +++++++++++++ tools/chatgui/installer/vibechat.iss | 15 ++++++++++++ tools/chatgui/src/main.cpp | 34 ++++++++++++++++++++++++++-- tools/chatgui/src/mainwindow.cpp | 6 ++++- 5 files changed, 69 insertions(+), 4 deletions(-) diff --git a/tools/chatgui/CMakeLists.txt b/tools/chatgui/CMakeLists.txt index 2247759e..5db4c428 100644 --- a/tools/chatgui/CMakeLists.txt +++ b/tools/chatgui/CMakeLists.txt @@ -216,7 +216,7 @@ set_source_files_properties(${CMAKE_SOURCE_DIR}/../../src/radio/radio_audio.c PR set_target_properties(utun_voice PROPERTIES POSITION_INDEPENDENT_CODE ON) -add_executable(vibechat +add_executable(vibechat WIN32 src/main.cpp src/mainwindow.cpp src/channellist.cpp diff --git a/tools/chatgui/installer/README.txt b/tools/chatgui/installer/README.txt index 10912acc..bf585c61 100644 --- a/tools/chatgui/installer/README.txt +++ b/tools/chatgui/installer/README.txt @@ -51,6 +51,18 @@ BAT-скрипты автоматически включают --installer. От запускается от исходного пользователя. Входящие подключения разрешаются правилом "VibeChat (installer)" только для установленного EXE. +На странице дополнительных задач включена по умолчанию опция автозапуска +при входе в Windows для всех пользователей. Ярлык автозагрузки передаёт +--minimized: главное окно запускается свёрнутым, обычные ярлыки открывают его +как раньше. Выбор сохраняется при обновлении; снятие галочки удаляет прежний +ярлык автозагрузки. При удалении приложения этот ярлык также удаляется. + +EXE собирается как GUI-приложение: запуск через Проводник, установщик или +автозагрузку не создаёт терминал. Запуск из терминала подключает родительскую +консоль и сохраняет перенаправление stdout/stderr, в том числе в файл или pipe. +Лог приложения показывает режим окна и наличие консоли, лог установки — +выбранный режим автозапуска и путь ярлыка. + При обновлении установщик предлагает закрыть приложение, использующее EXE/DLL. Обновление и удаление сохраняют конфиг, ключи, историю и медиа пользователя. Деинсталлятор удаляет своё правило firewall и установленные файлы программы. @@ -68,3 +80,7 @@ BAT-скрипты автоматически включают --installer. От На чистой Windows без MSYS2/Qt: установка, запуск обычным пользователем, сообщения/звуки/медиа, обновление с сохранением идентичности, удаление, повторная установка. Проверить пути с пробелами и имя пользователя с кириллицей. +Проверить автозапуск после входа в Windows, свёрнутость при ранее развёрнутом +окне, восстановление из трея, сохранение выбора и отключение при обновлении. +При запуске из Проводника не должно быть консоли; из терминала проверить +вывод, перенаправление и Ctrl+C. Дочерний процесс камеры также не создаёт консоль. diff --git a/tools/chatgui/installer/vibechat.iss b/tools/chatgui/installer/vibechat.iss index f3ebc311..557b7507 100644 --- a/tools/chatgui/installer/vibechat.iss +++ b/tools/chatgui/installer/vibechat.iss @@ -14,6 +14,7 @@ AppPublisher=uTun DefaultDirName={autopf}\VibeChat DisableDirPage=no DisableProgramGroupPage=yes +UsePreviousTasks=yes ArchitecturesAllowed=x64compatible ArchitecturesInstallIn64BitMode=x64compatible MinVersion=10.0 @@ -36,6 +37,11 @@ Name: "russian"; MessagesFile: "compiler:Languages\Russian.isl" [Tasks] Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked +Name: "autostart"; Description: "{cm:AutoStart}"; GroupDescription: "{cm:AutoStartGroup}" + +[InstallDelete] +; Снятая при обновлении галочка удаляет только наш прежний ярлык автозагрузки. +Type: files; Name: "{autostartup}\VibeChat.lnk"; Tasks: not autostart [Files] Source: "{#PackageDir}\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs @@ -43,11 +49,16 @@ Source: "{#PackageDir}\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs [Icons] Name: "{autoprograms}\VibeChat"; Filename: "{app}\vibechat.exe"; WorkingDir: "{app}" Name: "{autodesktop}\VibeChat"; Filename: "{app}\vibechat.exe"; WorkingDir: "{app}"; Tasks: desktopicon +Name: "{autostartup}\VibeChat"; Filename: "{app}\vibechat.exe"; Parameters: "--minimized"; WorkingDir: "{app}"; Tasks: autostart [Run] Filename: "{app}\vibechat.exe"; Description: "{cm:LaunchProgram,VibeChat}"; WorkingDir: "{app}"; Flags: nowait postinstall skipifsilent runasoriginaluser [CustomMessages] +english.AutoStartGroup=Startup: +russian.AutoStartGroup=Автозагрузка: +english.AutoStart=Start VibeChat minimized when signing in to Windows (for all users) +russian.AutoStart=Запускать VibeChat свёрнутым при входе в Windows (для всех пользователей) english.FirewallFailed=Could not configure the VibeChat firewall rule. See the installation log for details. russian.FirewallFailed=Не удалось настроить правило брандмауэра VibeChat. Подробности в журнале установки. @@ -75,6 +86,10 @@ var begin if CurStep <> ssPostInstall then Exit; Log('VibeChat: installed to ' + ExpandConstant('{app}') + '; user config, keys and history are preserved'); + if WizardIsTaskSelected('autostart') then + Log('Startup: enabled with --minimized; shortcut=' + ExpandConstant('{autostartup}\VibeChat.lnk')) + else + Log('Startup: disabled; shortcut=' + ExpandConstant('{autostartup}\VibeChat.lnk')); RemoveFirewallRule; Parameters := 'advfirewall firewall add rule name="VibeChat (installer)" dir=in action=allow program=' + AddQuotes(ExpandConstant('{app}\vibechat.exe')) + ' enable=yes profile=any'; diff --git a/tools/chatgui/src/main.cpp b/tools/chatgui/src/main.cpp index 204377de..905e7e6c 100644 --- a/tools/chatgui/src/main.cpp +++ b/tools/chatgui/src/main.cpp @@ -15,10 +15,12 @@ #include "call_camera_capture.h" #include #include +#include #ifdef _WIN32 #include #include #include +#include #endif #include "mainwindow.h" #include "../db/db_manager.h" @@ -30,6 +32,28 @@ static UtunNode* g_ctrl_node = nullptr; static MainWindow* g_mainWindow = nullptr; +// GUI EXE не создаёт консоль; подключаем только родительскую, сохраняя перенаправленные потоки. +static bool attach_parent_console(void) { + if (!AttachConsole(ATTACH_PARENT_PROCESS)) { + const DWORD error = GetLastError(); + if (error != ERROR_ACCESS_DENIED) { + if (error != ERROR_INVALID_HANDLE && error != ERROR_INVALID_PARAMETER) + DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "Windows parent console attach failed: error=%lu", (unsigned long)error); + return false; + } + } + FILE* streams[] = {stdin, stdout, stderr}; + const char* devices[] = {"CONIN$", "CONOUT$", "CONOUT$"}; + for (int i = 0; i < 3; ++i) { + const int fd = _fileno(streams[i]); + const HANDLE handle = fd < 0 ? INVALID_HANDLE_VALUE : (HANDLE)_get_osfhandle(fd); + if (handle && handle != INVALID_HANDLE_VALUE && GetFileType(handle) != FILE_TYPE_UNKNOWN) continue; + if (!freopen(devices[i], i == 0 ? "r" : "w", streams[i])) + DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "Windows console stream reopen failed: stream=%d errno=%d", i, errno); + } + return true; +} + static DWORD count_threads(void) { DWORD pid = GetCurrentProcessId(), count = 0; HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0); @@ -75,6 +99,10 @@ static struct ShutdownSentinel { int main(int argc, char *argv[]) { if (argc >= 2 && std::strcmp(argv[1], "--call-camera-worker") == 0) return call_camera_worker_main(argc, argv); + debug_config_init(); +#ifdef _WIN32 + const bool consoleAttached = attach_parent_console(); +#endif #ifdef Q_OS_LINUX /* На Wayland композитор владеет позицией окна (move() игнорируется) — * для полноценного ручного move/resize с пропорцией запускаемся через XWayland. */ @@ -87,7 +115,7 @@ int main(int argc, char *argv[]) { app.setDesktopFileName("org.utun.vibechat"); app.setWindowIcon(QIcon(":/icons/app_icon.png")); app.setQuitOnLastWindowClosed(false); - debug_config_init(); + const bool minimized = app.arguments().contains(QStringLiteral("--minimized")); QString cfgDir = QCoreApplication::applicationDirPath(); #ifdef _WIN32 @@ -218,6 +246,7 @@ int main(int argc, char *argv[]) { debug_apply_global_level(lvl.toUtf8().constData()); DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "vibechat: version=%s", qPrintable(app.applicationVersion())); #ifdef _WIN32 + DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "Windows console: attached=%d", consoleAttached); DEBUG_INFO(DEBUG_CATEGORY_CONFIG, "Windows storage: reason=%s config=%s db=%s log=%s lock=%s", qPrintable(storageReason), qPrintable(cfgPath), qPrintable(dbPath), qPrintable(debugFile), qPrintable(cfgDir + "/vibechat.lock")); @@ -228,7 +257,8 @@ int main(int argc, char *argv[]) { DbManager db; MainWindow w(nullptr, &db, cfgPath, dbPath, debugFile, cfg.debugLevel(), cfg.debugCategories()); - w.show(); + if (minimized) w.showMinimized(); else w.show(); + DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "GUI startup: minimized_requested=%d minimized=%d", minimized, w.isMinimized()); #ifdef _WIN32 g_ctrl_node = w.utunNode(); diff --git a/tools/chatgui/src/mainwindow.cpp b/tools/chatgui/src/mainwindow.cpp index b4e9ff9f..70d608c7 100644 --- a/tools/chatgui/src/mainwindow.cpp +++ b/tools/chatgui/src/mainwindow.cpp @@ -301,8 +301,10 @@ void MainWindow::restoreWindowGeometry() { if (!visible) target.moveTo(0, 0); setGeometry(target); + // Поздний ответ БД сохраняет свёрнутость при --minimized и не показывает скрытое в tray окно. if (m_db->getUiStateInt("window_maximized", 0)) - showMaximized(); + setWindowState(windowState() | Qt::WindowMaximized); + GUI_DEBUG("window geometry restored: maximized=%d minimized=%d visible=%d", isMaximized(), isMinimized(), isVisible()); if (m_splitter) { const QStringList sp = m_db->getUiState("window_splitter").split(','); @@ -933,6 +935,7 @@ void MainWindow::setupTray() { QAction *showAction = m_trayMenu->addAction(QCoreApplication::translate("ChatGui", "Show")); connect(showAction, &QAction::triggered, this, [this]() { + if (isMinimized()) setWindowState(windowState() & ~Qt::WindowMinimized); show(); raise(); activateWindow(); @@ -999,6 +1002,7 @@ void MainWindow::onShutdownCheck() { void MainWindow::onTrayActivated(QSystemTrayIcon::ActivationReason reason) { if (reason == QSystemTrayIcon::Trigger || reason == QSystemTrayIcon::DoubleClick) { + if (isMinimized()) setWindowState(windowState() & ~Qt::WindowMinimized); show(); raise(); activateWindow();