Browse Source

Add minimized Windows autostart and preserve terminal output

stream-multiview
evgeny 6 days ago
parent
commit
7c5b76b482
  1. 2
      tools/chatgui/CMakeLists.txt
  2. 16
      tools/chatgui/installer/README.txt
  3. 15
      tools/chatgui/installer/vibechat.iss
  4. 34
      tools/chatgui/src/main.cpp
  5. 6
      tools/chatgui/src/mainwindow.cpp

2
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

16
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. Дочерний процесс камеры также не создаёт консоль.

15
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';

34
tools/chatgui/src/main.cpp

@ -15,10 +15,12 @@
#include "call_camera_capture.h"
#include <cstdio>
#include <cstring>
#include <cerrno>
#ifdef _WIN32
#include <QStandardPaths>
#include <windows.h>
#include <tlhelp32.h>
#include <io.h>
#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();

6
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();

Loading…
Cancel
Save