Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
011664cc53 | ||
|
|
4d8d8d47b9 | ||
|
|
d0387904a0 |
No files matched your search
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
cmake_minimum_required(VERSION 3.24)
|
cmake_minimum_required(VERSION 3.24)
|
||||||
project(KSwitcher VERSION 0.6.57 LANGUAGES CXX RC)
|
project(KSwitcher VERSION 0.6.60 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()
|
||||||
|
|||||||
+6
-2
@@ -1,4 +1,4 @@
|
|||||||
# LAN mouse, keyboard and clipboard — v0.6.57 (experimental)
|
# LAN mouse, keyboard and clipboard — v0.6.58 (experimental)
|
||||||
|
|
||||||
KSwitcher discovers other enabled KSwitcher instances, pairs them with the same
|
KSwitcher discovers other enabled KSwitcher instances, pairs them with the same
|
||||||
code on both computers, and shares mouse and keyboard through a screen edge.
|
code on both computers, and shares mouse and keyboard through a screen edge.
|
||||||
@@ -10,7 +10,7 @@ is not implemented by clipboard synchronization.
|
|||||||
|
|
||||||
## Set up multiple computers
|
## Set up multiple computers
|
||||||
|
|
||||||
1. Install the v0.6.57 or newer on every participating PC. Input protocol v2 requires all participants to update; earlier one-pair builds cannot connect. Preserve local data; do not copy pairing identities between PCs.
|
1. Install the v0.6.58 or newer on every participating PC. Input protocol v2 requires all participants to update; earlier one-pair builds cannot connect. Preserve local data; do not copy pairing identities between PCs.
|
||||||
2. Open Settings → Tools → KSwitcher computers and enable discovery on both PCs.
|
2. Open Settings → Tools → KSwitcher computers and enable discovery on both PCs.
|
||||||
Only running LAN-enabled KSwitcher instances appear, not generic network devices.
|
Only running LAN-enabled KSwitcher instances appear, not generic network devices.
|
||||||
3. Match the computer name and instance-ID prefix displayed on the other PC.
|
3. Match the computer name and instance-ID prefix displayed on the other PC.
|
||||||
@@ -116,3 +116,7 @@ Three physical computers still need an end-to-end trial for their adapters,
|
|||||||
firewalls, displays and application privilege levels. The automated TCP tests
|
firewalls, displays and application privilege levels. The automated TCP tests
|
||||||
use local sockets, and input-state tests use simulated desktop environments;
|
use local sockets, and input-state tests use simulated desktop environments;
|
||||||
they do not claim a completed three-machine hardware test.
|
they do not claim a completed three-machine hardware test.
|
||||||
|
|
||||||
|
## v0.6.58 connection fixes
|
||||||
|
|
||||||
|
Saved trust keeps TCP listeners available even when discovery announcements are missed. Working authenticated input sessions no longer depend on discovery presence. Pair saved is distinct from input connected; the list refreshes every second. A changed instance ID requires a new pairing. Keep each PC's own data folder during upgrades. Connection state changes and listener Windows errors are written to the diagnostic log.
|
||||||
@@ -10,14 +10,18 @@ The original [Typlo](https://github.com/bitcodepro/Typlo) project and its data r
|
|||||||
|
|
||||||
[Donate via Monobank](https://send.monobank.ua/jar/88h2TddKfS) · Thank you for supporting KSwitcher.
|
[Donate via Monobank](https://send.monobank.ua/jar/88h2TddKfS) · Thank you for supporting KSwitcher.
|
||||||
|
|
||||||
## KSwitcher computers — v0.6.57 (experimental)
|
## KSwitcher computers — v0.6.58 (experimental)
|
||||||
|
|
||||||
|
TCP input listeners and authenticated connections survive missed discovery announcements.
|
||||||
|
The list distinguishes saved pairing from connected input and refreshes every second.
|
||||||
|
Keep each PC’s own data folder during updates; a different instance ID requires new pairing.
|
||||||
|
|
||||||
Arrange multiple paired PCs on a monitor grid. Mouse and keyboard follow shared
|
Arrange multiple paired PCs on a monitor grid. Mouse and keyboard follow shared
|
||||||
edges, including A → B → C and back. New text and images go to every online paired
|
edges, including A → B → C and back. New text and images go to every online paired
|
||||||
PC's history and active clipboard. The history Computers menu filters by multiple
|
PC's history and active clipboard. The history Computers menu filters by multiple
|
||||||
sources using checkboxes. See [LAN-PREVIEW.md](LAN-PREVIEW.md).
|
sources using checkboxes. See [LAN-PREVIEW.md](LAN-PREVIEW.md).
|
||||||
|
|
||||||
**Update every participating PC to v0.6.57:** input protocol v2 is incompatible
|
**Update every participating PC to v0.6.58:** input protocol v2 is incompatible
|
||||||
with older one-pair builds. Keep each PC's own data directory. Pair every input
|
with older one-pair builds. Keep each PC's own data directory. Pair every input
|
||||||
source with all destinations, arrange the same physical layout on each PC and
|
source with all destinations, arrange the same physical layout on each PC and
|
||||||
enable input sharing. Empty cells and offline neighbors block passage.
|
enable input sharing. Empty cells and offline neighbors block passage.
|
||||||
@@ -25,7 +29,7 @@ enable input sharing. Empty cells and offline neighbors block passage.
|
|||||||
**Українською.** Розставте кілька спарованих ПК по сітці: миша й клавіатура
|
**Українською.** Розставте кілька спарованих ПК по сітці: миша й клавіатура
|
||||||
переходять через спільний край, зокрема A → B → C і назад. Нові текст та зображення
|
переходять через спільний край, зокрема A → B → C і назад. Нові текст та зображення
|
||||||
передаються всім доступним спарованим ПК в історію й активний буфер. Фільтр
|
передаються всім доступним спарованим ПК в історію й активний буфер. Фільтр
|
||||||
Computers в історії має чекбокси. Оновіть усі ПК до v0.6.57, залишивши кожному
|
Computers в історії має чекбокси. Оновіть усі ПК до v0.6.58, залишивши кожному
|
||||||
власну папку data. Спаруйте джерело вводу з усіма потрібними ПК й задайте
|
власну папку data. Спаруйте джерело вводу з усіма потрібними ПК й задайте
|
||||||
однакове фізичне розташування на кожному. Передача файлів не реалізована.
|
однакове фізичне розташування на кожному. Передача файлів не реалізована.
|
||||||
|
|
||||||
@@ -586,8 +590,8 @@ Nonconflicting changes synchronize immediately, and unresolved conflicts survive
|
|||||||
- **Upload local** creates the remote profile or replaces it with this PC's saved shared settings.
|
- **Upload local** creates the remote profile or replaces it with this PC's saved shared settings.
|
||||||
- **Download remote** replaces this PC's shared settings with the server profile.
|
- **Download remote** replaces this PC's shared settings with the server profile.
|
||||||
- If the server has no profile yet, choose **Upload local** to create it.
|
- If the server has no profile yet, choose **Upload local** to create it.
|
||||||
- Unsaved edits in Settings are not synchronized. Close and reopen Settings after a download;
|
- Unsaved edits in Settings are not synchronized. After a successful **Download remote**, the open Settings window refreshes automatically and keeps the selected tab. The download confirmation warns that unsaved edits will be discarded.
|
||||||
the existing stale-editor check prevents an old Settings window overwriting downloaded changes.
|
- The status distinguishes downloading, downloaded settings, no local changes and failure. HTTP 400 errors identify authentication, download or upload; field errors remain visible until the next action.
|
||||||
|
|
||||||
Once connected, the resident checks at startup, every **10 minutes**, and about two seconds after saving settings, on a separate
|
Once connected, the resident checks at startup, every **10 minutes**, and about two seconds after saving settings, on a separate
|
||||||
network thread. Errors back off for 15 minutes; the Sync button allows a manual retry. Local use
|
network thread. Errors back off for 15 minutes; the Sync button allows a manual retry. Local use
|
||||||
@@ -719,9 +723,11 @@ default and require the main **Automatic correction (experimental)** checkbox to
|
|||||||
`првиіт=привіт` or `helo=hello`. Entries are normalized to lowercase; duplicate source words
|
`првиіт=привіт` or `helo=hello`. Entries are normalized to lowercase; duplicate source words
|
||||||
are rejected. The typo must contain 1–32 letters. The correction may contain several words
|
are rejected. The typo must contain 1–32 letters. The correction may contain several words
|
||||||
separated by single spaces, for example `мамамила=мама мила`, up to 32 characters including
|
separated by single spaces, for example `мамамила=мама мила`, up to 32 characters including
|
||||||
spaces. Both sides must use the same supported language, without punctuation; leading,
|
spaces. Corrections also allow apostrophes (`'`, `’`, `ʼ`) between letters, for example
|
||||||
|
`ревю=рев'ю`. Both sides must use the same supported language, without other punctuation; leading,
|
||||||
trailing and repeated spaces are not allowed. Explicit pairs take priority over dictionary recognition, including
|
trailing and repeated spaces are not allowed. Explicit pairs take priority over dictionary recognition, including
|
||||||
already valid words, and do not require dictionary membership or satisfy the frequency limit.
|
already valid words, and do not require dictionary membership or satisfy the frequency limit.
|
||||||
|
Update every synchronizing PC to v0.6.60 or newer before sharing apostrophe pairs; older clients reject them.
|
||||||
- **Correct spelling typos using dictionaries**: checks unknown words in the current language
|
- **Correct spelling typos using dictionaries**: checks unknown words in the current language
|
||||||
after no layout correction is found. It accepts exactly one dictionary candidate reachable
|
after no layout correction is found. It accepts exactly one dictionary candidate reachable
|
||||||
by inserting, deleting or replacing one letter, or swapping two adjacent letters. Both input
|
by inserting, deleting or replacing one letter, or swapping two adjacent letters. Both input
|
||||||
|
|||||||
Binary file not shown.
+8
-10
@@ -1,19 +1,15 @@
|
|||||||
# Latest Windows release
|
# Latest Windows release
|
||||||
|
|
||||||
## v0.6.57 — 2026-11-06
|
## v0.6.60 — 2026-11-09
|
||||||
|
|
||||||
## Changes
|
## Changes
|
||||||
- Settings Save merges independent edits with newer resident settings. Same-field conflicts offer a choice; declining keeps the window and draft open.
|
- Fix custom auto-correction pairs such as `ревю=рев'ю`: correction text now accepts straight (`'`), typographic (`’`) and modifier-letter (`ʼ`) apostrophes between letters, both when saving settings and applying replacements.
|
||||||
- Multiple paired PCs on a draggable monitor grid; mouse and keyboard follow neighboring edges, including A → B → C and back. Connections reconnect independently.
|
- Preserve the exact apostrophe, initial capitalization, surrounding punctuation and current layout. Multi-word corrections remain supported, with a maximum of 32 characters including spaces and apostrophes.
|
||||||
- New text, formatted text and screenshots synchronize to all online paired PCs: history and active clipboard. Newer local copies are preserved during incoming transfers.
|
- Use the same correction validator for settings and runtime. Source words still contain only letters; leading/trailing or adjacent apostrophes, apostrophes next to spaces, other punctuation and mixed-language replacements remain invalid. Update the validation message and user documentation.
|
||||||
- Clipboard history has a persistent Computers checkbox filter and source labels. Lossless PNG transport avoids duplicate image payloads.
|
- Retain all v0.6.59 sync, LAN, clipboard, screenshot, snippet and chat-profile behavior. No new setting, shortcut or default.
|
||||||
- LAN modifier chords, DPI-scaled motion, cursor parking/return, automatic conversion ownership and queued wheel reversal fixes. Local shortcut latches reset on ownership changes.
|
|
||||||
- Stable paired status, pairing/input button states, persistent selection and a fixed-size LAN window with scrolling where needed.
|
|
||||||
|
|
||||||
## Validation
|
## Validation
|
||||||
Passed: 5 settings-save merge checks, 203 LAN input/three-peer TCP, 289 clipboard transfer, 43 pairing, 828 settings/chat UI, 570 input, 341 native and 126 sync checks; dictionary equivalence for 140,939 words; wheel, StayHere, Utilities, screenshot UI/save/registration, 54 updater checks and updater progress. Three physical PCs have not been verified together. LAN remains experimental; follow LAN-PREVIEW.md.
|
Passed: 349 native checks (including apostrophe validation, correction, capitalization/punctuation and settings roundtrips), 571 input/UI, 829 chat-profile/UI, 132 sync, 205 LAN input, 289 LAN clipboard, 43 LAN pairing and 5 settings-save merge checks; dictionary equivalence for 140,939 words; wheel, StayHere, Utilities, screenshot UI/save/registration, 54 updater checks and updater progress. Release packaging verifies the manifest, all file hashes and the ZIP checksum.
|
||||||
|
|
||||||
**Update ALL LAN participants to v0.6.57. Input protocol v2 cannot connect to older one-pair builds.** Pair every physical input source with each destination. Configure the same physical arrangement on each PC; layouts are not automatically synchronized. File transfer, offline clipboard backfill, cross-PC dragging and secure desktops remain unsupported.
|
|
||||||
|
|
||||||
## Upgrade and compatibility
|
## Upgrade and compatibility
|
||||||
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.
|
||||||
@@ -21,6 +17,8 @@ For manual installation, exit KSwitcher, close its configurator and screenshot e
|
|||||||
|
|
||||||
**Update every synchronizing PC to v0.6.51 or newer before sharing application-specific snippets. Older versions do not understand application restrictions and may treat scoped snippets as global.** Keep a backup before downgrading. Settings with Telegram Web profiles require v0.6.51 or newer. Chat profiles now synchronize from v0.6.53. Update every synchronizing PC to v0.6.53 or newer before syncing: older clients reject the chat-profile field. Synchronization stores application names, visible chat names and language IDs on your configured OpenBao server. No message contents are included.
|
**Update every synchronizing PC to v0.6.51 or newer before sharing application-specific snippets. Older versions do not understand application restrictions and may treat scoped snippets as global.** Keep a backup before downgrading. Settings with Telegram Web profiles require v0.6.51 or newer. Chat profiles now synchronize from v0.6.53. Update every synchronizing PC to v0.6.53 or newer before syncing: older clients reject the chat-profile field. Synchronization stores application names, visible chat names and language IDs on your configured OpenBao server. No message contents are included.
|
||||||
|
|
||||||
|
**Update every synchronizing PC to v0.6.60 or newer before sharing replacement pairs containing apostrophes. Older clients reject these settings.** Existing letter-only and phrase pairs require no migration.
|
||||||
|
|
||||||
Download **KSwitcher-win-x64.zip** with **SHA256SUMS.txt**. The package contains no personal settings, credentials or clipboard history.
|
Download **KSwitcher-win-x64.zip** with **SHA256SUMS.txt**. 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 @@
|
|||||||
f3ec4f7a62b4479929d94d6d6f7f68edb8b4a47b84edac7258afb64dbe07aef2 KSwitcher-win-x64.zip
|
31e6d5571db106154e3b72391c4f13b856523e602c84a1f8a2935f05c9508854 KSwitcher-win-x64.zip
|
||||||
+35
-35
@@ -1,86 +1,86 @@
|
|||||||
{
|
{
|
||||||
"version": "0.6.57",
|
|
||||||
"schema": 1,
|
|
||||||
"files": [
|
"files": [
|
||||||
{
|
{
|
||||||
"size": 2106368,
|
"sha256": "a7be2f749082bb6417b4cf7e8c8ea221ef4f7ba63dabc3cbbe8694ab1a3d942c",
|
||||||
"path": "KSwitcher.exe",
|
"path": "KSwitcher.exe",
|
||||||
"sha256": "3b7315e1545f70739fd1a7c099ddeab28362b323521d93879fa43fdd76133f8e"
|
"size": 2108416
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"size": 2003456,
|
"sha256": "c8a88364c9967ce5fd150519d632b14460e225a0cfae51a8de50f39782d8010c",
|
||||||
"path": "KSwitcher.Config.exe",
|
"path": "KSwitcher.Config.exe",
|
||||||
"sha256": "a819ec42030990725326730396680388cbcb870dd00c0a97856cb1615f9db27b"
|
"size": 2005504
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"size": 3151360,
|
"sha256": "68eed5abefed0208ef7bdd431094c7357c53bd3ca03dfd91aaeb7dbb4f291e06",
|
||||||
"path": "KSwitcher.Screenshot.exe",
|
"path": "KSwitcher.Screenshot.exe",
|
||||||
"sha256": "8473697245c2c961496135eaaff6de9e9a19e917f2fd7299a16068210223d396"
|
"size": 3153408
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"size": 4401,
|
"sha256": "d3c81140cce01c26bf6cbf52aa40e0825e1fa35b9a91d1387170a65d2597082c",
|
||||||
"path": "LICENSE",
|
"path": "LICENSE",
|
||||||
"sha256": "d3c81140cce01c26bf6cbf52aa40e0825e1fa35b9a91d1387170a65d2597082c"
|
"size": 4401
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"size": 1096,
|
"sha256": "61e08da79c44061654015ce6b0c4061c23ee1dd18237fbf09e9bfda3c0ea2831",
|
||||||
"path": "json-LICENSE.txt",
|
"path": "json-LICENSE.txt",
|
||||||
"sha256": "61e08da79c44061654015ce6b0c4061c23ee1dd18237fbf09e9bfda3c0ea2831"
|
"size": 1096
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"size": 79896,
|
"sha256": "87f396a6bf988725b14bf5b6eb7c2b60ce33ff51828290ce40e136c66942cc09",
|
||||||
"path": "README.md",
|
"path": "README.md",
|
||||||
"sha256": "8b4b1e905a2f0a39d532e61136ec7eca473492a39a538eb073ed829307adefae"
|
"size": 80168
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"size": 7921,
|
"sha256": "72e131bdf46d32a8a17fb352292bc8983c0e5a7ed84812218b11cf6b69c9afc4",
|
||||||
"path": "LAN-PREVIEW.md",
|
"path": "LAN-PREVIEW.md",
|
||||||
"sha256": "2467b86bbad7d7dce1e0c65c8c0faf2320e4804e90a85784a058afa357127b6b"
|
"size": 8376
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"size": 732485,
|
"sha256": "d0c022b27b9fbc2ff7b6b8bec723cc1adc4eef517202f9ff77eed5d8bea3acb9",
|
||||||
"path": "dictionaries/en.txt",
|
"path": "dictionaries/en.txt",
|
||||||
"sha256": "d0c022b27b9fbc2ff7b6b8bec723cc1adc4eef517202f9ff77eed5d8bea3acb9"
|
"size": 732485
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"size": 20566,
|
"sha256": "0bdfb0201d1803634f268cdff0993b8349d2c70e956a06dbb667c8dd65254ec5",
|
||||||
"path": "dictionaries/LICENSE-CC-BY-SA-4.0.txt",
|
"path": "dictionaries/LICENSE-CC-BY-SA-4.0.txt",
|
||||||
"sha256": "0bdfb0201d1803634f268cdff0993b8349d2c70e956a06dbb667c8dd65254ec5"
|
"size": 20566
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"size": 2265,
|
"sha256": "3017bf44d3be9d9dfab21bd6778aa52b7947e61acf9ce2bc86b41d6e949ac311",
|
||||||
"path": "dictionaries/README.md",
|
"path": "dictionaries/README.md",
|
||||||
"sha256": "3017bf44d3be9d9dfab21bd6778aa52b7947e61acf9ce2bc86b41d6e949ac311"
|
"size": 2265
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"size": 1109134,
|
"sha256": "80e6b6a27815973535f7facb636c597eb845dfee45cfabfc4e9b3341b9332849",
|
||||||
"path": "dictionaries/ru.txt",
|
"path": "dictionaries/ru.txt",
|
||||||
"sha256": "80e6b6a27815973535f7facb636c597eb845dfee45cfabfc4e9b3341b9332849"
|
"size": 1109134
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"size": 1280,
|
"sha256": "98e5b3fe7304e37705c9e5ff48b0b399ccb16e806736d21f09921b99e5f2a408",
|
||||||
"path": "dictionaries/sources.json",
|
"path": "dictionaries/sources.json",
|
||||||
"sha256": "98e5b3fe7304e37705c9e5ff48b0b399ccb16e806736d21f09921b99e5f2a408"
|
"size": 1280
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"size": 940799,
|
"sha256": "d30e990d6ec2983c04f3644a3c61c6c668e8c542323f5931916fdd55b916eebf",
|
||||||
"path": "dictionaries/uk.txt",
|
"path": "dictionaries/uk.txt",
|
||||||
"sha256": "d30e990d6ec2983c04f3644a3c61c6c668e8c542323f5931916fdd55b916eebf"
|
"size": 940799
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"size": 993,
|
"sha256": "a34b5161a8315af037e1ec71d816bfc688f7c364141d4ee2347cf0515ef0c82a",
|
||||||
"path": "tools/README.txt",
|
"path": "tools/README.txt",
|
||||||
"sha256": "a34b5161a8315af037e1ec71d816bfc688f7c364141d4ee2347cf0515ef0c82a"
|
"size": 993
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"size": 134656,
|
"sha256": "6ef5cf64708d62b7fc63c3c793cc99188a513433ff4774cae01dc014f6664376",
|
||||||
"path": "tools/StayHere.exe",
|
"path": "tools/StayHere.exe",
|
||||||
"sha256": "6ef5cf64708d62b7fc63c3c793cc99188a513433ff4774cae01dc014f6664376"
|
"size": 134656
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"size": 2796,
|
"sha256": "ba214e2aca7405e698b3ab21ceb1f41ce088edd996b3e30e4a9d58ff496f9413",
|
||||||
"path": "tools/StayHere-README.md",
|
"path": "tools/StayHere-README.md",
|
||||||
"sha256": "ba214e2aca7405e698b3ab21ceb1f41ce088edd996b3e30e4a9d58ff496f9413"
|
"size": 2796
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
"schema": 1,
|
||||||
|
"version": "0.6.58"
|
||||||
}
|
}
|
||||||
+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,57,0
|
FILEVERSION 0,6,60,0
|
||||||
PRODUCTVERSION 0,6,57,0
|
PRODUCTVERSION 0,6,60,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.57"
|
VALUE "FileVersion", "0.6.60"
|
||||||
END
|
END
|
||||||
END
|
END
|
||||||
BLOCK "VarFileInfo"
|
BLOCK "VarFileInfo"
|
||||||
|
|||||||
+16
-2
@@ -13,7 +13,7 @@ std::wstring App::syncCommand(int action, const std::vector<SyncChoice> &choices
|
|||||||
validateSyncConfig(c);
|
validateSyncConfig(c);
|
||||||
if (action == 4 && (c != conflictConnection || choices.empty()))
|
if (action == 4 && (c != conflictConnection || choices.empty()))
|
||||||
throw std::runtime_error("Connection changed or no choices selected. Sync again.");
|
throw std::runtime_error("Connection changed or no choices selected. Sync again.");
|
||||||
if (busy) return L"Wait for the current text operation to finish.";
|
if (busy) return syncStatus = L"Sync did not start. Wait for the current text operation to finish, then try again.";
|
||||||
syncRetryAfter = 0; // Explicit retries may bypass the automatic error backoff.
|
syncRetryAfter = 0; // Explicit retries may bypass the automatic error backoff.
|
||||||
if (syncWorker.joinable()) syncWorker.join();
|
if (syncWorker.joinable()) syncWorker.join();
|
||||||
syncConnection = c;
|
syncConnection = c;
|
||||||
@@ -22,7 +22,8 @@ std::wstring App::syncCommand(int action, const std::vector<SyncChoice> &choices
|
|||||||
auto availableLayouts = layouts;
|
auto availableLayouts = layouts;
|
||||||
syncRunning = true;
|
syncRunning = true;
|
||||||
syncDone = false;
|
syncDone = false;
|
||||||
syncStatus = L"Connecting to OpenBao...";
|
syncStatus = action == 2 ? L"Downloading remote settings from OpenBao..." :
|
||||||
|
action == 3 ? L"Uploading local settings to OpenBao..." : L"Connecting to OpenBao...";
|
||||||
nextSync = GetTickCount64() + SyncIntervalMs;
|
nextSync = GetTickCount64() + SyncIntervalMs;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -42,6 +43,9 @@ std::wstring App::syncCommand(int action, const std::vector<SyncChoice> &choices
|
|||||||
}
|
}
|
||||||
catch (const nlohmann::json::exception &) { result.status = L"Invalid sync data or server response."; }
|
catch (const nlohmann::json::exception &) { result.status = L"Invalid sync data or server response."; }
|
||||||
catch (const std::exception &e) { result.status = wide(e.what()); }
|
catch (const std::exception &e) { result.status = wide(e.what()); }
|
||||||
|
if (!result.success)
|
||||||
|
result.status = std::wstring(action == 2 ? L"Download failed. Local settings were not changed. " :
|
||||||
|
action == 3 ? L"Upload failed. " : L"Synchronization failed. ") + result.status;
|
||||||
syncResult = std::move(result);
|
syncResult = std::move(result);
|
||||||
syncDone.store(true);
|
syncDone.store(true);
|
||||||
});
|
});
|
||||||
@@ -76,8 +80,18 @@ void App::pollSync()
|
|||||||
{
|
{
|
||||||
atomicWrite(data / L"settings-before-sync.bin", protect(snapshot));
|
atomicWrite(data / L"settings-before-sync.bin", protect(snapshot));
|
||||||
applySettings(next);
|
applySettings(next);
|
||||||
|
syncStatus += L" Changes applied on this PC; local backup saved.";
|
||||||
}
|
}
|
||||||
|
else syncStatus += result.conflicts.empty() ? L" This PC's settings are already up to date; no local changes needed." :
|
||||||
|
L" No local settings were changed; conflicts still need your choice.";
|
||||||
saveSyncBase(data, syncConnection, result.base.empty() ? result.settings : result.base);
|
saveSyncBase(data, syncConnection, result.base.empty() ? result.settings : result.base);
|
||||||
|
if (result.downloaded)
|
||||||
|
{
|
||||||
|
if (settingsWindow) SendMessageW(settingsWindow, ConfigSettingsDownloaded, 0, 0);
|
||||||
|
if (auto config = FindWindowW(L"KSwitcher.Config", nullptr);
|
||||||
|
config && configurationPeer(config, L"KSwitcher.Config.exe"))
|
||||||
|
PostMessageW(config, ConfigSettingsDownloaded, 0, 0);
|
||||||
|
}
|
||||||
pendingSyncConflicts = std::move(result.conflicts);
|
pendingSyncConflicts = std::move(result.conflicts);
|
||||||
conflictConnection = syncConnection;
|
conflictConnection = syncConnection;
|
||||||
SYSTEMTIME time{}; GetLocalTime(&time);
|
SYSTEMTIME time{}; GetLocalTime(&time);
|
||||||
|
|||||||
+1
-1
@@ -257,7 +257,7 @@ std::optional<AutoSuggestion> AutoCorrector::suggest(const std::wstring &typed,
|
|||||||
{
|
{
|
||||||
auto replacement = settings.autoReplacements.find(small);
|
auto replacement = settings.autoReplacements.find(small);
|
||||||
if (replacement != settings.autoReplacements.end() &&
|
if (replacement != settings.autoReplacements.end() &&
|
||||||
replacement->second.find_first_not_of(alphabet + L" ") == small.npos)
|
validAutoCorrection(replacement->second, alphabet))
|
||||||
return sameLayout(replacement->second);
|
return sameLayout(replacement->second);
|
||||||
}
|
}
|
||||||
auto personal = [&](const std::wstring &language, const std::wstring &candidate)
|
auto personal = [&](const std::wstring &language, const std::wstring &candidate)
|
||||||
|
|||||||
@@ -52,6 +52,19 @@ LRESULT CALLBACK controller(HWND window, UINT message, WPARAM wp, LPARAM lp)
|
|||||||
}
|
}
|
||||||
return FALSE;
|
return FALSE;
|
||||||
}
|
}
|
||||||
|
if (message == ConfigSettingsDownloaded)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
app->settings = decodeSettings(request(*app, ConfigRead), app->layouts);
|
||||||
|
if (app->settingsWindow) SendMessageW(app->settingsWindow, ConfigSettingsDownloaded, 0, 0);
|
||||||
|
}
|
||||||
|
catch (const std::exception &e)
|
||||||
|
{
|
||||||
|
MessageBoxW(app->settingsWindow, wide(e.what()).c_str(), L"Unable to refresh downloaded settings", MB_OK | MB_ICONERROR);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
if (message == WM_APP + 20)
|
if (message == WM_APP + 20)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
#include "app.h"
|
#include "app.h"
|
||||||
namespace ks
|
namespace ks
|
||||||
{
|
{
|
||||||
|
inline constexpr UINT ConfigSettingsDownloaded = WM_APP + 24;
|
||||||
inline constexpr ULONG_PTR ConfigRead = 0x4b530100, ConfigSave = ConfigRead + 1,
|
inline constexpr ULONG_PTR ConfigRead = 0x4b530100, ConfigSave = ConfigRead + 1,
|
||||||
ConfigLayouts = ConfigRead + 2, ConfigClearHistory = ConfigRead + 3,
|
ConfigLayouts = ConfigRead + 2, ConfigClearHistory = ConfigRead + 3,
|
||||||
ConfigCapture = ConfigRead + 4, ConfigLogs = ConfigRead + 5,
|
ConfigCapture = ConfigRead + 4, ConfigLogs = ConfigRead + 5,
|
||||||
|
|||||||
@@ -21,6 +21,20 @@ int snippetAppPriority(const Snippet &snippet, const std::wstring &process)
|
|||||||
for (const auto &app : snippet.apps) if (snippetAppName(app) == name && !name.empty()) return 2;
|
for (const auto &app : snippet.apps) if (snippetAppName(app) == name && !name.empty()) return 2;
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
bool validAutoCorrection(const std::wstring &text, const std::wstring &alphabet)
|
||||||
|
{
|
||||||
|
if (text.empty() || text.size() > 32) return false;
|
||||||
|
auto isLetter = [&](wchar_t c) { return alphabet.find(c) != alphabet.npos; };
|
||||||
|
for (size_t i = 0; i < text.size(); ++i)
|
||||||
|
{
|
||||||
|
if (isLetter(text[i])) continue;
|
||||||
|
if (text[i] != L' ' && text[i] != L'\'' && text[i] != L'\u2019' && text[i] != L'\u02bc')
|
||||||
|
return false;
|
||||||
|
if (i == 0 || i + 1 == text.size() || !isLetter(text[i - 1]) || !isLetter(text[i + 1]))
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
int HotkeyHook::update(UINT key, bool down, UINT modifiers,
|
int HotkeyHook::update(UINT key, bool down, UINT modifiers,
|
||||||
const std::vector<std::pair<int, Hotkey>> &bindings)
|
const std::vector<std::pair<int, Hotkey>> &bindings)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ struct Layout
|
|||||||
bool hasLetter(wchar_t ch) const;
|
bool hasLetter(wchar_t ch) const;
|
||||||
};
|
};
|
||||||
bool letter(wchar_t ch);
|
bool letter(wchar_t ch);
|
||||||
|
bool validAutoCorrection(const std::wstring &text, const std::wstring &alphabet);
|
||||||
std::wstring lower(std::wstring text);
|
std::wstring lower(std::wstring text);
|
||||||
std::wstring upper(std::wstring text);
|
std::wstring upper(std::wstring text);
|
||||||
std::wstring convert(const std::wstring &, const Layout &, const Layout &, bool force = false);
|
std::wstring convert(const std::wstring &, const Layout &, const Layout &, bool force = false);
|
||||||
|
|||||||
+2
-2
@@ -242,8 +242,8 @@ struct LanService::Impl
|
|||||||
if(peers.contains(target))send(socket,peers.at(target).address,packet);
|
if(peers.contains(target))send(socket,peers.at(target).address,packet);
|
||||||
});
|
});
|
||||||
clipboardActive=clipboard->active();
|
clipboardActive=clipboard->active();
|
||||||
if(shareInput)for(auto &[peer,entry]:trusted.items())if(peers.contains(peer))
|
if(shareInput)for(auto &[peer,entry]:trusted.items())
|
||||||
endpoints.push_back({peer,entry.at("key"),peers.at(peer).address.sin_addr.s_addr});
|
endpoints.push_back({peer,entry.at("key"),peers.contains(peer)?peers.at(peer).address.sin_addr.s_addr:0});
|
||||||
if(now>=nextHello)
|
if(now>=nextHello)
|
||||||
{
|
{
|
||||||
epoch=lan::randomHex(16); // Fresh challenge prevents replayed presence from staying online.
|
epoch=lan::randomHex(16); // Fresh challenge prevents replayed presence from staying online.
|
||||||
|
|||||||
+16
-5
@@ -207,7 +207,7 @@ std::string InputChannel::status()const{return impl->state;}
|
|||||||
void InputChannel::disconnect(){impl->reset(GetTickCount64(),"Input disconnected; local control restored.");}
|
void InputChannel::disconnect(){impl->reset(GetTickCount64(),"Input disconnected; local control restored.");}
|
||||||
struct InputHub::Impl
|
struct InputHub::Impl
|
||||||
{
|
{
|
||||||
struct Link { InputEndpoint endpoint;std::unique_ptr<InputChannel> channel;uint64_t generation{},epoch{}; };
|
struct Link { InputEndpoint endpoint;std::unique_ptr<InputChannel> channel;uint64_t generation{},epoch{};std::string lastState; };
|
||||||
struct Pending {SOCKET socket;uint32_t address;uint64_t since;};
|
struct Pending {SOCKET socket;uint32_t address;uint64_t since;};
|
||||||
std::string local;uint16_t port;SOCKET listener=INVALID_SOCKET;
|
std::string local;uint16_t port;SOCKET listener=INVALID_SOCKET;
|
||||||
std::map<std::string,Link> links;std::vector<Pending> pending;uint64_t epoch{},retry{};
|
std::map<std::string,Link> links;std::vector<Pending> pending;uint64_t epoch{},retry{};
|
||||||
@@ -217,19 +217,29 @@ struct InputHub::Impl
|
|||||||
void poll(const std::vector<InputEndpoint> &endpoints,const std::vector<RoutedInput> &out,std::vector<RoutedInput> &in,uint64_t now)
|
void poll(const std::vector<InputEndpoint> &endpoints,const std::vector<RoutedInput> &out,std::vector<RoutedInput> &in,uint64_t now)
|
||||||
{
|
{
|
||||||
for(auto it=links.begin();it!=links.end();){
|
for(auto it=links.begin();it!=links.end();){
|
||||||
auto match=std::find_if(endpoints.begin(),endpoints.end(),[&](auto &e){return e.id==it->first&&e.key==it->second.endpoint.key&&e.port==it->second.endpoint.port&&e.address==it->second.endpoint.address;});
|
auto match=std::find_if(endpoints.begin(),endpoints.end(),[&](auto &e){return e.id==it->first&&e.key==it->second.endpoint.key&&e.port==it->second.endpoint.port;});
|
||||||
if(match==endpoints.end())it=links.erase(it);else ++it;
|
if(match==endpoints.end())it=links.erase(it);else ++it;
|
||||||
}
|
}
|
||||||
for(auto &e:endpoints)if(e.id!=local&&!links.contains(e.id)){
|
for(auto &e:endpoints)if(e.id!=local&&!links.contains(e.id)){
|
||||||
Link link;link.endpoint=e;link.channel=std::make_unique<InputChannel>(local,e.id,e.key,0,e.port,true);link.epoch=++epoch;links.emplace(e.id,std::move(link));
|
Link link;link.endpoint=e;link.channel=std::make_unique<InputChannel>(local,e.id,e.key,0,e.port,true);link.epoch=++epoch;links.emplace(e.id,std::move(link));
|
||||||
}
|
}
|
||||||
|
// Discovery is a hint, not the lifetime of authenticated input sessions.
|
||||||
|
// Keep the last usable address across missed announcements. A new address
|
||||||
|
// is used only once a channel is down, never to tear down working input.
|
||||||
|
for(auto &e:endpoints){if(e.id==local)continue;auto &link=links.at(e.id);
|
||||||
|
if(e.address&&!link.channel->ready()&&e.address!=link.endpoint.address){
|
||||||
|
link.channel->disconnect();link.endpoint.address=e.address;
|
||||||
|
}
|
||||||
|
}
|
||||||
bool listenNeeded=std::any_of(endpoints.begin(),endpoints.end(),[&](auto &e){return e.id<local;});
|
bool listenNeeded=std::any_of(endpoints.begin(),endpoints.end(),[&](auto &e){return e.id<local;});
|
||||||
if(!listenNeeded){closeSocket(listener);for(auto &p:pending)closeSocket(p.socket);pending.clear();}
|
if(!listenNeeded){closeSocket(listener);for(auto &p:pending)closeSocket(p.socket);pending.clear();}
|
||||||
if(listenNeeded&&listener==INVALID_SOCKET&&now>=retry){
|
if(listenNeeded&&listener==INVALID_SOCKET&&now>=retry){
|
||||||
listener=::socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);BOOL yes=TRUE;sockaddr_in a{};a.sin_family=AF_INET;a.sin_port=htons(port);
|
listener=::socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);BOOL yes=TRUE;sockaddr_in a{};a.sin_family=AF_INET;a.sin_port=htons(port);
|
||||||
if(listener==INVALID_SOCKET||setsockopt(listener,SOL_SOCKET,SO_EXCLUSIVEADDRUSE,(char*)&yes,sizeof(yes))||!nonblocking(listener)||bind(listener,(sockaddr*)&a,sizeof(a))||listen(listener,SOMAXCONN)){
|
if(listener==INVALID_SOCKET||setsockopt(listener,SOL_SOCKET,SO_EXCLUSIVEADDRUSE,(char*)&yes,sizeof(yes))||!nonblocking(listener)||bind(listener,(sockaddr*)&a,sizeof(a))||listen(listener,SOMAXCONN)){
|
||||||
closeSocket(listener);retry=now+1500;error="TCP input port is unavailable.";
|
auto detail="TCP input port "+std::to_string(port)+" is unavailable (Windows error "+std::to_string(WSAGetLastError())+").";
|
||||||
}else error.clear();
|
if(error!=detail)log(L"LAN input: "+wide(detail));
|
||||||
|
closeSocket(listener);retry=now+1500;error=std::move(detail);
|
||||||
|
}else {error.clear();log(L"LAN input listener ready on TCP "+std::to_wstring(port));}
|
||||||
}
|
}
|
||||||
if(listener!=INVALID_SOCKET)for(int n=0;n<16&&pending.size()<64;++n){
|
if(listener!=INVALID_SOCKET)for(int n=0;n<16&&pending.size()<64;++n){
|
||||||
sockaddr_in a{};int size=sizeof(a);SOCKET sock=accept(listener,(sockaddr*)&a,&size);if(sock==INVALID_SOCKET)break;
|
sockaddr_in a{};int size=sizeof(a);SOCKET sock=accept(listener,(sockaddr*)&a,&size);if(sock==INVALID_SOCKET)break;
|
||||||
@@ -243,7 +253,7 @@ struct InputHub::Impl
|
|||||||
else if(count>=int(length+4)){
|
else if(count>=int(length+4)){
|
||||||
done=true;
|
done=true;
|
||||||
try{auto hello=Json::parse(bytes+4,bytes+4+length);auto id=hello.value("id","");auto link=links.find(id);
|
try{auto hello=Json::parse(bytes+4,bytes+4+length);auto id=hello.value("id","");auto link=links.find(id);
|
||||||
if(id<local&&hello.value("to","")==local&&hello.value("version",0)==2&&link!=links.end()&&link->second.endpoint.address==it->address&&link->second.channel->adopt(uintptr_t(it->socket),now))it->socket=INVALID_SOCKET;
|
if(id<local&&hello.value("to","")==local&&hello.value("version",0)==2&&link!=links.end()&&link->second.channel->adopt(uintptr_t(it->socket),now))it->socket=INVALID_SOCKET;
|
||||||
}catch(const std::exception &){}
|
}catch(const std::exception &){}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -252,6 +262,7 @@ struct InputHub::Impl
|
|||||||
for(auto &[id,link]:links){
|
for(auto &[id,link]:links){
|
||||||
std::vector<InputFrame> frames,received;for(auto &f:out)if(f.peer==id&&(!f.epoch||f.epoch==link.epoch))enqueueInput(frames,f.frame);
|
std::vector<InputFrame> frames,received;for(auto &f:out)if(f.peer==id&&(!f.epoch||f.epoch==link.epoch))enqueueInput(frames,f.frame);
|
||||||
link.channel->poll(link.endpoint.address,frames,received,now);
|
link.channel->poll(link.endpoint.address,frames,received,now);
|
||||||
|
auto state=link.channel->status();if(state!=link.lastState){link.lastState=state;log(L"LAN input peer "+wide(id.substr(0,8))+L": "+wide(state));}
|
||||||
if(link.generation!=link.channel->generation()){link.generation=link.channel->generation();link.epoch=++epoch;}
|
if(link.generation!=link.channel->generation()){link.generation=link.channel->generation();link.epoch=++epoch;}
|
||||||
for(auto &f:received)in.push_back({id,f,link.epoch});
|
for(auto &f:received)in.push_back({id,f,link.epoch});
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-3
@@ -138,10 +138,9 @@ void validateSettings(const Settings &s)
|
|||||||
valid |= !from.empty() && !to.empty() && from.size() <= 32 && to.size() <= 32 &&
|
valid |= !from.empty() && !to.empty() && from.size() <= 32 && to.size() <= 32 &&
|
||||||
from == lower(from) && to == lower(to) && from != to &&
|
from == lower(from) && to == lower(to) && from != to &&
|
||||||
from.find_first_not_of(alphabet) == from.npos &&
|
from.find_first_not_of(alphabet) == from.npos &&
|
||||||
to.find_first_not_of(std::wstring(alphabet) + L" ") == to.npos &&
|
validAutoCorrection(to, alphabet);
|
||||||
to.front() != L' ' && to.back() != L' ' && to.find(L" ") == to.npos;
|
|
||||||
if (!valid)
|
if (!valid)
|
||||||
throw std::runtime_error("Use a lowercase typo of 1-32 letters and a different correction of 1-32 characters in the same language. Single spaces between correction words are allowed.");
|
throw std::runtime_error("Use a lowercase typo of 1-32 letters and a different correction of 1-32 characters in the same language. Single spaces between words and apostrophes between letters are allowed.");
|
||||||
}
|
}
|
||||||
for (const auto &[language, words] : s.autoWords)
|
for (const auto &[language, words] : s.autoWords)
|
||||||
{
|
{
|
||||||
|
|||||||
+14
-6
@@ -39,7 +39,7 @@ class OpenBao final : public SyncProvider
|
|||||||
SyncConfig config;
|
SyncConfig config;
|
||||||
SyncTransport transport;
|
SyncTransport transport;
|
||||||
std::wstring token;
|
std::wstring token;
|
||||||
Json response(unsigned status, const Bytes &result, bool missing)
|
Json response(unsigned status, const Bytes &result, bool missing, const std::wstring &path, const wchar_t *method)
|
||||||
{
|
{
|
||||||
if (result.size() > Limit) throw std::runtime_error("OpenBao response is too large.");
|
if (result.size() > Limit) throw std::runtime_error("OpenBao response is too large.");
|
||||||
if (status == 404 && missing)
|
if (status == 404 && missing)
|
||||||
@@ -50,7 +50,14 @@ class OpenBao final : public SyncProvider
|
|||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
if (status == 401 || status == 403) throw std::runtime_error("OpenBao denied access. Check login and profile policy.");
|
if (status == 401 || status == 403) throw std::runtime_error("OpenBao denied access. Check login and profile policy.");
|
||||||
if (status == 400) throw std::runtime_error("OpenBao rejected the write or its version changed. Sync again; check KV v2 configuration.");
|
if (status == 400)
|
||||||
|
{
|
||||||
|
if (path.starts_with(L"/v1/auth/"))
|
||||||
|
throw std::runtime_error("OpenBao rejected the authentication request (HTTP 400). Check username, password and login mount.");
|
||||||
|
if (std::wstring_view(method) == L"GET")
|
||||||
|
throw std::runtime_error("OpenBao rejected the download request (HTTP 400). Check KV v2 mount and profile path.");
|
||||||
|
throw std::runtime_error("OpenBao rejected the write (HTTP 400) or its version changed. Sync again; check KV v2 configuration.");
|
||||||
|
}
|
||||||
if (status < 200 || status >= 300) throw std::runtime_error("OpenBao HTTP error " + std::to_string(status) + ".");
|
if (status < 200 || status >= 300) throw std::runtime_error("OpenBao HTTP error " + std::to_string(status) + ".");
|
||||||
return result.empty() ? Json::object() : parse(result);
|
return result.empty() ? Json::object() : parse(result);
|
||||||
}
|
}
|
||||||
@@ -59,7 +66,7 @@ class OpenBao final : public SyncProvider
|
|||||||
if (transport)
|
if (transport)
|
||||||
{
|
{
|
||||||
auto reply = transport(method, path, token, body ? bytes(*body) : Bytes{});
|
auto reply = transport(method, path, token, body ? bytes(*body) : Bytes{});
|
||||||
return response(reply.status, reply.body, missing);
|
return response(reply.status, reply.body, missing, path, method);
|
||||||
}
|
}
|
||||||
URL_COMPONENTS url{sizeof(url)};
|
URL_COMPONENTS url{sizeof(url)};
|
||||||
url.dwHostNameLength = url.dwUrlPathLength = url.dwExtraInfoLength = (DWORD)-1;
|
url.dwHostNameLength = url.dwUrlPathLength = url.dwExtraInfoLength = (DWORD)-1;
|
||||||
@@ -98,7 +105,7 @@ class OpenBao final : public SyncProvider
|
|||||||
if (result.size() + received > Limit) throw std::runtime_error("OpenBao response is too large.");
|
if (result.size() + received > Limit) throw std::runtime_error("OpenBao response is too large.");
|
||||||
result.insert(result.end(), chunk, chunk + received);
|
result.insert(result.end(), chunk, chunk + received);
|
||||||
}
|
}
|
||||||
return response(status, result, missing);
|
return response(status, result, missing, path, method);
|
||||||
}
|
}
|
||||||
public:
|
public:
|
||||||
explicit OpenBao(const SyncConfig &c, SyncTransport t) : config(c), transport(std::move(t))
|
explicit OpenBao(const SyncConfig &c, SyncTransport t) : config(c), transport(std::move(t))
|
||||||
@@ -306,9 +313,10 @@ SyncResult synchronize(SyncProvider &provider, const Bytes &base, const Bytes &l
|
|||||||
auto revision = remote.revision;
|
auto revision = remote.revision;
|
||||||
if (parse(merged.remote) != parse(remote.settings) && action != SyncAction::Download) revision = provider.write(merged.remote, revision);
|
if (parse(merged.remote) != parse(remote.settings) && action != SyncAction::Download) revision = provider.write(merged.remote, revision);
|
||||||
return {true, merged.local, revision,
|
return {true, merged.local, revision,
|
||||||
merged.conflicts.empty() ? L"Synchronized. Server version " + std::to_wstring(revision) + L"." :
|
merged.conflicts.empty() ? std::wstring(action == SyncAction::Download ? L"Downloaded remote settings. Server version " :
|
||||||
|
action == SyncAction::Upload ? L"Uploaded local settings. Server version " : L"Synchronized. Server version ") + std::to_wstring(revision) + L"." :
|
||||||
L"Other changes synchronized. Resolve " + std::to_wstring(merged.conflicts.size()) + L" conflicting setting(s).",
|
L"Other changes synchronized. Resolve " + std::to_wstring(merged.conflicts.size()) + L" conflicting setting(s).",
|
||||||
merged.base, merged.conflicts};
|
merged.base, merged.conflicts, action == SyncAction::Download};
|
||||||
}
|
}
|
||||||
Bytes encodeSyncConflicts(const std::vector<SyncConflict> &conflicts)
|
Bytes encodeSyncConflicts(const std::vector<SyncConflict> &conflicts)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ struct SyncResult
|
|||||||
std::wstring status;
|
std::wstring status;
|
||||||
Bytes base;
|
Bytes base;
|
||||||
std::vector<SyncConflict> conflicts;
|
std::vector<SyncConflict> conflicts;
|
||||||
|
bool downloaded{};
|
||||||
};
|
};
|
||||||
SyncResult synchronize(SyncProvider &, const Bytes &base, const Bytes &local, SyncAction,
|
SyncResult synchronize(SyncProvider &, const Bytes &base, const Bytes &local, SyncAction,
|
||||||
const std::function<void(const Bytes &)> &validate = {}, const std::vector<SyncChoice> &choices = {},
|
const std::function<void(const Bytes &)> &validate = {}, const std::vector<SyncChoice> &choices = {},
|
||||||
|
|||||||
@@ -1688,6 +1688,10 @@ LRESULT CALLBACK settingsProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
|
|||||||
{
|
{
|
||||||
switch (msg)
|
switch (msg)
|
||||||
{
|
{
|
||||||
|
case ConfigSettingsDownloaded:
|
||||||
|
v->draft = v->app->settings;
|
||||||
|
fillSettings(w, v);
|
||||||
|
return 0;
|
||||||
case WM_ENABLE:
|
case WM_ENABLE:
|
||||||
if (wp) updateSettingsState(w, v);
|
if (wp) updateSettingsState(w, v);
|
||||||
break;
|
break;
|
||||||
|
|||||||
+7
-1
@@ -36,7 +36,13 @@ void refreshLan(HWND w,LanView &v)
|
|||||||
for(const auto &peer:next["peers"])
|
for(const auto &peer:next["peers"])
|
||||||
{
|
{
|
||||||
auto id=peer.at("id").get<std::string>();ids.push_back(id);
|
auto id=peer.at("id").get<std::string>();ids.push_back(id);
|
||||||
auto state=peer.at("paired").get<bool>() ? L"Paired" : L"Not paired";
|
std::wstring state=L"Not paired";
|
||||||
|
if(peer.at("paired").get<bool>()){
|
||||||
|
bool connected=next.value("inputReady",nlohmann::json::object()).contains(id);
|
||||||
|
state=connected?L"Pair saved — input connected":
|
||||||
|
!next.value("shareInput",false)?L"Pair saved — input disabled here":
|
||||||
|
!peer.value("online",false)?L"Pair saved — computer offline":L"Pair saved — input not connected";
|
||||||
|
}
|
||||||
rows.push_back(wide(peer.at("name").get<std::string>())+L" ["+wide(id.substr(0,8))+L"] — "+state);
|
rows.push_back(wide(peer.at("name").get<std::string>())+L" ["+wide(id.substr(0,8))+L"] — "+state);
|
||||||
}
|
}
|
||||||
// Presence/challenge refreshes must not repaint or reset the pairing list.
|
// Presence/challenge refreshes must not repaint or reset the pairing list.
|
||||||
|
|||||||
+9
-4
@@ -1,5 +1,5 @@
|
|||||||
#include "ui_sync_conflicts.inl"
|
#include "ui_sync_conflicts.inl"
|
||||||
struct SyncView { HWND parent; App *app; };
|
struct SyncView { HWND parent; App *app; bool actionError{}; };
|
||||||
void updateSyncControls(HWND w, App &app)
|
void updateSyncControls(HWND w, App &app)
|
||||||
{
|
{
|
||||||
bool enabled = checked(w, 601);
|
bool enabled = checked(w, 601);
|
||||||
@@ -17,7 +17,8 @@ void updateSyncControls(HWND w, App &app)
|
|||||||
status = L"Configure the connection, then click Save / Sign in / Sync to enable synchronization.";
|
status = L"Configure the connection, then click Save / Sign in / Sync to enable synchronization.";
|
||||||
else
|
else
|
||||||
status = app.syncCommand();
|
status = app.syncCommand();
|
||||||
if (value(w, 608) != status) value(w, 608, status);
|
auto *view = reinterpret_cast<SyncView *>(GetWindowLongPtrW(w, GWLP_USERDATA));
|
||||||
|
if ((!view || !view->actionError) && value(w, 608) != status) value(w, 608, status);
|
||||||
}
|
}
|
||||||
SyncConfig syncFields(HWND w)
|
SyncConfig syncFields(HWND w)
|
||||||
{
|
{
|
||||||
@@ -47,6 +48,7 @@ LRESULT CALLBACK syncProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
|
|||||||
int id = LOWORD(wp);
|
int id = LOWORD(wp);
|
||||||
if (id == 601 && HIWORD(wp) == BN_CLICKED)
|
if (id == 601 && HIWORD(wp) == BN_CLICKED)
|
||||||
{
|
{
|
||||||
|
v->actionError = false;
|
||||||
updateSyncControls(w, *v->app);
|
updateSyncControls(w, *v->app);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -56,6 +58,7 @@ LRESULT CALLBACK syncProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
|
|||||||
if (id == IDCANCEL) { DestroyWindow(w); return 0; }
|
if (id == IDCANCEL) { DestroyWindow(w); return 0; }
|
||||||
if (id == 609)
|
if (id == 609)
|
||||||
{
|
{
|
||||||
|
v->actionError = false;
|
||||||
auto c = loadSyncConfig(v->app->data);
|
auto c = loadSyncConfig(v->app->data);
|
||||||
c.enabled = false;
|
c.enabled = false;
|
||||||
saveSyncConfig(v->app->data, c);
|
saveSyncConfig(v->app->data, c);
|
||||||
@@ -69,6 +72,7 @@ LRESULT CALLBACK syncProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
|
|||||||
}
|
}
|
||||||
if (id == IDOK || id == 610 || id == 611)
|
if (id == IDOK || id == 610 || id == 611)
|
||||||
{
|
{
|
||||||
|
v->actionError = false;
|
||||||
if (!checked(w, 601))
|
if (!checked(w, 601))
|
||||||
{
|
{
|
||||||
if (id != IDOK) return 0;
|
if (id != IDOK) return 0;
|
||||||
@@ -80,7 +84,7 @@ LRESULT CALLBACK syncProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
if (id != IDOK && MessageBoxW(w, id == 610 ?
|
if (id != IDOK && MessageBoxW(w, id == 610 ?
|
||||||
L"Replace this PC's shared settings with the remote profile? A local backup will be saved. Unsaved edits in Settings are not included." :
|
L"Replace this PC's shared settings with the remote profile? A local backup will be saved. The open Settings window will refresh automatically; its unsaved edits will be discarded." :
|
||||||
L"Replace the remote profile with this PC's saved settings? OpenBao retains older versions according to its retention policy. Unsaved edits are not included.",
|
L"Replace the remote profile with this PC's saved settings? OpenBao retains older versions according to its retention policy. Unsaved edits are not included.",
|
||||||
L"Resolve synchronization", MB_YESNO | MB_ICONWARNING) != IDYES) return 0;
|
L"Resolve synchronization", MB_YESNO | MB_ICONWARNING) != IDYES) return 0;
|
||||||
auto c = syncFields(w);
|
auto c = syncFields(w);
|
||||||
@@ -100,6 +104,7 @@ LRESULT CALLBACK syncProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
|
|||||||
}
|
}
|
||||||
catch (const std::exception &e)
|
catch (const std::exception &e)
|
||||||
{
|
{
|
||||||
|
v->actionError = true; // Keep this action's error visible across status polling.
|
||||||
// Never display parser exceptions containing connection file contents.
|
// Never display parser exceptions containing connection file contents.
|
||||||
value(w, 608, L"Unable to complete sync action. Check connection fields and saved configuration.");
|
value(w, 608, L"Unable to complete sync action. Check connection fields and saved configuration.");
|
||||||
if (dynamic_cast<const std::runtime_error *>(&e))
|
if (dynamic_cast<const std::runtime_error *>(&e))
|
||||||
@@ -130,7 +135,7 @@ void showSyncOptions(HWND parent, App &app)
|
|||||||
label(w, L"KV v2 &mount", 18, 210, 160); edit(w, 605, 190, 206, 450); value(w, 605, c.mount);
|
label(w, L"KV v2 &mount", 18, 210, 160); edit(w, 605, 190, 206, 450); value(w, 605, c.mount);
|
||||||
label(w, L"Profile pa&th", 18, 250, 160); edit(w, 606, 190, 246, 450); value(w, 606, c.path);
|
label(w, L"Profile pa&th", 18, 250, 160); edit(w, 606, 190, 246, 450); value(w, 606, c.path);
|
||||||
label(w, L"&Login mount", 18, 290, 160); edit(w, 607, 190, 286, 450); value(w, 607, c.authMount);
|
label(w, L"&Login mount", 18, 290, 160); edit(w, 607, 190, 286, 450); value(w, 607, c.authMount);
|
||||||
label(w, L"Sync uses saved settings. Close and reopen Settings after downloading changes.", 18, 330, 622);
|
label(w, L"Sync uses saved settings. Download remote refreshes the open Settings window.", 18, 330, 622);
|
||||||
edit(w, 608, 18, 362, 622, 76, ES_MULTILINE | ES_READONLY | WS_VSCROLL | ES_AUTOVSCROLL);
|
edit(w, 608, 18, 362, 622, 76, ES_MULTILINE | ES_READONLY | WS_VSCROLL | ES_AUTOVSCROLL);
|
||||||
value(w, 608, app.syncCommand());
|
value(w, 608, app.syncCommand());
|
||||||
checkbox(w, L"&Keep this PC's layouts and layout shortcuts (sync other settings)", 613, 18, 446, 622);
|
checkbox(w, L"&Keep this PC's layouts and layout shortcuts (sync other settings)", 613, 18, 446, 622);
|
||||||
|
|||||||
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
#include "platform.h"
|
#include "platform.h"
|
||||||
namespace ks
|
namespace ks
|
||||||
{
|
{
|
||||||
inline constexpr wchar_t AppVersion[] = L"0.6.57";
|
inline constexpr wchar_t AppVersion[] = L"0.6.60";
|
||||||
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;
|
||||||
|
|||||||
+22
-11
@@ -7,6 +7,15 @@
|
|||||||
|
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
|
HWND findTestWindow(LPCWSTR className, LPCWSTR title)
|
||||||
|
{
|
||||||
|
HWND window=nullptr;
|
||||||
|
while((window=FindWindowExW(nullptr,window,className,title))) {
|
||||||
|
DWORD process{}; GetWindowThreadProcessId(window,&process);
|
||||||
|
if(process==GetCurrentProcessId()) return window;
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
int passed{};
|
int passed{};
|
||||||
void require(bool ok, const char *message)
|
void require(bool ok, const char *message)
|
||||||
{
|
{
|
||||||
@@ -141,31 +150,31 @@ int main()
|
|||||||
SetDlgItemTextW(parent,240,originalLimit);
|
SetDlgItemTextW(parent,240,originalLimit);
|
||||||
require(!saveEnabled(),"restoring valid original text clears dirty state");
|
require(!saveEnabled(),"restoring valid original text clears dirty state");
|
||||||
SendMessageW(parent,WM_COMMAND,257,0);
|
SendMessageW(parent,WM_COMMAND,257,0);
|
||||||
HWND ocr=FindWindowW(L"KSwitcher.OcrOptions",nullptr);
|
HWND ocr=findTestWindow(L"KSwitcher.OcrOptions",nullptr);
|
||||||
require(ocr!=nullptr,"OCR preferences open");
|
require(ocr!=nullptr,"OCR preferences open");
|
||||||
wchar_t languages[128]{}; GetDlgItemTextW(ocr,503,languages,128);
|
wchar_t languages[128]{}; GetDlgItemTextW(ocr,503,languages,128);
|
||||||
SetDlgItemTextW(ocr,503,L"eng");
|
SetDlgItemTextW(ocr,503,L"eng");
|
||||||
SendMessageW(ocr,WM_COMMAND,IDCANCEL,0);
|
SendMessageW(ocr,WM_COMMAND,IDCANCEL,0);
|
||||||
require(!saveEnabled(),"cancelled child changes leave Settings clean");
|
require(!saveEnabled(),"cancelled child changes leave Settings clean");
|
||||||
SendMessageW(parent,WM_COMMAND,257,0);
|
SendMessageW(parent,WM_COMMAND,257,0);
|
||||||
ocr=FindWindowW(L"KSwitcher.OcrOptions",nullptr);
|
ocr=findTestWindow(L"KSwitcher.OcrOptions",nullptr);
|
||||||
SetDlgItemTextW(ocr,503,std::wstring(languages)==L"eng"?L"ukr":L"eng");
|
SetDlgItemTextW(ocr,503,std::wstring(languages)==L"eng"?L"ukr":L"eng");
|
||||||
SendMessageW(ocr,WM_COMMAND,IDOK,0);
|
SendMessageW(ocr,WM_COMMAND,IDOK,0);
|
||||||
require(!IsWindow(ocr) && saveEnabled(),"accepted OCR edits enable parent Save");
|
require(!IsWindow(ocr) && saveEnabled(),"accepted OCR edits enable parent Save");
|
||||||
SendMessageW(parent,WM_COMMAND,257,0);
|
SendMessageW(parent,WM_COMMAND,257,0);
|
||||||
ocr=FindWindowW(L"KSwitcher.OcrOptions",nullptr);
|
ocr=findTestWindow(L"KSwitcher.OcrOptions",nullptr);
|
||||||
SetDlgItemTextW(ocr,503,languages);
|
SetDlgItemTextW(ocr,503,languages);
|
||||||
SendMessageW(ocr,WM_COMMAND,IDOK,0);
|
SendMessageW(ocr,WM_COMMAND,IDOK,0);
|
||||||
require(!saveEnabled(),"reverting OCR draft in child clears parent dirty state");
|
require(!saveEnabled(),"reverting OCR draft in child clears parent dirty state");
|
||||||
SendMessageW(parent,WM_COMMAND,255,0);
|
SendMessageW(parent,WM_COMMAND,255,0);
|
||||||
HWND automatic=FindWindowW(L"KSwitcher.AutoOptions",nullptr);
|
HWND automatic=findTestWindow(L"KSwitcher.AutoOptions",nullptr);
|
||||||
require(automatic!=nullptr,"automatic correction preferences open");
|
require(automatic!=nullptr,"automatic correction preferences open");
|
||||||
bool replace=IsDlgButtonChecked(automatic,414)==BST_CHECKED;
|
bool replace=IsDlgButtonChecked(automatic,414)==BST_CHECKED;
|
||||||
CheckDlgButton(automatic,414,replace?BST_UNCHECKED:BST_CHECKED);
|
CheckDlgButton(automatic,414,replace?BST_UNCHECKED:BST_CHECKED);
|
||||||
SendMessageW(automatic,WM_COMMAND,IDOK,0);
|
SendMessageW(automatic,WM_COMMAND,IDOK,0);
|
||||||
require(!IsWindow(automatic) && saveEnabled(),"accepted auto-correction edits enable Save");
|
require(!IsWindow(automatic) && saveEnabled(),"accepted auto-correction edits enable Save");
|
||||||
SendMessageW(parent,WM_COMMAND,255,0);
|
SendMessageW(parent,WM_COMMAND,255,0);
|
||||||
automatic=FindWindowW(L"KSwitcher.AutoOptions",nullptr);
|
automatic=findTestWindow(L"KSwitcher.AutoOptions",nullptr);
|
||||||
CheckDlgButton(automatic,414,replace?BST_CHECKED:BST_UNCHECKED);
|
CheckDlgButton(automatic,414,replace?BST_CHECKED:BST_UNCHECKED);
|
||||||
SendMessageW(automatic,WM_COMMAND,IDOK,0);
|
SendMessageW(automatic,WM_COMMAND,IDOK,0);
|
||||||
require(!saveEnabled(),"reverting auto-correction draft disables Save");
|
require(!saveEnabled(),"reverting auto-correction draft disables Save");
|
||||||
@@ -201,7 +210,7 @@ int main()
|
|||||||
SendMessageW(parent, WM_NOTIFY, 260, (LPARAM)¬ification);
|
SendMessageW(parent, WM_NOTIFY, 260, (LPARAM)¬ification);
|
||||||
require(!saveEnabled(),"changing tabs leaves Settings clean");
|
require(!saveEnabled(),"changing tabs leaves Settings clean");
|
||||||
SendMessageW(parent, WM_COMMAND, 259, 0);
|
SendMessageW(parent, WM_COMMAND, 259, 0);
|
||||||
HWND dialog = FindWindowW(L"KSwitcher.ChatLanguages", nullptr);
|
HWND dialog = findTestWindow(L"KSwitcher.ChatLanguages", nullptr);
|
||||||
require(dialog && !IsWindowEnabled(parent), "chat dialog opens with disabled parent");
|
require(dialog && !IsWindowEnabled(parent), "chat dialog opens with disabled parent");
|
||||||
int index = (int)SendDlgItemMessageW(dialog, 710, LB_FINDSTRINGEXACT, -1, (LPARAM)L"Telegram: ! Fixture chat");
|
int index = (int)SendDlgItemMessageW(dialog, 710, LB_FINDSTRINGEXACT, -1, (LPARAM)L"Telegram: ! Fixture chat");
|
||||||
require(index >= 0, "saved chat listed");
|
require(index >= 0, "saved chat listed");
|
||||||
@@ -220,7 +229,7 @@ int main()
|
|||||||
require(!IsWindow(dialog) && IsWindowEnabled(parent), "cancel closes dialog");
|
require(!IsWindow(dialog) && IsWindowEnabled(parent), "cancel closes dialog");
|
||||||
require(!saveEnabled(),"cancelled chat edits do not enable Save");
|
require(!saveEnabled(),"cancelled chat edits do not enable Save");
|
||||||
SendMessageW(parent, WM_COMMAND, 259, 0);
|
SendMessageW(parent, WM_COMMAND, 259, 0);
|
||||||
dialog = FindWindowW(L"KSwitcher.ChatLanguages", nullptr);
|
dialog = findTestWindow(L"KSwitcher.ChatLanguages", nullptr);
|
||||||
index = (int)SendDlgItemMessageW(dialog, 710, LB_FINDSTRINGEXACT, -1, (LPARAM)L"Telegram: ! Fixture chat");
|
index = (int)SendDlgItemMessageW(dialog, 710, LB_FINDSTRINGEXACT, -1, (LPARAM)L"Telegram: ! Fixture chat");
|
||||||
SendDlgItemMessageW(dialog, 710, LB_SETCURSEL, index, 0);
|
SendDlgItemMessageW(dialog, 710, LB_SETCURSEL, index, 0);
|
||||||
SendMessageW(dialog, WM_COMMAND, MAKEWPARAM(710, LBN_SELCHANGE), 0);
|
SendMessageW(dialog, WM_COMMAND, MAKEWPARAM(710, LBN_SELCHANGE), 0);
|
||||||
@@ -236,7 +245,7 @@ int main()
|
|||||||
parent = app.settingsWindow;
|
parent = app.settingsWindow;
|
||||||
require(!saveEnabled(),"reopened saved Settings is clean");
|
require(!saveEnabled(),"reopened saved Settings is clean");
|
||||||
SendMessageW(parent, WM_COMMAND, 259, 0);
|
SendMessageW(parent, WM_COMMAND, 259, 0);
|
||||||
dialog = FindWindowW(L"KSwitcher.ChatLanguages", nullptr);
|
dialog = findTestWindow(L"KSwitcher.ChatLanguages", nullptr);
|
||||||
index = (int)SendDlgItemMessageW(dialog, 710, LB_FINDSTRINGEXACT, -1, (LPARAM)L"Telegram: ! Fixture chat");
|
index = (int)SendDlgItemMessageW(dialog, 710, LB_FINDSTRINGEXACT, -1, (LPARAM)L"Telegram: ! Fixture chat");
|
||||||
SendDlgItemMessageW(dialog, 710, LB_SETCURSEL, index, 0);
|
SendDlgItemMessageW(dialog, 710, LB_SETCURSEL, index, 0);
|
||||||
SendMessageW(dialog, WM_COMMAND, MAKEWPARAM(710, LBN_SELCHANGE), 0);
|
SendMessageW(dialog, WM_COMMAND, MAKEWPARAM(710, LBN_SELCHANGE), 0);
|
||||||
@@ -286,7 +295,7 @@ int main()
|
|||||||
require(app.settings.snippets[0].apps==std::vector<std::wstring>{L"notepad",L"ms-teams"}, "cancel preserves existing scope");
|
require(app.settings.snippets[0].apps==std::vector<std::wstring>{L"notepad",L"ms-teams"}, "cancel preserves existing scope");
|
||||||
ks::showSettings(app);parent=app.settingsWindow;
|
ks::showSettings(app);parent=app.settingsWindow;
|
||||||
SendMessageW(parent,WM_COMMAND,262,0);
|
SendMessageW(parent,WM_COMMAND,262,0);
|
||||||
HWND lan=FindWindowW(L"KSwitcher.LAN",nullptr);
|
HWND lan=findTestWindow(L"KSwitcher.LAN",nullptr);
|
||||||
require(lan && !IsWindowEnabled(parent),"LAN pairing opens a native child window");
|
require(lan && !IsWindowEnabled(parent),"LAN pairing opens a native child window");
|
||||||
require(!IsWindowEnabled(GetDlgItem(lan,705)),"pairing disabled without discovered peer");
|
require(!IsWindowEnabled(GetDlgItem(lan,705)),"pairing disabled without discovered peer");
|
||||||
require(!IsWindowEnabled(GetDlgItem(lan,706)),"no cancellation without pending pairing");
|
require(!IsWindowEnabled(GetDlgItem(lan,706)),"no cancellation without pending pairing");
|
||||||
@@ -312,8 +321,10 @@ int main()
|
|||||||
auto text=fixture.dump();ks::atomicWrite(root/L"lan-pairing.bin",ks::protect(ks::Bytes(text.begin(),text.end())));
|
auto text=fixture.dump();ks::atomicWrite(root/L"lan-pairing.bin",ks::protect(ks::Bytes(text.begin(),text.end())));
|
||||||
ks::App pairedApp(GetModuleHandleW(nullptr),true,root);ks::showSettings(pairedApp);
|
ks::App pairedApp(GetModuleHandleW(nullptr),true,root);ks::showSettings(pairedApp);
|
||||||
SendMessageW(pairedApp.settingsWindow,WM_COMMAND,262,0);
|
SendMessageW(pairedApp.settingsWindow,WM_COMMAND,262,0);
|
||||||
HWND lan=FindWindowW(L"KSwitcher.LAN",nullptr);
|
HWND lan=findTestWindow(L"KSwitcher.LAN",nullptr);
|
||||||
require(SendDlgItemMessageW(lan,702,LB_GETCURSEL,0,0)==0,"single saved pair selected automatically when opening LAN settings");
|
require(SendDlgItemMessageW(lan,702,LB_GETCURSEL,0,0)==0,"single saved pair selected automatically when opening LAN settings");
|
||||||
|
wchar_t pairState[512]{};SendDlgItemMessageW(lan,702,LB_GETTEXT,0,(LPARAM)pairState);
|
||||||
|
require(std::wstring(pairState).find(L"Pair saved — input disabled here")!=std::wstring::npos,"saved pairing does not imply an active input connection");
|
||||||
require(!IsWindowEnabled(GetDlgItem(lan,705))&&!IsWindowEnabled(GetDlgItem(lan,703))&&!IsWindowEnabled(GetDlgItem(lan,704)),"saved pair disables pairing and code controls");
|
require(!IsWindowEnabled(GetDlgItem(lan,705))&&!IsWindowEnabled(GetDlgItem(lan,703))&&!IsWindowEnabled(GetDlgItem(lan,704)),"saved pair disables pairing and code controls");
|
||||||
SendDlgItemMessageW(lan,702,LB_SETCURSEL,-1,0);
|
SendDlgItemMessageW(lan,702,LB_SETCURSEL,-1,0);
|
||||||
SendMessageW(lan,WM_TIMER,1,0);
|
SendMessageW(lan,WM_TIMER,1,0);
|
||||||
@@ -346,7 +357,7 @@ int main()
|
|||||||
{"positions",{{b,J::array({-1,0})},{c,J::array({1,0})}}}};
|
{"positions",{{b,J::array({-1,0})},{c,J::array({1,0})}}}};
|
||||||
auto text=fixture.dump();ks::atomicWrite(root/L"lan-pairing.bin",ks::protect(ks::Bytes(text.begin(),text.end())));
|
auto text=fixture.dump();ks::atomicWrite(root/L"lan-pairing.bin",ks::protect(ks::Bytes(text.begin(),text.end())));
|
||||||
ks::App layoutApp(GetModuleHandleW(nullptr),true,root);ks::showSettings(layoutApp);
|
ks::App layoutApp(GetModuleHandleW(nullptr),true,root);ks::showSettings(layoutApp);
|
||||||
SendMessageW(layoutApp.settingsWindow,WM_COMMAND,262,0);auto lan=FindWindowW(L"KSwitcher.LAN",nullptr);auto grid=GetDlgItem(lan,721);
|
SendMessageW(layoutApp.settingsWindow,WM_COMMAND,262,0);auto lan=findTestWindow(L"KSwitcher.LAN",nullptr);auto grid=GetDlgItem(lan,721);
|
||||||
RECT r{};GetClientRect(grid,&r);
|
RECT r{};GetClientRect(grid,&r);
|
||||||
auto point=[&](int column,int row){return MAKELPARAM((2*column+1)*r.right/10,(2*row+1)*r.bottom/6);};
|
auto point=[&](int column,int row){return MAKELPARAM((2*column+1)*r.right/10,(2*row+1)*r.bottom/6);};
|
||||||
SendMessageW(grid,WM_LBUTTONDOWN,MK_LBUTTON,point(1,1));SendMessageW(grid,WM_MOUSEMOVE,MK_LBUTTON,point(1,0));SendMessageW(grid,WM_LBUTTONUP,0,point(1,0));
|
SendMessageW(grid,WM_LBUTTONDOWN,MK_LBUTTON,point(1,1));SendMessageW(grid,WM_MOUSEMOVE,MK_LBUTTON,point(1,0));SendMessageW(grid,WM_LBUTTONUP,0,point(1,0));
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
#include "app.h"
|
#include "app.h"
|
||||||
|
#include "configuration.h"
|
||||||
#include "ui_scroll.h"
|
#include "ui_scroll.h"
|
||||||
#include "ui_geometry.h"
|
#include "ui_geometry.h"
|
||||||
#include "screenshot.h"
|
#include "screenshot.h"
|
||||||
@@ -752,6 +753,21 @@ int main(int argc, char **argv) {
|
|||||||
require(fitted.left==-600&&fitted.top==680&&fitted.right==0,"negative-coordinate monitor supported");
|
require(fitted.left==-600&&fitted.top==680&&fitted.right==0,"negative-coordinate monitor supported");
|
||||||
ks::showSettings(app,false);
|
ks::showSettings(app,false);
|
||||||
{
|
{
|
||||||
|
auto window = app.settingsWindow;
|
||||||
|
auto previous = app.settings;
|
||||||
|
TabCtrl_SetCurSel(GetDlgItem(window,260),3);
|
||||||
|
app.settings.awake = !previous.awake;
|
||||||
|
app.settings.maxItems = previous.maxItems + 1;
|
||||||
|
SendMessageW(window,ks::ConfigSettingsDownloaded,0,0);
|
||||||
|
wchar_t count[32]{};
|
||||||
|
GetDlgItemTextW(window,240,count,32);
|
||||||
|
require(app.settingsWindow == window &&
|
||||||
|
SendDlgItemMessageW(window,205,BM_GETCHECK,0,0) == (app.settings.awake ? BST_CHECKED : BST_UNCHECKED) &&
|
||||||
|
std::stoi(count) == app.settings.maxItems && TabCtrl_GetCurSel(GetDlgItem(window,260)) == 3,
|
||||||
|
"download refreshes existing settings controls while preserving window and active tab");
|
||||||
|
app.settings = previous;
|
||||||
|
SendMessageW(window,ks::ConfigSettingsDownloaded,0,0);
|
||||||
|
TabCtrl_SetCurSel(GetDlgItem(window,260),0);
|
||||||
auto timer = SetTimer(nullptr, 0, 10, answerSettingsClose);
|
auto timer = SetTimer(nullptr, 0, 10, answerSettingsClose);
|
||||||
require(timer != 0, "settings close prompt responder");
|
require(timer != 0, "settings close prompt responder");
|
||||||
SendMessageW(app.settingsWindow, WM_CLOSE, 0, 0);
|
SendMessageW(app.settingsWindow, WM_CLOSE, 0, 0);
|
||||||
|
|||||||
@@ -317,6 +317,11 @@ void hubChecks()
|
|||||||
auto deadline=GetTickCount64()+5000;
|
auto deadline=GetTickCount64()+5000;
|
||||||
while((hubs[0]->ready().size()!=2||hubs[1]->ready().size()!=2||hubs[2]->ready().size()!=2)&&GetTickCount64()<deadline)pump();
|
while((hubs[0]->ready().size()!=2||hubs[1]->ready().size()!=2||hubs[2]->ready().size()!=2)&&GetTickCount64()<deadline)pump();
|
||||||
check(hubs[0]->ready().size()==2&&hubs[1]->ready().size()==2&&hubs[2]->ready().size()==2,"three TCP hubs authenticate all independent pair channels on one listener per PC");
|
check(hubs[0]->ready().size()==2&&hubs[1]->ready().size()==2&&hubs[2]->ready().size()==2,"three TCP hubs authenticate all independent pair channels on one listener per PC");
|
||||||
|
auto generation=hubs[0]->ready();
|
||||||
|
for(auto &list:endpoints)for(auto &e:list)e.address=0;
|
||||||
|
for(int n=0;n<20;++n)pump();
|
||||||
|
check(hubs[0]->ready()==generation&&hubs[1]->ready().size()==2&&hubs[2]->ready().size()==2,"expired discovery does not destroy authenticated channels");
|
||||||
|
for(auto &list:endpoints)for(auto &e:list)e.address=htonl(INADDR_LOOPBACK);
|
||||||
InputFrame keyFrame{InputType::Key,7,'A',0,1},cross{InputType::Cross,8,2,30000};
|
InputFrame keyFrame{InputType::Key,7,'A',0,1},cross{InputType::Cross,8,2,30000};
|
||||||
hubs[0]->poll(endpoints[0],{{ids[1],keyFrame},{ids[2],cross}},incoming[0],GetTickCount64());
|
hubs[0]->poll(endpoints[0],{{ids[1],keyFrame},{ids[2],cross}},incoming[0],GetTickCount64());
|
||||||
for(int n=0;n<100&&(incoming[1].empty()||incoming[2].empty());++n)pump();
|
for(int n=0;n<100&&(incoming[1].empty()||incoming[2].empty());++n)pump();
|
||||||
@@ -327,6 +332,15 @@ void hubChecks()
|
|||||||
check(hubs[0]->ready().contains(ids[2])&&hubs[0]->ready().at(ids[2])==survivor,"one computer disconnect leaves other authenticated channel intact");
|
check(hubs[0]->ready().contains(ids[2])&&hubs[0]->ready().at(ids[2])==survivor,"one computer disconnect leaves other authenticated channel intact");
|
||||||
for(int n=0;n<300&&(hubs[0]->ready().size()!=2||hubs[1]->ready().size()!=2);++n)pump(1800);
|
for(int n=0;n<300&&(hubs[0]->ready().size()!=2||hubs[1]->ready().size()!=2);++n)pump(1800);
|
||||||
check(hubs[0]->ready().size()==2&&hubs[0]->ready().at(ids[1])!=old,"mesh reconnect restores channel with fresh ownership generation");
|
check(hubs[0]->ready().size()==2&&hubs[0]->ready().at(ids[1])!=old,"mesh reconnect restores channel with fresh ownership generation");
|
||||||
|
{
|
||||||
|
auto serverPort=freePort();InputHub server(ids[2],serverPort),client(ids[0],freePort());
|
||||||
|
std::vector<InputEndpoint> unknown{{ids[0],secret,0}},known{{ids[2],secret,htonl(INADDR_LOOPBACK),serverPort}};
|
||||||
|
std::vector<RoutedInput> a,b;
|
||||||
|
for(int n=0;n<300&&(server.ready().empty()||client.ready().empty());++n){
|
||||||
|
server.poll(unknown,{},a,GetTickCount64());client.poll(known,{},b,GetTickCount64());Sleep(1);
|
||||||
|
}
|
||||||
|
check(server.ready().size()==1&&client.ready().size()==1,"saved pair listens and authenticates without receiving a discovery announcement");
|
||||||
|
}
|
||||||
endpoints[0].erase(endpoints[0].begin());hubs[0]->poll(endpoints[0],{},incoming[0],GetTickCount64()+1800);
|
endpoints[0].erase(endpoints[0].begin());hubs[0]->poll(endpoints[0],{},incoming[0],GetTickCount64()+1800);
|
||||||
check(!hubs[0]->ready().contains(ids[1])&&hubs[0]->ready().contains(ids[2]),"forgetting one pair revokes only its channel");
|
check(!hubs[0]->ready().contains(ids[1])&&hubs[0]->ready().contains(ids[2]),"forgetting one pair revokes only its channel");
|
||||||
}
|
}
|
||||||
|
|||||||
+19
-1
@@ -298,6 +298,9 @@ int main()
|
|||||||
require(provider.writes == 1 && result.revision == 1, "no-op avoids versions");
|
require(provider.writes == 1 && result.revision == 1, "no-op avoids versions");
|
||||||
result = synchronize(provider, base, bytes(left), SyncAction::Download);
|
result = synchronize(provider, base, bytes(left), SyncAction::Download);
|
||||||
require(parse(result.settings) == parse(base) && provider.writes == 1, "download never writes");
|
require(parse(result.settings) == parse(base) && provider.writes == 1, "download never writes");
|
||||||
|
require(result.downloaded, "explicit download requests a settings window refresh");
|
||||||
|
require(result.status.find(L"Downloaded remote settings. Server version 1.") != std::wstring::npos,
|
||||||
|
"explicit download reports its result and server version");
|
||||||
provider.race = true;
|
provider.race = true;
|
||||||
rejects([&] { synchronize(provider, base, bytes(left), SyncAction::Upload); }, "racing writer protected");
|
rejects([&] { synchronize(provider, base, bytes(left), SyncAction::Upload); }, "racing writer protected");
|
||||||
require(provider.writes == 1 && provider.remote.settings == base, "CAS failure preserves remote");
|
require(provider.writes == 1 && provider.remote.settings == base, "CAS failure preserves remote");
|
||||||
@@ -395,10 +398,25 @@ int main()
|
|||||||
rejects([&] { bao->read(); }, "permission denied is not absent");
|
rejects([&] { bao->read(); }, "permission denied is not absent");
|
||||||
readStatus = 302;
|
readStatus = 302;
|
||||||
rejects([&] { bao->read(); }, "redirect response rejected");
|
rejects([&] { bao->read(); }, "redirect response rejected");
|
||||||
|
readStatus = 400;
|
||||||
|
try { bao->read(); require(false, "download HTTP 400 must fail"); }
|
||||||
|
catch (const std::runtime_error &e) {
|
||||||
|
require(std::string(e.what()).find("download request (HTTP 400)") != std::string::npos,
|
||||||
|
"download failure is not mislabeled as a write failure");
|
||||||
|
}
|
||||||
writeStatus = 400;
|
writeStatus = 400;
|
||||||
rejects([&] { bao->write(base, 7); }, "server CAS refusal propagated");
|
rejects([&] { bao->write(base, 7); }, "server CAS refusal propagated");
|
||||||
}
|
}
|
||||||
require(logins == 1 && revokes == 1 && reads == 5 && writes == 2, "temporary login lifecycle");
|
require(logins == 1 && revokes == 1 && reads == 6 && writes == 2, "temporary login lifecycle");
|
||||||
|
try {
|
||||||
|
makeSyncProvider(config, [](const std::wstring &, const std::wstring &, const std::wstring &, const Bytes &) {
|
||||||
|
return SyncHttpResponse{400, {}};
|
||||||
|
});
|
||||||
|
require(false, "login HTTP 400 must fail");
|
||||||
|
} catch (const std::runtime_error &e) {
|
||||||
|
require(std::string(e.what()).find("authentication request (HTTP 400)") != std::string::npos,
|
||||||
|
"login failure identifies authentication instead of a write");
|
||||||
|
}
|
||||||
forgetSyncPassword(config); forgetSyncPassword(config);
|
forgetSyncPassword(config); forgetSyncPassword(config);
|
||||||
++checks;
|
++checks;
|
||||||
std::filesystem::remove_all(root);
|
std::filesystem::remove_all(root);
|
||||||
|
|||||||
+14
-1
@@ -592,6 +592,16 @@ int main(int argc, char **argv)
|
|||||||
require(!edited.suggest(L"hlelo ", 0, layouts, options), "never-correct overrides explicit pairs");
|
require(!edited.suggest(L"hlelo ", 0, layouts, options), "never-correct overrides explicit pairs");
|
||||||
options.autoNever.clear();
|
options.autoNever.clear();
|
||||||
options.autoReplacements[L"мамамила"] = L"мама мила";
|
options.autoReplacements[L"мамамила"] = L"мама мила";
|
||||||
|
for (const auto *text : {L"рев'ю", L"рев’ю", L"ревʼю", L"моє рев'ю"})
|
||||||
|
{
|
||||||
|
options.autoReplacements[L"ревю"] = text;
|
||||||
|
ks::validateSettings(options);
|
||||||
|
correction = edited.suggest(L"(Ревю)! ", 2, layouts, options);
|
||||||
|
auto expected = std::wstring(text);
|
||||||
|
expected[0] = ks::upper(expected.substr(0, 1))[0];
|
||||||
|
require(correction && correction->text == L"(" + expected + L")! " && correction->target == 2,
|
||||||
|
"apostrophe corrections preserve punctuation, capitalization and layout");
|
||||||
|
}
|
||||||
correction = edited.suggest(L"(Мамамила)! ", 2, layouts, options);
|
correction = edited.suggest(L"(Мамамила)! ", 2, layouts, options);
|
||||||
require(correction && correction->text == L"(Мама мила)! " && correction->target == 2,
|
require(correction && correction->text == L"(Мама мила)! " && correction->target == 2,
|
||||||
"joined words split while preserving capitalization, punctuation and layout");
|
"joined words split while preserving capitalization, punctuation and layout");
|
||||||
@@ -718,7 +728,8 @@ int main(int argc, char **argv)
|
|||||||
settings.elevatedAutostart = true;
|
settings.elevatedAutostart = true;
|
||||||
settings.autoCorrect = true;
|
settings.autoCorrect = true;
|
||||||
settings.autoReplace = settings.autoTypos = true;
|
settings.autoReplace = settings.autoTypos = true;
|
||||||
settings.autoReplacements = {{L"првиіт", L"привіт"}, {L"helo", L"hello"}, {L"мамамила", L"мама мила"}};
|
settings.autoReplacements = {{L"првиіт", L"привіт"}, {L"helo", L"hello"}, {L"мамамила", L"мама мила"},
|
||||||
|
{L"ревю", L"рев'ю"}, {L"пять", L"п’ять"}, {L"імя", L"імʼя"}};
|
||||||
settings.autoMaxRank = 10000;
|
settings.autoMaxRank = 10000;
|
||||||
settings.autoMinLength = 5;
|
settings.autoMinLength = 5;
|
||||||
settings.autoPrefixChars = L"(#";
|
settings.autoPrefixChars = L"(#";
|
||||||
@@ -739,6 +750,8 @@ int main(int argc, char **argv)
|
|||||||
{L"helloworld", L"hello\nworld"}, {L"hello world", L"helloworld"},
|
{L"helloworld", L"hello\nworld"}, {L"hello world", L"helloworld"},
|
||||||
{L"helloworld", L"hello світ"}, {L"helloworld", L"hello world!"},
|
{L"helloworld", L"hello світ"}, {L"helloworld", L"hello world!"},
|
||||||
{L"helloworld", L"hello Hello"}, {L"helloworld", L""},
|
{L"helloworld", L"hello Hello"}, {L"helloworld", L""},
|
||||||
|
{L"review", L"'review"}, {L"review", L"review’"},
|
||||||
|
{L"review", L"re''view"}, {L"review", L"reʼ view"},
|
||||||
{L"helloworld", L"abcdefghijklmnopqrstuvwxyz abcdef"}})
|
{L"helloworld", L"abcdefghijklmnopqrstuvwxyz abcdef"}})
|
||||||
{
|
{
|
||||||
auto invalid = settings;
|
auto invalid = settings;
|
||||||
|
|||||||
Reference in new issue
Block a user