Compare commits
1
Commits
release-v0.6.57
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d0387904a0 |
No files matched your search
+1
-1
@@ -1,5 +1,5 @@
|
||||
cmake_minimum_required(VERSION 3.24)
|
||||
project(KSwitcher VERSION 0.6.57 LANGUAGES CXX RC)
|
||||
project(KSwitcher VERSION 0.6.58 LANGUAGES CXX RC)
|
||||
if(NOT WIN32)
|
||||
message(FATAL_ERROR "KSwitcher uses native Windows APIs")
|
||||
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
|
||||
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
|
||||
|
||||
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.
|
||||
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.
|
||||
@@ -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
|
||||
use local sockets, and input-state tests use simulated desktop environments;
|
||||
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.
|
||||
|
||||
## 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
|
||||
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
|
||||
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
|
||||
source with all destinations, arrange the same physical layout on each PC and
|
||||
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 і назад. Нові текст та зображення
|
||||
передаються всім доступним спарованим ПК в історію й активний буфер. Фільтр
|
||||
Computers в історії має чекбокси. Оновіть усі ПК до v0.6.57, залишивши кожному
|
||||
Computers в історії має чекбокси. Оновіть усі ПК до v0.6.58, залишивши кожному
|
||||
власну папку data. Спаруйте джерело вводу з усіма потрібними ПК й задайте
|
||||
однакове фізичне розташування на кожному. Передача файлів не реалізована.
|
||||
|
||||
|
||||
Binary file not shown.
+6
-9
@@ -1,19 +1,16 @@
|
||||
# Latest Windows release
|
||||
|
||||
## v0.6.57 — 2026-11-06
|
||||
## v0.6.58 — 2026-11-06
|
||||
|
||||
## Changes
|
||||
- Settings Save merges independent edits with newer resident settings. Same-field conflicts offer a choice; declining keeps the window and draft open.
|
||||
- Multiple paired PCs on a draggable monitor grid; mouse and keyboard follow neighboring edges, including A → B → C and back. Connections reconnect independently.
|
||||
- New text, formatted text and screenshots synchronize to all online paired PCs: history and active clipboard. Newer local copies are preserved during incoming transfers.
|
||||
- Clipboard history has a persistent Computers checkbox filter and source labels. Lossless PNG transport avoids duplicate image payloads.
|
||||
- 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.
|
||||
- Keep trusted TCP input listeners and connections alive across missing discovery announcements. Retain the last known address for reconnection; authenticate incoming trusted peers even before discovery sees their address.
|
||||
- Show saved pairing separately from actual input connectivity: connected, disabled here, offline or not connected. Background identity verification does not flash the list.
|
||||
- Log TCP listener failures with the Windows error and peer connection state changes for troubleshooting.
|
||||
|
||||
## 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: 205 LAN input checks (including missed discovery and authentication before discovery), 289 clipboard transfer, 43 pairing, 829 settings/chat UI, 570 input, 341 native, 126 sync 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. LAN remains experimental; three physical PCs have not been verified together.
|
||||
|
||||
**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.
|
||||
Update both PCs to receive these fixes. Preserve each PC's own data directory and pairing identity. Input protocol v2 remains compatible with v0.6.57. A changed instance ID requires pairing again. The computer list refreshes automatically once per second while open.
|
||||
|
||||
## 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.
|
||||
|
||||
@@ -1 +1 @@
|
||||
f3ec4f7a62b4479929d94d6d6f7f68edb8b4a47b84edac7258afb64dbe07aef2 KSwitcher-win-x64.zip
|
||||
16d8138c63442931bf947707b28820d9b89943169ac9f25db3cb50d23c0ff319 KSwitcher-win-x64.zip
|
||||
+35
-35
@@ -1,86 +1,86 @@
|
||||
{
|
||||
"version": "0.6.57",
|
||||
"schema": 1,
|
||||
"files": [
|
||||
{
|
||||
"size": 2106368,
|
||||
"sha256": "a7be2f749082bb6417b4cf7e8c8ea221ef4f7ba63dabc3cbbe8694ab1a3d942c",
|
||||
"path": "KSwitcher.exe",
|
||||
"sha256": "3b7315e1545f70739fd1a7c099ddeab28362b323521d93879fa43fdd76133f8e"
|
||||
"size": 2108416
|
||||
},
|
||||
{
|
||||
"size": 2003456,
|
||||
"sha256": "c8a88364c9967ce5fd150519d632b14460e225a0cfae51a8de50f39782d8010c",
|
||||
"path": "KSwitcher.Config.exe",
|
||||
"sha256": "a819ec42030990725326730396680388cbcb870dd00c0a97856cb1615f9db27b"
|
||||
"size": 2005504
|
||||
},
|
||||
{
|
||||
"size": 3151360,
|
||||
"sha256": "68eed5abefed0208ef7bdd431094c7357c53bd3ca03dfd91aaeb7dbb4f291e06",
|
||||
"path": "KSwitcher.Screenshot.exe",
|
||||
"sha256": "8473697245c2c961496135eaaff6de9e9a19e917f2fd7299a16068210223d396"
|
||||
"size": 3153408
|
||||
},
|
||||
{
|
||||
"size": 4401,
|
||||
"sha256": "d3c81140cce01c26bf6cbf52aa40e0825e1fa35b9a91d1387170a65d2597082c",
|
||||
"path": "LICENSE",
|
||||
"sha256": "d3c81140cce01c26bf6cbf52aa40e0825e1fa35b9a91d1387170a65d2597082c"
|
||||
"size": 4401
|
||||
},
|
||||
{
|
||||
"size": 1096,
|
||||
"sha256": "61e08da79c44061654015ce6b0c4061c23ee1dd18237fbf09e9bfda3c0ea2831",
|
||||
"path": "json-LICENSE.txt",
|
||||
"sha256": "61e08da79c44061654015ce6b0c4061c23ee1dd18237fbf09e9bfda3c0ea2831"
|
||||
"size": 1096
|
||||
},
|
||||
{
|
||||
"size": 79896,
|
||||
"sha256": "87f396a6bf988725b14bf5b6eb7c2b60ce33ff51828290ce40e136c66942cc09",
|
||||
"path": "README.md",
|
||||
"sha256": "8b4b1e905a2f0a39d532e61136ec7eca473492a39a538eb073ed829307adefae"
|
||||
"size": 80168
|
||||
},
|
||||
{
|
||||
"size": 7921,
|
||||
"sha256": "72e131bdf46d32a8a17fb352292bc8983c0e5a7ed84812218b11cf6b69c9afc4",
|
||||
"path": "LAN-PREVIEW.md",
|
||||
"sha256": "2467b86bbad7d7dce1e0c65c8c0faf2320e4804e90a85784a058afa357127b6b"
|
||||
"size": 8376
|
||||
},
|
||||
{
|
||||
"size": 732485,
|
||||
"sha256": "d0c022b27b9fbc2ff7b6b8bec723cc1adc4eef517202f9ff77eed5d8bea3acb9",
|
||||
"path": "dictionaries/en.txt",
|
||||
"sha256": "d0c022b27b9fbc2ff7b6b8bec723cc1adc4eef517202f9ff77eed5d8bea3acb9"
|
||||
"size": 732485
|
||||
},
|
||||
{
|
||||
"size": 20566,
|
||||
"sha256": "0bdfb0201d1803634f268cdff0993b8349d2c70e956a06dbb667c8dd65254ec5",
|
||||
"path": "dictionaries/LICENSE-CC-BY-SA-4.0.txt",
|
||||
"sha256": "0bdfb0201d1803634f268cdff0993b8349d2c70e956a06dbb667c8dd65254ec5"
|
||||
"size": 20566
|
||||
},
|
||||
{
|
||||
"size": 2265,
|
||||
"sha256": "3017bf44d3be9d9dfab21bd6778aa52b7947e61acf9ce2bc86b41d6e949ac311",
|
||||
"path": "dictionaries/README.md",
|
||||
"sha256": "3017bf44d3be9d9dfab21bd6778aa52b7947e61acf9ce2bc86b41d6e949ac311"
|
||||
"size": 2265
|
||||
},
|
||||
{
|
||||
"size": 1109134,
|
||||
"sha256": "80e6b6a27815973535f7facb636c597eb845dfee45cfabfc4e9b3341b9332849",
|
||||
"path": "dictionaries/ru.txt",
|
||||
"sha256": "80e6b6a27815973535f7facb636c597eb845dfee45cfabfc4e9b3341b9332849"
|
||||
"size": 1109134
|
||||
},
|
||||
{
|
||||
"size": 1280,
|
||||
"sha256": "98e5b3fe7304e37705c9e5ff48b0b399ccb16e806736d21f09921b99e5f2a408",
|
||||
"path": "dictionaries/sources.json",
|
||||
"sha256": "98e5b3fe7304e37705c9e5ff48b0b399ccb16e806736d21f09921b99e5f2a408"
|
||||
"size": 1280
|
||||
},
|
||||
{
|
||||
"size": 940799,
|
||||
"sha256": "d30e990d6ec2983c04f3644a3c61c6c668e8c542323f5931916fdd55b916eebf",
|
||||
"path": "dictionaries/uk.txt",
|
||||
"sha256": "d30e990d6ec2983c04f3644a3c61c6c668e8c542323f5931916fdd55b916eebf"
|
||||
"size": 940799
|
||||
},
|
||||
{
|
||||
"size": 993,
|
||||
"sha256": "a34b5161a8315af037e1ec71d816bfc688f7c364141d4ee2347cf0515ef0c82a",
|
||||
"path": "tools/README.txt",
|
||||
"sha256": "a34b5161a8315af037e1ec71d816bfc688f7c364141d4ee2347cf0515ef0c82a"
|
||||
"size": 993
|
||||
},
|
||||
{
|
||||
"size": 134656,
|
||||
"sha256": "6ef5cf64708d62b7fc63c3c793cc99188a513433ff4774cae01dc014f6664376",
|
||||
"path": "tools/StayHere.exe",
|
||||
"sha256": "6ef5cf64708d62b7fc63c3c793cc99188a513433ff4774cae01dc014f6664376"
|
||||
"size": 134656
|
||||
},
|
||||
{
|
||||
"size": 2796,
|
||||
"sha256": "ba214e2aca7405e698b3ab21ceb1f41ce088edd996b3e30e4a9d58ff496f9413",
|
||||
"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"
|
||||
201 RCDATA "update.ps1"
|
||||
1 VERSIONINFO
|
||||
FILEVERSION 0,6,57,0
|
||||
PRODUCTVERSION 0,6,57,0
|
||||
FILEVERSION 0,6,58,0
|
||||
PRODUCTVERSION 0,6,58,0
|
||||
FILEOS VOS_NT_WINDOWS32
|
||||
FILETYPE VFT_APP
|
||||
BEGIN
|
||||
@@ -20,7 +20,7 @@ BEGIN
|
||||
BEGIN
|
||||
VALUE "FileDescription", "KSwitcher — native keyboard and clipboard utility"
|
||||
VALUE "ProductName", "KSwitcher"
|
||||
VALUE "FileVersion", "0.6.57"
|
||||
VALUE "FileVersion", "0.6.58"
|
||||
END
|
||||
END
|
||||
BLOCK "VarFileInfo"
|
||||
|
||||
+2
-2
@@ -242,8 +242,8 @@ struct LanService::Impl
|
||||
if(peers.contains(target))send(socket,peers.at(target).address,packet);
|
||||
});
|
||||
clipboardActive=clipboard->active();
|
||||
if(shareInput)for(auto &[peer,entry]:trusted.items())if(peers.contains(peer))
|
||||
endpoints.push_back({peer,entry.at("key"),peers.at(peer).address.sin_addr.s_addr});
|
||||
if(shareInput)for(auto &[peer,entry]:trusted.items())
|
||||
endpoints.push_back({peer,entry.at("key"),peers.contains(peer)?peers.at(peer).address.sin_addr.s_addr:0});
|
||||
if(now>=nextHello)
|
||||
{
|
||||
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.");}
|
||||
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;};
|
||||
std::string local;uint16_t port;SOCKET listener=INVALID_SOCKET;
|
||||
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)
|
||||
{
|
||||
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;
|
||||
}
|
||||
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));
|
||||
}
|
||||
// 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;});
|
||||
if(!listenNeeded){closeSocket(listener);for(auto &p:pending)closeSocket(p.socket);pending.clear();}
|
||||
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);
|
||||
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.";
|
||||
}else error.clear();
|
||||
auto detail="TCP input port "+std::to_string(port)+" is unavailable (Windows error "+std::to_string(WSAGetLastError())+").";
|
||||
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){
|
||||
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)){
|
||||
done=true;
|
||||
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 &){}
|
||||
}
|
||||
}
|
||||
@@ -252,6 +262,7 @@ struct InputHub::Impl
|
||||
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);
|
||||
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;}
|
||||
for(auto &f:received)in.push_back({id,f,link.epoch});
|
||||
}
|
||||
|
||||
+7
-1
@@ -36,7 +36,13 @@ void refreshLan(HWND w,LanView &v)
|
||||
for(const auto &peer:next["peers"])
|
||||
{
|
||||
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);
|
||||
}
|
||||
// Presence/challenge refreshes must not repaint or reset the pairing list.
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
#include "platform.h"
|
||||
namespace ks
|
||||
{
|
||||
inline constexpr wchar_t AppVersion[] = L"0.6.57";
|
||||
inline constexpr wchar_t AppVersion[] = L"0.6.58";
|
||||
inline constexpr UINT UpdateMessage = WM_APP + 7;
|
||||
inline constexpr UINT UpdateCheckIntervalMs = 6 * 60 * 60 * 1000;
|
||||
inline constexpr UINT UpdateNotificationRetryMs = 5 * 60 * 1000;
|
||||
|
||||
+22
-11
@@ -7,6 +7,15 @@
|
||||
|
||||
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{};
|
||||
void require(bool ok, const char *message)
|
||||
{
|
||||
@@ -141,31 +150,31 @@ int main()
|
||||
SetDlgItemTextW(parent,240,originalLimit);
|
||||
require(!saveEnabled(),"restoring valid original text clears dirty state");
|
||||
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");
|
||||
wchar_t languages[128]{}; GetDlgItemTextW(ocr,503,languages,128);
|
||||
SetDlgItemTextW(ocr,503,L"eng");
|
||||
SendMessageW(ocr,WM_COMMAND,IDCANCEL,0);
|
||||
require(!saveEnabled(),"cancelled child changes leave Settings clean");
|
||||
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");
|
||||
SendMessageW(ocr,WM_COMMAND,IDOK,0);
|
||||
require(!IsWindow(ocr) && saveEnabled(),"accepted OCR edits enable parent Save");
|
||||
SendMessageW(parent,WM_COMMAND,257,0);
|
||||
ocr=FindWindowW(L"KSwitcher.OcrOptions",nullptr);
|
||||
ocr=findTestWindow(L"KSwitcher.OcrOptions",nullptr);
|
||||
SetDlgItemTextW(ocr,503,languages);
|
||||
SendMessageW(ocr,WM_COMMAND,IDOK,0);
|
||||
require(!saveEnabled(),"reverting OCR draft in child clears parent dirty state");
|
||||
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");
|
||||
bool replace=IsDlgButtonChecked(automatic,414)==BST_CHECKED;
|
||||
CheckDlgButton(automatic,414,replace?BST_UNCHECKED:BST_CHECKED);
|
||||
SendMessageW(automatic,WM_COMMAND,IDOK,0);
|
||||
require(!IsWindow(automatic) && saveEnabled(),"accepted auto-correction edits enable Save");
|
||||
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);
|
||||
SendMessageW(automatic,WM_COMMAND,IDOK,0);
|
||||
require(!saveEnabled(),"reverting auto-correction draft disables Save");
|
||||
@@ -201,7 +210,7 @@ int main()
|
||||
SendMessageW(parent, WM_NOTIFY, 260, (LPARAM)¬ification);
|
||||
require(!saveEnabled(),"changing tabs leaves Settings clean");
|
||||
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");
|
||||
int index = (int)SendDlgItemMessageW(dialog, 710, LB_FINDSTRINGEXACT, -1, (LPARAM)L"Telegram: ! Fixture chat");
|
||||
require(index >= 0, "saved chat listed");
|
||||
@@ -220,7 +229,7 @@ int main()
|
||||
require(!IsWindow(dialog) && IsWindowEnabled(parent), "cancel closes dialog");
|
||||
require(!saveEnabled(),"cancelled chat edits do not enable Save");
|
||||
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");
|
||||
SendDlgItemMessageW(dialog, 710, LB_SETCURSEL, index, 0);
|
||||
SendMessageW(dialog, WM_COMMAND, MAKEWPARAM(710, LBN_SELCHANGE), 0);
|
||||
@@ -236,7 +245,7 @@ int main()
|
||||
parent = app.settingsWindow;
|
||||
require(!saveEnabled(),"reopened saved Settings is clean");
|
||||
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");
|
||||
SendDlgItemMessageW(dialog, 710, LB_SETCURSEL, index, 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");
|
||||
ks::showSettings(app);parent=app.settingsWindow;
|
||||
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(!IsWindowEnabled(GetDlgItem(lan,705)),"pairing disabled without discovered peer");
|
||||
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())));
|
||||
ks::App pairedApp(GetModuleHandleW(nullptr),true,root);ks::showSettings(pairedApp);
|
||||
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");
|
||||
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");
|
||||
SendDlgItemMessageW(lan,702,LB_SETCURSEL,-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})}}}};
|
||||
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);
|
||||
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);
|
||||
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));
|
||||
|
||||
@@ -317,6 +317,11 @@ void hubChecks()
|
||||
auto deadline=GetTickCount64()+5000;
|
||||
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");
|
||||
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};
|
||||
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();
|
||||
@@ -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");
|
||||
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");
|
||||
{
|
||||
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);
|
||||
check(!hubs[0]->ready().contains(ids[1])&&hubs[0]->ready().contains(ids[2]),"forgetting one pair revokes only its channel");
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user