Release v0.3.0: move settings into an on-demand single-instance configurator
This commit is contained in:
+13
-6
@@ -1,5 +1,5 @@
|
|||||||
cmake_minimum_required(VERSION 3.24)
|
cmake_minimum_required(VERSION 3.24)
|
||||||
project(KSwitcher VERSION 0.2.3 LANGUAGES CXX RC)
|
project(KSwitcher VERSION 0.3.0 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()
|
||||||
@@ -9,22 +9,29 @@ add_library(kscore src/core.cpp src/platform.cpp src/autostart.cpp src/autocorre
|
|||||||
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)
|
||||||
target_link_libraries(kscore PUBLIC user32 shell32 advapi32 crypt32 imm32 comctl32 gdi32 ole32 oleaut32 taskschd comdlg32 uuid windowscodecs runtimeobject uiautomationcore)
|
target_link_libraries(kscore PUBLIC user32 shell32 advapi32 crypt32 imm32 comctl32 gdi32 ole32 oleaut32 taskschd comdlg32 uuid windowscodecs runtimeobject uiautomationcore)
|
||||||
add_executable(KSwitcher WIN32 src/main.cpp src/app.cpp src/ui.cpp resources/app.rc)
|
add_executable(KSwitcher WIN32 src/main.cpp src/app.cpp src/configuration.cpp src/ui.cpp resources/app.rc)
|
||||||
|
target_compile_definitions(KSwitcher PRIVATE KS_RESIDENT_APP)
|
||||||
target_link_libraries(KSwitcher PRIVATE kscore)
|
target_link_libraries(KSwitcher PRIVATE kscore)
|
||||||
|
add_executable(KSwitcherConfig WIN32 src/config_main.cpp src/configuration.cpp src/ui.cpp resources/app.rc)
|
||||||
|
set_target_properties(KSwitcherConfig PROPERTIES OUTPUT_NAME "KSwitcher.Config")
|
||||||
|
target_compile_definitions(KSwitcherConfig PRIVATE KS_CONFIG_APP)
|
||||||
|
target_link_libraries(KSwitcherConfig PRIVATE kscore)
|
||||||
add_executable(KSwitcherTests tests/tests.cpp)
|
add_executable(KSwitcherTests tests/tests.cpp)
|
||||||
target_link_libraries(KSwitcherTests PRIVATE kscore)
|
target_link_libraries(KSwitcherTests PRIVATE kscore)
|
||||||
add_executable(KSwitcherIntegration tests/integration.cpp src/app.cpp src/ui.cpp resources/app.rc)
|
add_executable(KSwitcherIntegration tests/integration.cpp src/app.cpp src/configuration.cpp src/ui.cpp resources/app.rc)
|
||||||
target_link_libraries(KSwitcherIntegration PRIVATE kscore)
|
target_link_libraries(KSwitcherIntegration PRIVATE kscore)
|
||||||
add_executable(KSwitcherInputTests tests/input.cpp src/app.cpp src/ui.cpp resources/app.rc)
|
add_executable(KSwitcherInputTests tests/input.cpp src/app.cpp src/configuration.cpp src/ui.cpp resources/app.rc)
|
||||||
target_link_libraries(KSwitcherInputTests PRIVATE kscore)
|
target_link_libraries(KSwitcherInputTests PRIVATE kscore)
|
||||||
add_executable(KSwitcherOcrTests tests/ocr.cpp)
|
add_executable(KSwitcherOcrTests tests/ocr.cpp)
|
||||||
target_link_libraries(KSwitcherOcrTests PRIVATE kscore)
|
target_link_libraries(KSwitcherOcrTests PRIVATE kscore)
|
||||||
if(MSVC)
|
if(MSVC)
|
||||||
set_property(TARGET kscore KSwitcher KSwitcherTests KSwitcherIntegration KSwitcherInputTests KSwitcherOcrTests PROPERTY MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>")
|
set_property(TARGET kscore KSwitcher KSwitcherConfig KSwitcherTests KSwitcherIntegration KSwitcherInputTests KSwitcherOcrTests PROPERTY MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>")
|
||||||
target_compile_options(kscore PRIVATE /W4 /utf-8)
|
target_compile_options(kscore PRIVATE /W4 /utf-8)
|
||||||
target_compile_options(KSwitcher PRIVATE /W4 /utf-8)
|
target_compile_options(KSwitcher PRIVATE /W4 /utf-8)
|
||||||
|
target_compile_options(KSwitcherConfig PRIVATE /W4 /utf-8)
|
||||||
else()
|
else()
|
||||||
target_link_options(KSwitcher PRIVATE -municode -static -static-libgcc -static-libstdc++)
|
target_link_options(KSwitcher PRIVATE -municode -static -static-libgcc -static-libstdc++)
|
||||||
|
target_link_options(KSwitcherConfig PRIVATE -municode -static -static-libgcc -static-libstdc++)
|
||||||
target_link_options(KSwitcherTests PRIVATE -static -static-libgcc -static-libstdc++)
|
target_link_options(KSwitcherTests PRIVATE -static -static-libgcc -static-libstdc++)
|
||||||
target_link_options(KSwitcherIntegration PRIVATE -static -static-libgcc -static-libstdc++)
|
target_link_options(KSwitcherIntegration PRIVATE -static -static-libgcc -static-libstdc++)
|
||||||
target_link_options(KSwitcherInputTests PRIVATE -static -static-libgcc -static-libstdc++)
|
target_link_options(KSwitcherInputTests PRIVATE -static -static-libgcc -static-libstdc++)
|
||||||
@@ -35,7 +42,7 @@ add_test(NAME native-regressions COMMAND KSwitcherTests)
|
|||||||
add_test(NAME input-regressions COMMAND KSwitcherInputTests)
|
add_test(NAME input-regressions COMMAND KSwitcherInputTests)
|
||||||
add_test(NAME updater-regressions COMMAND powershell.exe -NoProfile -ExecutionPolicy Bypass -File "${CMAKE_CURRENT_SOURCE_DIR}/tests/update.ps1" -Executable "$<TARGET_FILE:KSwitcher>")
|
add_test(NAME updater-regressions COMMAND powershell.exe -NoProfile -ExecutionPolicy Bypass -File "${CMAKE_CURRENT_SOURCE_DIR}/tests/update.ps1" -Executable "$<TARGET_FILE:KSwitcher>")
|
||||||
set_tests_properties(native-regressions PROPERTIES WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}")
|
set_tests_properties(native-regressions PROPERTIES WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}")
|
||||||
install(TARGETS KSwitcher RUNTIME DESTINATION .)
|
install(TARGETS KSwitcher KSwitcherConfig RUNTIME DESTINATION .)
|
||||||
install(FILES LICENSE vendor/json-LICENSE.txt DESTINATION .)
|
install(FILES LICENSE vendor/json-LICENSE.txt DESTINATION .)
|
||||||
|
|
||||||
add_custom_command(TARGET KSwitcher POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_directory "${CMAKE_CURRENT_SOURCE_DIR}/dictionaries" "$<TARGET_FILE_DIR:KSwitcher>/dictionaries")
|
add_custom_command(TARGET KSwitcher POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_directory "${CMAKE_CURRENT_SOURCE_DIR}/dictionaries" "$<TARGET_FILE_DIR:KSwitcher>/dictionaries")
|
||||||
|
|||||||
@@ -12,6 +12,16 @@ Run `build/KSwitcher.exe`, or extract `dist/KSwitcher-win-x64.zip` and run the e
|
|||||||
The build also refreshes the release copy in `build/release/`. Exit KSwitcher before rebuilding.
|
The build also refreshes the release copy in `build/release/`. Exit KSwitcher before rebuilding.
|
||||||
The application lives in the notification area. Right-click its icon for Settings, Snippets and history.
|
The application lives in the notification area. Right-click its icon for Settings, Snippets and history.
|
||||||
Double-click the tray icon to open Settings, the bold default menu item.
|
Double-click the tray icon to open Settings, the bold default menu item.
|
||||||
|
Settings and the snippet editor run on demand in **KSwitcher.Config.exe**, beside the main executable.
|
||||||
|
Repeated launches activate the existing configurator; closing its last window exits that process.
|
||||||
|
Saving applies changes immediately through the resident KSwitcher process. The configurator does not
|
||||||
|
load clipboard history, dictionaries or global input hooks. Clipboard history and the log viewer remain
|
||||||
|
in the resident app. Keep both executables together when copying or updating the application.
|
||||||
|
If settings change in the tray while the configurator is open, a conflicting Save is rejected; reopen
|
||||||
|
the configurator to load the latest settings.
|
||||||
|
The first upgrade from v0.2.3 or earlier to this two-executable build requires manual extraction:
|
||||||
|
the older updater rejects additional executable files. Exit KSwitcher, replace both executables,
|
||||||
|
and retain `data/` and customized dictionaries. Subsequent updates support both executables.
|
||||||
In Settings, enable **Start with Windows** and **Start with Windows as administrator**
|
In Settings, enable **Start with Windows** and **Start with Windows as administrator**
|
||||||
to use a per-user Task Scheduler logon task with highest privileges. Enabling or changing
|
to use a per-user Task Scheduler logon task with highest privileges. Enabling or changing
|
||||||
the elevated task requests UAC approval if the app is not already elevated. No password is stored;
|
the elevated task requests UAC approval if the app is not already elevated. No password is stored;
|
||||||
@@ -227,6 +237,7 @@ Run `./package.ps1 -PublishRelease` after building and testing to refresh the tr
|
|||||||
.\build.ps1 -Test # Pure/native regression checks; does not touch the user's clipboard
|
.\build.ps1 -Test # Pure/native regression checks; does not touch the user's clipboard
|
||||||
.\build.ps1 -Ocr # Render a synthetic image, decode it with WIC and recognize it with Windows OCR
|
.\build.ps1 -Ocr # Render a synthetic image, decode it with WIC and recognize it with Windows OCR
|
||||||
.\build.ps1 -Integration # Interactive, isolated test editor; avoid input for about 10 seconds
|
.\build.ps1 -Integration # Interactive, isolated test editor; avoid input for about 10 seconds
|
||||||
|
.\tests\configuration.ps1 # Separate-process settings tests; close KSwitcher first
|
||||||
```
|
```
|
||||||
|
|
||||||
`-Test` also checks remote/own input separation, hidden Settings layout and scrolling, and updater
|
`-Test` also checks remote/own input separation, hidden Settings layout and scrolling, and updater
|
||||||
@@ -255,6 +266,7 @@ It still uses the normal instance lock; close a running KSwitcher first if measu
|
|||||||
- `platform.*`: Win32 layouts, clipboard, input, ownership, DPAPI and file primitives.
|
- `platform.*`: Win32 layouts, clipboard, input, ownership, DPAPI and file primitives.
|
||||||
- `storage.*`: settings and encrypted, disk-backed history.
|
- `storage.*`: settings and encrypted, disk-backed history.
|
||||||
- `app.*`: message loop, hooks, background work and guarded text transactions.
|
- `app.*`: message loop, hooks, background work and guarded text transactions.
|
||||||
|
- `config_main.cpp`, `configuration.*`: single-instance configurator and validated settings IPC.
|
||||||
- `ui.cpp`: native windows and shortcut recording.
|
- `ui.cpp`: native windows and shortcut recording.
|
||||||
- `ocr.*`: Windows WIC and OCR via the native Windows Runtime ABI.
|
- `ocr.*`: Windows WIC and OCR via the native Windows Runtime ABI.
|
||||||
- `tests/`: regression checks, real editor integration and OCR fixture.
|
- `tests/`: regression checks, real editor integration and OCR fixture.
|
||||||
|
|||||||
Binary file not shown.
+15
-12
@@ -1,22 +1,25 @@
|
|||||||
# Latest Windows release
|
# Latest Windows release
|
||||||
|
|
||||||
## v0.2.3 — 2026-10-28
|
## v0.3.0 — 2026-10-28
|
||||||
|
|
||||||
- Fix overlapping Settings controls that obscured the Next layout and Change case labels.
|
- Move Settings and the snippet editor into the on-demand `KSwitcher.Config.exe` process.
|
||||||
- Stop treating successful repeated conversions as diagnostic incidents. Failed operations still flush their diagnostic context to the log.
|
- Repeated launches activate the existing configurator. Closing its last window releases the process and its memory.
|
||||||
|
- Apply settings immediately through validated IPC with the resident process; reject conflicting saves instead of overwriting newer settings.
|
||||||
|
- Keep shortcut capture in the resident hook, with no extra global hooks in the configurator.
|
||||||
|
- Support packaging, validation and rollback of both executables. About now displays the actual application version.
|
||||||
|
|
||||||
Verified: 168 native checks, 51 input/layout checks, and 26 updater/metadata checks.
|
Verified: 170 native checks, 51 input/layout checks, 28 updater/metadata checks and 21 configurator process checks.
|
||||||
|
A local before/after measurement after opening and closing Settings reduced resident working set from about 21.0 MiB to 14.7 MiB. Results vary; total memory is higher while the separate configurator is open.
|
||||||
|
|
||||||
Download **KSwitcher-win-x64.zip**, extract the entire archive to a writable folder,
|
**Upgrade from v0.2.3 or earlier: install this release manually once.** The old updater rejects the additional EXE.
|
||||||
and launch `KSwitcher.exe`. Keep `dictionaries/` next to the executable.
|
Exit KSwitcher, extract the entire ZIP into the installation folder, and retain `data/` and any customized dictionaries.
|
||||||
The archive contains the application, offline dictionaries, documentation and license notices.
|
Keep `KSwitcher.exe`, `KSwitcher.Config.exe` and `dictionaries/` together. Launch `KSwitcher.exe`.
|
||||||
It does not contain personal settings or clipboard history.
|
Subsequent updates support both executables through the tray Update menu.
|
||||||
|
|
||||||
|
Download **KSwitcher-win-x64.zip**. The archive contains both applications, offline dictionaries,
|
||||||
|
documentation and license notices. It does not contain personal settings or clipboard history.
|
||||||
`SHA256SUMS.txt` contains the archive checksum.
|
`SHA256SUMS.txt` contains the archive checksum.
|
||||||
|
|
||||||
From version 0.2.1 onward, use **Update** in the tray for future releases. Startup checks notify;
|
|
||||||
installation starts only after confirming Update. Older versions need this one manual upgrade:
|
|
||||||
exit KSwitcher and replace the application files, retaining `data/` and any customized dictionaries.
|
|
||||||
|
|
||||||
This folder holds the latest compiled version; older versions remain in Git history and Releases.
|
This folder holds the latest compiled version; older versions remain in Git history and Releases.
|
||||||
To refresh the tracked package from source:
|
To refresh the tracked package from source:
|
||||||
|
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
ef3f21574f02edeb63661109056480506041372f1fa4ed40b07cdd2f544d79b5 KSwitcher-win-x64.zip
|
104c79b64bb30a2161e3ec2f5108a731da0d274e015d53d8835ea7e64f431ee1 KSwitcher-win-x64.zip
|
||||||
|
|||||||
@@ -14,22 +14,30 @@ 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 = @('-luser32','-lshell32','-ladvapi32','-lcrypt32','-limm32','-lcomctl32','-lgdi32','-lole32','-loleaut32','-ltaskschd','-lcomdlg32','-luuid','-lwindowscodecs','-lruntimeobject','-luiautomationcore')
|
$libs = @('-luser32','-lshell32','-ladvapi32','-lcrypt32','-limm32','-lcomctl32','-lgdi32','-lole32','-loleaut32','-ltaskschd','-lcomdlg32','-luuid','-lwindowscodecs','-lruntimeobject','-luiautomationcore')
|
||||||
foreach ($name in @('core','platform','autostart','autocorrect','storage','ocr','richtext','update','app','ui','main')) {
|
foreach ($name in @('core','platform','autostart','autocorrect','storage','ocr','richtext','update','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" }
|
||||||
}
|
}
|
||||||
|
& $compiler @common -DKS_RESIDENT_APP -c src/ui.cpp -o build/ui-resident.o
|
||||||
|
if ($LASTEXITCODE) { throw 'Resident UI compilation failed' }
|
||||||
|
& $compiler @common -DKS_CONFIG_APP -c src/ui.cpp -o build/ui-config.o
|
||||||
|
if ($LASTEXITCODE) { throw 'Configurator UI compilation failed' }
|
||||||
& windres --codepage=65001 resources/app.rc -O coff -o build/app-res.o
|
& windres --codepage=65001 resources/app.rc -O coff -o build/app-res.o
|
||||||
if ($LASTEXITCODE) { throw 'Resource compilation failed' }
|
if ($LASTEXITCODE) { throw 'Resource compilation failed' }
|
||||||
New-Item -ItemType Directory -Force build/release | Out-Null
|
New-Item -ItemType Directory -Force build/release | Out-Null
|
||||||
& $compiler -mwindows -municode -static -s '-Wl,--gc-sections' build/core.o build/platform.o build/autostart.o build/autocorrect.o build/storage.o build/ocr.o build/richtext.o build/update.o build/app.o build/ui.o build/main.o build/app-res.o @libs -o $executablePath
|
& $compiler -mwindows -municode -static -s '-Wl,--gc-sections' build/core.o build/platform.o build/autostart.o build/autocorrect.o build/storage.o build/ocr.o build/richtext.o build/update.o build/app.o build/configuration.o build/ui-resident.o build/main.o build/app-res.o @libs -o $executablePath
|
||||||
if ($LASTEXITCODE) { throw 'Link failed' }
|
if ($LASTEXITCODE) { throw 'Link failed' }
|
||||||
Copy-Item -LiteralPath $executablePath -Destination build/release/KSwitcher.exe -Force
|
Copy-Item -LiteralPath $executablePath -Destination build/release/KSwitcher.exe -Force
|
||||||
|
$configPath = Join-Path (Split-Path $executablePath) 'KSwitcher.Config.exe'
|
||||||
|
& $compiler -mwindows -municode -static -s '-Wl,--gc-sections' build/core.o build/platform.o build/storage.o build/ocr.o build/configuration.o build/config_main.o build/ui-config.o build/app-res.o @libs -o $configPath
|
||||||
|
if ($LASTEXITCODE) { throw 'Configurator link failed' }
|
||||||
|
Copy-Item -LiteralPath $configPath -Destination build/release/KSwitcher.Config.exe -Force
|
||||||
if ($Test) {
|
if ($Test) {
|
||||||
& $compiler @common tests/tests.cpp build/core.o build/platform.o build/autostart.o build/autocorrect.o build/storage.o build/ocr.o build/richtext.o build/update.o @libs -static -s -o build/KSwitcherTests.exe
|
& $compiler @common tests/tests.cpp build/core.o build/platform.o build/autostart.o build/autocorrect.o build/storage.o build/ocr.o build/richtext.o build/update.o @libs -static -s -o build/KSwitcherTests.exe
|
||||||
if ($LASTEXITCODE) { throw 'Test build failed' }
|
if ($LASTEXITCODE) { throw 'Test build failed' }
|
||||||
& .\build\KSwitcherTests.exe
|
& .\build\KSwitcherTests.exe
|
||||||
if ($LASTEXITCODE) { throw 'Tests failed' }
|
if ($LASTEXITCODE) { throw 'Tests failed' }
|
||||||
& $compiler @common tests/input.cpp build/core.o build/platform.o build/autostart.o build/autocorrect.o build/storage.o build/ocr.o build/richtext.o build/update.o build/app.o build/ui.o build/app-res.o @libs -static -s -o build/KSwitcherInputTests.exe
|
& $compiler @common tests/input.cpp build/core.o build/platform.o build/autostart.o build/autocorrect.o build/storage.o build/ocr.o build/richtext.o build/update.o build/app.o build/configuration.o build/ui.o build/app-res.o @libs -static -s -o build/KSwitcherInputTests.exe
|
||||||
if ($LASTEXITCODE) { throw 'Input test build failed' }
|
if ($LASTEXITCODE) { throw 'Input test build failed' }
|
||||||
& .\build\KSwitcherInputTests.exe
|
& .\build\KSwitcherInputTests.exe
|
||||||
if ($LASTEXITCODE) { throw 'Input tests failed' }
|
if ($LASTEXITCODE) { throw 'Input tests failed' }
|
||||||
@@ -37,7 +45,7 @@ try {
|
|||||||
if ($LASTEXITCODE) { throw 'Updater tests failed' }
|
if ($LASTEXITCODE) { throw 'Updater tests failed' }
|
||||||
}
|
}
|
||||||
if ($Integration) {
|
if ($Integration) {
|
||||||
& $compiler @common tests/integration.cpp build/core.o build/platform.o build/autostart.o build/autocorrect.o build/storage.o build/ocr.o build/richtext.o build/update.o build/app.o build/ui.o build/app-res.o @libs -static -s -o build/KSwitcherIntegration.exe
|
& $compiler @common tests/integration.cpp build/core.o build/platform.o build/autostart.o build/autocorrect.o build/storage.o build/ocr.o build/richtext.o build/update.o build/app.o build/configuration.o build/ui.o build/app-res.o @libs -static -s -o build/KSwitcherIntegration.exe
|
||||||
if ($LASTEXITCODE) { throw 'Integration test build failed' }
|
if ($LASTEXITCODE) { throw 'Integration test build failed' }
|
||||||
& .\build\KSwitcherIntegration.exe
|
& .\build\KSwitcherIntegration.exe
|
||||||
if ($LASTEXITCODE) { throw 'Integration tests failed' }
|
if ($LASTEXITCODE) { throw 'Integration tests failed' }
|
||||||
|
|||||||
+2
-1
@@ -4,12 +4,13 @@ $root = $PSScriptRoot
|
|||||||
$destination = Join-Path $root 'dist\KSwitcher'
|
$destination = Join-Path $root 'dist\KSwitcher'
|
||||||
New-Item -ItemType Directory -Force $destination | Out-Null
|
New-Item -ItemType Directory -Force $destination | Out-Null
|
||||||
Copy-Item -LiteralPath (Join-Path $root 'build\release\KSwitcher.exe') -Destination $destination
|
Copy-Item -LiteralPath (Join-Path $root 'build\release\KSwitcher.exe') -Destination $destination
|
||||||
|
Copy-Item -LiteralPath (Join-Path $root 'build\release\KSwitcher.Config.exe') -Destination $destination
|
||||||
Copy-Item -LiteralPath (Join-Path $root 'LICENSE') -Destination $destination
|
Copy-Item -LiteralPath (Join-Path $root 'LICENSE') -Destination $destination
|
||||||
Copy-Item -LiteralPath (Join-Path $root 'vendor\json-LICENSE.txt') -Destination $destination
|
Copy-Item -LiteralPath (Join-Path $root 'vendor\json-LICENSE.txt') -Destination $destination
|
||||||
Copy-Item -LiteralPath (Join-Path $root 'README.md') -Destination $destination
|
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
|
||||||
# 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','LICENSE','json-LICENSE.txt','README.md','dictionaries') | ForEach-Object { Join-Path $destination $_ }
|
$files = @('KSwitcher.exe','KSwitcher.Config.exe','LICENSE','json-LICENSE.txt','README.md','dictionaries') | ForEach-Object { Join-Path $destination $_ }
|
||||||
$archive = Join-Path $root 'dist\KSwitcher-win-x64.zip'
|
$archive = Join-Path $root 'dist\KSwitcher-win-x64.zip'
|
||||||
Compress-Archive -LiteralPath $files -DestinationPath $archive -Force
|
Compress-Archive -LiteralPath $files -DestinationPath $archive -Force
|
||||||
Get-FileHash -LiteralPath $archive -Algorithm SHA256
|
Get-FileHash -LiteralPath $archive -Algorithm SHA256
|
||||||
|
|||||||
+3
-3
@@ -5,8 +5,8 @@ IDI_KSWITCHER ICON "kswitcher.ico"
|
|||||||
1 RT_MANIFEST "app.manifest"
|
1 RT_MANIFEST "app.manifest"
|
||||||
201 RCDATA "update.ps1"
|
201 RCDATA "update.ps1"
|
||||||
1 VERSIONINFO
|
1 VERSIONINFO
|
||||||
FILEVERSION 0,2,3,0
|
FILEVERSION 0,3,0,0
|
||||||
PRODUCTVERSION 0,2,3,0
|
PRODUCTVERSION 0,3,0,0
|
||||||
FILEOS VOS_NT_WINDOWS32
|
FILEOS VOS_NT_WINDOWS32
|
||||||
FILETYPE VFT_APP
|
FILETYPE VFT_APP
|
||||||
BEGIN
|
BEGIN
|
||||||
@@ -16,7 +16,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.2.3"
|
VALUE "FileVersion", "0.3.0"
|
||||||
END
|
END
|
||||||
END
|
END
|
||||||
BLOCK "VarFileInfo"
|
BLOCK "VarFileInfo"
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ function Expand-Release([string]$Archive,[string]$Destination,[version]$Expected
|
|||||||
foreach ($entry in $zip.Entries) {
|
foreach ($entry in $zip.Entries) {
|
||||||
$name = $entry.FullName.Replace('\','/')
|
$name = $entry.FullName.Replace('\','/')
|
||||||
if ($name.EndsWith('/') -and $name -eq 'dictionaries/') { continue }
|
if ($name.EndsWith('/') -and $name -eq 'dictionaries/') { continue }
|
||||||
if ($name -cnotmatch '^(KSwitcher\.exe|LICENSE|json-LICENSE\.txt|README\.md|dictionaries/[a-zA-Z0-9_.-]+)$' -or
|
if ($name -cnotmatch '^(KSwitcher(?:\.Config)?\.exe|LICENSE|json-LICENSE\.txt|README\.md|dictionaries/[a-zA-Z0-9_.-]+)$' -or
|
||||||
$name.EndsWith('/.') -or $name.EndsWith('/..') -or $name.EndsWith('.') -or
|
$name.EndsWith('/.') -or $name.EndsWith('/..') -or $name.EndsWith('.') -or
|
||||||
(($entry.ExternalAttributes -shr 16) -band 0xF000) -eq 0xA000 -or !$seen.Add($name)) {
|
(($entry.ExternalAttributes -shr 16) -band 0xF000) -eq 0xA000 -or !$seen.Add($name)) {
|
||||||
throw "Unexpected or unsafe archive entry: $name"
|
throw "Unexpected or unsafe archive entry: $name"
|
||||||
@@ -88,8 +88,13 @@ function Expand-Release([string]$Archive,[string]$Destination,[version]$Expected
|
|||||||
foreach ($required in @('KSwitcher.exe','LICENSE','json-LICENSE.txt','README.md')) {
|
foreach ($required in @('KSwitcher.exe','LICENSE','json-LICENSE.txt','README.md')) {
|
||||||
if (!$seen.Contains($required)) { throw "Incomplete release: $required" }
|
if (!$seen.Contains($required)) { throw "Incomplete release: $required" }
|
||||||
}
|
}
|
||||||
|
if ($Expected -gt [version]'0.2.3' -and !$seen.Contains('KSwitcher.Config.exe')) {
|
||||||
|
throw 'Incomplete release: KSwitcher.Config.exe'
|
||||||
|
}
|
||||||
} finally { $zip.Dispose() }
|
} finally { $zip.Dispose() }
|
||||||
$exe = Join-Path $Destination 'KSwitcher.exe'
|
foreach ($binary in @('KSwitcher.exe','KSwitcher.Config.exe')) {
|
||||||
|
$exe = Join-Path $Destination $binary
|
||||||
|
if (!(Test-Path -LiteralPath $exe)) { continue }
|
||||||
$version = [Diagnostics.FileVersionInfo]::GetVersionInfo($exe)
|
$version = [Diagnostics.FileVersionInfo]::GetVersionInfo($exe)
|
||||||
if ($version.ProductName -ne 'KSwitcher' -or (Get-StableVersion $version.FileVersion) -ne $Expected) {
|
if ($version.ProductName -ne 'KSwitcher' -or (Get-StableVersion $version.FileVersion) -ne $Expected) {
|
||||||
throw 'Executable identity/version does not match the release.'
|
throw 'Executable identity/version does not match the release.'
|
||||||
@@ -99,6 +104,7 @@ function Expand-Release([string]$Archive,[string]$Destination,[version]$Expected
|
|||||||
$pe = [BitConverter]::ToInt32($bytes,60)
|
$pe = [BitConverter]::ToInt32($bytes,60)
|
||||||
if ($pe -lt 64 -or $pe -gt $bytes.Length-6 -or [BitConverter]::ToUInt32($bytes,$pe) -ne 0x4550 -or
|
if ($pe -lt 64 -or $pe -gt $bytes.Length-6 -or [BitConverter]::ToUInt32($bytes,$pe) -ne 0x4550 -or
|
||||||
[BitConverter]::ToUInt16($bytes,$pe+4) -ne 0x8664) { throw 'Release is not a Windows x64 executable.' }
|
[BitConverter]::ToUInt16($bytes,$pe+4) -ne 0x8664) { throw 'Release is not a Windows x64 executable.' }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
function Assert-PlainDirectory([string]$Directory) {
|
function Assert-PlainDirectory([string]$Directory) {
|
||||||
$item = Get-Item -LiteralPath $Directory -Force
|
$item = Get-Item -LiteralPath $Directory -Force
|
||||||
|
|||||||
+37
-2
@@ -1,6 +1,7 @@
|
|||||||
#include "app.h"
|
#include "app.h"
|
||||||
#include "../resources/resource.h"
|
#include "../resources/resource.h"
|
||||||
#include "ocr.h"
|
#include "ocr.h"
|
||||||
|
#include "configuration.h"
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <commctrl.h>
|
#include <commctrl.h>
|
||||||
#include <set>
|
#include <set>
|
||||||
@@ -398,6 +399,18 @@ void App::reloadLayouts()
|
|||||||
}
|
}
|
||||||
registerHotkeys(settings);
|
registerHotkeys(settings);
|
||||||
}
|
}
|
||||||
|
void App::clearHistory()
|
||||||
|
{
|
||||||
|
if (busy) throw std::runtime_error("Wait for the current operation to finish.");
|
||||||
|
if (history) history->clear();
|
||||||
|
if (historyWindow) refreshHistory(*this);
|
||||||
|
}
|
||||||
|
void App::setHotkeyCapture(HWND field)
|
||||||
|
{
|
||||||
|
hotkeyCapture = field;
|
||||||
|
hotkeyCaptureProcess = 0;
|
||||||
|
if (field) GetWindowThreadProcessId(field, &hotkeyCaptureProcess);
|
||||||
|
}
|
||||||
bool App::isHotkey(DWORD key, UINT mods)
|
bool App::isHotkey(DWORD key, UINT mods)
|
||||||
{
|
{
|
||||||
for (auto &[id, text] : shortcuts(settings))
|
for (auto &[id, text] : shortcuts(settings))
|
||||||
@@ -496,6 +509,10 @@ bool App::handleKeyboard(const KBDLLHOOKSTRUCT &key, bool down, UINT mods)
|
|||||||
// down AND up events use exactly the same path as physical keyboard input.
|
// down AND up events use exactly the same path as physical keyboard input.
|
||||||
if (ownKeyboardInput(key) || installingUpdate)
|
if (ownKeyboardInput(key) || installingUpdate)
|
||||||
return false;
|
return false;
|
||||||
|
DWORD captureProcess{};
|
||||||
|
if (hotkeyCapture) GetWindowThreadProcessId(hotkeyCapture, &captureProcess);
|
||||||
|
if (hotkeyCapture && (!captureProcess || captureProcess != hotkeyCaptureProcess))
|
||||||
|
setHotkeyCapture(nullptr);
|
||||||
if (hotkeyCapture && GetForegroundWindow() == GetAncestor(hotkeyCapture, GA_ROOT))
|
if (hotkeyCapture && GetForegroundWindow() == GetAncestor(hotkeyCapture, GA_ROOT))
|
||||||
{
|
{
|
||||||
bool modifier = key.vkCode == VK_CONTROL || key.vkCode == VK_SHIFT || key.vkCode == VK_MENU ||
|
bool modifier = key.vkCode == VK_CONTROL || key.vkCode == VK_SHIFT || key.vkCode == VK_MENU ||
|
||||||
@@ -579,6 +596,11 @@ void App::dispatch(int command)
|
|||||||
showSettings(*this);
|
showSettings(*this);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (command == 22)
|
||||||
|
{
|
||||||
|
showLogs(*this);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (command == 21)
|
if (command == 21)
|
||||||
{
|
{
|
||||||
showSnippets(*this);
|
showSnippets(*this);
|
||||||
@@ -1129,9 +1151,9 @@ void App::trayMenu()
|
|||||||
break;
|
break;
|
||||||
case 16:
|
case 16:
|
||||||
MessageBoxW(window,
|
MessageBoxW(window,
|
||||||
L"KSwitcher 0.2.2\nNative C++ / Win32 keyboard and clipboard utility\n\nBased on "
|
(std::wstring(L"KSwitcher ") + AppVersion + L"\nNative C++ / Win32 keyboard and clipboard utility\n\nBased on "
|
||||||
L"Typlo by bitcodepro. MIT license.\nHistory is encrypted for your Windows "
|
L"Typlo by bitcodepro. MIT license.\nHistory is encrypted for your Windows "
|
||||||
L"account.\nNo .NET, WebView or graphics framework.",
|
L"account.\nNo .NET, WebView or graphics framework.").c_str(),
|
||||||
L"About KSwitcher", MB_OK);
|
L"About KSwitcher", MB_OK);
|
||||||
break;
|
break;
|
||||||
case 17:
|
case 17:
|
||||||
@@ -1165,6 +1187,11 @@ void App::trayMenu()
|
|||||||
void App::checkUpdate(bool interactive, bool install, const std::wstring &version)
|
void App::checkUpdate(bool interactive, bool install, const std::wstring &version)
|
||||||
{
|
{
|
||||||
if (updateRunning) return;
|
if (updateRunning) return;
|
||||||
|
if (install && FindWindowW(L"KSwitcher.Config", nullptr))
|
||||||
|
{
|
||||||
|
report(L"Close the configurator before installing an update.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (install && busy)
|
if (install && busy)
|
||||||
{
|
{
|
||||||
report(L"Wait for the current text operation to finish, then select Update again.");
|
report(L"Wait for the current text operation to finish, then select Update again.");
|
||||||
@@ -1284,6 +1311,11 @@ LRESULT CALLBACK App::windowProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp)
|
|||||||
case WM_COPYDATA:
|
case WM_COPYDATA:
|
||||||
{
|
{
|
||||||
auto *data = (COPYDATASTRUCT *)lp;
|
auto *data = (COPYDATASTRUCT *)lp;
|
||||||
|
if (data && data->dwData >= ConfigRead && data->dwData < ConfigReply)
|
||||||
|
{
|
||||||
|
if (app->installingUpdate) return FALSE;
|
||||||
|
return configurationRequest(*app, (HWND)wp, *data);
|
||||||
|
}
|
||||||
if (data && data->dwData == 1 && data->cbData == sizeof(int))
|
if (data && data->dwData == 1 && data->cbData == sizeof(int))
|
||||||
{
|
{
|
||||||
int cmd;
|
int cmd;
|
||||||
@@ -1340,6 +1372,9 @@ LRESULT CALLBACK App::windowProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp)
|
|||||||
DestroyWindow(hwnd);
|
DestroyWindow(hwnd);
|
||||||
return 0;
|
return 0;
|
||||||
case WM_DESTROY:
|
case WM_DESTROY:
|
||||||
|
if (auto config = FindWindowW(L"KSwitcher.Config", nullptr);
|
||||||
|
config && configurationPeer(config, L"KSwitcher.Config.exe"))
|
||||||
|
PostMessageW(config, WM_CLOSE, 0, 0);
|
||||||
PostQuitMessage(0);
|
PostQuitMessage(0);
|
||||||
return 0;
|
return 0;
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ class App
|
|||||||
std::atomic<ULONGLONG> pauseUntil{0};
|
std::atomic<ULONGLONG> pauseUntil{0};
|
||||||
HWND pasteTarget{};
|
HWND pasteTarget{};
|
||||||
HWND hotkeyCapture{};
|
HWND hotkeyCapture{};
|
||||||
|
DWORD hotkeyCaptureProcess{};
|
||||||
HWND logWindow{};
|
HWND logWindow{};
|
||||||
std::mutex stateMutex;
|
std::mutex stateMutex;
|
||||||
Run run;
|
Run run;
|
||||||
@@ -66,6 +67,8 @@ class App
|
|||||||
bool registerHotkeys(const Settings &, bool rollback = false);
|
bool registerHotkeys(const Settings &, bool rollback = false);
|
||||||
void applySettings(const Settings &);
|
void applySettings(const Settings &);
|
||||||
void reloadLayouts();
|
void reloadLayouts();
|
||||||
|
void clearHistory();
|
||||||
|
void setHotkeyCapture(HWND);
|
||||||
bool isHotkey(DWORD, UINT);
|
bool isHotkey(DWORD, UINT);
|
||||||
void keyboard(const KBDLLHOOKSTRUCT &, UINT modifiers);
|
void keyboard(const KBDLLHOOKSTRUCT &, UINT modifiers);
|
||||||
bool handleKeyboard(const KBDLLHOOKSTRUCT &, bool down, UINT modifiers);
|
bool handleKeyboard(const KBDLLHOOKSTRUCT &, bool down, UINT modifiers);
|
||||||
|
|||||||
@@ -0,0 +1,205 @@
|
|||||||
|
#include "configuration.h"
|
||||||
|
#include <commctrl.h>
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
|
namespace ks
|
||||||
|
{
|
||||||
|
App *App::current = nullptr;
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
HWND service{};
|
||||||
|
Bytes response;
|
||||||
|
bool replied{};
|
||||||
|
Bytes request(App &app, ULONG_PTR command, const Bytes &payload = {})
|
||||||
|
{
|
||||||
|
if (!service || !configurationPeer(service, L"KSwitcher.exe"))
|
||||||
|
throw std::runtime_error("KSwitcher has stopped. Close and reopen the configurator.");
|
||||||
|
if (payload.size() > ConfigLimit) throw std::runtime_error("Settings are too large.");
|
||||||
|
response.clear(); replied = false;
|
||||||
|
COPYDATASTRUCT message{command, (DWORD)payload.size(), const_cast<unsigned char *>(payload.data())};
|
||||||
|
DWORD_PTR result{};
|
||||||
|
SetLastError(ERROR_SUCCESS);
|
||||||
|
if (!SendMessageTimeoutW(service, WM_COPYDATA, (WPARAM)app.window, (LPARAM)&message,
|
||||||
|
SMTO_ABORTIFHUNG | SMTO_ERRORONEXIT, 15000, &result) || !replied)
|
||||||
|
{
|
||||||
|
if (GetLastError() == ERROR_ACCESS_DENIED)
|
||||||
|
throw std::runtime_error("Open Settings from the KSwitcher tray menu so both processes use the same privileges.");
|
||||||
|
throw std::runtime_error("KSwitcher did not respond. The operation may still finish; reopen Settings to check.");
|
||||||
|
}
|
||||||
|
if (result != 1)
|
||||||
|
throw std::runtime_error(response.empty() ? "KSwitcher rejected the request." :
|
||||||
|
std::string(response.begin(), response.end()));
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
LRESULT CALLBACK controller(HWND window, UINT message, WPARAM wp, LPARAM lp)
|
||||||
|
{
|
||||||
|
auto *app = App::current;
|
||||||
|
if (!app) return DefWindowProcW(window, message, wp, lp);
|
||||||
|
if (message == WM_COPYDATA)
|
||||||
|
{
|
||||||
|
auto *data = reinterpret_cast<COPYDATASTRUCT *>(lp);
|
||||||
|
if ((HWND)wp == service && data && data->dwData == ConfigReply && data->cbData <= ConfigLimit &&
|
||||||
|
(!data->cbData || data->lpData))
|
||||||
|
{
|
||||||
|
response.clear();
|
||||||
|
if (data->cbData)
|
||||||
|
{
|
||||||
|
auto first = static_cast<const unsigned char *>(data->lpData);
|
||||||
|
response.assign(first, first + data->cbData);
|
||||||
|
}
|
||||||
|
replied = true;
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
if (message == WM_APP + 20)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (wp == 21) showSnippets(*app);
|
||||||
|
else showSettings(*app);
|
||||||
|
}
|
||||||
|
catch (const std::exception &e)
|
||||||
|
{
|
||||||
|
MessageBoxW(app->settingsWindow, wide(e.what()).c_str(), L"KSwitcher configurator", MB_OK | MB_ICONERROR);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (message == WM_CLOSE)
|
||||||
|
{
|
||||||
|
if (app->settingsWindow) DestroyWindow(app->settingsWindow);
|
||||||
|
if (app->snippetsWindow) DestroyWindow(app->snippetsWindow);
|
||||||
|
PostQuitMessage(0);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return DefWindowProcW(window, message, wp, lp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// The configurator owns no history store, dictionaries, worker or input hooks.
|
||||||
|
// All mutations are validated and committed by the resident process.
|
||||||
|
App::App(HINSTANCE h, bool, const std::filesystem::path &) : instance(h)
|
||||||
|
{
|
||||||
|
current = this;
|
||||||
|
layouts = readLayouts();
|
||||||
|
}
|
||||||
|
App::~App()
|
||||||
|
{
|
||||||
|
if (settingsWindow) DestroyWindow(settingsWindow);
|
||||||
|
if (snippetsWindow) DestroyWindow(snippetsWindow);
|
||||||
|
if (window) DestroyWindow(window);
|
||||||
|
current = nullptr;
|
||||||
|
}
|
||||||
|
void App::applySettings(const Settings &next)
|
||||||
|
{
|
||||||
|
validateSettings(next);
|
||||||
|
auto before = encodeSettings(settings), after = encodeSettings(next);
|
||||||
|
DWORD length = (DWORD)before.size();
|
||||||
|
Bytes payload(sizeof(length));
|
||||||
|
memcpy(payload.data(), &length, sizeof(length));
|
||||||
|
payload.insert(payload.end(), before.begin(), before.end());
|
||||||
|
payload.insert(payload.end(), after.begin(), after.end());
|
||||||
|
settings = decodeSettings(request(*this, ConfigSave, payload), layouts);
|
||||||
|
}
|
||||||
|
void App::reloadLayouts()
|
||||||
|
{
|
||||||
|
auto bytes = request(*this, ConfigLayouts);
|
||||||
|
layouts = readLayouts();
|
||||||
|
settings = decodeSettings(bytes, layouts);
|
||||||
|
}
|
||||||
|
void App::clearHistory() { request(*this, ConfigClearHistory); }
|
||||||
|
void App::setHotkeyCapture(HWND field)
|
||||||
|
{
|
||||||
|
hotkeyCapture = field;
|
||||||
|
Bytes payload(sizeof(field));
|
||||||
|
memcpy(payload.data(), &field, sizeof(field));
|
||||||
|
// Focus notifications must not let an exception escape a window callback.
|
||||||
|
try { request(*this, ConfigCapture, payload); }
|
||||||
|
catch (const std::exception &e)
|
||||||
|
{
|
||||||
|
hotkeyCapture = nullptr;
|
||||||
|
if (field) MessageBoxW(settingsWindow, wide(e.what()).c_str(), L"Shortcut capture unavailable", MB_OK | MB_ICONWARNING);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
void showLogs(App &app) { request(app, ConfigLogs); }
|
||||||
|
|
||||||
|
int runConfigurator(HINSTANCE instance, bool snippets)
|
||||||
|
{
|
||||||
|
HANDLE singleton = CreateMutexW(nullptr, TRUE, L"Local\\KSwitcher.Config.Singleton");
|
||||||
|
if (!singleton) throw std::runtime_error("Cannot create configurator instance lock.");
|
||||||
|
const bool existing = GetLastError() == ERROR_ALREADY_EXISTS;
|
||||||
|
struct Close { HANDLE handle; ~Close() { CloseHandle(handle); } } close{singleton};
|
||||||
|
if (existing)
|
||||||
|
{
|
||||||
|
HWND window{};
|
||||||
|
for (int i = 0; i < 100 && !window; ++i)
|
||||||
|
{
|
||||||
|
window = FindWindowW(L"KSwitcher.Config", nullptr);
|
||||||
|
if (!window) Sleep(50);
|
||||||
|
}
|
||||||
|
if (!window || !configurationPeer(window, L"KSwitcher.Config.exe"))
|
||||||
|
throw std::runtime_error("The configurator is starting or belongs to another installation. Try again shortly.");
|
||||||
|
DWORD pid{}; GetWindowThreadProcessId(window, &pid);
|
||||||
|
AllowSetForegroundWindow(pid);
|
||||||
|
PostMessageW(window, WM_APP + 20, snippets ? 21 : 20, 0);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
service = FindWindowW(L"KSwitcher.Main", L"KSwitcher");
|
||||||
|
if (!service)
|
||||||
|
{
|
||||||
|
auto path = std::filesystem::path(executable()).parent_path() / L"KSwitcher.exe";
|
||||||
|
auto command = L"\"" + path.wstring() + L"\"";
|
||||||
|
STARTUPINFOW startup{sizeof(startup)}; PROCESS_INFORMATION process{};
|
||||||
|
if (!CreateProcessW(path.c_str(), command.data(), nullptr, nullptr, FALSE, 0, nullptr,
|
||||||
|
path.parent_path().c_str(), &startup, &process))
|
||||||
|
throw std::runtime_error("Cannot start KSwitcher beside the configurator.");
|
||||||
|
CloseHandle(process.hThread); CloseHandle(process.hProcess);
|
||||||
|
for (int i = 0; i < 100 && !service; ++i)
|
||||||
|
{
|
||||||
|
Sleep(50); service = FindWindowW(L"KSwitcher.Main", L"KSwitcher");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!configurationPeer(service, L"KSwitcher.exe"))
|
||||||
|
throw std::runtime_error("Run the configurator from the active KSwitcher installation.");
|
||||||
|
INITCOMMONCONTROLSEX controls{sizeof(controls), ICC_STANDARD_CLASSES};
|
||||||
|
InitCommonControlsEx(&controls);
|
||||||
|
App app(instance);
|
||||||
|
WNDCLASSW cls{}; cls.hInstance = instance; cls.lpfnWndProc = controller; cls.lpszClassName = L"KSwitcher.Config";
|
||||||
|
RegisterClassW(&cls);
|
||||||
|
app.window = CreateWindowExW(WS_EX_TOOLWINDOW, cls.lpszClassName, L"KSwitcher configurator", WS_POPUP,
|
||||||
|
0, 0, 0, 0, nullptr, nullptr, instance, nullptr);
|
||||||
|
if (!app.window) throw std::runtime_error("Cannot create configurator window.");
|
||||||
|
app.settings = decodeSettings(request(app, ConfigRead), app.layouts);
|
||||||
|
if (snippets) showSnippets(app); else showSettings(app);
|
||||||
|
MSG message;
|
||||||
|
while ((app.settingsWindow || app.snippetsWindow) && GetMessageW(&message, nullptr, 0, 0) > 0)
|
||||||
|
{
|
||||||
|
if (app.settingsWindow && !IsWindowEnabled(app.settingsWindow))
|
||||||
|
{
|
||||||
|
HWND popup = GetLastActivePopup(app.settingsWindow);
|
||||||
|
if (popup != app.settingsWindow && IsDialogMessageW(popup, &message)) continue;
|
||||||
|
}
|
||||||
|
if (app.settingsWindow && IsDialogMessageW(app.settingsWindow, &message)) continue;
|
||||||
|
if (app.snippetsWindow && IsDialogMessageW(app.snippetsWindow, &message)) continue;
|
||||||
|
TranslateMessage(&message); DispatchMessageW(&message);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
int WINAPI wWinMain(HINSTANCE instance, HINSTANCE, PWSTR, int)
|
||||||
|
{
|
||||||
|
CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
|
||||||
|
int result = 1;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
int count{}; auto args = CommandLineToArgvW(GetCommandLineW(), &count);
|
||||||
|
bool snippets = count > 1 && std::wstring(args[1]) == L"--snippets";
|
||||||
|
LocalFree(args);
|
||||||
|
result = ks::runConfigurator(instance, snippets);
|
||||||
|
}
|
||||||
|
catch (const std::exception &e)
|
||||||
|
{
|
||||||
|
MessageBoxW(nullptr, ks::wide(e.what()).c_str(), L"KSwitcher configurator", MB_OK | MB_ICONERROR);
|
||||||
|
}
|
||||||
|
CoUninitialize();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
#include "configuration.h"
|
||||||
|
#include <cstring>
|
||||||
|
namespace ks
|
||||||
|
{
|
||||||
|
bool configurationPeer(HWND window, const wchar_t *filename)
|
||||||
|
{
|
||||||
|
DWORD pid{};
|
||||||
|
GetWindowThreadProcessId(window, &pid);
|
||||||
|
HANDLE process = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pid);
|
||||||
|
if (!process) return false;
|
||||||
|
wchar_t path[32768]; DWORD count = std::size(path);
|
||||||
|
bool ok = QueryFullProcessImageNameW(process, 0, path, &count) != 0;
|
||||||
|
CloseHandle(process);
|
||||||
|
auto expected = std::filesystem::path(executable()).parent_path() / filename;
|
||||||
|
return ok && _wcsicmp(path, expected.c_str()) == 0;
|
||||||
|
}
|
||||||
|
void launchConfiguration(App &app, bool snippets)
|
||||||
|
{
|
||||||
|
auto path = std::filesystem::path(executable()).parent_path() / L"KSwitcher.Config.exe";
|
||||||
|
if (!std::filesystem::exists(path))
|
||||||
|
throw std::runtime_error("KSwitcher.Config.exe is missing. Extract the complete application package.");
|
||||||
|
auto command = L"\"" + path.wstring() + L"\"" + (snippets ? L" --snippets" : L"");
|
||||||
|
STARTUPINFOW startup{sizeof(startup)};
|
||||||
|
PROCESS_INFORMATION process{};
|
||||||
|
if (!CreateProcessW(path.c_str(), command.data(), nullptr, nullptr, FALSE, 0, nullptr,
|
||||||
|
path.parent_path().c_str(), &startup, &process))
|
||||||
|
throw std::runtime_error(utf8(lastError(L"Cannot start configurator")));
|
||||||
|
AllowSetForegroundWindow(process.dwProcessId);
|
||||||
|
CloseHandle(process.hThread);
|
||||||
|
CloseHandle(process.hProcess);
|
||||||
|
(void)app;
|
||||||
|
}
|
||||||
|
LRESULT configurationRequest(App &app, HWND sender, const COPYDATASTRUCT &message)
|
||||||
|
{
|
||||||
|
if (!configurationPeer(sender, L"KSwitcher.Config.exe") || message.cbData > ConfigLimit ||
|
||||||
|
(message.cbData && !message.lpData)) return FALSE;
|
||||||
|
Bytes reply;
|
||||||
|
bool ok = false;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
switch (message.dwData)
|
||||||
|
{
|
||||||
|
case ConfigSave:
|
||||||
|
{
|
||||||
|
if (app.busy) throw std::runtime_error("Wait for the current text operation to finish.");
|
||||||
|
if (message.cbData < sizeof(DWORD)) throw std::runtime_error("Invalid settings request.");
|
||||||
|
DWORD length{};
|
||||||
|
memcpy(&length, message.lpData, sizeof(length));
|
||||||
|
if (length > message.cbData - sizeof(length)) throw std::runtime_error("Invalid settings snapshot.");
|
||||||
|
auto begin = static_cast<const unsigned char *>(message.lpData) + sizeof(length);
|
||||||
|
if (Bytes(begin, begin + length) != encodeSettings(app.settings))
|
||||||
|
throw std::runtime_error("Settings changed in KSwitcher. Close and reopen the configurator before saving.");
|
||||||
|
Bytes next(begin + length, static_cast<const unsigned char *>(message.lpData) + message.cbData);
|
||||||
|
if (next.empty()) throw std::runtime_error("Empty settings request.");
|
||||||
|
app.applySettings(decodeSettings(next, app.layouts));
|
||||||
|
reply = encodeSettings(app.settings);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case ConfigLayouts:
|
||||||
|
if (app.busy) throw std::runtime_error("Wait for the current text operation to finish.");
|
||||||
|
app.reloadLayouts();
|
||||||
|
[[fallthrough]];
|
||||||
|
case ConfigRead:
|
||||||
|
reply = encodeSettings(app.settings);
|
||||||
|
break;
|
||||||
|
case ConfigClearHistory:
|
||||||
|
app.clearHistory();
|
||||||
|
break;
|
||||||
|
case ConfigCapture:
|
||||||
|
{
|
||||||
|
if (message.cbData != sizeof(HWND)) throw std::runtime_error("Invalid shortcut field.");
|
||||||
|
HWND field{}; memcpy(&field, message.lpData, sizeof(field));
|
||||||
|
DWORD fieldPid{}, senderPid{};
|
||||||
|
GetWindowThreadProcessId(field, &fieldPid);
|
||||||
|
GetWindowThreadProcessId(sender, &senderPid);
|
||||||
|
if (field && fieldPid != senderPid) throw std::runtime_error("Invalid shortcut field owner.");
|
||||||
|
app.setHotkeyCapture(field);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case ConfigLogs:
|
||||||
|
PostMessageW(app.window, WM_HOTKEY, 22, 0);
|
||||||
|
break;
|
||||||
|
default: return FALSE;
|
||||||
|
}
|
||||||
|
ok = true;
|
||||||
|
}
|
||||||
|
catch (const std::exception &error)
|
||||||
|
{
|
||||||
|
std::string text = error.what();
|
||||||
|
reply.assign(text.begin(), text.end());
|
||||||
|
}
|
||||||
|
COPYDATASTRUCT response{ConfigReply, (DWORD)reply.size(), reply.data()};
|
||||||
|
DWORD_PTR received{};
|
||||||
|
if (!SendMessageTimeoutW(sender, WM_COPYDATA, (WPARAM)app.window, (LPARAM)&response,
|
||||||
|
SMTO_ABORTIFHUNG, 2000, &received)) return FALSE;
|
||||||
|
return ok ? 1 : 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
#pragma once
|
||||||
|
#include "app.h"
|
||||||
|
namespace ks
|
||||||
|
{
|
||||||
|
inline constexpr ULONG_PTR ConfigRead = 0x4b530100, ConfigSave = ConfigRead + 1,
|
||||||
|
ConfigLayouts = ConfigRead + 2, ConfigClearHistory = ConfigRead + 3,
|
||||||
|
ConfigCapture = ConfigRead + 4, ConfigLogs = ConfigRead + 5,
|
||||||
|
ConfigReply = ConfigRead + 6;
|
||||||
|
inline constexpr size_t ConfigLimit = 8 * 1024 * 1024;
|
||||||
|
bool configurationPeer(HWND, const wchar_t *filename);
|
||||||
|
void launchConfiguration(App &, bool snippets = false);
|
||||||
|
LRESULT configurationRequest(App &, HWND sender, const COPYDATASTRUCT &);
|
||||||
|
}
|
||||||
+11
-3
@@ -102,6 +102,10 @@ void validateSettings(const Settings &s)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Settings loadSettings(const std::filesystem::path &path, const std::vector<Layout> &layouts)
|
Settings loadSettings(const std::filesystem::path &path, const std::vector<Layout> &layouts)
|
||||||
|
{
|
||||||
|
return decodeSettings(std::filesystem::exists(path) ? readFile(path, 4 * 1024 * 1024) : Bytes{}, layouts);
|
||||||
|
}
|
||||||
|
Settings decodeSettings(const Bytes &bytes, const std::vector<Layout> &layouts)
|
||||||
{
|
{
|
||||||
Settings s;
|
Settings s;
|
||||||
for (size_t i = 0; i < layouts.size(); ++i)
|
for (size_t i = 0; i < layouts.size(); ++i)
|
||||||
@@ -111,9 +115,9 @@ Settings loadSettings(const std::filesystem::path &path, const std::vector<Layou
|
|||||||
s.direct.push_back(i < 3 ? L"Ctrl+Alt+Shift+" + std::to_wstring(i + 1) : L"");
|
s.direct.push_back(i < 3 ? L"Ctrl+Alt+Shift+" + std::to_wstring(i + 1) : L"");
|
||||||
s.directIds.push_back(layouts[i].id);
|
s.directIds.push_back(layouts[i].id);
|
||||||
}
|
}
|
||||||
if (!std::filesystem::exists(path))
|
if (bytes.empty())
|
||||||
return s;
|
return s;
|
||||||
auto j = parse(readFile(path, 4 * 1024 * 1024));
|
auto j = parse(bytes);
|
||||||
auto format = j.value("format", std::string());
|
auto format = j.value("format", std::string());
|
||||||
if (format != "kswitcher-settings" && format != "typlo-settings")
|
if (format != "kswitcher-settings" && format != "typlo-settings")
|
||||||
throw std::runtime_error("Not a KSwitcher or Typlo settings file");
|
throw std::runtime_error("Not a KSwitcher or Typlo settings file");
|
||||||
@@ -177,6 +181,10 @@ Settings loadSettings(const std::filesystem::path &path, const std::vector<Layou
|
|||||||
return s;
|
return s;
|
||||||
}
|
}
|
||||||
void saveSettings(const std::filesystem::path &path, const Settings &s)
|
void saveSettings(const std::filesystem::path &path, const Settings &s)
|
||||||
|
{
|
||||||
|
atomicWrite(path, encodeSettings(s));
|
||||||
|
}
|
||||||
|
Bytes encodeSettings(const Settings &s)
|
||||||
{
|
{
|
||||||
validateSettings(s);
|
validateSettings(s);
|
||||||
Json j = {{"format", "kswitcher-settings"},
|
Json j = {{"format", "kswitcher-settings"},
|
||||||
@@ -221,7 +229,7 @@ void saveSettings(const std::filesystem::path &path, const Settings &s)
|
|||||||
j["excludedApps"] = Json::array();
|
j["excludedApps"] = Json::array();
|
||||||
for (auto &name : s.excluded)
|
for (auto &name : s.excluded)
|
||||||
j["excludedApps"].push_back(utf8(name));
|
j["excludedApps"].push_back(utf8(name));
|
||||||
atomicWrite(path, jsonBytes(j));
|
return jsonBytes(j);
|
||||||
}
|
}
|
||||||
Bytes encodeClip(const Clip &clip)
|
Bytes encodeClip(const Clip &clip)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -4,6 +4,8 @@
|
|||||||
namespace ks
|
namespace ks
|
||||||
{
|
{
|
||||||
Settings loadSettings(const std::filesystem::path &, const std::vector<Layout> &);
|
Settings loadSettings(const std::filesystem::path &, const std::vector<Layout> &);
|
||||||
|
Settings decodeSettings(const Bytes &, const std::vector<Layout> &);
|
||||||
|
Bytes encodeSettings(const Settings &);
|
||||||
void saveSettings(const std::filesystem::path &, const Settings &);
|
void saveSettings(const std::filesystem::path &, const Settings &);
|
||||||
void validateSettings(const Settings &);
|
void validateSettings(const Settings &);
|
||||||
struct Entry
|
struct Entry
|
||||||
|
|||||||
+26
-10
@@ -1,5 +1,6 @@
|
|||||||
#include "../resources/resource.h"
|
#include "../resources/resource.h"
|
||||||
#include "app.h"
|
#include "app.h"
|
||||||
|
#include "configuration.h"
|
||||||
#include "ocr.h"
|
#include "ocr.h"
|
||||||
#include "richtext.h"
|
#include "richtext.h"
|
||||||
#include "ui_geometry.h"
|
#include "ui_geometry.h"
|
||||||
@@ -185,13 +186,13 @@ LRESULT CALLBACK hotkeyProc(HWND w, UINT msg, WPARAM wp, LPARAM lp, UINT_PTR, DW
|
|||||||
auto *app = (App *)data;
|
auto *app = (App *)data;
|
||||||
if (msg == WM_SETFOCUS)
|
if (msg == WM_SETFOCUS)
|
||||||
{
|
{
|
||||||
app->hotkeyCapture = w;
|
app->setHotkeyCapture(w);
|
||||||
SendMessageW(w, EM_SETSEL, 0, -1);
|
SendMessageW(w, EM_SETSEL, 0, -1);
|
||||||
}
|
}
|
||||||
if (msg == WM_KILLFOCUS || msg == WM_NCDESTROY)
|
if (msg == WM_KILLFOCUS || msg == WM_NCDESTROY)
|
||||||
{
|
{
|
||||||
if (app->hotkeyCapture == w)
|
if (app->hotkeyCapture == w)
|
||||||
app->hotkeyCapture = nullptr;
|
app->setHotkeyCapture(nullptr);
|
||||||
}
|
}
|
||||||
if (msg == CaptureHotkeyMessage)
|
if (msg == CaptureHotkeyMessage)
|
||||||
{
|
{
|
||||||
@@ -218,13 +219,13 @@ LRESULT CALLBACK hotkeyProc(HWND w, UINT msg, WPARAM wp, LPARAM lp, UINT_PTR, DW
|
|||||||
return 0;
|
return 0;
|
||||||
return DefSubclassProc(w, msg, wp, lp);
|
return DefSubclassProc(w, msg, wp, lp);
|
||||||
}
|
}
|
||||||
void hotkeyEdit(App &app, HWND w, int id, int x, int y, int width)
|
[[maybe_unused]] void hotkeyEdit(App &app, HWND w, int id, int x, int y, int width)
|
||||||
{
|
{
|
||||||
HWND field = edit(w, id, x, y, width, 25, ES_AUTOHSCROLL | ES_READONLY);
|
HWND field = edit(w, id, x, y, width, 25, ES_AUTOHSCROLL | ES_READONLY);
|
||||||
SendMessageW(field, EM_SETCUEBANNER, FALSE, (LPARAM)L"Click, then press a shortcut");
|
SendMessageW(field, EM_SETCUEBANNER, FALSE, (LPARAM)L"Click, then press a shortcut");
|
||||||
SetWindowSubclass(field, hotkeyProc, 1, (DWORD_PTR)&app);
|
SetWindowSubclass(field, hotkeyProc, 1, (DWORD_PTR)&app);
|
||||||
}
|
}
|
||||||
void checkbox(HWND w, const wchar_t *text, int id, int x, int y, int width = 300)
|
[[maybe_unused]] void checkbox(HWND w, const wchar_t *text, int id, int x, int y, int width = 300)
|
||||||
{
|
{
|
||||||
control(w, L"BUTTON", text, WS_TABSTOP | BS_AUTOCHECKBOX, id, x, y, width, 24);
|
control(w, L"BUTTON", text, WS_TABSTOP | BS_AUTOCHECKBOX, id, x, y, width, 24);
|
||||||
}
|
}
|
||||||
@@ -241,11 +242,11 @@ void value(HWND w, int id, const std::wstring &text)
|
|||||||
{
|
{
|
||||||
SetDlgItemTextW(w, id, text.c_str());
|
SetDlgItemTextW(w, id, text.c_str());
|
||||||
}
|
}
|
||||||
bool checked(HWND w, int id)
|
[[maybe_unused]] bool checked(HWND w, int id)
|
||||||
{
|
{
|
||||||
return IsDlgButtonChecked(w, id) == BST_CHECKED;
|
return IsDlgButtonChecked(w, id) == BST_CHECKED;
|
||||||
}
|
}
|
||||||
void check(HWND w, int id, bool yes)
|
[[maybe_unused]] void check(HWND w, int id, bool yes)
|
||||||
{
|
{
|
||||||
CheckDlgButton(w, id, yes ? BST_CHECKED : BST_UNCHECKED);
|
CheckDlgButton(w, id, yes ? BST_CHECKED : BST_UNCHECKED);
|
||||||
}
|
}
|
||||||
@@ -352,7 +353,7 @@ void resizeForDpi(HWND w, WPARAM wp, LPARAM lp)
|
|||||||
fitToMonitor(w, *(RECT *)lp);
|
fitToMonitor(w, *(RECT *)lp);
|
||||||
arrangeWindow(w);
|
arrangeWindow(w);
|
||||||
}
|
}
|
||||||
std::filesystem::path pickFile(HWND owner, bool save)
|
[[maybe_unused]] std::filesystem::path pickFile(HWND owner, bool save)
|
||||||
{
|
{
|
||||||
wchar_t path[32768]{};
|
wchar_t path[32768]{};
|
||||||
OPENFILENAMEW ofn{sizeof(ofn)};
|
OPENFILENAMEW ofn{sizeof(ofn)};
|
||||||
@@ -372,6 +373,7 @@ void disposeFont(HWND w)
|
|||||||
DeleteObject(h);
|
DeleteObject(h);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifndef KS_CONFIG_APP
|
||||||
struct SearchResult
|
struct SearchResult
|
||||||
{
|
{
|
||||||
uint64_t generation;
|
uint64_t generation;
|
||||||
@@ -755,6 +757,8 @@ LRESULT CALLBACK historyProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
|
|||||||
return DefWindowProcW(w, msg, wp, lp);
|
return DefWindowProcW(w, msg, wp, lp);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
|
#ifndef KS_RESIDENT_APP
|
||||||
struct SettingsView
|
struct SettingsView
|
||||||
{
|
{
|
||||||
App *app{};
|
App *app{};
|
||||||
@@ -1146,11 +1150,11 @@ LRESULT CALLBACK settingsProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
|
|||||||
fillSettings(w, v);
|
fillSettings(w, v);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
if (id == 254 && v->app->history)
|
if (id == 254)
|
||||||
{
|
{
|
||||||
if (MessageBoxW(w, L"Delete all clipboard history, including pinned items?", L"Clear history",
|
if (MessageBoxW(w, L"Delete all clipboard history, including pinned items?", L"Clear history",
|
||||||
MB_YESNO | MB_ICONWARNING) == IDYES)
|
MB_YESNO | MB_ICONWARNING) == IDYES)
|
||||||
v->app->history->clear();
|
v->app->clearHistory();
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -1294,6 +1298,8 @@ LRESULT CALLBACK snippetProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
|
|||||||
}
|
}
|
||||||
return DefWindowProcW(w, msg, wp, lp);
|
return DefWindowProcW(w, msg, wp, lp);
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
#ifndef KS_CONFIG_APP
|
||||||
LRESULT CALLBACK entryProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
|
LRESULT CALLBACK entryProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
|
||||||
{
|
{
|
||||||
if (msg == WM_SIZE)
|
if (msg == WM_SIZE)
|
||||||
@@ -1305,7 +1311,9 @@ LRESULT CALLBACK entryProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
|
|||||||
disposeFont(w);
|
disposeFont(w);
|
||||||
return DefWindowProcW(w, msg, wp, lp);
|
return DefWindowProcW(w, msg, wp, lp);
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
} // namespace
|
} // namespace
|
||||||
|
#ifndef KS_CONFIG_APP
|
||||||
void refreshHistory(App &app)
|
void refreshHistory(App &app)
|
||||||
{
|
{
|
||||||
if (app.historyWindow)
|
if (app.historyWindow)
|
||||||
@@ -1498,7 +1506,7 @@ void refreshLogView(HWND w, App &app)
|
|||||||
InvalidateRect(list, nullptr, TRUE);
|
InvalidateRect(list, nullptr, TRUE);
|
||||||
if (row)
|
if (row)
|
||||||
ListView_EnsureVisible(list, row - 1, FALSE);
|
ListView_EnsureVisible(list, row - 1, FALSE);
|
||||||
value(w, LogStatus, app.settings.writeLog ? L"Logging enabled: problems and suspicious repeats only."
|
value(w, LogStatus, app.settings.writeLog ? L"Logging enabled: failed operations only."
|
||||||
: L"Logging disabled. Showing previously saved entries.");
|
: L"Logging disabled. Showing previously saved entries.");
|
||||||
}
|
}
|
||||||
LRESULT CALLBACK logProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
|
LRESULT CALLBACK logProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
|
||||||
@@ -1609,6 +1617,8 @@ void showLogs(App &app)
|
|||||||
showRequestedWindow(w);
|
showRequestedWindow(w);
|
||||||
activateWindow(w);
|
activateWindow(w);
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
#ifndef KS_RESIDENT_APP
|
||||||
void showSettings(App &app, bool visible)
|
void showSettings(App &app, bool visible)
|
||||||
{
|
{
|
||||||
if (app.settingsWindow)
|
if (app.settingsWindow)
|
||||||
@@ -1699,6 +1709,11 @@ void showSnippets(App &app)
|
|||||||
showRequestedWindow(w);
|
showRequestedWindow(w);
|
||||||
SetForegroundWindow(w);
|
SetForegroundWindow(w);
|
||||||
}
|
}
|
||||||
|
#else
|
||||||
|
void showSettings(App &app, bool) { launchConfiguration(app); }
|
||||||
|
void showSnippets(App &app) { launchConfiguration(app, true); }
|
||||||
|
#endif
|
||||||
|
#ifndef KS_CONFIG_APP
|
||||||
void showShortcuts(App &app)
|
void showShortcuts(App &app)
|
||||||
{
|
{
|
||||||
auto &s = app.settings;
|
auto &s = app.settings;
|
||||||
@@ -1736,4 +1751,5 @@ void showEntry(App &app, const std::wstring &id)
|
|||||||
error(app.historyWindow, e);
|
error(app.historyWindow, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
} // namespace ks
|
} // namespace ks
|
||||||
|
|||||||
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
#include "platform.h"
|
#include "platform.h"
|
||||||
namespace ks
|
namespace ks
|
||||||
{
|
{
|
||||||
inline constexpr wchar_t AppVersion[] = L"0.2.3";
|
inline constexpr wchar_t AppVersion[] = L"0.3.0";
|
||||||
inline constexpr UINT UpdateMessage = WM_APP + 7;
|
inline constexpr UINT UpdateMessage = WM_APP + 7;
|
||||||
struct UpdateResult
|
struct UpdateResult
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,143 @@
|
|||||||
|
param([string]$BuildDirectory = (Join-Path $PSScriptRoot '../build/staged'))
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
if (Get-Process KSwitcher,'KSwitcher.Config' -ErrorAction SilentlyContinue) {
|
||||||
|
throw 'Close KSwitcher and its configurator before this isolated process test.'
|
||||||
|
}
|
||||||
|
Add-Type @'
|
||||||
|
using System;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
public static class ConfigurationTestUi {
|
||||||
|
[StructLayout(LayoutKind.Explicit, Size=40)] public struct Input {
|
||||||
|
[FieldOffset(0)] public uint type;
|
||||||
|
[FieldOffset(8)] public ushort key;
|
||||||
|
[FieldOffset(12)] public uint flags;
|
||||||
|
}
|
||||||
|
[DllImport("user32.dll")] public static extern uint SendInput(uint count,Input[] input,int size);
|
||||||
|
public static uint PressF24() { return SendInput(2,new Input[]{new Input{type=1,key=0x87},new Input{type=1,key=0x87,flags=2}},40); }
|
||||||
|
[DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow();
|
||||||
|
[DllImport("user32.dll", EntryPoint="SendMessageW", CharSet=CharSet.Unicode)] public static extern IntPtr GetText(IntPtr w,uint msg,IntPtr count,System.Text.StringBuilder text);
|
||||||
|
[DllImport("user32.dll", CharSet=CharSet.Unicode)] public static extern IntPtr FindWindow(string cls,string title);
|
||||||
|
[DllImport("user32.dll")] public static extern IntPtr GetDlgItem(IntPtr w,int id);
|
||||||
|
[DllImport("user32.dll")] public static extern bool PostMessage(IntPtr w,uint msg,IntPtr wp,IntPtr lp);
|
||||||
|
[DllImport("user32.dll")] public static extern IntPtr SendMessage(IntPtr w,uint msg,IntPtr wp,IntPtr lp);
|
||||||
|
[DllImport("user32.dll", EntryPoint="SendMessageW", CharSet=CharSet.Unicode)] public static extern IntPtr SetText(IntPtr w,uint msg,IntPtr wp,string text);
|
||||||
|
[DllImport("user32.dll")] public static extern uint GetWindowThreadProcessId(IntPtr w,out uint pid);
|
||||||
|
[DllImport("kernel32.dll")] public static extern uint GetCurrentThreadId();
|
||||||
|
[DllImport("user32.dll")] public static extern bool AttachThreadInput(uint a,uint b,bool attach);
|
||||||
|
[DllImport("user32.dll")] public static extern IntPtr SetFocus(IntPtr w);
|
||||||
|
public static bool FocusControl(IntPtr w) {
|
||||||
|
uint pid; uint target=GetWindowThreadProcessId(w,out pid), current=GetCurrentThreadId();
|
||||||
|
if(!AttachThreadInput(current,target,true)) return false;
|
||||||
|
try { SetFocus(w); return true; } finally { AttachThreadInput(current,target,false); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
'@
|
||||||
|
$root=Join-Path ([IO.Path]::GetTempPath()) ('KSwitcher-config-tests-'+[guid]::NewGuid().ToString('N'))
|
||||||
|
$processes=[Collections.Generic.List[Diagnostics.Process]]::new()
|
||||||
|
$checks=0
|
||||||
|
function Assert($ok,$label) { if (!$ok) { throw $label }; $script:checks++ }
|
||||||
|
function Window($class) {
|
||||||
|
for($i=0;$i -lt 100;$i++) {
|
||||||
|
$title=switch($class){'KSwitcher.Main'{'KSwitcher'};'KSwitcher.Settings'{'KSwitcher settings'};'KSwitcher.Snippets'{'Text snippets'}}
|
||||||
|
$window=[ConfigurationTestUi]::FindWindow($class,$title)
|
||||||
|
if($window -ne [IntPtr]::Zero){return $window}
|
||||||
|
Start-Sleep -Milliseconds 50
|
||||||
|
}
|
||||||
|
foreach($process in $processes){$process.Refresh(); Write-Host ("Diagnostic process {0}: exited={1} title={2}" -f $process.Id,$process.HasExited,$process.MainWindowTitle)}
|
||||||
|
throw "Window did not appear: $class"
|
||||||
|
}
|
||||||
|
function Launch($file,$argument) {
|
||||||
|
$options=@{FilePath=(Join-Path $root $file);PassThru=$true;WindowStyle='Hidden'}
|
||||||
|
if($argument){$options.ArgumentList=$argument}
|
||||||
|
$process=Start-Process @options
|
||||||
|
$processes.Add($process)
|
||||||
|
return $process
|
||||||
|
}
|
||||||
|
function Click($window,$id) {
|
||||||
|
$null=[ConfigurationTestUi]::PostMessage($window,0x111,[IntPtr]$id,[IntPtr]::Zero)
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
New-Item -ItemType Directory -Path (Join-Path $root 'data') -Force | Out-Null
|
||||||
|
foreach($file in @('KSwitcher.exe','KSwitcher.Config.exe')) {
|
||||||
|
Copy-Item -LiteralPath (Join-Path $BuildDirectory $file) -Destination $root
|
||||||
|
}
|
||||||
|
# No clipboard history capture, auto-correction, autostart or user data in the fixture.
|
||||||
|
$settingsPath=Join-Path $root 'data/settings.json'
|
||||||
|
@{format='kswitcher-settings';schemaVersion=1;historyEnabled=$false;autoCorrectLayout=$false;writeLog=$false} |
|
||||||
|
ConvertTo-Json | Set-Content -LiteralPath $settingsPath -Encoding utf8
|
||||||
|
$resident=Launch 'KSwitcher.exe' ''
|
||||||
|
$main=Window 'KSwitcher.Main'
|
||||||
|
$config=Launch 'KSwitcher.Config.exe' ''
|
||||||
|
$settings=Window 'KSwitcher.Settings'
|
||||||
|
[uint32]$ownerId=0
|
||||||
|
$null=[ConfigurationTestUi]::GetWindowThreadProcessId($settings,[ref]$ownerId)
|
||||||
|
Assert ($ownerId -eq $config.Id -and $ownerId -ne $resident.Id) 'settings live in the separate process'
|
||||||
|
$second=Launch 'KSwitcher.Config.exe' ''
|
||||||
|
Assert ($second.WaitForExit(6000) -and $second.ExitCode -eq 0) 'second launch forwards to existing instance'
|
||||||
|
Assert ((Window 'KSwitcher.Settings') -eq $settings) 'existing settings window reused'
|
||||||
|
# Only inject into the isolated fixture window, never another foreground app.
|
||||||
|
Assert ([ConfigurationTestUi]::GetForegroundWindow() -eq $settings) 'fixture owns foreground before shortcut capture test'
|
||||||
|
$field=[ConfigurationTestUi]::GetDlgItem($settings,210)
|
||||||
|
Assert ([ConfigurationTestUi]::FocusControl($field)) 'shortcut field receives actual focus'
|
||||||
|
Assert ([ConfigurationTestUi]::PressF24() -eq 2) 'test F24 events accepted'
|
||||||
|
$text=[Text.StringBuilder]::new(128)
|
||||||
|
for($i=0;$i -lt 100;$i++) {
|
||||||
|
$null=$text.Clear()
|
||||||
|
$null=[ConfigurationTestUi]::GetText($field,0x0d,[IntPtr]128,$text)
|
||||||
|
if($text.ToString() -eq 'F24'){break}
|
||||||
|
Start-Sleep -Milliseconds 20
|
||||||
|
}
|
||||||
|
Assert ($text.ToString() -eq 'F24') 'resident hook forwards shortcut capture to configurator'
|
||||||
|
$null=[ConfigurationTestUi]::SetText([ConfigurationTestUi]::GetDlgItem($settings,240),0x0c,[IntPtr]::Zero,'321')
|
||||||
|
Click $settings 1
|
||||||
|
Assert ($config.WaitForExit(6000) -and $config.ExitCode -eq 0) 'save exits configurator'
|
||||||
|
Assert ((Get-Content -LiteralPath $settingsPath -Raw | ConvertFrom-Json).maxItems -eq 321) 'save committed through resident process'
|
||||||
|
Assert ((Get-Content -LiteralPath $settingsPath -Raw | ConvertFrom-Json).toggleHotkey -eq 'F24') 'captured shortcut persisted by resident'
|
||||||
|
$resident.Refresh(); Assert (!$resident.HasExited) 'resident survives settings save'
|
||||||
|
$config=Launch 'KSwitcher.Config.exe' ''
|
||||||
|
$settings=Window 'KSwitcher.Settings'
|
||||||
|
$null=[ConfigurationTestUi]::SetText([ConfigurationTestUi]::GetDlgItem($settings,240),0x0c,[IntPtr]::Zero,'456')
|
||||||
|
Click $settings 2
|
||||||
|
Assert ($config.WaitForExit(6000)) 'cancel exits configurator'
|
||||||
|
Assert ((Get-Content -LiteralPath $settingsPath -Raw | ConvertFrom-Json).maxItems -eq 321) 'cancel preserves saved settings'
|
||||||
|
$config=Launch 'KSwitcher.Config.exe' '--snippets'
|
||||||
|
$snippets=Window 'KSwitcher.Snippets'
|
||||||
|
$second=Launch 'KSwitcher.Config.exe' ''
|
||||||
|
Assert ($second.WaitForExit(6000) -and $second.ExitCode -eq 0) 'settings and snippets share a single process'
|
||||||
|
$settings=Window 'KSwitcher.Settings'
|
||||||
|
Click $settings 2
|
||||||
|
Start-Sleep -Milliseconds 100
|
||||||
|
$config.Refresh(); Assert (!$config.HasExited) 'closing settings keeps open snippets alive'
|
||||||
|
Click $snippets 2
|
||||||
|
Assert ($config.WaitForExit(6000)) 'closing last window exits configurator'
|
||||||
|
$config=Launch 'KSwitcher.Config.exe' ''
|
||||||
|
$null=Window 'KSwitcher.Settings'
|
||||||
|
$null=[ConfigurationTestUi]::PostMessage($main,0x10,[IntPtr]::Zero,[IntPtr]::Zero)
|
||||||
|
Assert ($resident.WaitForExit(10000)) 'resident shuts down normally'
|
||||||
|
Assert ($config.WaitForExit(6000)) 'resident exit closes configurator'
|
||||||
|
$config=Launch 'KSwitcher.Config.exe' ''
|
||||||
|
$main=Window 'KSwitcher.Main'
|
||||||
|
[uint32]$bootstrappedId=0
|
||||||
|
$null=[ConfigurationTestUi]::GetWindowThreadProcessId($main,[ref]$bootstrappedId)
|
||||||
|
$bootstrapped=Get-Process -Id $bootstrappedId
|
||||||
|
$processes.Add($bootstrapped)
|
||||||
|
$settings=Window 'KSwitcher.Settings'
|
||||||
|
Assert ($bootstrappedId -ne $resident.Id) 'standalone configurator starts its resident sibling'
|
||||||
|
Click $settings 2
|
||||||
|
Assert ($config.WaitForExit(6000)) 'standalone configurator exits after cancel'
|
||||||
|
$null=[ConfigurationTestUi]::PostMessage($main,0x10,[IntPtr]::Zero,[IntPtr]::Zero)
|
||||||
|
Assert ($bootstrapped.WaitForExit(10000)) 'bootstrapped resident shuts down normally'
|
||||||
|
"Passed $checks configurator process checks"
|
||||||
|
} finally {
|
||||||
|
foreach($process in $processes) {
|
||||||
|
$process.Refresh()
|
||||||
|
if(!$process.HasExited) { Stop-Process -Id $process.Id -Force }
|
||||||
|
$process.Dispose()
|
||||||
|
}
|
||||||
|
# The generated fixture is the only recursive deletion target.
|
||||||
|
$resolved=[IO.Path]::GetFullPath($root)
|
||||||
|
$temp=[IO.Path]::GetFullPath([IO.Path]::GetTempPath())
|
||||||
|
if(!$resolved.StartsWith($temp,[StringComparison]::OrdinalIgnoreCase) -or
|
||||||
|
[IO.Path]::GetFileName($resolved) -notlike 'KSwitcher-config-tests-*') { throw 'Unexpected fixture path' }
|
||||||
|
Remove-Item -LiteralPath $resolved -Recurse -Force
|
||||||
|
}
|
||||||
@@ -389,6 +389,10 @@ int main(int argc, char **argv)
|
|||||||
settings.snippets = {{L";address", L"Line 1\r\n{cursor}Line 2"}};
|
settings.snippets = {{L";address", L"Line 1\r\n{cursor}Line 2"}};
|
||||||
ks::saveSettings(temp / L"settings.json", settings);
|
ks::saveSettings(temp / L"settings.json", settings);
|
||||||
auto restored = ks::loadSettings(temp / L"settings.json", {});
|
auto restored = ks::loadSettings(temp / L"settings.json", {});
|
||||||
|
require(ks::encodeSettings(ks::decodeSettings(ks::encodeSettings(settings), {})) == ks::encodeSettings(settings),
|
||||||
|
"configurator snapshot survives serialization without changing the concurrency token");
|
||||||
|
rejects([&] { ks::decodeSettings(ks::Bytes{'{', '}'}, {}); },
|
||||||
|
"configurator rejects missing settings format");
|
||||||
require(!restored.writeLog, "logging setting persists when disabled");
|
require(!restored.writeLog, "logging setting persists when disabled");
|
||||||
require(ks::loadSettings(temp / L"defaults.json", {}).writeLog,
|
require(ks::loadSettings(temp / L"defaults.json", {}).writeLog,
|
||||||
"logging defaults on for existing diagnostic installations");
|
"logging defaults on for existing diagnostic installations");
|
||||||
|
|||||||
@@ -30,6 +30,10 @@ try {
|
|||||||
}
|
}
|
||||||
$source=Join-Path $root 'source'; [IO.Directory]::CreateDirectory($source) | Out-Null
|
$source=Join-Path $root 'source'; [IO.Directory]::CreateDirectory($source) | Out-Null
|
||||||
Copy-Item -LiteralPath $Executable -Destination (Join-Path $source 'KSwitcher.exe')
|
Copy-Item -LiteralPath $Executable -Destination (Join-Path $source 'KSwitcher.exe')
|
||||||
|
$configurator = Join-Path (Split-Path $Executable) 'KSwitcher.Config.exe'
|
||||||
|
if (Test-Path -LiteralPath $configurator) {
|
||||||
|
Copy-Item -LiteralPath $configurator -Destination (Join-Path $source 'KSwitcher.Config.exe')
|
||||||
|
}
|
||||||
foreach ($name in @('LICENSE','json-LICENSE.txt','README.md')) { [IO.File]::WriteAllText((Join-Path $source $name),'fixture') }
|
foreach ($name in @('LICENSE','json-LICENSE.txt','README.md')) { [IO.File]::WriteAllText((Join-Path $source $name),'fixture') }
|
||||||
[IO.Directory]::CreateDirectory((Join-Path $source 'dictionaries')) | Out-Null
|
[IO.Directory]::CreateDirectory((Join-Path $source 'dictionaries')) | Out-Null
|
||||||
[IO.File]::WriteAllText((Join-Path $source 'dictionaries/en.txt'),'new dictionary')
|
[IO.File]::WriteAllText((Join-Path $source 'dictionaries/en.txt'),'new dictionary')
|
||||||
@@ -57,11 +61,15 @@ try {
|
|||||||
$changed=[Collections.Generic.List[string]]::new()
|
$changed=[Collections.Generic.List[string]]::new()
|
||||||
Install-Files $stage $target $backup $changed
|
Install-Files $stage $target $backup $changed
|
||||||
Assert ((Get-FileHash -LiteralPath (Join-Path $target 'KSwitcher.exe')).Hash -eq (Get-FileHash -LiteralPath $Executable).Hash) 'replacement installed'
|
Assert ((Get-FileHash -LiteralPath (Join-Path $target 'KSwitcher.exe')).Hash -eq (Get-FileHash -LiteralPath $Executable).Hash) 'replacement installed'
|
||||||
|
if (Test-Path -LiteralPath $configurator) {
|
||||||
|
Assert ((Get-FileHash -LiteralPath (Join-Path $target 'KSwitcher.Config.exe')).Hash -eq (Get-FileHash -LiteralPath $configurator).Hash) 'configurator installed'
|
||||||
|
}
|
||||||
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 '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 (!(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'
|
||||||
# Fail after the original exe has moved to backup, as if the staged file vanished.
|
# Fail after the original exe has moved to backup, as if the staged file vanished.
|
||||||
$stage2=Join-Path $root 'stage2'; [IO.Directory]::CreateDirectory($stage2) | Out-Null
|
$stage2=Join-Path $root 'stage2'; [IO.Directory]::CreateDirectory($stage2) | Out-Null
|
||||||
[IO.File]::WriteAllText((Join-Path $stage2 'KSwitcher.exe'),'new binary fixture')
|
[IO.File]::WriteAllText((Join-Path $stage2 'KSwitcher.exe'),'new binary fixture')
|
||||||
|
|||||||
@@ -6,6 +6,9 @@ try {
|
|||||||
$stop = Start-Process -FilePath $exe -ArgumentList '--exit' -WindowStyle Hidden -PassThru
|
$stop = Start-Process -FilePath $exe -ArgumentList '--exit' -WindowStyle Hidden -PassThru
|
||||||
if (-not $stop.WaitForExit(5000)) { throw 'Exit command timed out.' }
|
if (-not $stop.WaitForExit(5000)) { throw 'Exit command timed out.' }
|
||||||
foreach ($process in $running) { if (-not $process.WaitForExit(10000)) { throw 'KSwitcher did not stop.' } }
|
foreach ($process in $running) { if (-not $process.WaitForExit(10000)) { throw 'KSwitcher did not stop.' } }
|
||||||
|
$config = @(Get-Process 'KSwitcher.Config' -ErrorAction SilentlyContinue | Where-Object { $_.Path -eq (Join-Path $root 'build\KSwitcher.Config.exe') })
|
||||||
|
foreach ($process in $config) { if (-not $process.WaitForExit(10000)) { throw 'Close the configurator before updating.' } }
|
||||||
|
Copy-Item -LiteralPath (Join-Path $root 'build\staged\KSwitcher.Config.exe') -Destination (Join-Path $root 'build\KSwitcher.Config.exe') -Force
|
||||||
Copy-Item -LiteralPath (Join-Path $root 'build\staged\KSwitcher.exe') -Destination $exe -Force
|
Copy-Item -LiteralPath (Join-Path $root 'build\staged\KSwitcher.exe') -Destination $exe -Force
|
||||||
Start-Process -FilePath $exe -WindowStyle Hidden
|
Start-Process -FilePath $exe -WindowStyle Hidden
|
||||||
'OK' | Set-Content -LiteralPath (Join-Path $root 'build\update-result.txt')
|
'OK' | Set-Content -LiteralPath (Join-Path $root 'build\update-result.txt')
|
||||||
|
|||||||
Reference in New Issue
Block a user