Compare commits
1 Commits
release-v0.6.45
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 5de5b791bf |
+16
-1
@@ -1,10 +1,24 @@
|
|||||||
cmake_minimum_required(VERSION 3.24)
|
cmake_minimum_required(VERSION 3.24)
|
||||||
project(KSwitcher VERSION 0.6.45 LANGUAGES CXX RC)
|
project(KSwitcher VERSION 0.6.46 LANGUAGES CXX RC)
|
||||||
if(NOT WIN32)
|
if(NOT WIN32)
|
||||||
message(FATAL_ERROR "KSwitcher uses native Windows APIs")
|
message(FATAL_ERROR "KSwitcher uses native Windows APIs")
|
||||||
endif()
|
endif()
|
||||||
set(CMAKE_CXX_STANDARD 20)
|
set(CMAKE_CXX_STANDARD 20)
|
||||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||||
|
add_executable(StayHere WIN32 utilities/StayHere/main.cpp)
|
||||||
|
add_executable(StayHereTest WIN32 utilities/StayHere/main.cpp)
|
||||||
|
target_compile_definitions(StayHereTest PRIVATE STAYHERE_TEST_INSTANCE)
|
||||||
|
set_target_properties(StayHere PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/tools")
|
||||||
|
foreach(utilityTarget StayHere StayHereTest)
|
||||||
|
target_compile_definitions(${utilityTarget} PRIVATE UNICODE _UNICODE _WIN32_WINNT=0x0A00)
|
||||||
|
target_link_libraries(${utilityTarget} PRIVATE user32 shell32 gdi32 wtsapi32)
|
||||||
|
if(MSVC)
|
||||||
|
target_compile_options(${utilityTarget} PRIVATE /utf-8)
|
||||||
|
set_property(TARGET ${utilityTarget} PROPERTY MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>")
|
||||||
|
else()
|
||||||
|
target_link_options(${utilityTarget} PRIVATE -municode -static -static-libgcc -static-libstdc++)
|
||||||
|
endif()
|
||||||
|
endforeach()
|
||||||
add_library(kscore src/core.cpp src/platform.cpp src/autostart.cpp src/autocorrect.cpp src/storage.cpp src/ocr.cpp src/richtext.cpp src/update.cpp src/sync.cpp)
|
add_library(kscore src/core.cpp src/platform.cpp src/autostart.cpp src/autocorrect.cpp src/storage.cpp src/ocr.cpp src/richtext.cpp src/update.cpp src/sync.cpp)
|
||||||
target_include_directories(kscore PUBLIC src PRIVATE vendor)
|
target_include_directories(kscore PUBLIC src PRIVATE vendor)
|
||||||
target_compile_definitions(kscore PUBLIC UNICODE _UNICODE WIN32_LEAN_AND_MEAN NOMINMAX _WIN32_WINNT=0x0A00)
|
target_compile_definitions(kscore PUBLIC UNICODE _UNICODE WIN32_LEAN_AND_MEAN NOMINMAX _WIN32_WINNT=0x0A00)
|
||||||
@@ -127,6 +141,7 @@ else()
|
|||||||
target_link_options(KSwitcherUtilitiesTests PRIVATE -static -static-libgcc -static-libstdc++)
|
target_link_options(KSwitcherUtilitiesTests PRIVATE -static -static-libgcc -static-libstdc++)
|
||||||
endif()
|
endif()
|
||||||
add_test(NAME utilities-regressions COMMAND KSwitcherUtilitiesTests)
|
add_test(NAME utilities-regressions COMMAND KSwitcherUtilitiesTests)
|
||||||
|
add_test(NAME stayhere-lifecycle COMMAND powershell -NoProfile -ExecutionPolicy Bypass -File "${CMAKE_CURRENT_SOURCE_DIR}/tests/stayhere.ps1" -Executable "$<TARGET_FILE:StayHereTest>")
|
||||||
add_test(NAME scroll-regressions COMMAND KSwitcherScrollTests)
|
add_test(NAME scroll-regressions COMMAND KSwitcherScrollTests)
|
||||||
add_test(NAME input-regressions COMMAND KSwitcherInputTests)
|
add_test(NAME input-regressions COMMAND KSwitcherInputTests)
|
||||||
add_test(NAME screenshot-ui-regressions COMMAND KSwitcherScreenshotUITests)
|
add_test(NAME screenshot-ui-regressions COMMAND KSwitcherScreenshotUITests)
|
||||||
|
|||||||
@@ -554,10 +554,18 @@ directory and are never started automatically. Their lifetime is independent of
|
|||||||
An absent or empty folder displays a disabled placeholder; launch errors are reported.
|
An absent or empty folder displays a disabled placeholder; launch errors are reported.
|
||||||
From v0.6.45, the release includes `tools/README.txt`, so extracting the ZIP or
|
From v0.6.45, the release includes `tools/README.txt`, so extracting the ZIP or
|
||||||
installing an update creates the folder. Add your own trusted portable executables;
|
installing an update creates the folder. Add your own trusted portable executables;
|
||||||
none are bundled or transferred by settings synchronization. Updates preserve your
|
StayHere is bundled from v0.6.46, and settings synchronization does not transfer
|
||||||
utility files and may refresh the bundled README. Repository development scripts
|
utility files. Updates preserve personal utilities and refresh bundled StayHere and
|
||||||
|
documentation. Repository development scripts
|
||||||
are not included in the portable tools folder.
|
are not included in the portable tools folder.
|
||||||
|
|
||||||
|
Choose **Utilities → StayHere** to start the bundled activity utility. It starts
|
||||||
|
enabled; left-click its tray icon to pause/resume or right-click for Exit. While
|
||||||
|
enabled it prevents automatic sleep and, after three minutes without input, sends
|
||||||
|
F15 on its 15-second check. It does not move the mouse, bypass locking, start with
|
||||||
|
KSwitcher automatically, or directly control Teams status. Locked/disconnected
|
||||||
|
sessions do not receive simulated activity. See `tools/StayHere-README.md`.
|
||||||
|
|
||||||
## Build
|
## Build
|
||||||
|
|
||||||
The easiest reproducible local route uses the pinned portable w64devkit toolchain:
|
The easiest reproducible local route uses the pinned portable w64devkit toolchain:
|
||||||
|
|||||||
Binary file not shown.
+9
-9
@@ -1,21 +1,21 @@
|
|||||||
# Latest Windows release
|
# Latest Windows release
|
||||||
|
|
||||||
## v0.6.45 — 2026-11-05
|
## v0.6.46 — 2026-11-05
|
||||||
|
|
||||||
## Changes
|
## Changes
|
||||||
- Fix Far Manager snippet expansion and caret placement. Far uses Alt+Shift+Arrow selection, Ctrl+Insert copy and Shift+Insert paste, with exact trigger and inserted-text verification. Cancellation restores the full temporary selection. Both visible and hidden panels are supported.
|
- Bundle **tools/StayHere.exe** and **tools/StayHere-README.md**. Start the activity utility from **Utilities → StayHere**; it does not start automatically with KSwitcher.
|
||||||
- Run Far `{cursor}` and `{enter}` actions only after verified insertion. Far replacements must be nonempty, single-line and no longer than 4096 caret steps. Unsupported templates are rejected before selection; multiline clipboard pastes could execute commands before verification. Other applications retain accessibility verification.
|
- StayHere starts enabled. Left-click its tray icon to pause/resume or right-click for Exit. While enabled, it prevents automatic display/system sleep and checks inactivity every 15 seconds; after three minutes without input it sends F15. It does not move the mouse, bypass locking, install autostart or directly control Teams presence. Locked/disconnected sessions receive no simulated activity.
|
||||||
- Add the tray **Utilities** menu. It scans trusted `.exe` files directly in `tools` beside the running KSwitcher.exe, sorted by filename and refreshed whenever the menu opens. Utilities run only when selected, use `tools` as their working directory and remain running independently.
|
- Include StayHere source and reproducible GCC/CMake builds. CLI status/pause/resume/exit parsing tolerates launcher whitespace. Normal updates replace bundled StayHere and documentation while preserving personal utilities. A running old instance continues until exited; relaunch to use the updated executable.
|
||||||
- Include **tools/README.txt** in the release ZIP and manifest. Both extraction and automatic updates create the folder. Personal utility executables are preserved by updates; none are bundled or transferred through settings synchronization. Source-repository development scripts are excluded.
|
- Retain all v0.6.45 Far snippet, Utilities and tools-folder changes and the v0.6.44 newest-history-selection fix.
|
||||||
- Retain v0.6.44's newest-item selection when reopening clipboard history, including after a screenshot. Saved search/pinned filters remain active; background refreshes preserve the selected record while browsing.
|
- Correct the Far test fixture's working directory: a long temporary path could scroll the command line and invalidate absolute cursor-coordinate assertions. Application snippet behavior is unchanged in this release.
|
||||||
|
|
||||||
## Validation
|
## Validation
|
||||||
Passed Utilities, dictionary equivalence (140,939 words), 105 sync checks, shared wheel regressions, 319 native checks, 574 input checks, screenshot UI/save/registration, 52 updater checks and updater progress UI. Updater tests cover creation of tools, preserving personal utilities and rollback. The actual ZIP was validated and installed through the unchanged v0.6.44 updater; all installed manifest hashes matched, tools/README.txt was created, and a staging-only utility was excluded from the ZIP. An 18-check real-Far integration run passed during development (visible/hidden panels, caret restoration, cursor markers and verified Enter). The final release-build Far run was canceled by an unrelated foreground change before expansion; an uninterrupted final-build desktop run remains outstanding.
|
Passed isolated StayHere start/status/pause/resume/exit checks, Utilities regressions, dictionary equivalence (140,939 words), 105 sync checks, shared wheel tests, 319 native checks, 574 input checks, screenshot UI/save/registration, 54 updater checks and updater progress UI. The updater replaced a running isolated StayHere executable successfully. Final package manifests and downloaded assets are checked before completion. Real-Far spot checks confirmed expansion, caret placement and markers; the uninterrupted full interactive run remains outstanding because unrelated foreground changes cancel it. No additional Far application-code change is shipped in v0.6.46.
|
||||||
|
|
||||||
## Upgrade
|
## Upgrade
|
||||||
Choose **Update** in the tray menu. Legacy v0.3.0–v0.4.1 clients first receive the v0.4.2 compatibility bridge, then the current full release. v0.2.3 and earlier require manual installation.
|
Choose **Update** in the tray menu. Legacy v0.3.0–v0.4.1 clients first receive the v0.4.2 compatibility bridge, then the current full release. v0.2.3 and earlier require manual installation.
|
||||||
For manual installation, exit KSwitcher, close its configurator and screenshot editors, and extract the entire ZIP into its installation folder. Keep personal `data/`, customized dictionaries and your own tools. Look for `tools` beside the executable actually running.
|
For manual installation, exit KSwitcher, close its configurator and screenshot editors, and extract the entire ZIP into its installation folder. Keep personal `data/`, customized dictionaries and your own tools. Find `tools/StayHere.exe` beside the executable actually running.
|
||||||
|
|
||||||
Download **KSwitcher-win-x64.zip** with **SHA256SUMS.txt**. Keep all three executables together; history uses another instance of KSwitcher.exe. The package contains no personal settings, credentials or clipboard history.
|
Download **KSwitcher-win-x64.zip** with **SHA256SUMS.txt**. Keep the three KSwitcher executables together; history uses another instance of KSwitcher.exe. StayHere runs independently. The package contains no personal settings, credentials or clipboard history.
|
||||||
|
|
||||||
Publish stable `release-vX.Y.Z` tags matching the executable version. Keep `v0.4.2` as the last legacy tag.
|
Publish stable `release-vX.Y.Z` tags matching the executable version. Keep `v0.4.2` as the last legacy tag.
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
94b4a3e86c218a42ef4fe0c75c1a621af62e1dfd6b395eea2040d6c1e7f138a8 KSwitcher-win-x64.zip
|
e6d26d59b18559ad226359b35cd403c68326384f8e1918d31b368cbd1110f6b9 KSwitcher-win-x64.zip
|
||||||
|
|||||||
@@ -16,6 +16,12 @@ Push-Location $root
|
|||||||
try {
|
try {
|
||||||
$common = @('-std=c++20','-Os','-DNDEBUG','-DUNICODE','-D_UNICODE','-DWIN32_LEAN_AND_MEAN','-DNOMINMAX','-D_WIN32_WINNT=0x0A00','-Wall','-Wextra','-Wno-missing-field-initializers','-Isrc','-Ivendor','-ffunction-sections','-fdata-sections')
|
$common = @('-std=c++20','-Os','-DNDEBUG','-DUNICODE','-D_UNICODE','-DWIN32_LEAN_AND_MEAN','-DNOMINMAX','-D_WIN32_WINNT=0x0A00','-Wall','-Wextra','-Wno-missing-field-initializers','-Isrc','-Ivendor','-ffunction-sections','-fdata-sections')
|
||||||
$libs = @('-lwinhttp','-luser32','-lshell32','-lshlwapi','-ladvapi32','-lcrypt32','-limm32','-lcomctl32','-lgdi32','-lole32','-loleaut32','-ltaskschd','-lcomdlg32','-luuid','-lwindowscodecs','-lruntimeobject','-luiautomationcore')
|
$libs = @('-lwinhttp','-luser32','-lshell32','-lshlwapi','-ladvapi32','-lcrypt32','-limm32','-lcomctl32','-lgdi32','-lole32','-loleaut32','-ltaskschd','-lcomdlg32','-luuid','-lwindowscodecs','-lruntimeobject','-luiautomationcore')
|
||||||
|
$utilityDirectory = Join-Path (Split-Path $executablePath) 'tools'
|
||||||
|
New-Item -ItemType Directory -Force $utilityDirectory,build/release/tools | Out-Null
|
||||||
|
$stayHere = Join-Path $utilityDirectory 'StayHere.exe'
|
||||||
|
& $compiler @common -Werror -mwindows -municode -static -s utilities/StayHere/main.cpp -luser32 -lshell32 -lgdi32 -lwtsapi32 -o $stayHere
|
||||||
|
if ($LASTEXITCODE) { throw 'StayHere build failed' }
|
||||||
|
Copy-Item -LiteralPath $stayHere -Destination build/release/tools/StayHere.exe -Force
|
||||||
foreach ($name in @('core','platform','autostart','autocorrect','storage','sync','ocr','richtext','update','screenshot','screenshot_main','screenshot_notifications','screenshot_activation','screenshot_launcher','app','ui','main','configuration','config_main')) {
|
foreach ($name in @('core','platform','autostart','autocorrect','storage','sync','ocr','richtext','update','screenshot','screenshot_main','screenshot_notifications','screenshot_activation','screenshot_launcher','app','ui','main','configuration','config_main')) {
|
||||||
& $compiler @common -c "src/$name.cpp" -o "build/$name.o"
|
& $compiler @common -c "src/$name.cpp" -o "build/$name.o"
|
||||||
if ($LASTEXITCODE) { throw "Compilation failed: $name" }
|
if ($LASTEXITCODE) { throw "Compilation failed: $name" }
|
||||||
@@ -41,6 +47,10 @@ try {
|
|||||||
if ($LASTEXITCODE) { throw 'Screenshot link failed' }
|
if ($LASTEXITCODE) { throw 'Screenshot link failed' }
|
||||||
Copy-Item -LiteralPath $screenshotPath -Destination build/release/KSwitcher.Screenshot.exe -Force
|
Copy-Item -LiteralPath $screenshotPath -Destination build/release/KSwitcher.Screenshot.exe -Force
|
||||||
if ($Test) {
|
if ($Test) {
|
||||||
|
& $compiler @common -Werror -DSTAYHERE_TEST_INSTANCE -mwindows -municode -static -s utilities/StayHere/main.cpp -luser32 -lshell32 -lgdi32 -lwtsapi32 -o build/StayHereTest.exe
|
||||||
|
if ($LASTEXITCODE) { throw 'StayHere fixture build failed' }
|
||||||
|
& "$env:SystemRoot\System32\WindowsPowerShell\v1.0\powershell.exe" -NoProfile -ExecutionPolicy Bypass -File tests/stayhere.ps1 -Executable (Join-Path $outDir 'StayHereTest.exe')
|
||||||
|
if ($LASTEXITCODE) { throw 'StayHere lifecycle tests failed' }
|
||||||
& $compiler @common tests/utilities.cpp -static -s -o build/KSwitcherUtilitiesTests.exe
|
& $compiler @common tests/utilities.cpp -static -s -o build/KSwitcherUtilitiesTests.exe
|
||||||
if ($LASTEXITCODE) { throw 'Utilities test build failed' }
|
if ($LASTEXITCODE) { throw 'Utilities test build failed' }
|
||||||
& .\build\KSwitcherUtilitiesTests.exe
|
& .\build\KSwitcherUtilitiesTests.exe
|
||||||
|
|||||||
+3
-1
@@ -12,8 +12,10 @@ Copy-Item -LiteralPath (Join-Path $root 'README.md') -Destination $destination
|
|||||||
Copy-Item -LiteralPath (Join-Path $root 'dictionaries') -Destination $destination -Recurse -Force
|
Copy-Item -LiteralPath (Join-Path $root 'dictionaries') -Destination $destination -Recurse -Force
|
||||||
New-Item -ItemType Directory -Force (Join-Path $destination 'tools') | Out-Null
|
New-Item -ItemType Directory -Force (Join-Path $destination 'tools') | Out-Null
|
||||||
Copy-Item -LiteralPath (Join-Path $root 'resources\portable-tools-README.txt') -Destination (Join-Path $destination 'tools\README.txt') -Force
|
Copy-Item -LiteralPath (Join-Path $root 'resources\portable-tools-README.txt') -Destination (Join-Path $destination 'tools\README.txt') -Force
|
||||||
|
Copy-Item -LiteralPath (Join-Path $root 'build\release\tools\StayHere.exe') -Destination (Join-Path $destination 'tools\StayHere.exe') -Force
|
||||||
|
Copy-Item -LiteralPath (Join-Path $root 'utilities\StayHere\README.md') -Destination (Join-Path $destination 'tools\StayHere-README.md') -Force
|
||||||
# Enumerate the distributable files explicitly; do not include a user's data directory.
|
# Enumerate the distributable files explicitly; do not include a user's data directory.
|
||||||
$files = @('KSwitcher.exe','KSwitcher.Config.exe','KSwitcher.Screenshot.exe','LICENSE','json-LICENSE.txt','README.md','dictionaries','tools\README.txt') | ForEach-Object { Join-Path $destination $_ }
|
$files = @('KSwitcher.exe','KSwitcher.Config.exe','KSwitcher.Screenshot.exe','LICENSE','json-LICENSE.txt','README.md','dictionaries','tools\README.txt','tools\StayHere.exe','tools\StayHere-README.md') | ForEach-Object { Join-Path $destination $_ }
|
||||||
$entries = foreach ($path in $files) {
|
$entries = foreach ($path in $files) {
|
||||||
foreach ($file in @(Get-ChildItem -LiteralPath $path -File -Recurse)) {
|
foreach ($file in @(Get-ChildItem -LiteralPath $path -File -Recurse)) {
|
||||||
@{path=$file.FullName.Substring($destination.Length+1).Replace('\','/'); size=$file.Length; sha256=(Get-FileHash -LiteralPath $file.FullName -Algorithm SHA256).Hash.ToLowerInvariant()}
|
@{path=$file.FullName.Substring($destination.Length+1).Replace('\','/'); size=$file.Length; sha256=(Get-FileHash -LiteralPath $file.FullName -Algorithm SHA256).Hash.ToLowerInvariant()}
|
||||||
|
|||||||
+3
-3
@@ -9,8 +9,8 @@ IDC_SCREENSHOT_PENCIL CURSOR "icons/lucide/pencil.cur"
|
|||||||
1 RT_MANIFEST "app.manifest"
|
1 RT_MANIFEST "app.manifest"
|
||||||
201 RCDATA "update.ps1"
|
201 RCDATA "update.ps1"
|
||||||
1 VERSIONINFO
|
1 VERSIONINFO
|
||||||
FILEVERSION 0,6,45,0
|
FILEVERSION 0,6,46,0
|
||||||
PRODUCTVERSION 0,6,45,0
|
PRODUCTVERSION 0,6,46,0
|
||||||
FILEOS VOS_NT_WINDOWS32
|
FILEOS VOS_NT_WINDOWS32
|
||||||
FILETYPE VFT_APP
|
FILETYPE VFT_APP
|
||||||
BEGIN
|
BEGIN
|
||||||
@@ -20,7 +20,7 @@ BEGIN
|
|||||||
BEGIN
|
BEGIN
|
||||||
VALUE "FileDescription", "KSwitcher — native keyboard and clipboard utility"
|
VALUE "FileDescription", "KSwitcher — native keyboard and clipboard utility"
|
||||||
VALUE "ProductName", "KSwitcher"
|
VALUE "ProductName", "KSwitcher"
|
||||||
VALUE "FileVersion", "0.6.45"
|
VALUE "FileVersion", "0.6.46"
|
||||||
END
|
END
|
||||||
END
|
END
|
||||||
BLOCK "VarFileInfo"
|
BLOCK "VarFileInfo"
|
||||||
|
|||||||
@@ -8,6 +8,11 @@ The list refreshes each time the tray menu opens. Subfolders and shortcuts
|
|||||||
are not listed. Utilities start only when you choose them, with this folder
|
are not listed. Utilities start only when you choose them, with this folder
|
||||||
as their working directory. Closing KSwitcher does not close a utility.
|
as their working directory. Closing KSwitcher does not close a utility.
|
||||||
|
|
||||||
No utility executables are bundled. Supply them separately on each computer.
|
StayHere.exe is bundled. Choose Utilities > StayHere to start it; it starts
|
||||||
Normal KSwitcher updates preserve your own files in this folder; this README
|
enabled, prevents automatic sleep, and sends F15 after 3 minutes of inactivity.
|
||||||
may be refreshed. Utilities are not transferred by settings synchronization.
|
Left-click its tray icon to pause/resume; right-click for Exit. See StayHere-README.md.
|
||||||
|
It does not start automatically with KSwitcher or bypass Windows locking.
|
||||||
|
|
||||||
|
Add other utilities separately. Updates preserve your own files in this folder;
|
||||||
|
bundled StayHere.exe and documentation may be refreshed. Utilities are not
|
||||||
|
transferred by settings synchronization.
|
||||||
|
|||||||
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
#include "platform.h"
|
#include "platform.h"
|
||||||
namespace ks
|
namespace ks
|
||||||
{
|
{
|
||||||
inline constexpr wchar_t AppVersion[] = L"0.6.45";
|
inline constexpr wchar_t AppVersion[] = L"0.6.46";
|
||||||
inline constexpr UINT UpdateMessage = WM_APP + 7;
|
inline constexpr UINT UpdateMessage = WM_APP + 7;
|
||||||
inline constexpr UINT UpdateCheckIntervalMs = 6 * 60 * 60 * 1000;
|
inline constexpr UINT UpdateCheckIntervalMs = 6 * 60 * 60 * 1000;
|
||||||
inline constexpr UINT UpdateNotificationRetryMs = 5 * 60 * 1000;
|
inline constexpr UINT UpdateNotificationRetryMs = 5 * 60 * 1000;
|
||||||
|
|||||||
@@ -50,9 +50,13 @@ int WINAPI wWinMain(HINSTANCE instance,HINSTANCE,PWSTR,int) {
|
|||||||
const auto programFiles=_wgetenv(L"ProgramFiles");
|
const auto programFiles=_wgetenv(L"ProgramFiles");
|
||||||
if(!programFiles)throw std::runtime_error("ProgramFiles is unavailable");
|
if(!programFiles)throw std::runtime_error("ProgramFiles is unavailable");
|
||||||
auto farExecutable=std::filesystem::path(programFiles)/L"Far Manager/Far.exe";
|
auto farExecutable=std::filesystem::path(programFiles)/L"Far Manager/Far.exe";
|
||||||
std::wstring command=L"\""+farExecutable.wstring()+L"\" -m -p -s \""+folder.wstring()+L"\\profile\" \""+folder.wstring()+L"\"";
|
std::wstring command=L"\""+farExecutable.wstring()+L"\" -m -p -s \""+folder.wstring()+L"\\profile\"";
|
||||||
|
// A GUID-length working directory can horizontally scroll Far's command
|
||||||
|
// line, making absolute cursor-cell assertions invalid. Keep the prompt
|
||||||
|
// short; profile and application data still live in the unique fixture.
|
||||||
|
auto workingDirectory=folder.root_path();
|
||||||
STARTUPINFOW startup{sizeof(startup)};startup.dwFlags=STARTF_USESHOWWINDOW;startup.wShowWindow=SW_HIDE;
|
STARTUPINFOW startup{sizeof(startup)};startup.dwFlags=STARTF_USESHOWWINDOW;startup.wShowWindow=SW_HIDE;
|
||||||
require(CreateProcessW(nullptr,command.data(),nullptr,nullptr,FALSE,CREATE_NEW_CONSOLE,nullptr,folder.c_str(),&startup,&child),"start isolated Far without plugins/macros");
|
require(CreateProcessW(nullptr,command.data(),nullptr,nullptr,FALSE,CREATE_NEW_CONSOLE,nullptr,workingDirectory.c_str(),&startup,&child),"start isolated Far without plugins/macros");
|
||||||
CloseHandle(child.hThread);Sleep(1800);
|
CloseHandle(child.hThread);Sleep(1800);
|
||||||
require(AttachConsole(child.dwProcessId),"attach isolated console for test-only input and observation");
|
require(AttachConsole(child.dwProcessId),"attach isolated console for test-only input and observation");
|
||||||
input=CreateFileW(L"CONIN$",GENERIC_READ|GENERIC_WRITE,FILE_SHARE_READ|FILE_SHARE_WRITE,nullptr,OPEN_EXISTING,0,nullptr);
|
input=CreateFileW(L"CONIN$",GENERIC_READ|GENERIC_WRITE,FILE_SHARE_READ|FILE_SHARE_WRITE,nullptr,OPEN_EXISTING,0,nullptr);
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
param([Parameter(Mandatory=$true)][string]$Executable)
|
||||||
|
$ErrorActionPreference='Stop'
|
||||||
|
function Command([string]$Argument) {
|
||||||
|
$commandProcess=Start-Process -FilePath $Executable -ArgumentList $Argument -WindowStyle Hidden -PassThru
|
||||||
|
try {
|
||||||
|
$null=$commandProcess.Handle
|
||||||
|
$commandProcess.WaitForExit()
|
||||||
|
return $commandProcess.ExitCode
|
||||||
|
} finally { $commandProcess.Dispose() }
|
||||||
|
}
|
||||||
|
if ((Command '--status') -ne 3) { throw 'An isolated StayHere test instance is already running' }
|
||||||
|
$process=Start-Process -FilePath $Executable -WindowStyle Hidden -PassThru
|
||||||
|
$null=$process.Handle
|
||||||
|
try {
|
||||||
|
$ready=$false
|
||||||
|
for($i=0;$i -lt 20;$i++) {
|
||||||
|
if((Command '--status') -eq 1){$ready=$true;break}
|
||||||
|
Start-Sleep -Milliseconds 50
|
||||||
|
}
|
||||||
|
if(!$ready){throw 'StayHere did not start enabled'}
|
||||||
|
if((Command '--pause') -ne 0 -or (Command '--status') -ne 2){throw 'StayHere pause failed'}
|
||||||
|
if((Command '--resume') -ne 0 -or (Command '--status') -ne 1){throw 'StayHere resume failed'}
|
||||||
|
if((Command '--exit') -ne 0 -or !$process.WaitForExit(3000)){throw 'StayHere exit failed'}
|
||||||
|
if((Command '--status') -ne 3){throw 'StayHere instance remained after exit'}
|
||||||
|
Write-Output 'Passed isolated StayHere start/status/pause/resume/exit checks'
|
||||||
|
} finally {
|
||||||
|
if(!$process.HasExited){$null=Command '--exit';if(!$process.WaitForExit(3000)){$process.Kill()}}
|
||||||
|
$process.Dispose()
|
||||||
|
}
|
||||||
@@ -52,6 +52,7 @@ try {
|
|||||||
[IO.File]::WriteAllText((Join-Path $source 'dictionaries/en.txt'),'new dictionary')
|
[IO.File]::WriteAllText((Join-Path $source 'dictionaries/en.txt'),'new dictionary')
|
||||||
[IO.Directory]::CreateDirectory((Join-Path $source 'tools')) | Out-Null
|
[IO.Directory]::CreateDirectory((Join-Path $source 'tools')) | Out-Null
|
||||||
[IO.File]::WriteAllText((Join-Path $source 'tools/README.txt'),'portable utilities instructions')
|
[IO.File]::WriteAllText((Join-Path $source 'tools/README.txt'),'portable utilities instructions')
|
||||||
|
[IO.File]::WriteAllText((Join-Path $source 'tools/StayHere.exe'),'bundled StayHere fixture')
|
||||||
function Write-TestManifest($Folder) {
|
function Write-TestManifest($Folder) {
|
||||||
$entries=@(Get-ChildItem -LiteralPath $Folder -File -Recurse | Where-Object Name -ne 'release-files.json' | ForEach-Object {
|
$entries=@(Get-ChildItem -LiteralPath $Folder -File -Recurse | Where-Object Name -ne 'release-files.json' | ForEach-Object {
|
||||||
@{path=$_.FullName.Substring($Folder.Length+1).Replace('\','/');size=$_.Length;sha256=(Get-FileHash -LiteralPath $_.FullName).Hash}
|
@{path=$_.FullName.Substring($Folder.Length+1).Replace('\','/');size=$_.Length;sha256=(Get-FileHash -LiteralPath $_.FullName).Hash}
|
||||||
@@ -105,6 +106,7 @@ try {
|
|||||||
[IO.Directory]::CreateDirectory((Join-Path $target 'dictionaries')) | Out-Null
|
[IO.Directory]::CreateDirectory((Join-Path $target 'dictionaries')) | Out-Null
|
||||||
[IO.Directory]::CreateDirectory((Join-Path $target 'tools')) | Out-Null
|
[IO.Directory]::CreateDirectory((Join-Path $target 'tools')) | Out-Null
|
||||||
[IO.File]::WriteAllText((Join-Path $target 'tools/Personal.exe'),'personal utility fixture')
|
[IO.File]::WriteAllText((Join-Path $target 'tools/Personal.exe'),'personal utility fixture')
|
||||||
|
[IO.File]::WriteAllText((Join-Path $target 'tools/StayHere.exe'),'older StayHere fixture')
|
||||||
[IO.File]::WriteAllText((Join-Path $target 'data/settings.json'),'personal settings')
|
[IO.File]::WriteAllText((Join-Path $target 'data/settings.json'),'personal settings')
|
||||||
[IO.File]::WriteAllText((Join-Path $target 'dictionaries/en.txt'),'personal dictionary')
|
[IO.File]::WriteAllText((Join-Path $target 'dictionaries/en.txt'),'personal dictionary')
|
||||||
[IO.File]::WriteAllText((Join-Path $target 'KSwitcher.exe'),'old binary fixture')
|
[IO.File]::WriteAllText((Join-Path $target 'KSwitcher.exe'),'old binary fixture')
|
||||||
@@ -118,10 +120,12 @@ try {
|
|||||||
Assert ([IO.File]::ReadAllText((Join-Path $target 'data/settings.json')) -eq 'personal settings') 'settings preserved'
|
Assert ([IO.File]::ReadAllText((Join-Path $target 'data/settings.json')) -eq 'personal settings') 'settings preserved'
|
||||||
Assert ([IO.File]::ReadAllText((Join-Path $target 'tools/Personal.exe')) -eq 'personal utility fixture') 'personal utilities preserved'
|
Assert ([IO.File]::ReadAllText((Join-Path $target 'tools/Personal.exe')) -eq 'personal utility fixture') 'personal utilities preserved'
|
||||||
Assert ([IO.File]::ReadAllText((Join-Path $target 'tools/README.txt')) -eq 'portable utilities instructions') 'bundled tools documentation installed'
|
Assert ([IO.File]::ReadAllText((Join-Path $target 'tools/README.txt')) -eq 'portable utilities instructions') 'bundled tools documentation installed'
|
||||||
|
Assert ([IO.File]::ReadAllText((Join-Path $target 'tools/StayHere.exe')) -eq 'bundled StayHere fixture') 'bundled StayHere updated'
|
||||||
Assert ([IO.File]::ReadAllText((Join-Path $target 'dictionaries/en.txt')) -eq 'personal dictionary') 'dictionary preserved'
|
Assert ([IO.File]::ReadAllText((Join-Path $target 'dictionaries/en.txt')) -eq 'personal dictionary') 'dictionary preserved'
|
||||||
Undo-Files $target $backup $changed
|
Undo-Files $target $backup $changed
|
||||||
Assert ([IO.File]::ReadAllText((Join-Path $target 'KSwitcher.exe')) -eq 'old binary fixture') 'rollback restores original'
|
Assert ([IO.File]::ReadAllText((Join-Path $target 'KSwitcher.exe')) -eq 'old binary fixture') 'rollback restores original'
|
||||||
Assert ([IO.File]::ReadAllText((Join-Path $target 'tools/Personal.exe')) -eq 'personal utility fixture') 'rollback preserves personal utilities'
|
Assert ([IO.File]::ReadAllText((Join-Path $target 'tools/Personal.exe')) -eq 'personal utility fixture') 'rollback preserves personal utilities'
|
||||||
|
Assert ([IO.File]::ReadAllText((Join-Path $target 'tools/StayHere.exe')) -eq 'older StayHere fixture') 'rollback restores previous StayHere'
|
||||||
Assert (!(Test-Path -LiteralPath (Join-Path $target 'README.md'))) 'rollback removes newly introduced files'
|
Assert (!(Test-Path -LiteralPath (Join-Path $target 'README.md'))) 'rollback removes newly introduced files'
|
||||||
Assert (!(Test-Path -LiteralPath (Join-Path $target 'KSwitcher.Config.exe'))) 'rollback removes newly introduced configurator'
|
Assert (!(Test-Path -LiteralPath (Join-Path $target 'KSwitcher.Config.exe'))) 'rollback removes newly introduced configurator'
|
||||||
Assert (!(Test-Path -LiteralPath (Join-Path $target 'KSwitcher.Screenshot.exe'))) 'rollback removes newly introduced screenshot editor'
|
Assert (!(Test-Path -LiteralPath (Join-Path $target 'KSwitcher.Screenshot.exe'))) 'rollback removes newly introduced screenshot editor'
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
# StayHere
|
||||||
|
|
||||||
|
Маленька портативна утиліта для Windows, без .NET та інсталяції.
|
||||||
|
|
||||||
|
У KSwitcher v0.6.46 і новіших виберіть **Utilities → StayHere** у меню трея
|
||||||
|
або запустіть `tools/StayHere.exe` поруч із KSwitcher.exe.
|
||||||
|
Зелена крапка в треї означає, що підтримка активності ввімкнена.
|
||||||
|
Клацання лівою кнопкою перемикає режим; правою — відкриває меню з перемикачем і виходом.
|
||||||
|
Після кожного запуску режим увімкнений. Автозапуск не додається.
|
||||||
|
|
||||||
|
Раз на 15 секунд перевіряється час бездіяльності. Після 3 хвилин без вводу
|
||||||
|
надсилається натискання й відпускання F15. Поки ви працюєте, клавіші не надсилаються.
|
||||||
|
Курсор миші не рухається. Увімкнений режим також утримує екран і систему від
|
||||||
|
автоматичного сну. На заблокованому екрані та у відключеному сеансі активність
|
||||||
|
не імітується. Ручне блокування Windows не обходиться.
|
||||||
|
|
||||||
|
Механізм перенесено за поведінкою з Typlo. Він підтримує системну активність,
|
||||||
|
але не керує статусом Teams через API: статус залежить також від Teams,
|
||||||
|
календаря, інших пристроїв та політик організації. F15 може мати власне
|
||||||
|
призначення в окремих програмах. На підвищених вікнах Windows може блокувати ввід;
|
||||||
|
тоді підказка іконки показує помилку. Утиліта не запитує права адміністратора.
|
||||||
|
|
||||||
|
## Збірка
|
||||||
|
|
||||||
|
У репозиторії KSwitcher виконайте `./build.ps1 -Stage` з кореня проєкту.
|
||||||
|
Утиліта збирається з `utilities/StayHere/main.cpp` у `build/staged/tools/StayHere.exe`
|
||||||
|
і включається пакувальником у реліз. CMake також має ціль `StayHere`.
|
||||||
|
Для запуску достатньо одного `StayHere.exe`; .NET не потрібний.
|
||||||
|
|
||||||
|
CLI: `--pause`, `--resume`, `--exit` керують запущеним екземпляром.
|
||||||
|
`--status` повертає код 1 для активного режиму, 2 для паузи, 3 якщо утиліта не запущена.
|
||||||
@@ -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