Release v0.6.46: bundle StayHere activity utility
This commit is contained in:
@@ -0,0 +1,238 @@
|
||||
#include <windows.h>
|
||||
#include <shellapi.h>
|
||||
#include <wtsapi32.h>
|
||||
#include <cwchar>
|
||||
#include <initializer_list>
|
||||
#include <string>
|
||||
|
||||
namespace {
|
||||
#ifdef STAYHERE_TEST_INSTANCE
|
||||
constexpr wchar_t ClassName[] = L"StayHere.Test.Tray.1";
|
||||
constexpr wchar_t MutexName[] = L"Local\\StayHere.Test.Tray.1";
|
||||
#else
|
||||
constexpr wchar_t ClassName[] = L"StayHere.Tray.1";
|
||||
constexpr wchar_t MutexName[] = L"Local\\StayHere.Tray.1";
|
||||
#endif
|
||||
constexpr UINT TrayMessage = WM_APP + 1;
|
||||
constexpr UINT ControlMessage = WM_APP + 2;
|
||||
constexpr UINT Toggle = 1, Quit = 2, Pause = 3, Resume = 4, Status = 5;
|
||||
constexpr DWORD IdleThreshold = 180000;
|
||||
bool enabled = true;
|
||||
bool sessionLocked = false;
|
||||
bool powerOk = true;
|
||||
bool inputOk = true;
|
||||
UINT taskbarCreated;
|
||||
HICON activeIcon, pausedIcon;
|
||||
NOTIFYICONDATAW tray{};
|
||||
|
||||
constexpr bool shouldSignal(bool active, bool unlocked, DWORD now, DWORD lastInput) {
|
||||
return active && unlocked && DWORD(now - lastInput) >= IdleThreshold;
|
||||
}
|
||||
static_assert(!shouldSignal(true, true, 179999, 0));
|
||||
static_assert(shouldSignal(true, true, 180000, 0));
|
||||
static_assert(!shouldSignal(false, true, 180000, 0));
|
||||
static_assert(!shouldSignal(true, false, 180000, 0));
|
||||
static_assert(shouldSignal(true, true, 179000, 0xfffffc18)); // tick counter wrap
|
||||
|
||||
bool interactiveDesktop() {
|
||||
if (sessionLocked) return false;
|
||||
HDESK desktop = OpenInputDesktop(0, FALSE, DESKTOP_READOBJECTS);
|
||||
if (!desktop) return false;
|
||||
wchar_t name[128]{};
|
||||
DWORD needed = 0;
|
||||
bool ok = GetUserObjectInformationW(desktop, UOI_NAME, name, sizeof(name), &needed)
|
||||
&& _wcsicmp(name, L"Default") == 0;
|
||||
CloseDesktop(desktop);
|
||||
if (!ok) return false;
|
||||
DWORD session;
|
||||
LPWSTR buffer = nullptr;
|
||||
DWORD bytes = 0;
|
||||
if (!ProcessIdToSessionId(GetCurrentProcessId(), &session)) return false;
|
||||
if (!WTSQuerySessionInformationW(WTS_CURRENT_SERVER_HANDLE, session, WTSConnectState, &buffer, &bytes)) return false;
|
||||
ok = bytes >= sizeof(WTS_CONNECTSTATE_CLASS)
|
||||
&& *reinterpret_cast<WTS_CONNECTSTATE_CLASS*>(buffer) == WTSActive;
|
||||
WTSFreeMemory(buffer);
|
||||
return ok;
|
||||
}
|
||||
|
||||
HICON makeIcon(COLORREF color) {
|
||||
HDC screen = GetDC(nullptr);
|
||||
HDC dc = CreateCompatibleDC(screen);
|
||||
HBITMAP bitmap = CreateCompatibleBitmap(screen, 32, 32);
|
||||
HGDIOBJ previous = SelectObject(dc, bitmap);
|
||||
RECT rect{0, 0, 32, 32};
|
||||
FillRect(dc, &rect, static_cast<HBRUSH>(GetStockObject(BLACK_BRUSH)));
|
||||
HBRUSH brush = CreateSolidBrush(color);
|
||||
HGDIOBJ oldBrush = SelectObject(dc, brush);
|
||||
HGDIOBJ oldPen = SelectObject(dc, GetStockObject(NULL_PEN));
|
||||
Ellipse(dc, 3, 3, 29, 29);
|
||||
SelectObject(dc, oldBrush);
|
||||
SelectObject(dc, oldPen);
|
||||
DeleteObject(brush);
|
||||
SelectObject(dc, previous);
|
||||
HBITMAP mask = CreateBitmap(32, 32, 1, 1, nullptr);
|
||||
HGDIOBJ maskPrevious = SelectObject(dc, mask);
|
||||
PatBlt(dc, 0, 0, 32, 32, WHITENESS);
|
||||
oldBrush = SelectObject(dc, GetStockObject(BLACK_BRUSH));
|
||||
oldPen = SelectObject(dc, GetStockObject(NULL_PEN));
|
||||
Ellipse(dc, 3, 3, 29, 29);
|
||||
SelectObject(dc, oldPen);
|
||||
SelectObject(dc, oldBrush);
|
||||
SelectObject(dc, maskPrevious);
|
||||
ICONINFO info{TRUE, 0, 0, mask, bitmap};
|
||||
HICON icon = CreateIconIndirect(&info);
|
||||
DeleteObject(bitmap);
|
||||
DeleteObject(mask);
|
||||
DeleteDC(dc);
|
||||
ReleaseDC(nullptr, screen);
|
||||
return icon;
|
||||
}
|
||||
|
||||
void updateTray() {
|
||||
tray.hIcon = enabled ? activeIcon : pausedIcon;
|
||||
const wchar_t* tip = !enabled ? L"StayHere — призупинено"
|
||||
: !powerOk ? L"StayHere — помилка утримання від сну"
|
||||
: !inputOk ? L"StayHere — не вдалося надіслати F15"
|
||||
: L"StayHere — активно (F15 після 3 хв простою)";
|
||||
lstrcpynW(tray.szTip, tip, ARRAYSIZE(tray.szTip));
|
||||
Shell_NotifyIconW(NIM_MODIFY, &tray);
|
||||
}
|
||||
|
||||
void setEnabled(bool value) {
|
||||
enabled = value;
|
||||
inputOk = true;
|
||||
powerOk = SetThreadExecutionState(ES_CONTINUOUS |
|
||||
(enabled && interactiveDesktop() ? ES_SYSTEM_REQUIRED | ES_DISPLAY_REQUIRED : 0)) != 0;
|
||||
updateTray();
|
||||
}
|
||||
|
||||
void tick() {
|
||||
const bool unlocked = interactiveDesktop();
|
||||
powerOk = SetThreadExecutionState(ES_CONTINUOUS |
|
||||
(enabled && unlocked ? ES_SYSTEM_REQUIRED | ES_DISPLAY_REQUIRED : 0)) != 0;
|
||||
LASTINPUTINFO idle{sizeof(idle), 0};
|
||||
if (GetLastInputInfo(&idle) && shouldSignal(enabled, unlocked, GetTickCount(), idle.dwTime)) {
|
||||
// Avoid inserting a key into a chord being held while the user is away.
|
||||
for (int key : {VK_SHIFT, VK_CONTROL, VK_MENU, VK_LWIN, VK_RWIN,
|
||||
VK_LBUTTON, VK_RBUTTON, VK_MBUTTON}) {
|
||||
if (GetAsyncKeyState(key) & 0x8000) return;
|
||||
}
|
||||
INPUT input[2]{};
|
||||
input[0].type = input[1].type = INPUT_KEYBOARD;
|
||||
input[0].ki.wVk = input[1].ki.wVk = VK_F15;
|
||||
input[1].ki.dwFlags = KEYEVENTF_KEYUP;
|
||||
inputOk = SendInput(2, input, sizeof(INPUT)) == 2;
|
||||
if (!inputOk) { // If only key-down succeeded, make a best effort to release it.
|
||||
SendInput(1, &input[1], sizeof(INPUT));
|
||||
}
|
||||
}
|
||||
updateTray();
|
||||
}
|
||||
|
||||
void showMenu(HWND window) {
|
||||
HMENU menu = CreatePopupMenu();
|
||||
AppendMenuW(menu, MF_STRING | (enabled ? MF_CHECKED : 0), Toggle, L"Підтримувати активність");
|
||||
AppendMenuW(menu, MF_SEPARATOR, 0, nullptr);
|
||||
AppendMenuW(menu, MF_STRING, Quit, L"Вийти");
|
||||
POINT pos;
|
||||
GetCursorPos(&pos);
|
||||
SetForegroundWindow(window);
|
||||
UINT command = TrackPopupMenu(menu, TPM_RETURNCMD | TPM_RIGHTBUTTON, pos.x, pos.y, 0, window, nullptr);
|
||||
DestroyMenu(menu);
|
||||
if (command) SendMessageW(window, ControlMessage, command, 0);
|
||||
PostMessageW(window, WM_NULL, 0, 0);
|
||||
}
|
||||
|
||||
LRESULT CALLBACK windowProc(HWND window, UINT message, WPARAM wp, LPARAM lp) {
|
||||
if (message == taskbarCreated) {
|
||||
Shell_NotifyIconW(NIM_ADD, &tray);
|
||||
return 0;
|
||||
}
|
||||
switch (message) {
|
||||
case TrayMessage:
|
||||
if (lp == WM_RBUTTONUP || lp == WM_CONTEXTMENU) showMenu(window);
|
||||
else if (lp == WM_LBUTTONUP) setEnabled(!enabled);
|
||||
return 0;
|
||||
case ControlMessage:
|
||||
if (wp == Status) return enabled ? 1 : 2;
|
||||
if (wp == Toggle) setEnabled(!enabled);
|
||||
else if (wp == Pause) setEnabled(false);
|
||||
else if (wp == Resume) setEnabled(true);
|
||||
else if (wp == Quit) DestroyWindow(window);
|
||||
return 0;
|
||||
case WM_TIMER: tick(); return 0;
|
||||
case WM_WTSSESSION_CHANGE:
|
||||
if (wp == WTS_SESSION_LOCK || wp == WTS_REMOTE_DISCONNECT || wp == WTS_CONSOLE_DISCONNECT)
|
||||
sessionLocked = true;
|
||||
else if (wp == WTS_SESSION_UNLOCK || wp == WTS_REMOTE_CONNECT || wp == WTS_CONSOLE_CONNECT)
|
||||
sessionLocked = false;
|
||||
setEnabled(enabled);
|
||||
return 0;
|
||||
case WM_DESTROY:
|
||||
KillTimer(window, 1);
|
||||
WTSUnRegisterSessionNotification(window);
|
||||
SetThreadExecutionState(ES_CONTINUOUS);
|
||||
Shell_NotifyIconW(NIM_DELETE, &tray);
|
||||
PostQuitMessage(0);
|
||||
return 0;
|
||||
}
|
||||
return DefWindowProcW(window, message, wp, lp);
|
||||
}
|
||||
}
|
||||
|
||||
int WINAPI wWinMain(HINSTANCE instance, HINSTANCE, PWSTR, int) {
|
||||
int argc{};
|
||||
auto argv=CommandLineToArgvW(GetCommandLineW(),&argc);
|
||||
if(!argv)return 10;
|
||||
const std::wstring argument=argc==2?argv[1]:L"";
|
||||
LocalFree(argv);
|
||||
if(argc>2)return 10;
|
||||
const auto commandLine=argument.c_str();
|
||||
UINT command = 0;
|
||||
if (wcscmp(commandLine, L"--pause") == 0) command = Pause;
|
||||
else if (wcscmp(commandLine, L"--resume") == 0) command = Resume;
|
||||
else if (wcscmp(commandLine, L"--exit") == 0) command = Quit;
|
||||
else if (wcscmp(commandLine, L"--status") == 0) command = Status;
|
||||
else if (*commandLine) return 10;
|
||||
HWND existing = FindWindowW(ClassName, nullptr);
|
||||
if (command) {
|
||||
if (!existing) return 3;
|
||||
DWORD_PTR result = 0;
|
||||
if (!SendMessageTimeoutW(existing, ControlMessage, command, 0, SMTO_ABORTIFHUNG, 2000, &result)) return 4;
|
||||
return command == Status ? static_cast<int>(result) : 0;
|
||||
}
|
||||
HANDLE mutex = CreateMutexW(nullptr, FALSE, MutexName);
|
||||
if (!mutex) return 5;
|
||||
if (GetLastError() == ERROR_ALREADY_EXISTS) { CloseHandle(mutex); return 0; }
|
||||
taskbarCreated = RegisterWindowMessageW(L"TaskbarCreated");
|
||||
activeIcon = makeIcon(RGB(40, 185, 100));
|
||||
pausedIcon = makeIcon(RGB(135, 140, 150));
|
||||
WNDCLASSW wc{};
|
||||
wc.lpfnWndProc = windowProc;
|
||||
wc.hInstance = instance;
|
||||
wc.lpszClassName = ClassName;
|
||||
if (!RegisterClassW(&wc)) return 6;
|
||||
HWND window = CreateWindowExW(0, ClassName, L"StayHere", WS_OVERLAPPED,
|
||||
0, 0, 0, 0, nullptr, nullptr, instance, nullptr);
|
||||
if (!window) return 7;
|
||||
tray.cbSize = sizeof(tray);
|
||||
tray.hWnd = window;
|
||||
tray.uID = 1;
|
||||
tray.uFlags = NIF_ICON | NIF_MESSAGE | NIF_TIP;
|
||||
tray.uCallbackMessage = TrayMessage;
|
||||
tray.hIcon = activeIcon;
|
||||
lstrcpyW(tray.szTip, L"StayHere");
|
||||
if (!Shell_NotifyIconW(NIM_ADD, &tray)) { DestroyWindow(window); return 8; }
|
||||
WTSRegisterSessionNotification(window, NOTIFY_FOR_THIS_SESSION);
|
||||
setEnabled(true);
|
||||
if (!SetTimer(window, 1, 15000, nullptr)) { DestroyWindow(window); return 9; }
|
||||
MSG msg{};
|
||||
while (GetMessageW(&msg, nullptr, 0, 0) > 0) {
|
||||
TranslateMessage(&msg);
|
||||
DispatchMessageW(&msg);
|
||||
}
|
||||
DestroyIcon(activeIcon);
|
||||
DestroyIcon(pausedIcon);
|
||||
CloseHandle(mutex);
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user