Compare commits

...
Author SHA1 Message Date
agrynco 7242c3f775 test(settings): keep picker fixture within the monitor 2026-11-14 15:17:54 +02:00
agrynco a43f6441b4 chore(release): refresh verified windows package 2026-11-14 15:15:20 +02:00
agrynco 2f5ae373a1 fix(settings): preserve unset files shortcut in options 2026-11-14 15:14:16 +02:00
agrynco b978361180 chore(release): package kswitcher v0.6.78 with files 2026-11-14 15:11:26 +02:00
agrynco 93b8055085 fix(files): synchronize settings across windows
Reload shared preferences in open windows and apply only fields changed in Options.
2026-11-14 15:09:17 +02:00
agrynco 70ee338fc3 Merge pull request 'Add searchable command history to KSwitcher Files' (#19) from codex/files-command-history into main 2026-11-14 14:43:10 +02:00
agrynco 46b4b51cea feat(files): add searchable command history
Persist command recall and align keyboard, mouse, and dialog behavior.

Refs #18
2026-11-14 14:42:48 +02:00
agrynco 721c3f0f38 docs: record KSwitcher issue workflow 2026-11-14 12:56:47 +02:00
agrynco 47851d75ef Merge pull request 'Add folder shortcuts to KSwitcher Files' (#17) from codex/issue-15-folder-shortcuts into main
Fixes #15
2026-11-14 12:20:53 +02:00
agrynco ad01951956 Add folder shortcuts to KSwitcher Files
Share Far dialog fields, palette, frame layout, and configurable padding. Document shortcut behavior and the shared-code rule.

Refs #15
2026-11-14 12:19:13 +02:00
agrynco bf092d2764 Merge pull request 'Customize file type styles and preview Files options' (#16) from codex/files-type-styles into main
Fixes #14
2026-11-14 11:26:01 +02:00
agrynco d5137887b9 Customize file type styles and preview Files options
Refs #14
2026-11-14 11:25:31 +02:00
agrynco c75cc989f4 Merge pull request 'Add Files shortcuts and unified Options' (#13) from codex/issue-12-files-options into main
Fixes #12
2026-11-14 10:47:30 +02:00
agrynco f580892ca9 Add Files shortcuts and unified Options
Refs #12
2026-11-14 10:44:54 +02:00
agrynco 372c4cb8bd Merge pull request 'Browse and manage files on paired KSwitcher PCs' (#11) from codex/issue-10-lan-files into main
Fixes #10
2026-11-14 09:49:36 +02:00
agrynco 37158c1ff0 Browse and manage files on paired KSwitcher PCs
Refs #10
2026-11-14 09:47:06 +02:00
agrynco 02f81327fd Improve Change drive and list KSwitcher LAN peers
Refs #9
2026-11-14 03:07:55 +02:00
agrynco 2834510680 Merge KSwitcher Files operations and embedded PowerShell 2026-11-14 02:39:34 +02:00
agrynco 2c53e99efc Embed PowerShell terminal and polish file panels
Refs #8
2026-11-14 02:38:47 +02:00
agrynco fb98731ec2 Add F6 rename and move to KSwitcher Files
Refs #7
2026-11-14 01:25:45 +02:00
agrynco 531efec2eb Add file deletion and Recycle Bin support
Refs #6
2026-11-14 01:00:40 +02:00
agrynco 27c397e375 Highlight the cursor only in the active file panel 2026-11-14 00:21:45 +02:00
agrynco 186621eb3c Integrate panel paths into the file manager frames 2026-11-14 00:15:55 +02:00
agrynco d3a0b83692 Merge pull request 'Activate focused dialog buttons with Enter' (#5) from codex/kswitcher-files into main 2026-11-14 00:06:19 +02:00
agrynco f38886d1ea Activate focused dialog buttons with Enter
Refs #4
2026-11-14 00:05:51 +02:00
agrynco e2c6734817 Merge pull request 'Add KSwitcher Files local manager and copy workflow' (#3) from codex/kswitcher-files into main 2026-11-14 00:01:06 +02:00
agrynco 10d5a42f2a Add KSwitcher Files local manager and copy workflow 2026-11-13 23:59:44 +02:00
agrynco 34d89db95b Release KSwitcher v0.6.77 with local pencil smoothing 2026-11-13 14:58:55 +02:00
agrynco c10b141483 Release KSwitcher v0.6.76 with incoming file notifications 2026-11-13 14:24:47 +02:00
agrynco 0c77d5e46d Release KSwitcher v0.6.75 with clearer diagnostics and custom replacements 2026-11-13 13:01:07 +02:00
agrynco 70a53834e3 Release KSwitcher v0.6.74 with LAN file transfer and diagnostics MCP 2026-11-13 12:22:52 +02:00
agrynco 73448058d7 Release KSwitcher v0.6.73 with autostart path repair 2026-11-13 10:28:41 +02:00
agrynco 12fdd7c549 Release KSwitcher v0.6.72 with dedicated settings tabs 2026-11-12 21:17:15 +02:00
agrynco 3cb2eee637 Release KSwitcher v0.6.71 with Enter snippet confirmation 2026-11-12 19:05:38 +02:00
agrynco 41a1426a16 Ignore whole-line caret bounds for snippet suggestions 2026-11-12 17:46:44 +02:00
agrynco 601a7288c4 Release KSwitcher v0.6.70 with text-caret snippet placement 2026-11-12 17:20:47 +02:00
agrynco 7eca94ea59 Release KSwitcher v0.6.69 with snippet autocomplete 2026-11-12 16:51:08 +02:00
agrynco 299e61a5aa Release KSwitcher v0.6.68 with elevated restart 2026-11-12 11:52:23 +02:00
agrynco 2bafdbf863 Release KSwitcher v0.6.67 2026-11-12 10:58:07 +02:00
agrynco 1df2e2e90b Apply mouse bindings to forwarded input 2026-11-12 10:54:47 +02:00
agrynco c0defcbfb2 Release KSwitcher v0.6.66 with tray icon retry 2026-11-12 09:16:38 +02:00
agrynco 8a65e7f7fb Release KSwitcher v0.6.65 2026-11-12 01:18:21 +02:00
agrynco 14f33012a3 feat: release kswitcher 0.6.64
Add mouse button actions and application profiles, with volume controls and per-monitor settings layout fixes.
2026-11-12 00:39:12 +02:00
agrynco 5a2106899d Release v0.6.63: automatic LAN transport fallback and connection help 2026-11-10 19:30:50 +02:00
agrynco 3761b790ae Release v0.6.62: identify applications by window 2026-11-10 00:46:07 +02:00
agrynco a0e1de40ef Pick an application by dragging onto its highlighted window 2026-11-10 00:43:02 +02:00
agrynco 5d96abb83e Release v0.6.61: configure languages per application 2026-11-09 23:24:42 +02:00
agrynco 011664cc53 Release v0.6.60: allow apostrophes in custom corrections 2026-11-09 11:59:49 +02:00
agrynco 4d8d8d47b9 Release v0.6.59: clarify sync results and refresh downloaded settings 2026-11-09 11:22:27 +02:00
agrynco d0387904a0 Release v0.6.58: preserve LAN connections across missed discovery 2026-11-06 22:08:47 +02:00
agrynco 5de010ee44 Release v0.6.57: multi-PC input, shared clipboard and resilient Settings Save 2026-11-06 21:33:37 +02:00
agrynco 82292c8a7e Release v0.6.56: LAN mouse and keyboard sharing 2026-11-06 16:39:04 +02:00
agrynco 4fbeb56421 Wait for native layout selection repaint in UI capture 2026-11-06 16:30:48 +02:00
agrynco e0bfdd9a90 Add encrypted LAN mouse and keyboard sharing with four-edge handoff 2026-11-06 16:28:43 +02:00
agrynco 9b565dd55e Add opt-in KSwitcher LAN discovery and mutual code pairing preview 2026-11-06 15:30:06 +02:00
agrynco 346af2256f Release v0.6.55: reliable snippet selection in desktop chats 2026-11-06 15:04:00 +02:00
agrynco 99db99e603 Fix partial snippet selection in desktop chat editors 2026-11-06 14:58:24 +02:00
agrynco 63b7b879a3 Release v0.6.54: adjustable pencil smoothing and unsaved settings state 2026-11-06 14:46:53 +02:00
agrynco 727dc96908 Release v0.6.53: synchronize chat language profiles 2026-11-06 12:39:00 +02:00
agrynco ede4ce47c0 Release v0.6.52: add Arc tool to screenshot editor 2026-11-06 12:32:27 +02:00
agrynco 3b8b8f26fe Release v0.6.51: Telegram Web and application-scoped snippets 2026-11-06 10:47:31 +02:00
agrynco 19f1e4531c Center Add app button against the application selector 2026-11-06 10:41:21 +02:00
agrynco a124e030ab Add application-scoped snippets with global fallback and scope-aware sync 2026-11-06 10:37:01 +02:00
agrynco c18148a574 Add event-driven Telegram Web K chat profiles for Chrome 2026-11-06 10:26:42 +02:00
agrynco 15197da548 Release v0.6.50: disable Viber chat OCR 2026-11-06 00:21:39 +02:00
agrynco 66e24b5efc Disable Viber chat OCR while preserving saved profiles 2026-11-06 00:18:04 +02:00
agrynco 178573233b Release v0.6.49: Viber chat language profiles with local OCR 2026-11-05 23:52:50 +02:00
agrynco 4e7a683c47 Release v0.6.48: chat language profiles and tabbed settings 2026-11-05 23:25:33 +02:00
agrynco e33387c2d5 Release v0.6.47: allow multi-word auto-correction replacements 2026-11-05 22:51:57 +02:00
agrynco 5de5b791bf Release v0.6.46: bundle StayHere activity utility 2026-11-05 12:50:37 +02:00
agrynco c109408a98 Release v0.6.45: fix Far snippets and ship portable Utilities folder 2026-11-05 12:35:44 +02:00
agrynco 90b2e0841f Release v0.6.44: select newest item when opening clipboard history 2026-11-05 00:15:54 +02:00
agrynco 6621a49022 Release v0.6.43: fix forwarded input and verify snippet pastes 2026-11-04 21:18:39 +02:00
agrynco a2b88f247c Release v0.6.42: use compact pinned checkbox beside history search 2026-11-04 20:38:36 +02:00
agrynco 684ca1b3cc Release v0.6.41: filter and remember pinned clipboard history 2026-11-04 19:58:15 +02:00
agrynco 524a836cc0 Release v0.6.40: align tagged source and Windows version metadata 2026-11-04 19:45:52 +02:00
agrynco 00c9cb0ee4 Release v0.6.39: isolate history UI and image clipboard memory 2026-11-04 19:40:54 +02:00
agrynco fa2f6d2ce4 Release v0.6.38: avoid unnecessary RichEdit and cancel hidden history previews 2026-11-04 17:24:57 +02:00
agrynco 30e70192dc Release v0.6.37: isolate image capture and OCR, fingerprint history duplicates 2026-11-04 16:58:34 +02:00
agrynco 94cab4c112 Release v0.6.36: offload notification registration and mitigate Viber paste timing 2026-11-04 15:40:50 +02:00
agrynco d13e9849a7 Release v0.6.35: reduce resident memory and compact shared dictionaries 2026-11-04 14:08:05 +02:00
agrynco 004fee4976 Release v0.6.34: use four-direction cursors for movable screenshot handles 2026-11-04 01:43:49 +02:00
agrynco b08c5ff8ff Release v0.6.33: warn about unsaved settings and expose menu access keys 2026-11-04 01:11:15 +02:00
agrynco b6e7bda220 Release v0.6.32: make screenshot pan test independent of viewport DPI 2026-11-03 15:34:23 +02:00
agrynco 89e506faae Release v0.6.31: verify trailing spaces omitted by editor copy 2026-11-03 14:45:35 +02:00
agrynco c78b51d341 Record completed v0.6.30 release validation 2026-11-02 11:57:19 +02:00
agrynco 0184b837f4 Release v0.6.30: guard paste retries and clear failed conversion state 2026-11-02 11:54:58 +02:00
agrynco ca553a5e7d Release v0.6.29: clear clipboard history from its window 2026-11-02 11:34:45 +02:00
agrynco 55993c0f90 Release v0.6.28: typo correction, partial sync and selection diagnostics 2026-11-02 11:19:13 +02:00
agrynco 738c81dce2 Release v0.6.27: menu and Alt access keys 2026-11-02 01:23:33 +02:00
agrynco 22edece7cf Release v0.6.26: OpenBao settings synchronization with per-setting conflicts 2026-11-02 00:37:57 +02:00
agrynco 908985d775 Release v0.6.25: recognize pencil strokes as straight lines 2026-11-01 23:00:59 +02:00
agrynco 20f3b72287 Release v0.6.24: smart shapes, rotation and canvas zoom 2026-11-01 22:25:15 +02:00
agrynco 9b231146cb Release v0.6.23: improve browser selection, Ukrainian correction and PNG saving 2026-11-01 13:39:37 +02:00
agrynco c0ab02df16 Release v0.6.22: recover verified partial text selections 2026-10-31 12:32:26 +02:00
agrynco 64a04b4382 Release v0.6.21: skip unchanged layouts during conversion 2026-10-31 11:39:19 +02:00
agrynco 4c5275631c Remove development and scrolling internals from README 2026-10-31 09:17:38 +02:00
agrynco 34e07e7107 Keep public release documentation focused on image editing 2026-10-31 09:17:17 +02:00
agrynco 70ea9b10bc Release v0.6.20: edit history images and share smooth wheel handling 2026-10-31 09:12:59 +02:00
agrynco 6603d28f68 Release v0.6.19: retry dropped copies and stabilize snippet integration 2026-10-31 02:03:07 +02:00
agrynco 0d3b2acae9 Release v0.6.18: verify conversion results and log problems only 2026-10-31 01:36:56 +02:00
agrynco 4eb9f6c671 Release v0.6.17: constrained pencil strokes and snippet editor states 2026-10-30 14:13:52 +02:00
agrynco a830f55575 Release v0.6.16: prevent reinstalling the current version 2026-10-30 13:24:11 +02:00
agrynco 07e31228d7 Release v0.6.15: reliable snippets and Enter actions 2026-10-30 13:15:50 +02:00
agrynco 39d3876fd2 Release v0.6.14: resize correction settings and personal dictionaries 2026-10-30 00:36:32 +02:00
agrynco 4cdcc92eb7 Release v0.6.13: correct words across punctuation keys 2026-10-30 00:28:16 +02:00
agrynco 77f27ea087 Release v0.6.12: improve continuous correction and resize log viewer 2026-10-30 00:15:09 +02:00
agrynco c326db7dc5 Release v0.6.11: show branded update progress and completion 2026-10-29 23:03:06 +02:00
agrynco 8b5f632ad7 Release v0.6.10: preserve typing during correction and add personal dictionaries 2026-10-29 22:44:08 +02:00
agrynco 5c786e12a9 Release v0.6.9: notify about updates and fix Settings scroll flicker 2026-10-29 22:15:30 +02:00
agrynco 107d2f8852 Release v0.6.8: add background image OCR and optional Tesseract 2026-10-29 21:40:29 +02:00
agrynco 95c7199027 Release v0.6.7: add DPI-aware cursors for screenshot tools 2026-10-29 18:17:54 +02:00
agrynco 8fedb54dfe Release v0.6.6: add pointer tool and isolate text rotation defaults 2026-10-29 17:57:28 +02:00
agrynco bbf20e8270 Release v0.6.5: preserve caption links and connect arrows to other objects 2026-10-29 17:15:47 +02:00
agrynco b8bb14a985 Release v0.6.4: preserve connector links when dragging numbered markers 2026-10-29 15:14:44 +02:00
agrynco d28e28c03c Release v0.6.3: fix capture banner ordering and check updates periodically 2026-10-29 13:06:58 +02:00
agrynco 6b180798f5 Release v0.6.2: configurable screenshot editor and DPI fixes 2026-10-29 12:41:48 +02:00
agrynco 21a09e33be Release v0.6.1 with Win+V default and bilingual wiki links 2026-10-29 00:34:27 +02:00
agrynco 3dbd7317f1 Style development support with branded donation cards 2026-10-29 00:19:34 +02:00
agrynco 510d0ed211 Add Monobank support link to README 2026-10-29 00:16:36 +02:00
agrynco dd9629f8fd Release v0.6.0: screenshot notifications with previews and responsive clipboard history 2026-10-29 00:01:04 +02:00
agrynco ccaa8dad3f Release v0.5.0: isolate screenshot editor and support manifest-based updates 2026-10-28 22:52:52 +02:00
agrynco d1eabfca88 Release v0.4.1: harden update checksum and fix Settings overlap 2026-10-28 22:22:08 +02:00
agrynco dc615d0223 Release v0.4.0: add screen capture and annotation editor 2026-10-28 22:15:59 +02:00
agrynco e5ff9495e4 Release v0.3.0: move settings into an on-demand single-instance configurator 2026-10-28 19:51:18 +02:00
agrynco eb30701e07 Release v0.2.3: fix Settings overlap and repeated-conversion logging 2026-10-28 13:36:42 +02:00
agrynco 855719863e Release v0.2.2: fix Unicode executable description 2026-10-28 11:14:31 +02:00
agrynco 1093bb5b2e Release v0.2.1: support remote input, safe updates and high-DPI windows 2026-10-28 11:02:15 +02:00
agrynco cf18a0a3c5 Release v0.2.0 with incident diagnostics and in-app log viewer 2026-10-28 01:38:30 +02:00
agrynco 076210dc9c Link every Typlo mention in README 2026-10-28 00:14:48 +02:00
agrynco 5531c49224 Link user wiki and original Typlo project in README 2026-10-28 00:14:21 +02:00
215 changed files with 35292 additions and 636 deletions

No files matched your search

+70
View File
@@ -0,0 +1,70 @@
# Project instructions
## Reuse shared code
Reuse existing shared code wherever possible. When the same behavior,
component, or logic is needed in two or more places, extract it into a
shared component or function and use it at every applicable call site.
## Mandatory Wiki updates for every release
A release is not complete until the published project Wiki reflects that release.
Apply this rule to every release, including patch releases and bug-fix-only releases.
Wiki: https://gitea.hrynco.com/hrynco/hrynco-kswitcher/wiki
1. Confirm the release version from `CMakeLists.txt`, the release tag and
`Release/README.md`. Use the complete version (for example, `v0.6.17`) and
preserve the release channel rules in `README.md`.
2. Update the latest-version information on the Wiki home pages and any other
pages identifying the current release. Update release dates, download links
and upgrade instructions where needed. Keep historical version references
and compatibility bridge instructions when they are still accurate.
3. Review the changes since the last release documented in the Wiki, not just
the immediately preceding release. Bring the relevant feature pages up to
date with the shipped behavior: new, changed and removed functionality,
settings, shortcuts, defaults, limitations and troubleshooting. Update
screenshots or examples when they no longer match the application.
For bug-fix-only releases, check whether documented behavior or workarounds
need correction; do not invent feature changes.
4. Keep English and Ukrainian pages synchronized, including `Home` and
`Home-uk`, and preserve working translation and navigation links.
5. Publish the Wiki changes as part of the release workflow. Read back the
published pages and verify the current version, feature descriptions,
download links and translation links. A local Wiki edit alone is not enough.
6. In the release completion report, link the updated Wiki pages and summarize
the documentation changes. If Wiki publication or verification is blocked,
explicitly report the outstanding step and do not claim the release workflow
is complete.
Updating only the release notes or changing only the Wiki version number does
not satisfy this rule; the current functionality must also be reviewed.
## Gitea issue authentication
For Gitea issue API requests from this checkout, use the project token stored
with Windows DPAPI in `kswitcher-gitea-token.dpapi` under the directory returned
by `git rev-parse --absolute-git-dir`. Read it with PowerShell
`Get-Content -LiteralPath $tokenFile | ConvertTo-SecureString` and use it only
for the Gitea authorization header. Keep the token out of tracked files and
command output.
## KSwitcher Files UI
Before changing the file manager UI, read [FILES-UI.md](FILES-UI.md).
Reuse the shared dialog rendering and layout helpers. Update the document
when an agreed UI requirement changes.
## Link changes to Gitea issues
For commits that implement a Gitea issue, include `Refs #<number>` in the
commit message body. In a pull request, use `Fixes #<number>` when merging
fully resolves the issue, or `Refs #<number>` when the work is partial.
Use the existing issue for follow-up commits on the same change.
Assign each new issue to the [KSwitcher project board](https://gitea.hrynco.com/hrynco/hrynco-kswitcher/projects/8)
using its default column, and verify that the issue appears on the board.
Use Markdown headings, lists, and bold emphasis to make issue descriptions
easy to scan. Embed relevant screenshots as Markdown images using their
Gitea attachment URLs, and verify that the images render in the description.
+192 -11
View File
@@ -1,37 +1,218 @@
cmake_minimum_required(VERSION 3.24)
project(KSwitcher VERSION 0.1.0 LANGUAGES CXX RC)
project(KSwitcher VERSION 0.6.78 LANGUAGES CXX RC)
if(NOT WIN32)
message(FATAL_ERROR "KSwitcher uses native Windows APIs")
endif()
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
add_library(kscore src/core.cpp src/platform.cpp src/autostart.cpp src/autocorrect.cpp src/storage.cpp src/ocr.cpp src/richtext.cpp)
add_executable(KSwitcherFiles WIN32 src/files_main.cpp resources/files.rc)
set_target_properties(KSwitcherFiles PROPERTIES OUTPUT_NAME "KSwitcher.Files")
target_compile_definitions(KSwitcherFiles PRIVATE UNICODE _UNICODE NOMINMAX _WIN32_WINNT=0x0A00)
find_program(KSWITCHER_POWERSHELL NAMES pwsh powershell REQUIRED)
add_custom_target(KSwitcherTerminalAssets
COMMAND "${KSWITCHER_POWERSHELL}" -NoProfile -File "${CMAKE_CURRENT_SOURCE_DIR}/tools/stage-files-terminal.ps1"
-Destination "$<TARGET_FILE_DIR:KSwitcherFiles>"
VERBATIM)
add_dependencies(KSwitcherFiles KSwitcherTerminalAssets)
target_include_directories(KSwitcherFiles PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/build/terminal-deps/sdk/build/native/include")
target_link_libraries(KSwitcherFiles PRIVATE user32 gdi32 comctl32 shell32 comdlg32 advapi32 ole32 uuid)
if(MSVC)
target_compile_options(KSwitcherFiles PRIVATE /utf-8)
else()
target_link_options(KSwitcherFiles PRIVATE -municode -static)
endif()
add_executable(StayHere WIN32 utilities/StayHere/main.cpp)
add_executable(StayHereTest WIN32 utilities/StayHere/main.cpp)
target_compile_definitions(StayHereTest PRIVATE STAYHERE_TEST_INSTANCE)
set_target_properties(StayHere PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/tools")
foreach(utilityTarget StayHere StayHereTest)
target_compile_definitions(${utilityTarget} PRIVATE UNICODE _UNICODE _WIN32_WINNT=0x0A00)
target_link_libraries(${utilityTarget} PRIVATE user32 shell32 gdi32 wtsapi32)
if(MSVC)
target_compile_options(${utilityTarget} PRIVATE /utf-8)
set_property(TARGET ${utilityTarget} PROPERTY MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>")
else()
target_link_options(${utilityTarget} PRIVATE -municode -static -static-libgcc -static-libstdc++)
endif()
endforeach()
add_library(kscore src/lan_files.cpp src/lan_channel.cpp src/lan_input.cpp src/lan.cpp src/lan_pairing.cpp src/core.cpp src/platform.cpp src/autostart.cpp src/autocorrect.cpp src/storage.cpp src/ocr.cpp src/richtext.cpp src/update.cpp src/sync.cpp)
target_link_libraries(KSwitcherFiles PRIVATE kscore)
target_include_directories(kscore PUBLIC src PRIVATE vendor)
target_compile_definitions(kscore PUBLIC UNICODE _UNICODE WIN32_LEAN_AND_MEAN NOMINMAX _WIN32_WINNT=0x0A00)
target_link_libraries(kscore PUBLIC user32 shell32 advapi32 crypt32 imm32 comctl32 gdi32 ole32 oleaut32 taskschd comdlg32 uuid windowscodecs runtimeobject uiautomationcore)
add_executable(KSwitcher WIN32 src/main.cpp src/app.cpp src/ui.cpp resources/app.rc)
target_link_libraries(kscore PUBLIC ws2_32 bcrypt iphlpapi winhttp user32 shell32 shlwapi advapi32 crypt32 imm32 comctl32 gdi32 ole32 oleaut32 taskschd comdlg32 uuid windowscodecs runtimeobject uiautomationcore)
add_executable(KSwitcher WIN32 src/main.cpp src/app.cpp src/screenshot_notifications.cpp src/screenshot_launcher.cpp src/screenshot_activation.cpp src/configuration.cpp src/ui.cpp resources/app.rc)
target_compile_definitions(KSwitcher PRIVATE KS_RESIDENT_APP)
target_link_libraries(KSwitcher PRIVATE kscore)
add_executable(KSwitcherConfig WIN32 src/config_main.cpp src/configuration.cpp src/ui.cpp resources/app.rc)
set_target_properties(KSwitcherConfig PROPERTIES OUTPUT_NAME "KSwitcher.Config")
target_compile_definitions(KSwitcherConfig PRIVATE KS_CONFIG_APP)
target_link_libraries(KSwitcherConfig PRIVATE kscore)
add_executable(KSwitcherTests tests/tests.cpp)
target_link_libraries(KSwitcherTests PRIVATE kscore)
add_executable(KSwitcherIntegration tests/integration.cpp src/app.cpp src/ui.cpp resources/app.rc)
target_link_libraries(KSwitcherIntegration PRIVATE kscore)
add_executable(KSwitcherOcrTests tests/ocr.cpp)
target_link_libraries(KSwitcherOcrTests PRIVATE kscore)
add_executable(KSwitcherSyncTests tests/sync.cpp)
target_include_directories(KSwitcherSyncTests PRIVATE vendor)
target_link_libraries(KSwitcherSyncTests PRIVATE kscore)
if(MSVC)
set_property(TARGET kscore KSwitcher KSwitcherTests KSwitcherIntegration KSwitcherOcrTests PROPERTY MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>")
set_property(TARGET KSwitcherSyncTests PROPERTY MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>")
else()
target_link_options(KSwitcherSyncTests PRIVATE -static -static-libgcc -static-libstdc++)
endif()
add_executable(KSwitcherScrollTests tests/scroll.cpp)
target_link_libraries(KSwitcherScrollTests PRIVATE kscore)
target_link_libraries(KSwitcherTests PRIVATE kscore)
add_executable(KSwitcherIntegration tests/integration.cpp src/screenshot_launcher.cpp src/screenshot_activation.cpp src/app.cpp src/screenshot_notifications.cpp src/screenshot.cpp src/configuration.cpp src/ui.cpp resources/app.rc)
target_link_libraries(KSwitcherIntegration PRIVATE kscore)
# Opt-in interactive fixture: requires an installed Far Manager.
add_executable(KSwitcherFarSnippetTests WIN32 tests/far_snippets.cpp src/screenshot_launcher.cpp src/screenshot_activation.cpp src/app.cpp src/screenshot_notifications.cpp src/screenshot.cpp src/configuration.cpp src/ui.cpp resources/app.rc)
target_link_libraries(KSwitcherFarSnippetTests PRIVATE kscore)
if(MSVC)
target_compile_options(KSwitcherFarSnippetTests PRIVATE /utf-8)
set_property(TARGET KSwitcherFarSnippetTests PROPERTY MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>")
else()
target_link_options(KSwitcherFarSnippetTests PRIVATE -municode -static -static-libgcc -static-libstdc++)
endif()
add_executable(KSwitcherInputTests tests/input.cpp src/screenshot_launcher.cpp src/screenshot_activation.cpp src/app.cpp src/screenshot_notifications.cpp src/screenshot.cpp src/configuration.cpp src/ui.cpp resources/app.rc)
target_link_libraries(KSwitcherInputTests PRIVATE kscore)
target_compile_definitions(KSwitcherInputTests PRIVATE KS_SCREENSHOT_RESOURCES)
add_executable(KSwitcherScreenshotUITests tests/screenshot_ui.cpp resources/app.rc)
target_link_libraries(KSwitcherScreenshotUITests PRIVATE kscore)
target_compile_definitions(KSwitcherScreenshotUITests PRIVATE KS_SCREENSHOT_RESOURCES)
add_executable(KSwitcherScreenshot WIN32 src/screenshot_main.cpp src/screenshot_notifications.cpp src/screenshot_activation.cpp src/screenshot.cpp resources/app.rc)
set_target_properties(KSwitcherScreenshot PROPERTIES OUTPUT_NAME "KSwitcher.Screenshot")
target_compile_definitions(KSwitcherScreenshot PRIVATE KS_SCREENSHOT_RESOURCES)
target_link_libraries(KSwitcherScreenshot PRIVATE kscore)
if(MSVC)
target_compile_options(KSwitcherScreenshotUITests PRIVATE /utf-8)
set_property(TARGET KSwitcherScreenshotUITests PROPERTY MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>")
else()
target_link_options(KSwitcherScreenshotUITests PRIVATE -static -static-libgcc -static-libstdc++)
endif()
add_executable(KSwitcherOcrTests tests/ocr.cpp src/screenshot_launcher.cpp src/screenshot_activation.cpp src/app.cpp src/screenshot_notifications.cpp src/screenshot.cpp src/configuration.cpp src/ui.cpp resources/app.rc)
target_link_libraries(KSwitcherOcrTests PRIVATE kscore)
add_dependencies(KSwitcherOcrTests KSwitcherScreenshot)
if(MSVC)
set_property(TARGET kscore KSwitcher KSwitcherConfig KSwitcherScreenshot KSwitcherTests KSwitcherScrollTests KSwitcherIntegration KSwitcherInputTests KSwitcherOcrTests PROPERTY MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>")
target_compile_options(kscore PRIVATE /W4 /utf-8)
target_compile_options(KSwitcher PRIVATE /W4 /utf-8)
target_compile_options(KSwitcherConfig PRIVATE /W4 /utf-8)
target_compile_options(KSwitcherScreenshot PRIVATE /W4 /utf-8)
else()
target_link_options(KSwitcher PRIVATE -municode -static -static-libgcc -static-libstdc++)
target_link_options(KSwitcherConfig PRIVATE -municode -static -static-libgcc -static-libstdc++)
target_link_options(KSwitcherScreenshot PRIVATE -municode -static -static-libgcc -static-libstdc++)
target_link_options(KSwitcherTests PRIVATE -static -static-libgcc -static-libstdc++)
target_link_options(KSwitcherScrollTests PRIVATE -static -static-libgcc -static-libstdc++)
target_link_options(KSwitcherIntegration PRIVATE -static -static-libgcc -static-libstdc++)
target_link_options(KSwitcherInputTests PRIVATE -static -static-libgcc -static-libstdc++)
target_link_options(KSwitcherOcrTests PRIVATE -static -static-libgcc -static-libstdc++)
endif()
enable_testing()
add_executable(KSwitcherLanTests tests/lan.cpp)
target_link_libraries(KSwitcherLanTests PRIVATE kscore)
if(MSVC)
set_property(TARGET KSwitcherLanTests PROPERTY MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>")
else()
target_link_options(KSwitcherLanTests PRIVATE -static -static-libgcc -static-libstdc++)
endif()
add_test(NAME lan-pairing-regressions COMMAND KSwitcherLanTests)
add_executable(KSwitcherLanFilesTests tests/lan_files.cpp)
target_link_libraries(KSwitcherLanFilesTests PRIVATE kscore)
add_test(NAME lan-files-regressions COMMAND KSwitcherLanFilesTests)
add_executable(KSwitcherLanInputTests tests/lan_input.cpp)
target_link_libraries(KSwitcherLanInputTests PRIVATE kscore)
if(MSVC)
set_property(TARGET KSwitcherLanInputTests PROPERTY MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>")
else()
target_link_options(KSwitcherLanInputTests PRIVATE -static -static-libgcc -static-libstdc++)
endif()
add_test(NAME lan-input-regressions COMMAND KSwitcherLanInputTests)
add_executable(KSwitcherChatProfileTests tests/chat_profiles.cpp src/screenshot_launcher.cpp src/screenshot_activation.cpp src/app.cpp src/screenshot_notifications.cpp src/screenshot.cpp src/configuration.cpp src/ui.cpp resources/app.rc)
target_link_libraries(KSwitcherChatProfileTests PRIVATE kscore)
if(MSVC)
target_compile_options(KSwitcherChatProfileTests PRIVATE /utf-8)
set_property(TARGET KSwitcherChatProfileTests PROPERTY MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>")
else()
target_link_options(KSwitcherChatProfileTests PRIVATE -static -static-libgcc -static-libstdc++)
endif()
add_test(NAME chat-profile-regressions COMMAND KSwitcherChatProfileTests)
add_executable(KSwitcherHistoryHostTests tests/history_host.cpp)
target_link_libraries(KSwitcherHistoryHostTests PRIVATE kscore psapi)
set_target_properties(KSwitcherHistoryHostTests PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/history-host-fixture-$<CONFIG>")
add_dependencies(KSwitcherHistoryHostTests KSwitcher KSwitcherScreenshot)
add_custom_command(TARGET KSwitcherHistoryHostTests POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy "$<TARGET_FILE:KSwitcher>" "$<TARGET_FILE_DIR:KSwitcherHistoryHostTests>/KSwitcher.exe"
COMMAND ${CMAKE_COMMAND} -E copy "$<TARGET_FILE:KSwitcherScreenshot>" "$<TARGET_FILE_DIR:KSwitcherHistoryHostTests>/KSwitcher.Screenshot.exe"
COMMAND ${CMAKE_COMMAND} -E touch "$<TARGET_FILE_DIR:KSwitcherHistoryHostTests>/.local-dev")
if(MSVC)
target_compile_options(KSwitcherHistoryHostTests PRIVATE /utf-8)
set_property(TARGET KSwitcherHistoryHostTests PROPERTY MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>")
else()
target_link_options(KSwitcherHistoryHostTests PRIVATE -static -static-libgcc -static-libstdc++)
endif()
add_executable(KSwitcherClipboardSnapshotTests tests/clipboard_snapshot.cpp)
target_link_libraries(KSwitcherClipboardSnapshotTests PRIVATE kscore psapi)
add_dependencies(KSwitcherClipboardSnapshotTests KSwitcherScreenshot)
if(MSVC)
target_compile_options(KSwitcherClipboardSnapshotTests PRIVATE /utf-8)
set_property(TARGET KSwitcherClipboardSnapshotTests PROPERTY MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>")
else()
target_link_options(KSwitcherClipboardSnapshotTests PRIVATE -static -static-libgcc -static-libstdc++)
endif()
add_executable(KSwitcherImageCaptureTests tests/image_capture.cpp)
target_link_libraries(KSwitcherImageCaptureTests PRIVATE kscore)
add_dependencies(KSwitcherImageCaptureTests KSwitcherScreenshot)
if(MSVC)
target_compile_options(KSwitcherImageCaptureTests PRIVATE /utf-8)
set_property(TARGET KSwitcherImageCaptureTests PROPERTY MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>")
else()
target_link_options(KSwitcherImageCaptureTests PRIVATE -static -static-libgcc -static-libstdc++)
endif()
# Explicit interactive test: stop the resident first so its clipboard listener
# does not capture the test fixtures. Like KSwitcherIntegration, not part of ctest.
add_executable(KSwitcherDictionaryTests tests/dictionary.cpp)
target_link_libraries(KSwitcherDictionaryTests PRIVATE kscore psapi)
if(MSVC)
set_property(TARGET KSwitcherDictionaryTests PROPERTY MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>")
else()
target_link_options(KSwitcherDictionaryTests PRIVATE -static -static-libgcc -static-libstdc++)
endif()
add_test(NAME dictionary-regressions COMMAND KSwitcherDictionaryTests)
set_tests_properties(dictionary-regressions PROPERTIES WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}")
add_test(NAME sync-regressions COMMAND KSwitcherSyncTests)
add_test(NAME native-regressions COMMAND KSwitcherTests)
add_executable(KSwitcherUtilitiesTests tests/utilities.cpp)
if(MSVC)
set_property(TARGET KSwitcherUtilitiesTests PROPERTY MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>")
else()
target_link_options(KSwitcherUtilitiesTests PRIVATE -static -static-libgcc -static-libstdc++)
endif()
add_test(NAME utilities-regressions COMMAND KSwitcherUtilitiesTests)
add_test(NAME stayhere-lifecycle COMMAND powershell -NoProfile -ExecutionPolicy Bypass -File "${CMAKE_CURRENT_SOURCE_DIR}/tests/stayhere.ps1" -Executable "$<TARGET_FILE:StayHereTest>")
add_test(NAME scroll-regressions COMMAND KSwitcherScrollTests)
add_test(NAME input-regressions COMMAND KSwitcherInputTests)
add_test(NAME screenshot-ui-regressions COMMAND KSwitcherScreenshotUITests)
add_test(NAME screenshot-save-regressions COMMAND KSwitcherScreenshotUITests --save-only)
add_test(NAME updater-regressions COMMAND powershell.exe -NoProfile -ExecutionPolicy Bypass -File "${CMAKE_CURRENT_SOURCE_DIR}/tests/update.ps1" -Executable "$<TARGET_FILE:KSwitcher>")
set_tests_properties(native-regressions PROPERTIES WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}")
install(TARGETS KSwitcher RUNTIME DESTINATION .)
install(TARGETS KSwitcher KSwitcherConfig KSwitcherScreenshot KSwitcherFiles RUNTIME DESTINATION .)
install(FILES LICENSE vendor/json-LICENSE.txt DESTINATION .)
add_custom_command(TARGET KSwitcher POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_directory "${CMAKE_CURRENT_SOURCE_DIR}/dictionaries" "$<TARGET_FILE_DIR:KSwitcher>/dictionaries")
install(DIRECTORY dictionaries DESTINATION .)
add_executable(KSwitcherLanClipboardTests tests/lan_clipboard.cpp)
target_link_libraries(KSwitcherLanClipboardTests PRIVATE kscore)
if(MSVC)
set_property(TARGET KSwitcherLanClipboardTests PROPERTY MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>")
else()
target_link_options(KSwitcherLanClipboardTests PRIVATE -static -static-libgcc -static-libstdc++)
endif()
add_test(NAME lan-clipboard-regressions COMMAND KSwitcherLanClipboardTests)
add_executable(KSwitcherSettingsMergeTests tests/settings_merge.cpp)
target_link_libraries(KSwitcherSettingsMergeTests PRIVATE kscore)
if(MSVC)
set_property(TARGET KSwitcherSettingsMergeTests PROPERTY MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>")
else()
target_link_options(KSwitcherSettingsMergeTests PRIVATE -static -static-libgcc -static-libstdc++)
endif()
add_test(NAME settings-save-merge COMMAND KSwitcherSettingsMergeTests)
+142
View File
@@ -0,0 +1,142 @@
# Оформлення KSwitcher Files
Far Manager є зразком для оформлення файлового менеджера. Основне вікно використовує системний заголовок Windows. Панелі, меню й діалоги операцій використовують моноширинний шрифт менеджера та оформлення в стилі Far.
## Скорочення
- У командному рядку Ctrl+V і Shift+Insert вставляють текст із буфера обміну
в позицію курсора або замінюють виділення. Якщо фокус на файловій панелі,
ці клавіші переводять його до командного рядка й вставляють текст там.
Enter запускає команду. Escape очищає поле й залишає фокус у командному рядку.
Промпт і текст команди вирівняні по спільній базовій лінії.
- Ctrl+E підставляє останню команду та переводить фокус із файлової панелі
до командного рядка. Повторне натискання
підставляє старішу, доки не досягне початку історії. Alt+F8 відкриває історію
з групуванням за датами. Enter або подвійний клік у списку підставляє вибрану
команду в поле. Для запуску натисни Enter у самому полі.
- Історія зберігає 1000 останніх непорожніх команд, надісланих із поля KSwitcher
Files, включно з повторами. Вона зберігається локально в профілі Windows між
запусками. Options → Command line → Command history змінює обидві комбінації,
перевіряє конфлікти та дозволяє відновити типові значення.
- Ширина історії підлаштовується під найдовшу команду в межах робочої області
монітора. Рядок, який не вміщується, закінчується трьома крапками. Вибір
підставляє до командного рядка повний текст.
- Пошук в історії фільтрує команди за всіма введеними словами, незалежно від
регістру. Збіги за текстом або датою йдуть перед тематичними. Тематичні
ключові слова визначаються локально й використовуються лише для пошуку,
зокрема в раніше збережених записах. Кожна команда відображається один
раз, за датою останнього запуску.
- Історія використовує оформлення діалогів Delete і Copy та спільне поле Far.
Підпис, поле й список вирівнюються за відступами Dialog frame. Рядки команд
і роздільники дат мають власний внутрішній відступ.
Вікно історії бірюзове, поле пошуку сіро-бірюзове. Поки історія відкрита,
дії в панелях, меню й командному рядку заблоковано. Заголовок і рамка
головного вікна доступні для переміщення, зміни розміру та керування вікном.
Escape або клік поза діалогом закриває історію. Заголовок і рамка головного
вікна залишають історію відкритою під час керування вікном.
Історія центрується відносно головного вікна й рухається разом із ним.
Tab циклічно переводить фокус між пошуком і списком.
Стрілка вгору або вниз із пошуку одразу пересуває вибір у списку.
Висота списку залежить від кількості результатів. Коли місця бракує,
прокручується список.
- Короткий Alt перемикає головне меню Files при відпусканні. F9 також перемикає меню. Після закриття фокус повертається до активної панелі. Alt з літерою верхнього рівня спрацьовує одразу при натисканні літери. Alt+O відкриває Options, а відпускання Alt після комбінації не перемикає меню.
- Коли головне меню відкрите, L, F, C, O та R відкривають Left, Files, Commands, Options та Right. У підменю підсвічена літера викликає відповідну команду. Літери унікальні в межах поточного рівня. Усталені мнемоніки мають пріоритет, зокрема X для Exit.
- Глобальне скорочення **Open KSwitcher Files** можна змінити або очистити в налаштуваннях. Для профілю без цього поля пропонується Ctrl+Alt+F, якщо Windows реєструє комбінацію і вона вільна серед прив’язок KSwitcher.
- Ctrl+Shift+0–9 відкриває діалог Title і Path із поточною папкою. OK додає запис без дублювання, Escape скасовує. Title необов’язковий, порожнє значення показує шлях. Записи LAN зберігають ID ПК, а редагування Path залишає запис на тому самому ПК.
- Ctrl+0–9 одразу відкриває єдину папку. Для кількох папок відкривається компактний попап Far з одним заголовком Folder shortcuts у подвійній рамці та коротким рядом клавіш у нижній рамці. Ширина залежить від найдовшого запису й обмежується розміром вікна, а висота — кількістю видимих рядків. Нижня рамка показує Ins Del F4 Ctrl+Up Ctrl+Down. Insert додає поточну папку через Title і Path, Delete відв’язує, F4 редагує, Ctrl+Up і Ctrl+Down змінюють і зберігають порядок. Enter відкриває папку. Escape або клік поза попапом закриває його й повертає фокус до панелі. Commands → Folder shortcuts дає вибрати зайняту цифру й відкрити той самий попап навіть для єдиного запису. Brief і Wide доступні в меню Left/Right та Options.
## Налаштування Files
- Options у головному меню одразу відкриває стандартний діалог Windows. Ліворуч є пошук за назвою категорії чи параметра й дерево результатів. Вибір дочірнього вузла відкриває лише його редактор праворуч. Пошук за назвою окремої властивості відкриває вузол, що її містить. Перемикання вузлів зберігає незастосовані зміни.
- File panel modes має вузли Panel layout, Wide panel columns і Status line columns. У Colors кожен колір має власний вузол. В Appearance шрифт редагується у вузлі Font. Діалог змінює розмір від розміру, який вміщує всі параметри. Ширина лівої навігації стала, дерево росте по висоті, а права область використовує додатковий простір. OK, Cancel та Apply лишаються внизу праворуч.
- Reset selected to defaults скидає вибраний рівень дерева. Корінь All settings скидає всі параметри, навіть коли пошук приховує частину дерева. У редакторах із кількома параметрами Reset property скидає лише вибрану властивість. Скидання змінює чернетку до Apply або OK. Стандартний Panel background — `#000080`.
- Apply доступна, коли чернетка відрізняється від застосованих налаштувань. Після Apply або повернення всіх значень до застосованих вона вимикається. Apply одразу зберігає зміни й перемальовує панелі за відкритим діалогом. OK робить те саме й закриває діалог. Cancel і Escape відкидають зміни після останнього Apply та повертають фокус до панелі.
- Colors → File type styles налаштовує текст, фон, підкреслення й закреслення для правил за розширенням і групи System and hidden. Розширення можна вибрати зі списку Windows або додати вручну з крапкою чи без неї. Вони зберігаються в KSwitcher, незалежно від асоціацій Windows. Курсор, позначки й результат передачі зберігають свої сигнальні кольори. Кольори правила можна перевизначити окремо для звичайного рядка, позначки Insert, курсора у фокусі та курсора без фокуса. Позначка має пріоритет над курсором, а підсвічування передачі має пріоритет над кольорами курсора. Без перевизначення стан зберігає звичайний вигляд панелі. Мініпанель унизу редактора одразу показує всі чотири стани на синтетичних рядках до Apply.
- Нижня область показує живе прев’ю для кольорів, шрифту, режимів панелей, широких колонок і рядка стану. Для Popup background, Menu selection і Command bar зразок показує відповідний елемент. Прев’ю читає чернетку, зокрема після Reset, і не потребує реальних файлів. Воно заповнює простір від останнього видимого елемента редактора до нижніх кнопок зі сталими відступами. Зі зміною розміру росте область зразка, а рядки зберігають масштаб шрифту.
- Підписи File type, Windows extension or custom extension і Row state стоять над відповідними полями зі спільним лівим краєм. Ширина списків враховує найдовші значення, шрифт, DPI й доступне місце. Add і Remove стоять одразу після своїх полів.
- Початковий набір правил охоплює EXE, Markdown, системні й приховані елементи, архіви, зображення, аудіо й відео, офісні документи й PDF, код і скрипти. Збережений набір правил завантажується як є, включно з видаленими правилами. Reset у File type styles відновлює весь початковий набір у чернетці.
## Панелі
- Застосовані налаштування Files одразу підхоплюють інші відкриті вікна.
Apply зберігає поля, змінені в поточній чернетці. Якщо два вікна одночасно
редагували набір стилів файлів, відкрий Options заново, щоб побачити свіжі правила.
- Типовий колір тексту для ZIP-архівів — світлий коралово-рожевий.
- Меню Change drive показує літеру, тип, загальний обсяг і вільне місце. Відкриття меню запускає нове LAN-виявлення. Секція KSwitcher LAN показує збережені пари, зокрема офлайн. Після кожного відкриття меню асинхронно запитує диски й показує їх під назвою відповідного ПК. Поки триває запит, видно стан завантаження. Якщо запит не вдався, видно помилку. Повторне відкриття запускає новий запит. Кожна панель має власний диск і шлях, а в заголовку віддаленої панелі перед шляхом показано назву ПК.
- Список дисків і файлові операції використовують права облікового запису Windows, під яким працює віддалений KSwitcher. Достатньо спарювання й доступного TCP 45885, спільні папки налаштовувати не потрібно.
- F5, F6, F8 і Shift+Delete у віддалених панелях використовують спільні підтвердження, попередження й прогрес. F5/F6 працюють із файлами та деревами папок між локальними й віддаленими панелями, зокрема між двома різними ПК. Між шляхами одного віддаленого ПК вміст копіюється на ньому. F8 використовує кошик того ПК, на якому лежить файл.
- Після розриву з’єднання повтори F5 або F6 до того самого призначення. Віддалене копіювання відновлюється з підтвердженої позиції, якщо збережені байти відповідають початку вихідного файла. Підтверджене скасування видаляє частковий запис поточного файла. Завершені файли лишаються на місці. Наявний файл призначення замінюється після повного запису, а при переміщенні джерело видаляється після підтвердження запису призначення.
- Після F5 або F6 успішно оброблені елементи коротко підсвічуються в панелі, що показує папку призначення. Підсвітка тексту відрізняється від курсора й не переносить фокус.
- У широкому режимі ширина під вирівняні розширення залежить від найдовшого розширення в поточній панелі. За вузької панелі залишай місце для імені та колонки Size.
- Бірюзовий курсор поточного рядка видно лише в активній панелі, зокрема коли клавіатурний фокус переходить у командний рядок. Кожна панель зберігає свою позицію курсора. Коли панелі видимі, F5 і F6 з командного рядка діють на активну панель.
- Клік у панелі нижче списку ставить курсор на останній рядок, а клік вище списку — на перший.
- Поточний шлях відображається по центру верхньої подвійної лінії панелі. Лінію видно з обох боків напису. Бірюзове тло активної панелі охоплює лише напис шляху з невеликими відступами. Довгий шлях скорочується в межах панелі.
## Спільні елементи діалогів
- Власні діалоги Files блокують дії панелей і командного рядка, залишаючи
системний заголовок і рамку головного вікна доступними. Клік поза діалогом
діє як Cancel. Для прогресу копіювання, переміщення й видалення це відкриває
підтвердження скасування.
- Enter активує кнопку у фокусі. Для кнопок із власним малюванням використовуй спільний обробник `dialogButtonEnter`. У підтвердженні скасування початковий фокус стоїть на No.
- При перенесенні вікна між моніторами з різним DPI оновлюй шрифт, висоту рядків панелей і компонування відкритого прогресу копіювання. Перевір перенесення під час активного копіювання.
- Для власних діалогів і списків Far використовуй спільні метрики та малювання подвійної рамки. Еталон Delete задає 8 px від краю тла до першої лінії та 14 px від внутрішньої лінії до бічної межі вмісту при 96 DPI. Обидва відступи масштабуються за DPI. Вертикальний відступ враховує висоту заголовка та нижніх підказок. Діалоги й списки зберігають свою палітру та поведінку.
- Appearance → Dialog frame налаштовує зовнішній і внутрішній відступи від 0 до 24 px при 96 DPI. Row text padding додає від 0 до 4 ширин символу з кожного боку тексту в рядках Folder shortcuts. Inner padding задає відстань від рамки до краю списку. Ширина попапа враховує обидва текстові відступи, а довгі шляхи скорочуються в межах доступної ширини. Живий зразок показує чернетку. Apply зберігає значення, Cancel відкидає незастосовані зміни, Reset selected to defaults повертає 8 px, 14 px і 0 символів. Профілі без цих параметрів використовують ті самі значення.
- У ресурсі чи виклику задавай назву кнопки й клавішу доступу. Спільна функція `paintDialogButton` центрує текст, підсвічує фокус і додає фігурні дужки для дії за замовчуванням та квадратні для решти кнопок.
- Нижні секції з кнопками у Copy, Warning і вікні прогресу мають однакову висоту, прив’язану до висоти рядка шрифту. Використовуй `layoutDialogButtons` для такої секції та `centerDialogRow` для інших рядків між роздільниками. Межі секції визначають положення рядка.
- Для текстових полів Far використовуй ресурс `FAR_EDIT` і спільну підготовку та палітру діалогів. Поля призначення Copy/Rename і Title/Path використовують нативний Win32 Edit без рамки, моноширинний шрифт і вертикальне центрування. Кольори Text і Popup background діють на всі ці поля, зокрема на поле Copy лише для читання. У відповідних розділах Options → Colors зміни видно на зразку поля. Existing files у Copy є списком без редагування. Поля та редагований список Windows Options мають системне оформлення.
- Для позиціонування роздільника використовуй невидимий маркер, якщо межу не задає інший елемент секції. Спільний обробник малює лінію.
- Випадаючі списки Far використовують бірюзове тло палітри менеджера. Текст і стрілку вирівнюй вертикально по центру. Прапорці в діалогах операцій використовують позначки `[ ]` та `[x]` і видимий клавіатурний фокус.
- Повторно використовуй спільне оформлення. Текст, дії та стан операції передавай із коду конкретної операції. Наприклад, Warning повертає відповідь і стан прапорця, а копіювання вирішує, чи застосувати відповідь до наступних конфліктів.
## Діалог Copy
- Вирівнюй підпис Existing files по вертикальному центру списку. Ширше поле списку починається ближче до підпису, а його правий край збігається з правим краєм поля імені.
- Зберігай однакові проміжки від верхнього роздільника до поля імені й до списку. Підпис, поля та прапорець мають читатися повністю.
- Відступ після прапорця до нижнього роздільника дорівнює відступу від верхнього роздільника до списку.
- Кнопки Copy та Cancel стоять по центру компактної нижньої секції. Висота кнопок має вміщати весь текст поточного шрифту.
## Вікно прогресу копіювання
- F6 використовує спільне оформлення діалогів Copy і прогресу. Заголовок підтвердження — Rename/Move, кнопка за замовчуванням — Rename, заголовок прогресу — Move.
- Поле призначення F6 початково містить шлях іншої панелі. Для одного елемента просте ім’я задає перейменування в поточній папці. Для кількох елементів діалог показує їх кількість і приймає один шлях до папки.
- Секції поточного файла й загального прогресу мають однаковий вертикальний відступ від вмісту до верхньої та нижньої ліній. Cancel стоїть по центру спільної нижньої секції.
- Обидві шкали займають усю ширину вмісту. Відсотки показуються по центру шкали, а її висота враховує поточний шрифт.
## Попередження про конфлікт
- Для однойменних файлів Warning показує точний розмір у байтах із розділенням тисяч і місцевий
час останньої зміни окремо для вхідного файла та файла в папці призначення.
- Після розрахунку остаточної висоти центр Warning збігається з центром вікна прогресу.
- Remember choice стоїть по вертикальному центру власної секції. Кнопки Overwrite, Skip і Cancel стоять по центру нижньої секції.
- Tab проходить Remember choice, Overwrite, Skip і Cancel. Стрілки вліво та вправо циклічно перемикають три кнопки в порядку їх розташування.
- Стрілки вгору й вниз переходять між сусідніми секціями з інтерактивними елементами. У Warning стрілка вгору з ряду кнопок переводить фокус на Remember choice, а вниз із прапорця — на Overwrite.
## Видалення
- F8 показує сіре підтвердження з дією Move. Shift+Delete і підтвердження непорожньої папки мають темно-червоне тло. Вікно прогресу видалення має сіре тло, як у Copy.
- Escape у початковому підтвердженні повертає фокус до панелі, з якої викликано видалення. Поточний рядок зберігається, а стрілки одразу працюють.
- F8 переміщує підтверджені елементи в кошик Windows. Shift+Delete видаляє підтверджене дерево від вкладених елементів до батьківської папки. Прогрес і підсумок окремо показують успішні, пропущені й невдалі дії.
- Cancel і Esc у вікні прогресу відкривають спільне червоне підтвердження скасування. Yes зупиняє операцію, а No або Esc у підтвердженні повертає до прогресу. Початковий фокус стоїть на No.
## Перевірка змін
Відкрий Copy і Warning в одній збірці та порівняй висоту нижніх секцій, центрування, повноту підписів і видимість фокуса. Перевір Tab, стрілки й Esc. Після узгодження нового правила онови відповідний розділ цього документа.
+168
View File
@@ -0,0 +1,168 @@
# 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.
The module runs inside the resident process. Multiple paired computers can be
arranged on a monitor grid. Mouse and keyboard follow neighboring cells, including
A → B → C, without relaying input through B. New text and images are also shared
with all online paired computers: history and the active clipboard.
## Files and diagnostic logs
In **Settings → Computers → Manage computers…**, select an online paired
computer and choose **Send file…**. Both computers need a build with file transfer.
Files up to 16 MiB travel over encrypted LAN. On the destination, open
**Incoming files…** to save or discard each file. The save dialog suggests the
original filename. KSwitcher shows a Windows notification when a file is ready
to save, and its tray tooltip shows the pending count. Pending files are held
in memory until saved, discarded, or KSwitcher closes.
To retrieve the current or rotated diagnostic log, enable **Allow paired
computers to download diagnostic logs** on the computer that owns the logs.
This setting defaults to off. On the requesting PC, select that computer and
choose **Download log…** or **Previous log…**. Diagnostic logs may contain
text copied from applications.
### MCP
In **Settings → MCP**, choose **Copy configuration** and paste the block into
Codex's `~/.codex/config.toml`. It starts the MCP server over stdio with the
absolute path to `KSwitcher.Config.exe` and the `--mcp` argument.
`list_log_computers` returns paired computer IDs and availability.
`read_diagnostic_log` reads the latest 100 lines by default (1–500 with
`lines`). Use `offset` to skip newer lines and page backward. Set `previous` to read the rotated log. Omit `peer` for the
local log or pass a paired computer ID for a remote log. Remote reads need
the local resident process running and log downloads enabled on the remote PC.
## Set up multiple computers
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 → Computers → Manage 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.
Select each other, generate one code, enter that same code on both, then click
Pair on both within two minutes. Spaces, hyphens and uppercase hex are accepted.
4. Pair each physical input source with every destination it must control. For
unrestricted control from any PC, pair every pair of PCs. Drag monitors into
neighboring grid cells and arrange the same physical layout relative to each
local PC. For A → B → C, A sees B at (1,0), C at (2,0); B sees A at (-1,0),
C at (1,0). Enable input sharing on all PCs. The button controls all pairs,
independently of the selected list item. Positions save immediately.
5. Stop MWB or another mouse/keyboard router on both computers before trying
KSwitcher input sharing, to avoid two tools forwarding the same events.
6. Wait for the authenticated input connection. Move the cursor through the
configured outer screen edge with no keys or buttons held. After the other
computer acknowledges the handoff, both mouse and keyboard control that PC.
Crossing another shared edge switches directly to that neighbor. Crossing back
into the source computer restores its cursor at the corresponding edge. Empty
cells, diagonal-only neighbors and offline PCs do not transfer control.
The source cursor is parked and hidden beneath a tiny no-activation window. A
small banner identifies remote control; no full-screen layered bitmap is retained.
The destination uses its own keyboard layout and runs its own hotkeys/snippets.
The source suppresses forwarded events before its local hotkeys, snippets and
other low-level hook consumers. Received input is marked so it is not forwarded
again. Source recent-input tracking and shortcut latches reset when ownership changes.
**Ctrl+Alt+Esc** is emergency return, not the normal switching mechanism. Local
physical input on a remotely controlled PC takes control back as well. Stop input
sharing, disabling discovery, forgetting the currently controlled pair, suspension, secure
desktop changes and connection loss cancel remote ownership. TCP heartbeat loss
is detected after two seconds; a handoff request expires after one second.
Held remote keys/buttons are released. Releases blocked by a secure desktop are
retried when the ordinary desktop is available. Source key-ups for keys already
sent remotely are consumed after return, avoiding stray local modifier events.
## Layout and limitations
- Positions are relative to this PC. All peers have independent authenticated
connections; a failed peer does not disconnect the others. The physical source
uses its own grid to choose every destination. Moving a monitor cancels current
control safely and applies the updated layout without dropping all connections.
Layouts are not synchronized automatically. One source controls a destination
at a time. Each grid tile represents an entire PC, not an individual monitor.
- Geometry uses the virtual desktop's outer bounding rectangle, including negative
monitor coordinates. Internal monitor boundaries do not transfer control.
Different resolutions map the entry position proportionally. Irregular layouts
can have gaps on an outer edge; this is not a per-monitor arrangement editor.
- Transferring while a key or mouse button is held is intentionally blocked.
Dragging works within the controlled computer; dragging objects across PCs is
not implemented. There is a short return cooldown to avoid bouncing.
- Windows can block SendInput to elevated apps and secure desktops. Run both
applications at suitable privilege levels for the target programs. The preview
does not install a service, bypass UAC or implement remote Ctrl+Alt+Delete.
- This is Windows input injection, not a virtual USB HID device. Raw-input games,
exclusive full-screen applications and special device keys need separate testing.
## Network, authentication and storage
Discovery broadcasts on private IPv4 and link-local interfaces using UDP 45883.
Input uses TCP 45884. Allow KSwitcher on your private network when Windows asks;
the preview does not change firewall rules. Broadcast-blocked networks, separate
VLANs, public IPv4 and IPv6-only networks are not supported by discovery yet.
A second LAN-enabled KSwitcher cannot use the same ports on one computer.
Input first tries TCP in the usual direction, then reverse TCP after 1.5 seconds.
If TCP has not authenticated after 5 seconds, the lower-ID peer negotiates an
encrypted UDP input session on the existing discovery socket (UDP 45883).
UDP uses fresh session challenges, authenticated encryption, ordered event
numbers, acknowledgements and bounded retries; duplicated packets do not repeat
key presses. Two seconds without authenticated traffic or acknowledgement
progress closes the session and returns control locally. A working UDP session
stays on UDP until disconnected; it does not oscillate between transports.
Both PCs must run an updated build to use all fallback methods. Older peers can
still use the original TCP protocol. No extra ports or external relay are used.
After 15 seconds without a connection, the UI points to **Connection help**.
The help window can copy PowerShell commands for the actual resident EXE path,
including a path containing apostrophes. Run each PC's own command in an
administrator PowerShell on that PC. These commands permit inbound TCP 45884
and UDP 45883 only from the local subnet; the program does not execute them.
Moving the EXE may require a new application firewall rule. A timeout alone
does not establish that the firewall is the cause: check that input sharing is
enabled on both PCs and that both are running the intended copy.
The instance ID belongs to a data directory, not a hostname/IP address. Discovered
names and addresses are untrusted. Pairing uses a generated 128-bit code and
mutual HMAC-SHA256 challenge proofs bound to both instance IDs. The code is never
transmitted or logged. The pair key is separate from the setup code.
The input channel authenticates both peers with the saved pair key and fresh
session challenges. Directional AES-256-GCM session keys bind the identities,
nonces and the input protocol domain. Screen positions determine routing after
authentication; they are no longer part of the pair channel key. Fixed-size authenticated frames use strict sequence
numbers, so tampering, reflection and replay are rejected. Input is not dispatched
until authentication succeeds. This PSK channel does not provide forward secrecy.
Queues and frame sizes are bounded; overflow disconnects rather than dropping a
key-up and leaving a key pressed. The input worker never injects Windows input;
the resident's main thread owns the input state and hooks.
Identity, trust, enablement and screen position stay in DPAPI-protected
`data/lan-pairing.bin`, outside settings export and OpenBao sync. Do not clone this
file between installations. Forget a pair on both PCs for bilateral removal.
Closing the settings window does not cancel pairing or disable sharing.
Disabled discovery has no socket or worker. Input sharing reuses the single LAN
worker; there is no extra resident executable or service. The main input timer
runs only while sharing is enabled. The existing configurator talks to the
resident through local IPC.
## Validation
Tests cover pairing, encrypted frames, replay/tamper rejection, actual loopback
TCP authentication and bidirectional delivery, all four edges, ownership races,
source suppression, scan codes, clicks/wheel, emergency return, held-key release,
blocked injection, secure desktop recovery, exact one-pixel movement and timeout.
Tests additionally exercise three actual TCP hubs, independent reconnect,
A → B → C and back, vertical turns, unavailable neighbors, stale ownership,
modifier delivery to only the active destination, and local shortcut recovery.
UI tests drag three monitors and check persistence.
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.
+47
View File
@@ -19,3 +19,50 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Third-party toolbar icons: Lucide (https://lucide.dev)
ISC License
Copyright (c) 2026 Lucide Icons and Contributors
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
---
The following Lucide icons are derived from the Feather project:
airplay, alert-circle, alert-octagon, alert-triangle, aperture, arrow-down-circle, arrow-down-left, arrow-down-right, arrow-down, arrow-left-circle, arrow-left, arrow-right-circle, arrow-right, arrow-up-circle, arrow-up-left, arrow-up-right, arrow-up, at-sign, calendar, cast, check, chevron-down, chevron-left, chevron-right, chevron-up, chevrons-down, chevrons-left, chevrons-right, chevrons-up, circle, clipboard, clock, code, columns, command, compass, corner-down-left, corner-down-right, corner-left-down, corner-left-up, corner-right-down, corner-right-up, corner-up-left, corner-up-right, crosshair, database, divide-circle, divide-square, dollar-sign, download, external-link, feather, frown, hash, headphones, help-circle, info, italic, key, layout, life-buoy, link-2, link, loader, lock, log-in, log-out, maximize, meh, minimize, minimize-2, minus-circle, minus-square, minus, monitor, moon, more-horizontal, more-vertical, move, music, navigation-2, navigation, octagon, pause-circle, percent, plus-circle, plus-square, plus, power, radio, rss, search, server, share, shopping-bag, sidebar, smartphone, smile, square, table-2, tablet, target, terminal, trash-2, trash, triangle, tv, type, upload, x-circle, x-octagon, x-square, x, zoom-in, zoom-out
The MIT License (MIT) (for the icons listed above)
Copyright (c) 2013-present Cole Bemis
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+897 -27
View File
File diff suppressed because it is too large. Load diff
Binary file not shown.
+205 -14
View File
@@ -1,21 +1,212 @@
# Latest Windows release
Download **KSwitcher-win-x64.zip**, extract the entire archive to a writable folder,
and launch `KSwitcher.exe`. Keep `dictionaries/` next to the executable.
The archive contains the application, offline dictionaries, documentation and license notices.
It does not contain personal settings or clipboard history.
## v0.6.78 — 2026-11-14
For upgrades, exit KSwitcher first. Back up any edited dictionaries before replacing files.
Keep your existing `data/` directory to retain settings and clipboard history.
`SHA256SUMS.txt` contains the checksum of the archive.
The Windows package includes KSwitcher Files, a two-panel file manager with
local and paired LAN drives, folder shortcuts, an embedded PowerShell terminal,
and searchable command history. Applied Files settings update other open
instances immediately.
This folder holds only the latest compiled version; older versions remain in Git history.
**Українською.** До пакета для Windows входить KSwitcher Files, двопанельний
файловий менеджер із локальними дисками й дисками спарених ПК, ярликами папок,
вбудованим PowerShell та пошуком в історії команд. Застосовані налаштування
Files одразу підхоплюють інші відкриті вікна.
To refresh it from source:
## v0.6.77 — 2026-11-13
```powershell
.\build.ps1 -Stage -Test
.\package.ps1 -PublishRelease
```
The screenshot editor remembers the local pencil smoothing default across
restarts. Adjust the slider with no pencil stroke selected, or use **Apply to
all**, to save the value for new strokes. Adjusting a selected stroke changes
only that stroke.
Commit the updated ZIP and checksum together with the corresponding source changes.
**Українською.** Редактор скріншотів зберігає типове згладжування олівця на
цьому ПК та відновлює його після перезапуску. Змініть повзунок без виділеного
штриха олівця або натисніть **Apply to all**, щоб запам’ятати значення для
нових штрихів. Зміна виділеного штриха впливає лише на нього.
## v0.6.76 — 2026-11-13
Incoming LAN files suggest their original filename when saved. KSwitcher shows
a Windows tray notification when a file is ready to save, and the tray tooltip
shows the pending count.
**Українською.** Під час збереження вхідного LAN-файлу пропонується його
оригінальна назва. KSwitcher показує сповіщення в треї, коли файл готовий
до збереження. Підказка іконки показує кількість файлів у черзі.
## v0.6.75 — 2026-11-13
Conversion cancels earlier when input changes and skips restoring an untouched
clipboard. Diagnostic records identify the active conversion behind `busy`,
separate clipboard and paste verification timings, and retain a text-free
summary of verified pastes that take at least two seconds. MCP log reads
support `offset` to page backward.
**Українською.** Конвертація раніше скасовується після зміни вводу й пропускає
відновлення буфера, якщо його не змінювала. Діагностика показує активну спробу
для `busy`, окремий час роботи з буфером і перевірки вставки, а також зберігає
підсумок перевірених вставок від двох секунд без скопійованого тексту. У MCP
параметр `offset` дає змогу читати старіші рядки.
Explicit replacements now accept digits and internal apostrophes in the source word, for example нав1язав=навʼязав. Invalid entries identify the offending pair.
**Українською.** Явні заміни підтримують цифри й внутрішні апострофи у вихідному слові, наприклад нав1язав=навʼязав. Повідомлення про помилку показує проблемну пару.
## v0.6.74 — 2026-11-13
- Send files up to 16 MiB to a selected paired PC over encrypted LAN. Save or discard them from **Incoming files**.
- Download current and previous diagnostic logs from paired PCs that explicitly allow remote log downloads.
- Copy the Codex configuration from **Settings → MCP**. The MCP server lists paired PCs and reads local or remote diagnostic logs.
- Search contacts in the **Languages for a chat** table. **Copy selected** in Diagnostics now uses normal clipboard synchronization.
**Українською.** Передавайте файли до 16 МіБ вибраному спареному ПК зашифрованою локальною мережею. У **Incoming files** можна зберегти або відхилити файл. Поточний і попередній журнали доступні для завантаження після ввімкнення відповідного дозволу на віддаленому ПК. У **Settings → MCP** можна скопіювати конфігурацію Codex для читання локальних і віддалених журналів. У таблиці **Languages for a chat** додано пошук контактів, а **Copy selected** у діагностиці тепер проходить через звичайну синхронізацію буфера.
## v0.6.73 — 2026-11-13
After moving KSwitcher, launch it from its current folder. If Windows autostart
still points to the old location, KSwitcher updates the path automatically when
the previous executable is missing and the process has sufficient rights. If
the previous copy may still be in use or the repair needs administrator approval,
choose **Update autostart path** and confirm UAC if prompted. To check again,
click **Save** in Settings without changing the autostart options.
**Українською.** Після перенесення KSwitcher запустіть його з нової папки.
Якщо автозапуск і далі вказує на старий шлях, KSwitcher виправить його
автоматично, коли старий файл відсутній і прав достатньо. Якщо стара копія
ще може використовуватися або потрібні права адміністратора, натисніть
**Update autostart path** і підтвердьте UAC, якщо з’явиться запит. Для повторної
перевірки натисніть **Save** у налаштуваннях без зміни параметрів автозапуску.
## v0.6.72 — 2026-11-12
Edit snippets directly in **Settings → Snippets**. **Save snippets** applies
snippet edits independently of the settings draft. Find OCR options in
**Clipboard** and open backup and synchronization, paired computers, and
logging from **Sync**, **Computers**, and **Diagnostics**, respectively.
**Українською.** Редагуйте сніпети безпосередньо у **Settings → Snippets**.
Кнопка **Save snippets** зберігає їх окремо від чернетки налаштувань.
Параметри OCR тепер у **Clipboard**. Для синхронізації, спарених ПК та
журналу є вкладки **Sync**, **Computers** і **Diagnostics**.
## v0.6.71 — 2026-11-12
After navigating snippet suggestions with **↑ / ↓**, press **Enter** to
insert the selected snippet and close the list. The editor does not receive
that keypress. If you have not navigated the list, **Enter** still goes to
the editor. **Tab** and mouse selection also work.
KSwitcher skips suggestions when VS Code's EditContext supplies the bounds
of the whole line in place of the text caret. See the [FAQ](https://gitea.hrynco.com/hrynco/hrynco-kswitcher/wiki/FAQ)
for the VS Code workaround.
**Українською.** Після вибору підказки клавішами **↑ / ↓** натисніть
**Enter**, щоб вставити сніпет і закрити список. Редактор не отримує це
натискання. Якщо ви не переміщалися списком, **Enter** і далі надходить
до редактора. Також можна вибрати сніпет через **Tab** або мишею.
KSwitcher приховує підказки, коли EditContext у VS Code повертає координати
всього рядка замість текстового курсора. Обхідний шлях описано у [FAQ українською](https://gitea.hrynco.com/hrynco/hrynco-kswitcher/wiki/FAQ-uk).
## v0.6.70 — 2026-11-12
Snippet suggestions are anchored to the text caret. KSwitcher uses the
editor's caret location or text accessibility information, including for
thin carets and wrapped text. If the editor provides no caret position,
the list stays hidden. Full triggers continue to expand automatically.
**Українською.** Підказки сніпетів прив’язані до текстового курсора.
KSwitcher використовує координати курсора або дані доступності редактора,
зокрема для тонкого курсора й перенесених рядків. Якщо редактор не надає
позицію текстового курсора, список не відкривається. Повні тригери й далі
спрацьовують автоматично.
## v0.6.69 — 2026-11-12
Snippet autocomplete opens beside the text caret as soon as you type `;` at
the start of a trigger. Other triggers offer suggestions after three
characters. The list stays open while you type, resizes to fit the matching
triggers, and respects application restrictions. Use **↑ / ↓** and **Tab** or
click a suggestion to insert it. Press **Esc** to dismiss the list. Full
triggers still expand automatically.
**Українською.** Автодоповнення сніпетів відкривається біля текстового курсора,
щойно ви вводите `;` на початку тригера. Для інших тригерів підказки
з’являються після трьох символів. Список залишається відкритим під час набору,
підлаштовує ширину під відповідні тригери й враховує обмеження за програмами.
Виберіть сніпет клавішами **↑ / ↓** і **Tab** або мишею. **Esc** закриває
список. Повні тригери й далі спрацьовують автоматично.
## v0.6.68 — 2026-11-12
With **Start with Windows** and **Start with Windows as administrator** enabled,
opening the regular KSwitcher after exiting a development build requests
administrator approval when needed. UAC prompts on the secure desktop still
require local input.
**Українською.** Якщо ввімкнено **Start with Windows** і **Start with Windows as administrator**,
звичайний KSwitcher після закриття dev-збірки за потреби запитує запуск від
адміністратора. Запити UAC на захищеному робочому столі підтверджуйте
безпосередньо на цьому ПК.
## v0.6.67 — 2026-11-12
Mouse buttons and wheel actions now use the profiles on the PC receiving
input through KSwitcher Computers or Mouse Without Borders. Update the
receiving PC and configure its actions in **Settings → Mouse**.
**Українською.** Кнопки й коліщатко тепер працюють за профілями ПК,
який отримує ввід через KSwitcher Computers або Mouse Without Borders.
Оновіть цей ПК і налаштуйте дії в **Settings → Mouse**.
## v0.6.66 — 2026-11-12
If Windows rejects registration of the notification-area icon at sign-in or
after Explorer restarts, KSwitcher retries about once a second until the
icon is registered. Settings become accessible from the icon without
restarting KSwitcher.
**Українською.** Якщо Windows не приймає значок KSwitcher в області
сповіщень під час входу або після перезапуску Провідника, KSwitcher
повторює спробу приблизно щосекунди. Коли значок з’явиться, з нього
можна відкрити налаштування без перезапуску KSwitcher.
## v0.6.65 — 2026-11-12
Use **Settings → Mouse → +** beside **Mouse events** to capture and name a
vertical or horizontal wheel direction. Capturing a known button or an
already registered direction selects its existing row. A new signal is
accepted only when Windows delivers it through both the test and runtime
input paths. A configured wheel action replaces scrolling for that direction
where the profile applies.
OpenBao now backs up all KSwitcher Settings, including mouse events, bindings,
application profiles, startup, clipboard history limits, and OCR preferences.
Update all clients that share a server profile before syncing. Downloading an
older backup preserves local settings for fields it never included. After
reinstalling Windows, reconnect to OpenBao and choose **Download remote**.
Executable paths and optional OCR software must still be available on the
restored PC. Clipboard contents and Windows credentials stay on the device.
**Українською.** У **Settings → Mouse** натисни **+** біля **Mouse events**,
щоб додати напрямок прокручування. Відомий сигнал виділить наявний рядок.
OpenBao тепер зберігає всі налаштування KSwitcher, зокрема профілі миші,
OCR й автозапуск. Перед синхронізацією онови всі клієнти спільного профілю.
Після перевстановлення Windows підключись до OpenBao й натисни
**Download remote**.
## v0.6.64 — 2026-11-12
The new **Settings → Mouse** tab lets you assign actions to mouse buttons and supported double-clicks. Use **Global** for fallback actions and create profiles for one or more applications. Edit a profile with a double-click, and enable or disable it with the **Active** checkbox. Choose applications by pointing at a window or opening an executable or shortcut.
Actions include keyboard combinations, browser navigation, launching applications, and system volume. Volume actions have a configurable step and an on-screen indicator. When Windows reports separate press and release events, holding the button repeats the volume change at increasing speed.
Use **Test mouse buttons here** to check which events Windows delivers. Mouse profiles are stored on the current PC. The configurator also keeps fixed-size settings dialogs fitted when moving them between displays with different scaling.
## Upgrade
Choose **Update** in the tray menu. Legacy v0.3.0–v0.4.1 clients first receive the v0.4.2 compatibility bridge, then the current full release. v0.2.3 and earlier require manual installation.
For a manual upgrade, exit KSwitcher and its configurator and screenshot editors, then extract the entire **KSwitcher-win-x64.zip** into its installation folder. Verify it with **SHA256SUMS.txt**. Keep personal data, customized dictionaries and tools. Keep the three KSwitcher executables together.
See the [English Wiki](https://gitea.hrynco.com/hrynco/hrynco-kswitcher/wiki/Home) or [Українська Wiki](https://gitea.hrynco.com/hrynco/hrynco-kswitcher/wiki/Home-uk).
+1 -1
View File
@@ -1 +1 @@
906de70d92045cc6ff50802841cc357c6b47536fa0cbbb63301d779b33179469 KSwitcher-win-x64.zip
d7362104941853bb30646fa44175686ed3d2fd3abcf1fd83daee04dd1d5c6912 KSwitcher-win-x64.zip
+131
View File
@@ -0,0 +1,131 @@
{
"schema": 1,
"files": [
{
"path": "KSwitcher.exe",
"sha256": "1e67e8e15eada80c902764f14a12a0bd370080709a94d454cc3c565dd3de1136",
"size": 2340864
},
{
"path": "KSwitcher.Config.exe",
"sha256": "26e9f5db4b6f79408e30fb2650f1a8318b5c17e39ae2bccf614901b815ca6606",
"size": 2341376
},
{
"path": "KSwitcher.Screenshot.exe",
"sha256": "d9593c6ff103070f1f72b93c0f350ba4782d9df814090565f48bd0aa34125059",
"size": 3319296
},
{
"path": "KSwitcher.Files.exe",
"sha256": "22f897cfdc3207350e3edb63f33747d71a0f9d60c7a8d3771f642ee589ed6ce1",
"size": 1815552
},
{
"path": "WebView2Loader.dll",
"sha256": "d3fce90bdeecb08596e377dea6a316354dd8949e32ff09b89d51be00c14e21b6",
"size": 194912
},
{
"path": "terminal/addon-fit-LICENSE.txt",
"sha256": "e256f01188af527e4d06d21d06fbf785ae9c50d4b328bf03cbe0ba7f0aa4228f",
"size": 1103
},
{
"path": "terminal/addon-fit.js",
"sha256": "ba3ea256ce0620a0992a197d6c9baea64823fc93d8da07a9e366ca9943c18527",
"size": 1521
},
{
"path": "terminal/index.html",
"sha256": "084d97a727b778fb10d31adcd557ee7840a38377ed60e0ef0f3ece93b349b1ef",
"size": 527
},
{
"path": "terminal/terminal.js",
"sha256": "142624d166314c4539dd14f922c17dca3d2bc7203f84aff2df03a196602c4b09",
"size": 3174
},
{
"path": "terminal/xterm-LICENSE.txt",
"sha256": "b569f629d00f2626a8100df2a1798210535621e42164dfd426a6fe5aac7b0ccd",
"size": 1261
},
{
"path": "terminal/xterm.css",
"sha256": "854a7c0fb70e8b1a083c16797ab827299fb18744f5ad34f227b48337e33293c6",
"size": 7112
},
{
"path": "terminal/xterm.js",
"sha256": "14903579ff54664cd72f8e8699e6961a6272c21863ec1c3b118cdc8af5d4a972",
"size": 488663
},
{
"path": "LICENSE",
"sha256": "d3c81140cce01c26bf6cbf52aa40e0825e1fa35b9a91d1387170a65d2597082c",
"size": 4401
},
{
"path": "json-LICENSE.txt",
"sha256": "c0d068392ea65358b798b8c165103560f06e9e3b38c4ab4e2d8810a7b931af86",
"size": 1075
},
{
"path": "README.md",
"sha256": "df6d40d0eb66ad7da8db1cffe6bd211de70dfee474cc228e041c821b3940718b",
"size": 93885
},
{
"path": "LAN-PREVIEW.md",
"sha256": "2c13d3ed4761e679c352fb1ade24a1ec3060ad2862376ede801cdee5fcc3bada",
"size": 11418
},
{
"path": "dictionaries/en.txt",
"sha256": "5f267c141c3d6837870e1c098f86b89a113ba300343119432644ead88f720c21",
"size": 682867
},
{
"path": "dictionaries/LICENSE-CC-BY-SA-4.0.txt",
"sha256": "28a9529c7d0bb4dc51f4bf5c116a3d16ef247a052f7591466768ddf563fd1cf5",
"size": 20138
},
{
"path": "dictionaries/README.md",
"sha256": "3017bf44d3be9d9dfab21bd6778aa52b7947e61acf9ce2bc86b41d6e949ac311",
"size": 2265
},
{
"path": "dictionaries/ru.txt",
"sha256": "9ef922c7699569304a428f6aba0fd13a21341c0d0cb9561f37ef83c41577a28b",
"size": 1060364
},
{
"path": "dictionaries/sources.json",
"sha256": "98e5b3fe7304e37705c9e5ff48b0b399ccb16e806736d21f09921b99e5f2a408",
"size": 1280
},
{
"path": "dictionaries/uk.txt",
"sha256": "d30e990d6ec2983c04f3644a3c61c6c668e8c542323f5931916fdd55b916eebf",
"size": 940799
},
{
"path": "tools/README.txt",
"sha256": "a34b5161a8315af037e1ec71d816bfc688f7c364141d4ee2347cf0515ef0c82a",
"size": 993
},
{
"path": "tools/StayHere.exe",
"sha256": "6ef5cf64708d62b7fc63c3c793cc99188a513433ff4774cae01dc014f6664376",
"size": 134656
},
{
"path": "tools/StayHere-README.md",
"sha256": "ba214e2aca7405e698b3ab21ceb1f41ce088edd996b3e30e4a9d58ff496f9413",
"size": 2796
}
],
"version": "0.6.78"
}
+132 -9
View File
@@ -1,4 +1,4 @@
param([switch]$Test, [switch]$Integration, [switch]$Ocr, [switch]$Stage)
param([switch]$Test, [switch]$Integration, [switch]$Ocr, [switch]$Stage, [switch]$Far)
$ErrorActionPreference = 'Stop'
$root = $PSScriptRoot
$compiler = Join-Path $root '.tools\w64devkit\bin\g++.exe'
@@ -10,36 +10,159 @@ $outDir = Join-Path $root 'build'
New-Item -ItemType Directory -Force $outDir | Out-Null
$executablePath = if ($Stage) { Join-Path $outDir 'staged\KSwitcher.exe' } else { Join-Path $outDir 'KSwitcher.exe' }
New-Item -ItemType Directory -Force (Split-Path $executablePath) | Out-Null
Set-Content -LiteralPath (Join-Path $outDir '.local-dev') -Value 'Local development build' -Encoding ascii
Set-Content -LiteralPath (Join-Path (Split-Path $executablePath) '.local-dev') -Value 'Local development build' -Encoding ascii
Push-Location $root
try {
$common = @('-std=c++20','-Os','-DNDEBUG','-DUNICODE','-D_UNICODE','-DWIN32_LEAN_AND_MEAN','-DNOMINMAX','-D_WIN32_WINNT=0x0A00','-Wall','-Wextra','-Wno-missing-field-initializers','-Isrc','-Ivendor','-ffunction-sections','-fdata-sections')
$libs = @('-luser32','-lshell32','-ladvapi32','-lcrypt32','-limm32','-lcomctl32','-lgdi32','-lole32','-loleaut32','-ltaskschd','-lcomdlg32','-luuid','-lwindowscodecs','-lruntimeobject','-luiautomationcore')
foreach ($name in @('core','platform','autostart','autocorrect','storage','ocr','richtext','app','ui','main')) {
$libs = @('-lws2_32','-lbcrypt','-liphlpapi','-lwinhttp','-luser32','-lshell32','-lshlwapi','-ladvapi32','-lcrypt32','-limm32','-lcomctl32','-lgdi32','-lole32','-loleaut32','-ltaskschd','-lcomdlg32','-luuid','-lwindowscodecs','-lruntimeobject','-luiautomationcore')
$utilityDirectory = Join-Path (Split-Path $executablePath) 'tools'
New-Item -ItemType Directory -Force $utilityDirectory,build/release/tools | Out-Null
$stayHere = Join-Path $utilityDirectory 'StayHere.exe'
& $compiler @common -Werror -mwindows -municode -static -s utilities/StayHere/main.cpp -luser32 -lshell32 -lgdi32 -lwtsapi32 -o $stayHere
if ($LASTEXITCODE) { throw 'StayHere build failed' }
Copy-Item -LiteralPath $stayHere -Destination build/release/tools/StayHere.exe -Force
foreach ($name in @('lan_files','lan_channel','lan_input','lan','lan_pairing','core','platform','autostart','autocorrect','storage','sync','ocr','richtext','update','screenshot','screenshot_main','screenshot_notifications','screenshot_activation','screenshot_launcher','app','ui','main','configuration','config_main')) {
& $compiler @common -c "src/$name.cpp" -o "build/$name.o"
if ($LASTEXITCODE) { throw "Compilation failed: $name" }
}
& windres resources/app.rc -O coff -o build/app-res.o
& $compiler @common -DKS_RESIDENT_APP -c src/ui.cpp -o build/ui-resident.o
if ($LASTEXITCODE) { throw 'Resident UI compilation failed' }
& $compiler @common -DKS_CONFIG_APP -c src/ui.cpp -o build/ui-config.o
if ($LASTEXITCODE) { throw 'Configurator UI compilation failed' }
& windres --codepage=65001 resources/app.rc -O coff -o build/app-res.o
if ($LASTEXITCODE) { throw 'Resource compilation failed' }
& windres --codepage=65001 -DKS_SCREENSHOT_RESOURCES resources/app.rc -O coff -o build/screenshot-res.o
if ($LASTEXITCODE) { throw 'Screenshot resource compilation failed' }
New-Item -ItemType Directory -Force build/release | Out-Null
& $compiler -mwindows -municode -static -s '-Wl,--gc-sections' build/core.o build/platform.o build/autostart.o build/autocorrect.o build/storage.o build/ocr.o build/richtext.o build/app.o build/ui.o build/main.o build/app-res.o @libs -o $executablePath
& $compiler -mwindows -municode -static -s '-Wl,--gc-sections' build/lan_channel.o build/lan_input.o build/lan.o build/lan_files.o build/lan_pairing.o build/core.o build/platform.o build/autostart.o build/autocorrect.o build/storage.o build/sync.o build/ocr.o build/richtext.o build/update.o build/screenshot_launcher.o build/screenshot_activation.o build/screenshot_notifications.o build/app.o build/configuration.o build/ui-resident.o build/main.o build/app-res.o @libs -o $executablePath
if ($LASTEXITCODE) { throw 'Link failed' }
Copy-Item -LiteralPath $executablePath -Destination build/release/KSwitcher.exe -Force
$configPath = Join-Path (Split-Path $executablePath) 'KSwitcher.Config.exe'
& $compiler -mwindows -municode -static -s '-Wl,--gc-sections' build/lan_channel.o build/lan_input.o build/lan.o build/lan_files.o build/lan_pairing.o build/core.o build/platform.o build/storage.o build/sync.o build/ocr.o build/configuration.o build/config_main.o build/ui-config.o build/app-res.o @libs -o $configPath
if ($LASTEXITCODE) { throw 'Configurator link failed' }
Copy-Item -LiteralPath $configPath -Destination build/release/KSwitcher.Config.exe -Force
$screenshotPath = Join-Path (Split-Path $executablePath) 'KSwitcher.Screenshot.exe'
& $compiler -mwindows -municode -static -s '-Wl,--gc-sections' build/lan_channel.o build/lan_input.o build/lan.o build/lan_files.o build/lan_pairing.o build/core.o build/platform.o build/storage.o build/ocr.o build/screenshot.o build/screenshot_notifications.o build/screenshot_activation.o build/screenshot_main.o build/screenshot-res.o @libs -o $screenshotPath
if ($LASTEXITCODE) { throw 'Screenshot link failed' }
Copy-Item -LiteralPath $screenshotPath -Destination build/release/KSwitcher.Screenshot.exe -Force
$filesPath = Join-Path (Split-Path $executablePath) 'KSwitcher.Files.exe'
$terminalIncludes = & "$root/tools/stage-files-terminal.ps1" -Destination (Split-Path $filesPath)
& "$root/tools/stage-files-terminal.ps1" -Destination build/release | Out-Null
& windres resources/files.rc -O coff -o build/files-res.o
if ($LASTEXITCODE) { throw 'File manager resource compilation failed' }
& $compiler @common "-I$terminalIncludes" -mwindows -municode -static -s src/files_main.cpp build/files-res.o build/lan_files.o build/lan_channel.o build/lan_pairing.o build/platform.o build/core.o '-Wl,--gc-sections' @libs -o $filesPath
if ($LASTEXITCODE) { throw 'File manager build failed' }
Copy-Item -LiteralPath $filesPath -Destination build/release/KSwitcher.Files.exe -Force
if ($Test) {
& $compiler @common tests/tests.cpp build/core.o build/platform.o build/autostart.o build/autocorrect.o build/storage.o build/ocr.o build/richtext.o @libs -static -s -o build/KSwitcherTests.exe
& $compiler @common -Werror tests/settings_merge.cpp build/sync.o build/storage.o build/core.o build/platform.o build/ocr.o @libs -static -s -o build/KSwitcherSettingsMergeTests.exe
if ($LASTEXITCODE) { throw 'Settings merge test build failed' }
& .\build\KSwitcherSettingsMergeTests.exe
if ($LASTEXITCODE) { throw 'Settings merge tests failed' }
& $compiler @common -Werror tests/lan_clipboard.cpp build/lan_channel.o build/lan_pairing.o build/core.o build/platform.o @libs -static -s -o build/KSwitcherLanClipboardTests.exe
if ($LASTEXITCODE) { throw 'LAN clipboard test build failed' }
& .\build\KSwitcherLanClipboardTests.exe
if ($LASTEXITCODE) { throw 'LAN clipboard tests failed' }
& $compiler @common -Werror tests/lan_input.cpp build/lan_channel.o build/lan_input.o build/lan_pairing.o build/core.o build/platform.o @libs -static -s -o build/KSwitcherLanInputTests.exe
if ($LASTEXITCODE) { throw 'LAN input test build failed' }
& .\build\KSwitcherLanInputTests.exe
if ($LASTEXITCODE) { throw 'LAN input tests failed' }
& $compiler @common -Werror tests/lan.cpp build/lan_channel.o build/lan_input.o build/lan.o build/lan_files.o build/lan_pairing.o build/core.o build/platform.o @libs -static -s -o build/KSwitcherLanTests.exe
if ($LASTEXITCODE) { throw 'LAN test build failed' }
& .\build\KSwitcherLanTests.exe
if ($LASTEXITCODE) { throw 'LAN pairing tests failed' }
& $compiler @common -Werror tests/lan_files.cpp build/lan_files.o build/lan_channel.o build/lan_pairing.o build/core.o build/platform.o @libs -static -s -o build/KSwitcherLanFilesTests.exe
if ($LASTEXITCODE) { throw 'LAN file test build failed' }
& .\build\KSwitcherLanFilesTests.exe
if ($LASTEXITCODE) { throw 'LAN file tests failed' }
& $compiler @common -Werror -DSTAYHERE_TEST_INSTANCE -mwindows -municode -static -s utilities/StayHere/main.cpp -luser32 -lshell32 -lgdi32 -lwtsapi32 -o build/StayHereTest.exe
if ($LASTEXITCODE) { throw 'StayHere fixture build failed' }
& "$env:SystemRoot\System32\WindowsPowerShell\v1.0\powershell.exe" -NoProfile -ExecutionPolicy Bypass -File tests/stayhere.ps1 -Executable (Join-Path $outDir 'StayHereTest.exe')
if ($LASTEXITCODE) { throw 'StayHere lifecycle tests failed' }
& $compiler @common tests/utilities.cpp -static -s -o build/KSwitcherUtilitiesTests.exe
if ($LASTEXITCODE) { throw 'Utilities test build failed' }
& .\build\KSwitcherUtilitiesTests.exe
if ($LASTEXITCODE) { throw 'Utilities tests failed' }
& $compiler @common tests/dictionary.cpp build/lan_channel.o build/lan_input.o build/lan.o build/lan_files.o build/lan_pairing.o build/core.o build/platform.o build/autocorrect.o @libs -lpsapi -static -s -o build/KSwitcherDictionaryTests.exe
if ($LASTEXITCODE) { throw 'Dictionary test build failed' }
& .\build\KSwitcherDictionaryTests.exe
if ($LASTEXITCODE) { throw 'Dictionary tests failed' }
& $compiler @common tests/sync.cpp build/lan_channel.o build/lan_input.o build/lan.o build/lan_files.o build/lan_pairing.o build/core.o build/platform.o build/storage.o build/sync.o build/ocr.o @libs -static -s -o build/KSwitcherSyncTests.exe
if ($LASTEXITCODE) { throw 'Sync test build failed' }
& .\build\KSwitcherSyncTests.exe
if ($LASTEXITCODE) { throw 'Sync tests failed' }
& $compiler @common tests/scroll.cpp @libs -static -s -o build/KSwitcherScrollTests.exe
if ($LASTEXITCODE) { throw 'Scroll test build failed' }
& .\build\KSwitcherScrollTests.exe
if ($LASTEXITCODE) { throw 'Scroll tests failed' }
& $compiler @common tests/tests.cpp build/lan_channel.o build/lan_input.o build/lan.o build/lan_files.o build/lan_pairing.o build/core.o build/platform.o build/autostart.o build/autocorrect.o build/storage.o build/sync.o build/ocr.o build/richtext.o build/update.o @libs -static -s -o build/KSwitcherTests.exe
if ($LASTEXITCODE) { throw 'Test build failed' }
& .\build\KSwitcherTests.exe
if ($LASTEXITCODE) { throw 'Tests failed' }
& $compiler @common tests/input.cpp build/lan_channel.o build/lan_input.o build/lan.o build/lan_files.o build/lan_pairing.o build/core.o build/platform.o build/autostart.o build/autocorrect.o build/storage.o build/sync.o build/ocr.o build/richtext.o build/update.o build/screenshot.o build/screenshot_launcher.o build/screenshot_activation.o build/screenshot_notifications.o build/app.o build/configuration.o build/ui.o build/screenshot-res.o @libs -static -s -o build/KSwitcherInputTests.exe
if ($LASTEXITCODE) { throw 'Input test build failed' }
& .\build\KSwitcherInputTests.exe
if ($LASTEXITCODE) { throw 'Input tests failed' }
& $compiler @common tests/chat_profiles.cpp build/lan_channel.o build/lan_input.o build/lan.o build/lan_files.o build/lan_pairing.o build/core.o build/platform.o build/autostart.o build/autocorrect.o build/storage.o build/sync.o build/ocr.o build/richtext.o build/update.o build/screenshot.o build/screenshot_launcher.o build/screenshot_activation.o build/screenshot_notifications.o build/app.o build/configuration.o build/ui.o build/screenshot-res.o @libs -static -s -o build/KSwitcherChatProfileTests.exe
if ($LASTEXITCODE) { throw 'Chat profile test build failed' }
& .\build\KSwitcherChatProfileTests.exe
if ($LASTEXITCODE) { throw 'Chat profile tests failed' }
& $compiler @common tests/screenshot_ui.cpp build/lan_channel.o build/lan_input.o build/lan.o build/lan_files.o build/lan_pairing.o build/core.o build/platform.o build/ocr.o build/screenshot-res.o @libs -static -s -o build/KSwitcherScreenshotUITests.exe
if ($LASTEXITCODE) { throw 'Screenshot UI test build failed' }
& .\build\KSwitcherScreenshotUITests.exe
if ($LASTEXITCODE) { throw 'Screenshot UI tests failed' }
& .\build\KSwitcherScreenshotUITests.exe --save-only
if ($LASTEXITCODE) { throw 'Screenshot save tests failed' }
$registrationTest = Join-Path (Split-Path $executablePath) 'KSwitcherRegistrationTests.exe'
try {
& $compiler @common tests/screenshot_registration.cpp build/lan_channel.o build/lan_input.o build/lan.o build/lan_files.o build/lan_pairing.o build/core.o build/platform.o @libs -static '-Wl,--gc-sections' -s -o $registrationTest
if ($LASTEXITCODE) { throw 'Screenshot registration test build failed' }
& $registrationTest
if ($LASTEXITCODE) { throw 'Screenshot registration tests failed' }
} finally { Remove-Item -LiteralPath $registrationTest -ErrorAction SilentlyContinue }
& "$env:SystemRoot\System32\WindowsPowerShell\v1.0\powershell.exe" -NoProfile -ExecutionPolicy Bypass -File tests/update.ps1 -Executable $executablePath
if ($LASTEXITCODE) { throw 'Updater tests failed' }
& "$env:SystemRoot\System32\WindowsPowerShell\v1.0\powershell.exe" -NoProfile -ExecutionPolicy Bypass -File tests/update_progress.ps1
if ($LASTEXITCODE) { throw 'Updater progress UI tests failed' }
}
if ($Far) {
& $compiler @common tests/far_snippets.cpp build/lan_channel.o build/lan_input.o build/lan.o build/lan_files.o build/lan_pairing.o build/core.o build/platform.o build/autostart.o build/autocorrect.o build/storage.o build/sync.o build/ocr.o build/richtext.o build/update.o build/screenshot.o build/screenshot_launcher.o build/screenshot_activation.o build/screenshot_notifications.o build/app.o build/configuration.o build/ui.o build/screenshot-res.o @libs -mwindows -municode -static -s -o build/KSwitcherFarSnippetTests.exe
if ($LASTEXITCODE) { throw 'Far snippet test build failed' }
$fixture = Start-Process -FilePath (Join-Path $outDir 'KSwitcherFarSnippetTests.exe') -WindowStyle Hidden -Wait -PassThru
Get-Content -LiteralPath (Join-Path $outDir 'far-snippets.log')
if ($fixture.ExitCode) { throw 'Far snippet tests failed' }
}
if ($Integration) {
& $compiler @common tests/integration.cpp build/core.o build/platform.o build/autostart.o build/autocorrect.o build/storage.o build/ocr.o build/richtext.o build/app.o build/ui.o build/app-res.o @libs -static -s -o build/KSwitcherIntegration.exe
$historyFixture = Join-Path (Resolve-Path build) ('history-host-fixture-' + [guid]::NewGuid().ToString('N'))
New-Item -ItemType Directory $historyFixture | Out-Null
Copy-Item $executablePath,(Join-Path (Split-Path $executablePath) 'KSwitcher.Screenshot.exe') -Destination $historyFixture
Set-Content (Join-Path $historyFixture '.local-dev') 'Isolated history integration test'
$historyTest = Join-Path $historyFixture 'KSwitcherHistoryHostTests.exe'
& $compiler @common tests/history_host.cpp build/lan_channel.o build/lan_input.o build/lan.o build/lan_files.o build/lan_pairing.o build/core.o build/platform.o build/storage.o build/ocr.o @libs -lpsapi -static -s -o $historyTest
if ($LASTEXITCODE) { throw 'History host test build failed' }
& $historyTest
if ($LASTEXITCODE) { throw 'History host tests failed' }
$snapshotTest = Join-Path (Split-Path $executablePath) 'KSwitcherClipboardSnapshotTests.exe'
& $compiler @common tests/clipboard_snapshot.cpp build/lan_channel.o build/lan_input.o build/lan.o build/lan_files.o build/lan_pairing.o build/core.o build/platform.o build/storage.o build/ocr.o @libs -lpsapi -static -s -o $snapshotTest
if ($LASTEXITCODE) { throw 'Clipboard snapshot test build failed' }
& $snapshotTest
if ($LASTEXITCODE) { throw 'Clipboard snapshot tests failed' }
$captureTest = Join-Path (Split-Path $executablePath) 'KSwitcherImageCaptureTests.exe'
& $compiler @common tests/image_capture.cpp build/lan_channel.o build/lan_input.o build/lan.o build/lan_files.o build/lan_pairing.o build/core.o build/platform.o build/storage.o build/ocr.o @libs -static -s -o $captureTest
if ($LASTEXITCODE) { throw 'Image capture test build failed' }
& $captureTest
if ($LASTEXITCODE) { throw 'Image capture tests failed' }
& $compiler @common tests/integration.cpp build/lan_channel.o build/lan_input.o build/lan.o build/lan_files.o build/lan_pairing.o build/core.o build/platform.o build/autostart.o build/autocorrect.o build/storage.o build/sync.o build/ocr.o build/richtext.o build/update.o build/screenshot.o build/screenshot_launcher.o build/screenshot_activation.o build/screenshot_notifications.o build/app.o build/configuration.o build/ui.o build/screenshot-res.o @libs -static -s -o build/KSwitcherIntegration.exe
if ($LASTEXITCODE) { throw 'Integration test build failed' }
& .\build\KSwitcherIntegration.exe
if ($LASTEXITCODE) { throw 'Integration tests failed' }
}
if ($Ocr) {
& $compiler @common tests/ocr.cpp build/core.o build/platform.o build/ocr.o @libs -static -s -o build/KSwitcherOcrTests.exe
$ocrTest = Join-Path (Split-Path $executablePath) 'KSwitcherOcrTests.exe'
& $compiler @common tests/ocr.cpp build/lan_channel.o build/lan_input.o build/lan.o build/lan_files.o build/lan_pairing.o build/core.o build/platform.o build/autostart.o build/autocorrect.o build/storage.o build/sync.o build/ocr.o build/richtext.o build/update.o build/screenshot.o build/screenshot_launcher.o build/screenshot_activation.o build/screenshot_notifications.o build/app.o build/configuration.o build/ui.o build/screenshot-res.o @libs -static -s -o $ocrTest
if ($LASTEXITCODE) { throw 'OCR test build failed' }
& .\build\KSwitcherOcrTests.exe
& $ocrTest
if ($LASTEXITCODE) { throw 'OCR tests failed' }
}
foreach ($folder in @('build/dictionaries','build/release/dictionaries','build/staged/dictionaries')) {
+12 -2
View File
@@ -17,9 +17,19 @@ replace counts with original frequency ranks, sort lexically. Reproduce with
hashes. The lists contain word forms from subtitles, not a complete spelling
or grammar dictionary. Recognition can miss names, jargon and uncommon forms.
File format: UTF-8 word, TAB, positive original frequency rank, newline.
Supplemental Ukrainian vocabulary: `реліз`, `поточні`. These additions use rank 50001,
after the upstream 50k list, rather than claiming a measured corpus frequency.
They are included with the default unlimited rank setting and recorded in sources.json.
File format: UTF-8 word, TAB, positive frequency rank (or supplemental rank), newline.
All entries protect known words. The frequency rank limit in Auto-correction options defaults to 0
(entire dictionary); a positive limit only permits correction candidates at or below that rank.
Personal words may use rank 1. Keep entries lowercase, unique and sorted, with a Tab separator.
Prefer Settings > Auto-correction options > My dictionary for personal words.
Enter correct spellings in the matching language field, separated by commas or new lines.
These words are stored in settings, survive dictionary updates, and need no ranks or sorting.
Click OK, then Save in Settings to apply them.
For advanced file edits, personal words may use rank 1. Keep entries lowercase with a Tab separator.
Words may be appended: loading sorts entries and merges duplicates using the lowest rank.
Save the UTF-8 file, then click OK in Auto-correction options and Save in Settings to reload it.
Back up edited files before replacing dictionaries with a new distribution.
+7 -3
View File
@@ -9,9 +9,13 @@
"uk": {
"source": "https://raw.githubusercontent.com/hermitdave/FrequencyWords/525f9b560de45753a5ea01069454e72e9aa541c6/content/2016/uk/uk_50k.txt",
"sourceSha256": "9548b79a608d33f39c5546037a69117d679730b1fda08dd0ce67c043dbf3335a",
"words": 42549,
"bytes": 898210,
"sha256": "b4104898dcccbc1331ec01e9cd690e30285fe58481460e47438e973964d09802"
"words": 42551,
"bytes": 898248,
"sha256": "caeb097749d4afc84b68ad96b3d7dfb6a6569561cd32ccce0bee94a13b43ba86",
"supplementalWords": {
"\u0440\u0435\u043b\u0456\u0437": 50001,
"\u043f\u043e\u0442\u043e\u0447\u043d\u0456": 50001
}
},
"ru": {
"source": "https://raw.githubusercontent.com/hermitdave/FrequencyWords/525f9b560de45753a5ea01069454e72e9aa541c6/content/2016/ru/ru_50k.txt",
+2
View File
@@ -27337,6 +27337,7 @@
поторопись 9062
поторопитесь 20656
поторопиться 15390
поточні 50001
потрапив 4168
потрапила 7383
потрапили 6631
@@ -31207,6 +31208,7 @@
релігію 26138
релігія 22202
релігії 23427
реліз 50001
рембо 28603
ремень 23872
ремені 28385
+30 -2
View File
@@ -4,14 +4,42 @@ $root = $PSScriptRoot
$destination = Join-Path $root 'dist\KSwitcher'
New-Item -ItemType Directory -Force $destination | Out-Null
Copy-Item -LiteralPath (Join-Path $root 'build\release\KSwitcher.exe') -Destination $destination
Copy-Item -LiteralPath (Join-Path $root 'build\release\KSwitcher.Config.exe') -Destination $destination
Copy-Item -LiteralPath (Join-Path $root 'build\release\KSwitcher.Screenshot.exe') -Destination $destination
Copy-Item -LiteralPath (Join-Path $root 'build\release\KSwitcher.Files.exe') -Destination $destination
Copy-Item -LiteralPath (Join-Path $root 'build\release\WebView2Loader.dll') -Destination $destination
Copy-Item -LiteralPath (Join-Path $root 'build\release\terminal') -Destination $destination -Recurse -Force
Copy-Item -LiteralPath (Join-Path $root 'LICENSE') -Destination $destination
Copy-Item -LiteralPath (Join-Path $root 'vendor\json-LICENSE.txt') -Destination $destination
Copy-Item -LiteralPath (Join-Path $root 'README.md') -Destination $destination
Copy-Item -LiteralPath (Join-Path $root 'LAN-PREVIEW.md') -Destination $destination
Copy-Item -LiteralPath (Join-Path $root 'dictionaries') -Destination $destination -Recurse -Force
New-Item -ItemType Directory -Force (Join-Path $destination 'tools') | Out-Null
Copy-Item -LiteralPath (Join-Path $root 'resources\portable-tools-README.txt') -Destination (Join-Path $destination 'tools\README.txt') -Force
Copy-Item -LiteralPath (Join-Path $root 'build\release\tools\StayHere.exe') -Destination (Join-Path $destination 'tools\StayHere.exe') -Force
Copy-Item -LiteralPath (Join-Path $root 'utilities\StayHere\README.md') -Destination (Join-Path $destination 'tools\StayHere-README.md') -Force
# Enumerate the distributable files explicitly; do not include a user's data directory.
$files = @('KSwitcher.exe','LICENSE','json-LICENSE.txt','README.md','dictionaries') | ForEach-Object { Join-Path $destination $_ }
$files = @('KSwitcher.exe','KSwitcher.Config.exe','KSwitcher.Screenshot.exe','KSwitcher.Files.exe','WebView2Loader.dll','terminal','LICENSE','json-LICENSE.txt','README.md','LAN-PREVIEW.md','dictionaries','tools\README.txt','tools\StayHere.exe','tools\StayHere-README.md') | ForEach-Object { Join-Path $destination $_ }
$entries = foreach ($path in $files) {
foreach ($file in @(Get-ChildItem -LiteralPath $path -File -Recurse)) {
@{path=$file.FullName.Substring($destination.Length+1).Replace('\','/'); size=$file.Length; sha256=(Get-FileHash -LiteralPath $file.FullName -Algorithm SHA256).Hash.ToLowerInvariant()}
}
}
$version=[Diagnostics.FileVersionInfo]::GetVersionInfo((Join-Path $destination 'KSwitcher.exe')).FileVersion
$manifest=Join-Path $destination 'release-files.json'
@{schema=1; version=$version; files=@($entries)} | ConvertTo-Json -Depth 4 | Set-Content -LiteralPath $manifest -Encoding utf8
$files += $manifest
$archive = Join-Path $root 'dist\KSwitcher-win-x64.zip'
Compress-Archive -LiteralPath $files -DestinationPath $archive -Force
# Archive only manifest-listed files, preserving relative paths. Never sweep up
# personal utilities or leftover developer files from the staging directory.
Add-Type -AssemblyName System.IO.Compression.FileSystem
$stream = [IO.File]::Open($archive,[IO.FileMode]::Create)
$zip = [IO.Compression.ZipArchive]::new($stream,[IO.Compression.ZipArchiveMode]::Create)
try {
foreach ($name in @($entries | ForEach-Object { $_.path }) + @('release-files.json')) {
[IO.Compression.ZipFileExtensions]::CreateEntryFromFile($zip,(Join-Path $destination $name),$name,[IO.Compression.CompressionLevel]::Optimal) | Out-Null
}
} finally { $zip.Dispose(); $stream.Dispose() }
Get-FileHash -LiteralPath $archive -Algorithm SHA256
if ($PublishRelease) {
$release = Join-Path $root 'Release'
+60 -3
View File
@@ -1,10 +1,16 @@
#pragma code_page(65001)
#include <windows.h>
#include "resource.h"
IDI_KSWITCHER ICON "kswitcher.ico"
#ifdef KS_SCREENSHOT_RESOURCES
IDI_SCREENSHOT_QUESTION ICON "icons/question.ico"
IDC_SCREENSHOT_PENCIL CURSOR "icons/lucide/pencil.cur"
#endif
1 RT_MANIFEST "app.manifest"
201 RCDATA "update.ps1"
1 VERSIONINFO
FILEVERSION 0,1,0,0
PRODUCTVERSION 0,1,0,0
FILEVERSION 0,6,78,0
PRODUCTVERSION 0,6,78,0
FILEOS VOS_NT_WINDOWS32
FILETYPE VFT_APP
BEGIN
@@ -14,7 +20,7 @@ BEGIN
BEGIN
VALUE "FileDescription", "KSwitcher — native keyboard and clipboard utility"
VALUE "ProductName", "KSwitcher"
VALUE "FileVersion", "0.1.0"
VALUE "FileVersion", "0.6.78"
END
END
BLOCK "VarFileInfo"
@@ -22,3 +28,54 @@ BEGIN
VALUE "Translation", 0x409, 1200
END
END
#ifdef KS_SCREENSHOT_RESOURCES
300 ICON "icons/lucide/circle-dot-normal.ico"
310 ICON "icons/lucide/circle-dot-active.ico"
320 ICON "icons/lucide/circle-dot-disabled.ico"
301 ICON "icons/lucide/arrow-up-right-normal.ico"
311 ICON "icons/lucide/arrow-up-right-active.ico"
321 ICON "icons/lucide/arrow-up-right-disabled.ico"
302 ICON "icons/lucide/undo-2-normal.ico"
312 ICON "icons/lucide/undo-2-active.ico"
322 ICON "icons/lucide/undo-2-disabled.ico"
303 ICON "icons/lucide/copy-normal.ico"
313 ICON "icons/lucide/copy-active.ico"
323 ICON "icons/lucide/copy-disabled.ico"
304 ICON "icons/lucide/x-normal.ico"
314 ICON "icons/lucide/x-active.ico"
324 ICON "icons/lucide/x-disabled.ico"
305 ICON "icons/lucide/redo-2-normal.ico"
315 ICON "icons/lucide/redo-2-active.ico"
325 ICON "icons/lucide/redo-2-disabled.ico"
306 ICON "icons/lucide/highlighter-normal.ico"
316 ICON "icons/lucide/highlighter-active.ico"
326 ICON "icons/lucide/highlighter-disabled.ico"
307 ICON "icons/lucide/palette-normal.ico"
317 ICON "icons/lucide/palette-active.ico"
327 ICON "icons/lucide/palette-disabled.ico"
308 ICON "icons/lucide/pencil-normal.ico"
318 ICON "icons/lucide/pencil-active.ico"
328 ICON "icons/lucide/pencil-disabled.ico"
400 ICON "icons/lucide/slash-normal.ico"
410 ICON "icons/lucide/slash-active.ico"
420 ICON "icons/lucide/slash-disabled.ico"
401 ICON "icons/lucide/square-normal.ico"
411 ICON "icons/lucide/square-active.ico"
421 ICON "icons/lucide/square-disabled.ico"
402 ICON "icons/lucide/circle-normal.ico"
412 ICON "icons/lucide/circle-active.ico"
422 ICON "icons/lucide/circle-disabled.ico"
403 ICON "icons/lucide/ellipse-normal.ico"
413 ICON "icons/lucide/ellipse-active.ico"
423 ICON "icons/lucide/ellipse-disabled.ico"
404 ICON "icons/lucide/type-normal.ico"
414 ICON "icons/lucide/type-active.ico"
424 ICON "icons/lucide/type-disabled.ico"
405 ICON "icons/lucide/a-large-small-normal.ico"
415 ICON "icons/lucide/a-large-small-active.ico"
425 ICON "icons/lucide/a-large-small-disabled.ico"
#endif
+186
View File
@@ -0,0 +1,186 @@
#include <windows.h>
#include <commctrl.h>
#define FAR_EDIT(id, mode, x, y, width, height) \
CONTROL "", id, "Edit", ES_AUTOHSCROLL | WS_TABSTOP | mode, x, y, width, height
1 ICON "kswitcher-files.ico"
1 RT_MANIFEST "app.manifest"
101 DIALOGEX 0, 0, 397, 310
STYLE DS_SETFONT | DS_MODALFRAME | DS_CENTER | WS_POPUP | WS_CAPTION | WS_SYSMENU | WS_THICKFRAME | WS_CLIPCHILDREN
CAPTION "KSwitcher Files Options"
FONT 9, "Segoe UI"
BEGIN
LTEXT "&Search settings", -1, 8, 8, 113, 10
EDITTEXT 270, 8, 21, 113, 14, ES_AUTOHSCROLL
CONTROL "", 271, "SysTreeView32", WS_TABSTOP | WS_BORDER | TVS_HASLINES | TVS_LINESATROOT | TVS_HASBUTTONS | TVS_SHOWSELALWAYS, 8, 40, 113, 240
LTEXT "", 272, 131, 8, 253, 12
AUTOCHECKBOX "Left panel: &Wide", 273, 132, 27, 115, 12
AUTOCHECKBOX "Right panel: W&ide", 274, 251, 27, 125, 12
GROUPBOX "Wide panel columns", 275, 129, 27, 259, 59
LTEXT "Size column &width (6–16 characters)", 276, 138, 44, 175, 10
EDITTEXT 201, 335, 41, 43, 14, ES_NUMBER | ES_AUTOHSCROLL
AUTOCHECKBOX "&Align file extensions", 202, 138, 64, 108, 12
AUTOCHECKBOX "Align &folder extensions", 204, 252, 64, 125, 12
GROUPBOX "Status line columns", 277, 129, 27, 259, 46
AUTOCHECKBOX "Show &details", 203, 138, 40, 100, 12
AUTOCHECKBOX "Name", 205, 138, 55, 47, 12
AUTOCHECKBOX "Size", 206, 196, 55, 42, 12
AUTOCHECKBOX "Date", 207, 254, 55, 45, 12
AUTOCHECKBOX "Time", 208, 312, 55, 45, 12
PUSHBUTTON "Change &color…", 279, 131, 48, 82, 15
LTEXT "", 280, 222, 50, 155, 12
LTEXT "", 281, 131, 30, 254, 14
PUSHBUTTON "Change &font…", 282, 131, 52, 82, 15
LTEXT "&Outer padding (px at 96 DPI)", 302, 131, 31, 173, 12
COMBOBOX 303, 311, 28, 56, 125, CBS_DROPDOWNLIST | WS_TABSTOP | WS_VSCROLL
LTEXT "&Inner padding (px at 96 DPI)", 304, 131, 54, 173, 12
COMBOBOX 305, 311, 51, 56, 125, CBS_DROPDOWNLIST | WS_TABSTOP | WS_VSCROLL
LTEXT "&Row text padding (characters)", 306, 131, 77, 173, 12
COMBOBOX 307, 311, 74, 56, 76, CBS_DROPDOWNLIST | WS_TABSTOP | WS_VSCROLL
LTEXT "Previous command", 320, 131, 30, 120, 12
CONTROL "", 321, "msctls_hotkey32", WS_TABSTOP | WS_BORDER, 260, 27, 110, 15
LTEXT "Open command history", 322, 131, 53, 120, 12
CONTROL "", 323, "msctls_hotkey32", WS_TABSTOP | WS_BORDER, 260, 50, 110, 15
PUSHBUTTON "Reset selected to defaults", 284, 131, 181, 125, 15
COMBOBOX 285, 131, 183, 139, 95, CBS_DROPDOWNLIST | WS_TABSTOP | WS_VSCROLL
PUSHBUTTON "Reset property", 286, 278, 183, 108, 15
LTEXT "File type", 287, 131, 26, 75, 10
COMBOBOX 288, 131, 38, 151, 100, CBS_DROPDOWNLIST | WS_TABSTOP | WS_VSCROLL
PUSHBUTTON "&Remove", 289, 290, 38, 77, 14
LTEXT "Windows extension or custom extension", 290, 131, 63, 246, 10
COMBOBOX 291, 131, 75, 151, 108, CBS_DROPDOWN | CBS_AUTOHSCROLL | WS_TABSTOP | WS_VSCROLL
PUSHBUTTON "A&dd", 292, 290, 75, 77, 14
LTEXT "Row state", 300, 131, 100, 72, 10
COMBOBOX 301, 131, 112, 174, 80, CBS_DROPDOWNLIST | WS_TABSTOP
PUSHBUTTON "&Foreground…", 293, 131, 135, 78, 14
PUSHBUTTON "&Background…", 294, 216, 135, 78, 14
PUSHBUTTON "&Inherit colors", 295, 301, 135, 83, 14
LTEXT "", 296, 131, 152, 253, 13
AUTOCHECKBOX "&Underline", 297, 131, 166, 80, 12
AUTOCHECKBOX "Stri&kethrough", 298, 218, 166, 100, 12
CONTROL "Settings preview", 299, "Static", SS_OWNERDRAW | WS_BORDER, 131, 202, 253, 81
DEFPUSHBUTTON "OK", IDOK, 222, 289, 50, 14
PUSHBUTTON "Cancel", IDCANCEL, 280, 289, 50, 14
PUSHBUTTON "&Apply", 283, 338, 289, 50, 14
END
102 DIALOGEX 0, 0, 350, 145
STYLE DS_SETFONT | DS_CENTER | WS_POPUP
CAPTION "Copy"
FONT 10, "Consolas"
BEGIN
LTEXT "Copy to", 210, 14, 15, 322, 12, SS_PATHELLIPSIS
FAR_EDIT(211, ES_MULTILINE, 14, 29, 322, 18)
LTEXT "", 328, 14, 49, 322, 35, SS_NOPREFIX
LTEXT "Existing files:", 212, 14, 92, 122, 14, SS_CENTERIMAGE
COMBOBOX 213, 140, 92, 196, 100, CBS_DROPDOWNLIST | CBS_OWNERDRAWFIXED | CBS_HASSTRINGS | WS_VSCROLL | WS_TABSTOP
CONTROL "Also ask on read-only files", 214, "Button", BS_OWNERDRAW | WS_TABSTOP, 14, 112, 322, 14
CONTROL "&Copy", IDOK, "Button", BS_OWNERDRAW | WS_TABSTOP, 87, 127, 86, 18
CONTROL "&Cancel", IDCANCEL, "Button", BS_OWNERDRAW | WS_TABSTOP, 177, 127, 86, 18
END
103 DIALOGEX 0, 0, 460, 126
STYLE DS_SETFONT | DS_CENTER | WS_POPUP
CAPTION "Copying"
FONT 10, "Consolas"
BEGIN
LTEXT "Counting files...", 220, 12, 16, 436, 12, SS_OWNERDRAW
LTEXT "", 221, 12, 31, 436, 12, SS_OWNERDRAW
CONTROL "", 222, "msctls_progress32", PBS_SMOOTH, 12, 46, 436, 14
LTEXT "Files:", 224, 12, 63, 436, 12
LTEXT "Bytes:", 225, 12, 77, 436, 12
CONTROL "", 226, "msctls_progress32", PBS_SMOOTH, 12, 91, 436, 14
LTEXT "", 228, 12, 60, 1, 1
LTEXT "", 229, 12, 104, 1, 1
CONTROL "&Cancel", IDCANCEL, "Button", BS_OWNERDRAW | WS_TABSTOP, 187, 106, 86, 18
END
104 DIALOGEX 0, 0, 360, 139
STYLE DS_SETFONT | DS_CENTER | WS_POPUP
CAPTION "Warning"
FONT 10, "Consolas"
BEGIN
LTEXT "", 230, 12, 16, 336, 12
LTEXT "", 231, 12, 33, 336, 12, SS_PATHELLIPSIS
LTEXT "", 233, 12, 55, 1, 1
LTEXT "New", 329, 12, 58, 65, 13
RTEXT "", 330, 78, 58, 270, 13
LTEXT "Existing", 331, 12, 72, 65, 13
RTEXT "", 332, 78, 72, 270, 13
LTEXT "", 333, 12, 88, 1, 1
LTEXT "", 234, 12, 114, 1, 1
CONTROL "Remember choice", 232, "Button", BS_OWNERDRAW | WS_TABSTOP, 12, 90, 336, 18
CONTROL "&Overwrite", IDYES, "Button", BS_OWNERDRAW | WS_TABSTOP, 42, 122, 110, 18
CONTROL "&Skip", IDNO, "Button", BS_OWNERDRAW | WS_TABSTOP, 158, 122, 68, 18
CONTROL "&Cancel", IDCANCEL, "Button", BS_OWNERDRAW | WS_TABSTOP, 232, 122, 86, 18
END
105 DIALOGEX 0, 0, 310, 65
STYLE DS_SETFONT | DS_CENTER | WS_POPUP
CAPTION "Cancel copy"
FONT 10, "Consolas"
BEGIN
CTEXT "Do you really want to cancel copying?", 235, 12, 17, 286, 16, SS_CENTERIMAGE
LTEXT "", 236, 12, 38, 1, 1
CONTROL "&Yes", IDYES, "Button", BS_OWNERDRAW | WS_TABSTOP, 83, 42, 68, 18
CONTROL "&No", IDNO, "Button", BS_OWNERDRAW | WS_TABSTOP, 159, 42, 68, 18
END
106 DIALOGEX 0, 0, 400, 190
STYLE DS_SETFONT | DS_CENTER | WS_POPUP
CAPTION "Delete"
FONT 10, "Consolas"
BEGIN
LTEXT "", 240, 14, 14, 372, 16
LTEXT "", 241, 14, 32, 372, 122, SS_NOPREFIX
LTEXT "", 243, 14, 177, 1, 1
CONTROL "&Move", IDYES, "Button", BS_OWNERDRAW | WS_TABSTOP, 37, 178, 86, 18
CONTROL "&All", 244, "Button", BS_OWNERDRAW | WS_TABSTOP, 129, 178, 63, 18
CONTROL "&Skip", IDNO, "Button", BS_OWNERDRAW | WS_TABSTOP, 197, 178, 63, 18
CONTROL "Skip a&ll", 249, "Button", BS_OWNERDRAW | WS_TABSTOP, 197, 178, 94, 18
CONTROL "&Cancel", IDCANCEL, "Button", BS_OWNERDRAW | WS_TABSTOP, 265, 178, 79, 18
END
107 DIALOGEX 0, 0, 400, 82
STYLE DS_SETFONT | DS_CENTER | WS_POPUP
CAPTION "Deleting"
FONT 10, "Consolas"
BEGIN
LTEXT "Scanning...", 245, 14, 15, 372, 14, SS_PATHELLIPSIS | SS_NOPREFIX
LTEXT "", 246, 14, 31, 372, 14
CONTROL "", 247, "msctls_progress32", PBS_SMOOTH, 14, 48, 372, 18
LTEXT "", 248, 14, 73, 1, 1
CONTROL "&Cancel", IDCANCEL, "Button", BS_OWNERDRAW | WS_TABSTOP, 157, 74, 86, 18
END
109 DIALOGEX 0, 0, 250, 85
STYLE DS_SETFONT | DS_CENTER | WS_POPUP | WS_CLIPCHILDREN
CAPTION "Folder shortcuts"
FONT 10, "Consolas"
BEGIN
CONTROL "", 310, "ListBox", LBS_NOTIFY | LBS_OWNERDRAWFIXED | LBS_HASSTRINGS | LBS_NOINTEGRALHEIGHT | WS_TABSTOP, 8, 14, 234, 50
END
110 DIALOGEX 0, 0, 370, 116
STYLE DS_SETFONT | DS_CENTER | WS_POPUP
CAPTION "Folder shortcuts"
FONT 10, "Consolas"
BEGIN
LTEXT "&Title", 316, 16, 18, 338, 12
FAR_EDIT(314, ES_LEFT, 16, 31, 338, 15)
LTEXT "&Path", 317, 16, 49, 338, 12
FAR_EDIT(211, ES_LEFT, 16, 62, 338, 15)
LTEXT "", 315, 10, 82, 1, 1, NOT WS_VISIBLE
CONTROL "&OK", IDOK, "Button", BS_OWNERDRAW | WS_TABSTOP, 97, 86, 86, 18
CONTROL "&Cancel", IDCANCEL, "Button", BS_OWNERDRAW | WS_TABSTOP, 187, 86, 86, 18
END
111 DIALOGEX 0, 0, 385, 220
STYLE DS_SETFONT | DS_CENTER | WS_POPUP
FONT 9, "Consolas"
BEGIN
LTEXT "&Search commands or topics", 326, 12, 14, 361, 12
FAR_EDIT(325, ES_MULTILINE, 12, 28, 361, 16)
LISTBOX 324, 12, 49, 361, 155, LBS_NOTIFY | LBS_OWNERDRAWFIXED | LBS_HASSTRINGS | LBS_NOINTEGRALHEIGHT | WS_TABSTOP
END
+43
View File
@@ -0,0 +1,43 @@
ISC License
Copyright (c) 2026 Lucide Icons and Contributors
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
---
The following Lucide icons are derived from the Feather project:
airplay, alert-circle, alert-octagon, alert-triangle, aperture, arrow-down-circle, arrow-down-left, arrow-down-right, arrow-down, arrow-left-circle, arrow-left, arrow-right-circle, arrow-right, arrow-up-circle, arrow-up-left, arrow-up-right, arrow-up, at-sign, calendar, cast, check, chevron-down, chevron-left, chevron-right, chevron-up, chevrons-down, chevrons-left, chevrons-right, chevrons-up, circle, clipboard, clock, code, columns, command, compass, corner-down-left, corner-down-right, corner-left-down, corner-left-up, corner-right-down, corner-right-up, corner-up-left, corner-up-right, crosshair, database, divide-circle, divide-square, dollar-sign, download, external-link, feather, frown, hash, headphones, help-circle, info, italic, key, layout, life-buoy, link-2, link, loader, lock, log-in, log-out, maximize, meh, minimize, minimize-2, minus-circle, minus-square, minus, monitor, moon, more-horizontal, more-vertical, move, music, navigation-2, navigation, octagon, pause-circle, percent, plus-circle, plus-square, plus, power, radio, rss, search, server, share, shopping-bag, sidebar, smartphone, smile, square, table-2, tablet, target, terminal, trash-2, trash, triangle, tv, type, upload, x-circle, x-octagon, x-square, x, zoom-in, zoom-out
The MIT License (MIT) (for the icons listed above)
Copyright (c) 2013-present Cole Bemis
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+5
View File
@@ -0,0 +1,5 @@
Lucide https://github.com/lucide-icons/lucide
Commit: be09619c1c1124ad09c19fc3e87ee3b939409bbd
SVG originals retained. ICO variants generated with tools/generate-toolbar-icons.py.
Ellipse is adapted from the pinned Lucide circle SVG (r replaced with rx/ry).
Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

+16
View File
@@ -0,0 +1,16 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="m15 16 2.536-7.328a1.02 1.02 1 0 1 1.928 0L22 16" />
<path d="M15.697 14h5.606" />
<path d="m2 16 4.039-9.69a.5.5 0 0 1 .923 0L11 16" />
<path d="M3.304 13h6.392" />
</svg>

After

Width:  |  Height:  |  Size: 391 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

+14
View File
@@ -0,0 +1,14 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M7 7h10v10" />
<path d="M7 17 17 7" />
</svg>

After

Width:  |  Height:  |  Size: 260 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

+14
View File
@@ -0,0 +1,14 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<circle cx="12" cy="12" r="1" />
<circle cx="12" cy="12" r="10" />
</svg>

After

Width:  |  Height:  |  Size: 279 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

+13
View File
@@ -0,0 +1,13 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<circle cx="12" cy="12" r="10" />
</svg>

After

Width:  |  Height:  |  Size: 244 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

+14
View File
@@ -0,0 +1,14 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<rect width="14" height="14" x="8" y="8" rx="2" ry="2" />
<path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2" />
</svg>

After

Width:  |  Height:  |  Size: 339 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

+13
View File
@@ -0,0 +1,13 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<ellipse cx="12" cy="12" rx="10" ry="7" />
</svg>

After

Width:  |  Height:  |  Size: 253 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

+14
View File
@@ -0,0 +1,14 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="m9 11-6 6v3h9l3-3" />
<path d="m22 12-4.6 4.6a2 2 0 0 1-2.8 0l-5.2-5.2a2 2 0 0 1 0-2.8L14 4" />
</svg>

After

Width:  |  Height:  |  Size: 317 B

+13
View File
@@ -0,0 +1,13 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M5 12h14" />
</svg>

After

Width:  |  Height:  |  Size: 232 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

+17
View File
@@ -0,0 +1,17 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M12 22a1 1 0 0 1 0-20 10 9 0 0 1 10 9 5 5 0 0 1-5 5h-2.25a1.75 1.75 0 0 0-1.4 2.8l.3.4a1.75 1.75 0 0 1-1.4 2.8z" />
<circle cx="13.5" cy="6.5" r=".5" fill="currentColor" />
<circle cx="17.5" cy="10.5" r=".5" fill="currentColor" />
<circle cx="6.5" cy="12.5" r=".5" fill="currentColor" />
<circle cx="8.5" cy="7.5" r=".5" fill="currentColor" />
</svg>

After

Width:  |  Height:  |  Size: 571 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

+14
View File
@@ -0,0 +1,14 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z" />
<path d="m15 5 4 4" />
</svg>

After

Width:  |  Height:  |  Size: 377 B

+14
View File
@@ -0,0 +1,14 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M5 12h14" />
<path d="M12 5v14" />
</svg>

After

Width:  |  Height:  |  Size: 256 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

+14
View File
@@ -0,0 +1,14 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="m15 14 5-5-5-5" />
<path d="M20 9H9.5A5.5 5.5 0 0 0 4 14.5A5.5 5.5 0 0 0 9.5 20H13" />
</svg>

After

Width:  |  Height:  |  Size: 308 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

+13
View File
@@ -0,0 +1,13 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M22 2 2 22" />
</svg>

After

Width:  |  Height:  |  Size: 234 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

+13
View File
@@ -0,0 +1,13 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<rect width="18" height="18" x="3" y="3" rx="2" />
</svg>

After

Width:  |  Height:  |  Size: 261 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

+15
View File
@@ -0,0 +1,15 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M12 4v16" />
<path d="M4 7V5a1 1 0 0 1 1-1h14a1 1 0 0 1 1 1v2" />
<path d="M9 20h6" />
</svg>

After

Width:  |  Height:  |  Size: 310 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

+14
View File
@@ -0,0 +1,14 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M9 14 4 9l5-5" />
<path d="M4 9h10.5a5.5 5.5 0 0 1 5.5 5.5a5.5 5.5 0 0 1-5.5 5.5H11" />
</svg>

After

Width:  |  Height:  |  Size: 309 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

+14
View File
@@ -0,0 +1,14 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M18 6 6 18" />
<path d="m6 6 12 12" />
</svg>

After

Width:  |  Height:  |  Size: 260 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

+14
View File
@@ -0,0 +1,14 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48">
<defs>
<linearGradient id="blue" x1="8" y1="4" x2="36" y2="44" gradientUnits="userSpaceOnUse">
<stop stop-color="#38bdf8"/>
<stop offset=".55" stop-color="#1687e8"/>
<stop offset="1" stop-color="#2563eb"/>
</linearGradient>
</defs>
<circle cx="24" cy="25" r="21" fill="#1e40af" opacity=".12"/>
<circle cx="24" cy="23" r="21" fill="url(#blue)"/>
<circle cx="24" cy="23" r="20.5" fill="none" stroke="white" stroke-opacity=".25"/>
<path d="M18 17.5c0-3.1 2.5-5 6-5s6 2 6 5c0 4.8-6 4.8-6 9" fill="none" stroke="white" stroke-width="3.5" stroke-linecap="round"/>
<circle cx="24" cy="33" r="2" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 711 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

+18
View File
@@ -0,0 +1,18 @@
KSwitcher portable utilities
Put your trusted portable .exe utilities directly in this tools folder,
beside this README.txt. Right-click the KSwitcher tray icon and open Tools.
For example: tools/StayHere.exe appears as Tools > StayHere.
The list refreshes each time the tray menu opens. Subfolders and shortcuts
are not listed. Utilities start only when you choose them, with this folder
as their working directory. Closing KSwitcher does not close a utility.
StayHere.exe is bundled. Choose Tools > StayHere to start it; it starts
enabled, prevents automatic sleep, and sends F15 after 3 minutes of inactivity.
Left-click its tray icon to pause/resume; right-click for Exit. See StayHere-README.md.
It does not start automatically with KSwitcher or bypass Windows locking.
Add other utilities separately. Updates preserve your own files in this folder;
bundled StayHere.exe and documentation may be refreshed. Utilities are not
transferred by settings synchronization.
+2
View File
@@ -1,2 +1,4 @@
#pragma once
#define IDI_KSWITCHER 101
#define IDI_SCREENSHOT_QUESTION 102
#define IDC_SCREENSHOT_PENCIL 500
Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

+22
View File
@@ -0,0 +1,22 @@
<svg xmlns="http://www.w3.org/2000/svg" width="960" height="330" viewBox="0 0 960 330">
<rect x="1" y="1" width="958" height="328" rx="24" fill="#F5F3FF" stroke="#DDD6FE" stroke-width="2"/>
<g font-family="Segoe UI" fill="#241B46">
<text x="40" y="58" font-size="20" font-weight="600" fill="#4F46E5">KSwitcher / Support development</text>
<text x="40" y="115" font-size="35" font-weight="700">KSwitcher part of your day?</text>
<text x="40" y="159" font-size="35" font-weight="700">Buy the developer a coffee.</text>
<text x="40" y="202" font-size="20" fill="#655B7C">Your donation helps the tool keep growing.</text>
<rect x="40" y="238" width="350" height="54" rx="12" fill="#4F46E5"/>
<text x="215" y="272" text-anchor="middle" font-size="20" font-weight="600" fill="white">Support via monobank</text>
</g>
<g transform="translate(759 48) rotate(10 66 66)">
<rect y="8" width="132" height="132" rx="26" fill="#C4B5FD"/>
<rect width="132" height="132" rx="26" fill="#4F46E5"/>
<g transform="translate(14 14) scale(3.25)"><path d="M8 7H12V25H8ZM11 15L22 6L25 9L14 18Z" fill="white"/><path d="M11 14L25 24L22 27L10 18Z" fill="#5EEAD4"/></g>
</g>
<g transform="translate(699 172) rotate(-8 67 60)">
<rect y="7" width="142" height="115" rx="22" fill="#C4B5FD"/>
<rect width="142" height="115" rx="22" fill="white" stroke="#DDD6FE" stroke-width="2"/>
<g stroke="#4F46E5" stroke-width="5" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path d="M43 41H92V66Q92 83 75 83H60Q43 83 43 66Z"/>
<path d="M92 46H99Q115 46 111 58Q108 67 92 66M37 92H100M56 28V18M69 28V18M82 28V18"/>
</g></g></svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

+22
View File
@@ -0,0 +1,22 @@
<svg xmlns="http://www.w3.org/2000/svg" width="960" height="330" viewBox="0 0 960 330">
<rect x="1" y="1" width="958" height="328" rx="24" fill="#F5F3FF" stroke="#DDD6FE" stroke-width="2"/>
<g font-family="Segoe UI" fill="#241B46">
<text x="40" y="58" font-size="20" font-weight="600" fill="#4F46E5">KSwitcher / Підтримати розробку</text>
<text x="40" y="115" font-size="35" font-weight="700">KSwitcher щодня під рукою?</text>
<text x="40" y="159" font-size="35" font-weight="700">Пригостіть розробника кавою.</text>
<text x="40" y="202" font-size="20" fill="#655B7C">Ваш донат допомагає розвивати інструмент.</text>
<rect x="40" y="238" width="350" height="54" rx="12" fill="#4F46E5"/>
<text x="215" y="272" text-anchor="middle" font-size="20" font-weight="600" fill="white">Підтримати через monobank</text>
</g>
<g transform="translate(759 48) rotate(10 66 66)">
<rect y="8" width="132" height="132" rx="26" fill="#C4B5FD"/>
<rect width="132" height="132" rx="26" fill="#4F46E5"/>
<g transform="translate(14 14) scale(3.25)"><path d="M8 7H12V25H8ZM11 15L22 6L25 9L14 18Z" fill="white"/><path d="M11 14L25 24L22 27L10 18Z" fill="#5EEAD4"/></g>
</g>
<g transform="translate(699 172) rotate(-8 67 60)">
<rect y="7" width="142" height="115" rx="22" fill="#C4B5FD"/>
<rect width="142" height="115" rx="22" fill="white" stroke="#DDD6FE" stroke-width="2"/>
<g stroke="#4F46E5" stroke-width="5" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path d="M43 41H92V66Q92 83 75 83H60Q43 83 43 66Z"/>
<path d="M92 46H99Q115 46 111 58Q108 67 92 66M37 92H100M56 28V18M69 28V18M82 28V18"/>
</g></g></svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

+14
View File
@@ -0,0 +1,14 @@
<!doctype html>
<html lang="en">
<meta charset="utf-8">
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline'">
<title>PowerShell terminal</title>
<link rel="stylesheet" href="xterm.css">
<style>
html, body, #terminal { width:100%; height:100%; margin:0; overflow:hidden; background:#181818; }
</style>
<div id="terminal"></div>
<script src="xterm.js"></script>
<script src="addon-fit.js"></script>
<script src="terminal.js"></script>
</html>
+89
View File
@@ -0,0 +1,89 @@
const bridge = window.chrome.webview;
const terminal = new Terminal({
fontFamily: 'Consolas, monospace',
fontSize: 16,
cursorBlink: false,
stdinDisabled: true,
theme: { background: '#181818', foreground: '#e8e8e8' },
});
const fit = new FitAddon.FitAddon();
terminal.loadAddon(fit);
const host = document.getElementById('terminal');
terminal.open(host);
host.addEventListener('pointerup', () => bridge.postMessage('focus-input'), true);
const screen = host.querySelector('.xterm-screen');
let traceEnabled = false;
let traceBudget = 4096;
let currentOffset = 0;
function trace(phase) {
if (!traceEnabled || traceBudget <= 0) return;
traceBudget--;
const buffer = terminal.buffer.active;
bridge.postMessage(`trace:${phase},${host.clientHeight},${terminal.rows},${buffer.viewportY},${buffer.baseY},${Math.round(currentOffset)}`);
}
function anchorOutput() {
const buffer = terminal.buffer.active;
let offset = 0;
if (buffer.type === 'normal' && buffer.viewportY === buffer.baseY) {
let last = 0;
for (let row = terminal.rows - 1; row >= 0; row--) {
if (buffer.getLine(buffer.baseY + row)?.translateToString(true)) {
last = row;
break;
}
}
offset = Math.max(0, terminal.rows - 1 - last) * screen.offsetHeight / terminal.rows;
}
screen.style.transform = `translateY(${offset}px)`;
currentOffset = offset;
trace(2);
}
terminal.attachCustomKeyEventHandler(event => {
if (event.type !== 'keydown' || !event.ctrlKey || event.altKey || event.shiftKey) return true;
if (event.code === 'KeyO') bridge.postMessage('toggle');
else if (event.code === 'ArrowUp') bridge.postMessage('grow');
else if (event.code === 'ArrowDown') bridge.postMessage('shrink');
else return true;
event.preventDefault();
return false;
});
const reportSize = () => bridge.postMessage(`size:${terminal.cols},${terminal.rows}`);
terminal.onResize(reportSize);
terminal.onScroll(() => {
trace(3);
anchorOutput();
});
bridge.addEventListener('message', ({ data }) => {
if (data === 'trace:on') {
traceEnabled = true;
trace(2);
return;
}
const font = /^font:(\d+):(\d+):([01]):(.*)$/u.exec(data);
if (font) {
const [, points, weight, italic, family] = font;
const quotedFamily = family.replace(/["\\]/g, '\\$&');
terminal.options.fontFamily = `"${quotedFamily}", monospace`;
terminal.options.fontSize = Number(points) * 96 / 720;
terminal.options.fontWeight = Number(weight);
host.style.fontStyle = italic === '1' ? 'italic' : 'normal';
fit.fit();
anchorOutput();
return;
}
const bytes = new Uint8Array(data.length / 2);
for (let i = 0; i < bytes.length; i++) bytes[i] = parseInt(data.slice(2*i, 2*i+2), 16);
terminal.write(bytes, anchorOutput);
});
new ResizeObserver(() => {
const buffer = terminal.buffer.active;
const wasAtBottom = buffer.viewportY === buffer.baseY;
trace(0);
fit.fit();
trace(1);
if (wasAtBottom) terminal.scrollToBottom();
requestAnimationFrame(anchorOutput);
}).observe(host);
bridge.postMessage('ready');
fit.fit();
reportSize();
+504
View File
@@ -0,0 +1,504 @@
param(
[ValidateSet('Check','Install')][string]$Mode = 'Check',
[string]$CurrentVersion, [string]$ExpectedVersion,
[string]$InstallDirectory, [string]$WorkDirectory,
[int]$ParentProcessId, [string]$ReadyEvent,
[switch]$ShowProgress,
[switch]$Library
)
$ErrorActionPreference = 'Stop'
Set-StrictMode -Version Latest
$script:progressWindow = $null
function Start-UpdateProgress {
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing
if (!('KSwitcherUpdateProgress' -as [type])) {
Add-Type -ReferencedAssemblies System.Windows.Forms,System.Drawing -TypeDefinition @'
using System;
using System.Drawing;
using System.Threading;
using System.Windows.Forms;
using System.Drawing.Drawing2D;
using System.Runtime.InteropServices;
public sealed class UpdateMeter : Control {
public int Percent = -1;
private int position;
private readonly System.Windows.Forms.Timer timer = new System.Windows.Forms.Timer { Interval = 35 };
public UpdateMeter() {
DoubleBuffered = true;
timer.Tick += delegate { if (Percent < 0) { position = (position + 5) % Math.Max(1,Width); Invalidate(); } };
timer.Start();
}
protected override void OnPaint(PaintEventArgs e) {
using(var track = new SolidBrush(Color.FromArgb(232,234,241))) e.Graphics.FillRectangle(track,0,0,Width,Height);
using(var fill = new SolidBrush(Color.FromArgb(79,70,229))) {
if(Percent < 0) e.Graphics.FillRectangle(fill,position-Width/4,0,Width/4,Height);
else e.Graphics.FillRectangle(fill,0,0,Width*Percent/100,Height);
}
}
protected override void Dispose(bool disposing) { if(disposing) timer.Dispose(); base.Dispose(disposing); }
}
public sealed class UpdateSteps : Control {
public int Current = -1;
public bool Complete;
public UpdateSteps() { DoubleBuffered = true; }
protected override void OnPaint(PaintEventArgs e) {
e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;
string[] names = {"Download","Verify","Install","Restart"};
for(int i=0;i<4;i++) {
int x = i*(Width-80)/3;
Color color = Complete || i<=Current ? Color.FromArgb(79,70,229) : Color.FromArgb(135,141,158);
using(var pen = new Pen(Color.FromArgb(231,233,240)))
if(i<3) e.Graphics.DrawLine(pen,x+22,8,x+(Width-80)/3-10,8);
using(var brush = new SolidBrush(color)) {
if(Complete || i<Current) {
e.Graphics.FillEllipse(brush,x,0,16,16);
using(var pen = new Pen(Color.White,1.6f)) e.Graphics.DrawLines(pen,new Point[]{new Point(x+4,8),new Point(x+7,11),new Point(x+12,5)});
} else {
using(var pen = new Pen(color,i==Current?2:1)) e.Graphics.DrawEllipse(pen,x+1,1,14,14);
if(i==Current) e.Graphics.FillEllipse(brush,x+5,5,6,6);
}
e.Graphics.DrawString(names[i],Font,brush,x,26);
}
}
}
}
public sealed class KSwitcherUpdateProgress : IDisposable {
private Form form;
private Label stage, detail, percentage, footer;
private UpdateMeter progress;
private UpdateSteps steps;
private Button done, close;
private bool closing;
private Exception failure;
private readonly ManualResetEvent ready = new ManualResetEvent(false);
private readonly Thread thread;
[DllImport("user32.dll")] private static extern bool ReleaseCapture();
[DllImport("user32.dll")] private static extern IntPtr SendMessage(IntPtr h,int m,IntPtr w,IntPtr l);
[DllImport("dwmapi.dll")] private static extern int DwmSetWindowAttribute(IntPtr h,int a,ref int v,int size);
private void Drag(object sender,MouseEventArgs e) { if(e.Button==MouseButtons.Left) { ReleaseCapture(); SendMessage(form.Handle,0xA1,(IntPtr)2,IntPtr.Zero); } }
public KSwitcherUpdateProgress() {
thread = new Thread(delegate() {
try {
Application.EnableVisualStyles();
form = new Form { Text = "KSwitcher Update", ClientSize = new Size(540,350), BackColor = Color.White,
ForeColor = Color.FromArgb(32,36,48), StartPosition = FormStartPosition.CenterScreen,
FormBorderStyle = FormBorderStyle.None, Font = new Font("Segoe UI",10), AutoScaleMode = AutoScaleMode.Dpi };
var logo = new Panel { Location = new Point(28,24), Size = new Size(34,34), BackColor = Color.FromArgb(79,70,229) };
logo.Paint += delegate(object sender,PaintEventArgs e) {
e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;
using(var pen = new Pen(Color.White,4)) { e.Graphics.DrawLine(pen,10,8,10,26); e.Graphics.DrawLine(pen,11,17,25,7); }
using(var pen = new Pen(Color.FromArgb(94,234,212),4)) e.Graphics.DrawLine(pen,12,17,25,27);
};
var brand = new Label { Text = "KSwitcher", Location = new Point(74,22), Size = new Size(260,24), Font = new Font("Segoe UI Semibold",11) };
var caption = new Label { Text = "Application update", Location = new Point(74,45), Size = new Size(260,20), ForeColor = Color.FromArgb(111,117,134), Font = new Font("Segoe UI",9) };
var minimize = new Button { Text = "\u2013", AccessibleName = "Minimize", Location = new Point(452,20), Size = new Size(32,32), FlatStyle = FlatStyle.Flat, TabStop = false };
minimize.FlatAppearance.BorderSize = 0; minimize.Click += delegate { form.WindowState = FormWindowState.Minimized; };
close = new Button { Text = "\u00d7", AccessibleName = "Close", Location = new Point(490,20), Size = new Size(32,32), FlatStyle = FlatStyle.Flat, Enabled = false };
close.FlatAppearance.BorderSize = 0; close.Click += delegate { form.Close(); };
stage = new Label { Text = "Checking for updates", Location = new Point(28,98), Size = new Size(414,36), Font = new Font("Segoe UI Semibold",20) };
percentage = new Label { Location = new Point(440,104), Size = new Size(72,30), TextAlign = ContentAlignment.MiddleRight, ForeColor = Color.FromArgb(79,70,229), Font = new Font("Segoe UI Semibold",16) };
detail = new Label { Text = "Connecting to the release server", Location = new Point(28,148), Size = new Size(484,42), ForeColor = Color.FromArgb(111,117,134) };
progress = new UpdateMeter { Location = new Point(28,202), Size = new Size(484,6), AccessibleName = "Update progress" };
steps = new UpdateSteps { Location = new Point(28,230), Size = new Size(484,48), Font = new Font("Segoe UI",9) };
var bottom = new Panel { Location = new Point(1,296), Size = new Size(538,53), BackColor = Color.FromArgb(247,248,251) };
footer = new Label { Text = "Your settings and history stay with you.", Location = new Point(27,18), Size = new Size(350,22), ForeColor = Color.FromArgb(111,117,134), Font = new Font("Segoe UI",9) };
done = new Button { Text = "Done", Location = new Point(410,9), Size = new Size(100,34), BackColor = Color.FromArgb(79,70,229), ForeColor = Color.White, FlatStyle = FlatStyle.Flat, Visible = false };
done.FlatAppearance.BorderSize = 0; done.Click += delegate { form.Close(); };
bottom.Controls.AddRange(new Control[]{footer,done});
form.Controls.AddRange(new Control[]{logo,brand,caption,minimize,close,stage,percentage,detail,progress,steps,bottom});
form.MouseDown += Drag; logo.MouseDown += Drag; brand.MouseDown += Drag; caption.MouseDown += Drag;
form.Paint += delegate(object sender,PaintEventArgs e) { using(var pen = new Pen(Color.FromArgb(220,223,232))) e.Graphics.DrawRectangle(pen,0,0,form.ClientSize.Width-1,form.ClientSize.Height-1); };
form.FormClosing += delegate(object sender, FormClosingEventArgs e) { if (!closing) e.Cancel = true; };
form.Shown += delegate { int rounded=2; try { DwmSetWindowAttribute(form.Handle,33,ref rounded,4); } catch { } ready.Set(); };
Application.Run(form);
} catch(Exception e) { failure = e; ready.Set(); }
});
thread.IsBackground = true;
thread.SetApartmentState(ApartmentState.STA);
thread.Start();
if (!ready.WaitOne(10000)) throw new Exception("Update progress window did not start.");
if (failure != null) throw new Exception("Cannot display update progress.",failure);
}
public void Report(string title, string text, int percent) {
if (form == null || form.IsDisposed) return;
form.BeginInvoke((Action)delegate {
stage.Text = title; detail.Text = text;
progress.Percent = percent < 0 ? -1 : Math.Max(0,Math.Min(100,percent));
percentage.Text = percent < 0 ? "" : progress.Percent+"%";
int separator = text.IndexOf("% - ");
if(separator>=0) detail.Text = text.Substring(separator+4)+" downloaded";
if(title.StartsWith("Downloading")) steps.Current=0;
else if(title.StartsWith("Verifying") || title.StartsWith("Preparing")) steps.Current=1;
else if(title.StartsWith("Installing") || title.StartsWith("Closing")) steps.Current=2;
else if(title.StartsWith("Restarting")) steps.Current=3;
progress.Invalidate(); steps.Invalidate();
});
}
public void SavePreview(string path) {
form.Invoke((Action)delegate {
using(var bitmap = new Bitmap(form.Width,form.Height)) {
form.DrawToBitmap(bitmap,new Rectangle(0,0,bitmap.Width,bitmap.Height));
bitmap.Save(path,System.Drawing.Imaging.ImageFormat.Png);
}
});
}
public string Status { get { return (string)form.Invoke((Func<string>)delegate { return stage.Text; }); } }
public int Percent { get { return (int)form.Invoke((Func<int>)delegate { return progress.Percent; }); } }
public void Finish(string title, string text, bool success) {
form.Invoke((Action)delegate {
closing = true;
stage.Text = title; detail.Text = text;
progress.Percent = success ? 100 : 0; percentage.Text = success ? "100%" : "";
steps.Complete = success;
if(!success) {
detail.Visible = false; steps.Visible = false;
var result = new TextBox { Text = text, Location = new Point(28,148), Size = new Size(484,126),
Multiline = true, ReadOnly = true, BorderStyle = BorderStyle.None,
BackColor = Color.White, ScrollBars = ScrollBars.Vertical };
form.Controls.Add(result); result.BringToFront(); progress.Visible = false;
}
footer.Text = success ? "Your settings and history are preserved." : "Review the details above.";
close.Enabled = true; done.Visible = true;
form.AcceptButton = done; form.CancelButton = done;
progress.Invalidate(); steps.Invalidate();
});
}
public void WaitForClose() { thread.Join(); }
public void Dispose() {
if (form != null && !form.IsDisposed)
form.BeginInvoke((Action)delegate { closing = true; form.Close(); });
thread.Join(3000);
}
}
'@
}
$script:progressWindow = New-Object KSwitcherUpdateProgress
}
function Show-UpdateProgress([string]$Stage,[string]$Detail,[int]$Percent = -1) {
if ($script:progressWindow) {
try { $script:progressWindow.Report($Stage,$Detail,$Percent) }
catch { Write-Warning 'Could not refresh update progress.' }
}
}
function Complete-UpdateProgress([string]$Title,[string]$Detail,[bool]$Success) {
if ($script:progressWindow) {
try { $script:progressWindow.Finish($Title,$Detail,$Success); return $true }
catch { Write-Warning 'Could not display the update result.' }
}
return $false
}
function Get-StableVersion([string]$Tag) {
if ($Tag -notmatch '^(?:v|release-v)?(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$') { return $null }
try { return [version]($Tag -replace '^(?:release-)?v','') } catch { return $null }
}
function Assert-ReleaseUrl([string]$Url) {
$uri = [uri]$Url
if (!$uri.IsAbsoluteUri -or $uri.Scheme -ne 'https' -or $uri.Host -ne 'gitea.hrynco.com' -or
!$uri.IsDefaultPort -or $uri.UserInfo -or $uri.Query -or $uri.Fragment -or
!$uri.AbsolutePath.StartsWith('/hrynco/hrynco-kswitcher/releases/download/',[StringComparison]::Ordinal)) {
throw 'Release asset URL is outside the KSwitcher repository.'
}
}
function Receive-File([string]$Url,[string]$Path,[long]$Limit,[string]$ProgressLabel = '') {
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
$request = [Net.HttpWebRequest]::Create($Url)
$request.AllowAutoRedirect = $false
$request.Timeout = 15000
$request.ReadWriteTimeout = 15000
$request.UserAgent = 'KSwitcher-Updater'
$response = $null; $inputStream = $null; $outputStream = $null
try {
$response = $request.GetResponse()
if ([int]$response.StatusCode -ne 200 -or $response.ContentLength -gt $Limit) { throw 'Invalid or oversized server response.' }
$inputStream = $response.GetResponseStream()
$outputStream = [IO.File]::Create($Path)
$buffer = New-Object byte[] 65536
$total = 0L
$deadline = [DateTime]::UtcNow.AddSeconds(60)
$lastProgress = [DateTime]::MinValue
while (($count = $inputStream.Read($buffer,0,$buffer.Length)) -gt 0) {
$total += $count
if ($total -gt $Limit -or [DateTime]::UtcNow -gt $deadline) { throw 'Download exceeded size or time limit.' }
$outputStream.Write($buffer,0,$count)
if ($ProgressLabel -and ([DateTime]::UtcNow - $lastProgress).TotalMilliseconds -ge 100) {
$percent = if ($response.ContentLength -gt 0) { [int][Math]::Min(100,100.0*$total/$response.ContentLength) } else { -1 }
$detail = if ($percent -ge 0) { '{0}% - {1:N1} / {2:N1} MB' -f $percent,($total/1MB),($response.ContentLength/1MB) } else { '{0:N1} MB received' -f ($total/1MB) }
Show-UpdateProgress $ProgressLabel $detail $percent
$lastProgress = [DateTime]::UtcNow
}
}
if ($response.ContentLength -ge 0 -and $total -ne $response.ContentLength) { throw 'The download ended before the complete file was received.' }
if ($ProgressLabel) { Show-UpdateProgress $ProgressLabel ('Downloaded {0:N1} MB' -f ($total/1MB)) 100 }
} finally {
if ($outputStream) { $outputStream.Dispose() }
if ($inputStream) { $inputStream.Dispose() }
if ($response) { $response.Dispose() }
}
}
function Select-Release($Releases,[version]$Current) {
$candidates = @($Releases | Where-Object { !$_.draft -and !$_.prerelease -and (Get-StableVersion $_.tag_name) } |
Sort-Object { Get-StableVersion $_.tag_name } -Descending)
if (!$candidates.Count -or (Get-StableVersion $candidates[0].tag_name) -le $Current) { return $null }
$release = $candidates[0]
$zip = @($release.assets | Where-Object name -CEQ 'KSwitcher-win-x64.zip')
$sum = @($release.assets | Where-Object name -CEQ 'SHA256SUMS.txt')
if ($zip.Count -ne 1 -or $sum.Count -ne 1) { throw 'The newest release lacks its x64 ZIP or SHA256SUMS.txt.' }
Assert-ReleaseUrl $zip[0].browser_download_url
Assert-ReleaseUrl $sum[0].browser_download_url
return @{ version=$release.tag_name; zip=$zip[0].browser_download_url; checksum=$sum[0].browser_download_url }
}
function Assert-Checksum([string]$Archive,[string]$Checksum) {
$matchesFound = @([IO.File]::ReadAllLines($Checksum) | Where-Object { $_ -cmatch '^([a-fA-F0-9]{64})\s+\*?KSwitcher-win-x64\.zip$' })
if ($matchesFound.Count -ne 1) { throw 'Missing or ambiguous archive checksum.' }
$expected = ($matchesFound[0] -split '\s+')[0]
# Do not depend on PowerShell module autoloading: hosts can inherit a
# PowerShell 7 PSModulePath while this helper runs in Windows PowerShell 5.1.
$stream = [IO.File]::OpenRead($Archive)
$sha = [Security.Cryptography.SHA256]::Create()
try { $actual = [BitConverter]::ToString($sha.ComputeHash($stream)).Replace('-','') }
finally { $sha.Dispose(); $stream.Dispose() }
if ($actual -ne $expected) { throw 'Release checksum verification failed.' }
}
function Expand-LegacyRelease([string]$Archive,[string]$Destination,[version]$Expected) {
Add-Type -AssemblyName System.IO.Compression.FileSystem
$zip = [IO.Compression.ZipFile]::OpenRead($Archive)
try {
$seen = [Collections.Generic.HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase)
$total = 0L
foreach ($entry in $zip.Entries) {
$name = $entry.FullName.Replace('\','/')
if ($name.EndsWith('/') -and $name -eq 'dictionaries/') { continue }
if ($name -cnotmatch '^(KSwitcher(?:\.(?:Config|Screenshot))?\.exe|LICENSE|json-LICENSE\.txt|README\.md|dictionaries/[a-zA-Z0-9_.-]+)$' -or
$name.EndsWith('/.') -or $name.EndsWith('/..') -or $name.EndsWith('.') -or
(($entry.ExternalAttributes -shr 16) -band 0xF000) -eq 0xA000 -or !$seen.Add($name)) {
throw "Unexpected or unsafe archive entry: $name"
}
$total += $entry.Length
if ($total -gt 100MB -or $zip.Entries.Count -gt 100) { throw 'Release expands beyond allowed limits.' }
$destinationFile = Join-Path $Destination $name
[IO.Directory]::CreateDirectory([IO.Path]::GetDirectoryName($destinationFile)) | Out-Null
[IO.Compression.ZipFileExtensions]::ExtractToFile($entry,$destinationFile,$false)
}
foreach ($required in @('KSwitcher.exe','LICENSE','json-LICENSE.txt','README.md')) {
if (!$seen.Contains($required)) { throw "Incomplete release: $required" }
}
if ($Expected -gt [version]'0.2.3' -and !$seen.Contains('KSwitcher.Config.exe')) {
throw 'Incomplete release: KSwitcher.Config.exe'
}
} finally { $zip.Dispose() }
foreach ($binary in @('KSwitcher.exe','KSwitcher.Config.exe','KSwitcher.Screenshot.exe')) {
$exe = Join-Path $Destination $binary
if (!(Test-Path -LiteralPath $exe)) { continue }
$version = [Diagnostics.FileVersionInfo]::GetVersionInfo($exe)
if ($version.ProductName -ne 'KSwitcher' -or (Get-StableVersion $version.FileVersion) -ne $Expected) {
throw 'Executable identity/version does not match the release.'
}
$bytes = [IO.File]::ReadAllBytes($exe)
if ($bytes.Length -lt 64 -or $bytes[0] -ne 0x4d -or $bytes[1] -ne 0x5a) { throw 'Invalid release executable.' }
$pe = [BitConverter]::ToInt32($bytes,60)
if ($pe -lt 64 -or $pe -gt $bytes.Length-6 -or [BitConverter]::ToUInt32($bytes,$pe) -ne 0x4550 -or
[BitConverter]::ToUInt16($bytes,$pe+4) -ne 0x8664) { throw 'Release is not a Windows x64 executable.' }
}
}
function Get-Sha256([IO.Stream]$Stream) {
$sha=[Security.Cryptography.SHA256]::Create()
try { return [BitConverter]::ToString($sha.ComputeHash($Stream)).Replace('-','').ToLowerInvariant() }
finally { $sha.Dispose() }
}
function Assert-PackagePath([string]$Name) {
if (!$Name -or $Name.Length -gt 220 -or $Name -match '[\\:<>"|?*]' -or $Name.StartsWith('/')) { throw "Unsafe package path: $Name" }
$parts=$Name.Split('/')
foreach ($part in $parts) {
if (!$part -or $part -in @('.','..') -or $part -match '[. ]$|[\x00-\x1f]' -or
$part -match '^(?i:CON|PRN|AUX|NUL|COM[0-9]|LPT[0-9])(?:\.|$)') { throw "Unsafe package path: $Name" }
}
if ($parts[0] -ieq 'data' -or $parts[0] -like '.kswitcher-*') { throw "Protected user path: $Name" }
}
function Expand-Release([string]$Archive,[string]$Destination,[version]$Expected) {
Add-Type -AssemblyName System.IO.Compression.FileSystem
$zip=[IO.Compression.ZipFile]::OpenRead($Archive)
try {
$manifests=@($zip.Entries | Where-Object FullName -CEQ 'release-files.json')
if (!$manifests.Count) {
if ($Expected -gt [version]'0.4.2') { throw 'Release manifest is missing.' }
$zip.Dispose(); $zip=$null
Expand-LegacyRelease $Archive $Destination $Expected
return
}
if ($manifests.Count -ne 1 -or $manifests[0].Length -gt 256KB) { throw 'Invalid release manifest.' }
$reader=[IO.StreamReader]::new($manifests[0].Open())
try { $manifest=$reader.ReadToEnd() | ConvertFrom-Json } finally { $reader.Dispose() }
if ($manifest.schema -ne 1 -or (Get-StableVersion $manifest.version) -ne $Expected -or
@($manifest.files).Count -lt 1 -or @($manifest.files).Count -gt 1000) { throw 'Unsupported or mismatched release manifest.' }
$declared=[Collections.Generic.Dictionary[string,object]]::new([StringComparer]::OrdinalIgnoreCase)
foreach ($file in $manifest.files) {
Assert-PackagePath $file.path
if ($file.path -ieq 'release-files.json' -or $declared.ContainsKey($file.path) -or
$file.sha256 -notmatch '^[a-fA-F0-9]{64}$' -or $file.size -lt 0 -or $file.size -gt 100MB) { throw 'Invalid manifest file entry.' }
$declared.Add($file.path,$file)
}
if (!$declared.ContainsKey('KSwitcher.exe')) { throw 'Main executable is missing from the manifest.' }
$seen=[Collections.Generic.HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase)
$total=0L
foreach ($entry in $zip.Entries) {
$name=$entry.FullName.Replace('\','/')
if ($name.EndsWith('/')) { Assert-PackagePath $name.TrimEnd('/'); continue }
Assert-PackagePath $name
if (!$seen.Add($name) -or (($entry.ExternalAttributes -shr 16) -band 0xF000) -eq 0xA000) { throw 'Duplicate or linked archive entry.' }
if ($name -ceq 'release-files.json') { continue }
if (!$declared.ContainsKey($name)) { throw "Undeclared package file: $name" }
$file=$declared[$name]
if ($entry.Length -ne $file.size) { throw "Package file size mismatch: $name" }
$total+=$entry.Length
if ($total -gt 100MB -or $zip.Entries.Count -gt 1100) { throw 'Release expands beyond allowed limits.' }
$stream=$entry.Open()
try { $hash=Get-Sha256 $stream } finally { $stream.Dispose() }
if ($hash -ne $file.sha256) { throw "Package file checksum mismatch: $name" }
}
if ($seen.Count -ne $declared.Count+1) { throw 'Archive does not contain every manifest file.' }
[IO.Directory]::CreateDirectory($Destination) | Out-Null
Assert-PlainDirectory $Destination
foreach ($entry in $zip.Entries) {
if ($entry.FullName.EndsWith('/')) { continue }
$target=Join-Path $Destination $entry.FullName
[IO.Directory]::CreateDirectory([IO.Path]::GetDirectoryName($target)) | Out-Null
Assert-PlainDirectory ([IO.Path]::GetDirectoryName($target))
[IO.Compression.ZipFileExtensions]::ExtractToFile($entry,$target,$false)
}
$info=[Diagnostics.FileVersionInfo]::GetVersionInfo((Join-Path $Destination 'KSwitcher.exe'))
if ($info.ProductName -ne 'KSwitcher' -or (Get-StableVersion $info.FileVersion) -ne $Expected) { throw 'Main executable identity/version mismatch.' }
} finally { if ($zip) { $zip.Dispose() } }
}
function Assert-PlainDirectory([string]$Directory) {
$item = Get-Item -LiteralPath $Directory -Force
while ($item) {
if ($item.Attributes -band [IO.FileAttributes]::ReparsePoint) { throw 'Update directory must not contain junctions or symbolic links.' }
$item = $item.Parent
}
}
function Install-Files([string]$Stage,[string]$Target,[string]$Backup,[Collections.Generic.List[string]]$Changed) {
Assert-PlainDirectory $Target
foreach ($file in @(Get-ChildItem -LiteralPath $Stage -File -Recurse)) {
$relative = $file.FullName.Substring($Stage.Length+1)
$destination = Join-Path $Target $relative
if ($relative.StartsWith('dictionaries\',[StringComparison]::OrdinalIgnoreCase) -and (Test-Path -LiteralPath $destination)) { continue }
[IO.Directory]::CreateDirectory([IO.Path]::GetDirectoryName($destination)) | Out-Null
Assert-PlainDirectory ([IO.Path]::GetDirectoryName($destination))
if (Test-Path -LiteralPath $destination) {
if ((Get-Item -LiteralPath $destination -Force).Attributes -band [IO.FileAttributes]::ReparsePoint) { throw 'Cannot replace a linked file.' }
$backupFile = Join-Path $Backup $relative
[IO.Directory]::CreateDirectory([IO.Path]::GetDirectoryName($backupFile)) | Out-Null
Move-Item -LiteralPath $destination -Destination $backupFile
}
$Changed.Add($relative)
Move-Item -LiteralPath $file.FullName -Destination $destination
}
}
function Undo-Files([string]$Target,[string]$Backup,[Collections.Generic.List[string]]$Changed) {
for ($i=$Changed.Count-1; $i -ge 0; --$i) {
$destination = Join-Path $Target $Changed[$i]
if (Test-Path -LiteralPath $destination) { Remove-Item -LiteralPath $destination -Force }
$original = Join-Path $Backup $Changed[$i]
if (Test-Path -LiteralPath $original) { Move-Item -LiteralPath $original -Destination $destination }
}
}
function Write-Result($Value) {
$Value | ConvertTo-Json -Compress | Set-Content -LiteralPath (Join-Path $WorkDirectory 'result.json') -Encoding UTF8
}
if ($Library) { return }
$transaction = $null; $parentProcess = $null; $ready = $null; $started = $null
$changed = [Collections.Generic.List[string]]::new()
$parentExited = $false; $success = $false; $preserveBackup = $false; $child = $null; $uiFinished = $false
try {
if ($ShowProgress -or $Mode -eq 'Install') { Start-UpdateProgress }
$current = Get-StableVersion $CurrentVersion
if (!$current) { throw 'Invalid installed version.' }
if ($Mode -eq 'Install') {
$parentProcess = Get-Process -Id $ParentProcessId
if ([IO.Path]::GetFullPath($parentProcess.Path) -ne [IO.Path]::GetFullPath((Join-Path $InstallDirectory 'KSwitcher.exe'))) {
throw 'The running executable does not match the update target.'
}
$ready = [Threading.EventWaitHandle]::OpenExisting($ReadyEvent)
}
$metadata = Join-Path $WorkDirectory 'releases.json'
Receive-File 'https://gitea.hrynco.com/api/v1/repos/hrynco/hrynco-kswitcher/releases?limit=100' $metadata 2MB
$release = Select-Release (Get-Content -LiteralPath $metadata -Raw | ConvertFrom-Json) $current
if (!$release) { Write-Result @{state='current'}; exit 0 }
if ($Mode -eq 'Check') { Write-Result @{state='available';version=$release.version}; exit 0 }
if ($ExpectedVersion -ne $release.version) { throw 'Release changed since checking. Please select Update again.' }
Assert-PlainDirectory $InstallDirectory
$transaction = Join-Path $InstallDirectory ('.kswitcher-update-'+[guid]::NewGuid().ToString('N'))
$stage = Join-Path $transaction 'stage'; $backup = Join-Path $transaction 'backup'
[IO.Directory]::CreateDirectory($stage) | Out-Null
$archive = Join-Path $WorkDirectory 'release.zip'; $checksum = Join-Path $WorkDirectory 'SHA256SUMS.txt'
Show-UpdateProgress 'Downloading update' $release.version 0
Receive-File $release.zip $archive 50MB 'Downloading update'
Show-UpdateProgress 'Verifying download' 'Checking the archive SHA-256 checksum'
Receive-File $release.checksum $checksum 64KB
Assert-Checksum $archive $checksum
Show-UpdateProgress 'Preparing installation' 'Validating and unpacking the release files'
Expand-Release $archive $stage (Get-StableVersion $release.version)
$screenshotPath = [IO.Path]::GetFullPath((Join-Path $InstallDirectory 'KSwitcher.Screenshot.exe'))
if (Get-Process 'KSwitcher.Screenshot' -ErrorAction SilentlyContinue | Where-Object { $_.Path -eq $screenshotPath }) { throw 'Close screenshot editors before installing an update.' }
Write-Result @{state='prepared';version=$release.version}
Show-UpdateProgress 'Closing KSwitcher' 'Waiting for the running application to exit safely'
$ready.Set() | Out-Null
if (!$parentProcess.WaitForExit(120000)) { throw 'KSwitcher did not exit; the installed files were not changed.' }
$parentExited = $true
Show-UpdateProgress 'Installing update' 'Replacing application files; keeping your personal data'
Install-Files $stage $InstallDirectory $backup $changed
$startedName = 'Local\KSwitcher.Update.Started.'+[guid]::NewGuid().ToString('N')
$started = [Threading.EventWaitHandle]::new($false,[Threading.EventResetMode]::ManualReset,$startedName)
Show-UpdateProgress 'Restarting KSwitcher' 'Waiting for the updated application to confirm startup'
$child = Start-Process -FilePath (Join-Path $InstallDirectory 'KSwitcher.exe') -ArgumentList "--update-ready-event=$startedName" -WorkingDirectory $InstallDirectory -WindowStyle Hidden -PassThru
if (!$started.WaitOne(30000) -or $child.HasExited) { throw 'The updated application did not confirm startup.' }
$success = $true
if ($script:progressWindow) {
$uiFinished = Complete-UpdateProgress 'Update completed' "KSwitcher $(Get-StableVersion $release.version) is now running." $true
}
} catch {
$message = $_.Exception.Message
if ($parentExited) {
Show-UpdateProgress 'Restoring previous version' 'The update failed; recovering the previous installation'
try {
if ($child -and !$child.HasExited) { $child.Kill(); $child.WaitForExit() }
Undo-Files $InstallDirectory $backup $changed
Start-Process -FilePath (Join-Path $InstallDirectory 'KSwitcher.exe') -WorkingDirectory $InstallDirectory -WindowStyle Hidden
$message += "`nThe previous version has been restored and restarted."
} catch {
$preserveBackup = $true
$message += "`nRecovery needs attention: $($_.Exception.Message)`nBackup: $backup"
}
}
Write-Result @{state='error';error=$message}
if ($ready -and $ready.WaitOne(0)) {
if ($script:progressWindow) {
$uiFinished = Complete-UpdateProgress 'Update failed' $message $false
} else {
Add-Type -AssemblyName System.Windows.Forms
[Windows.Forms.MessageBox]::Show($message,'KSwitcher update failed') | Out-Null
}
}
exit 1
} finally {
if ($ready) { $ready.Dispose() }
if ($started) { $started.Dispose() }
if ($parentProcess) { $parentProcess.Dispose() }
# Only remove the uniquely created transaction directory directly under the install directory.
if ($transaction -and !$preserveBackup -and [IO.Path]::GetDirectoryName($transaction) -eq [IO.Path]::GetFullPath($InstallDirectory).TrimEnd('\') -and (Test-Path -LiteralPath $transaction)) {
Remove-Item -LiteralPath $transaction -Recurse -Force -ErrorAction SilentlyContinue
}
if ($success) {
# The script is loaded in memory; its unique temporary working directory is no longer needed.
if ((Split-Path -Leaf $WorkDirectory) -like 'KSwitcher-update-*' -and [IO.Path]::GetDirectoryName($WorkDirectory) -eq [IO.Path]::GetTempPath().TrimEnd('\')) {
Remove-Item -LiteralPath $WorkDirectory -Recurse -Force -ErrorAction SilentlyContinue
}
}
if ($script:progressWindow) {
if ($uiFinished) { $script:progressWindow.WaitForClose() }
$script:progressWindow.Dispose(); $script:progressWindow = $null
}
}
+2390 -182
View File
File diff suppressed because it is too large. Load diff
+139 -8
View File
@@ -1,6 +1,12 @@
#pragma once
#include "autocorrect.h"
#include "storage.h"
#include "update.h"
#include "sync.h"
#include "chat_context.h"
#include "lan.h"
#include "mouse_buttons.h"
#include <array>
#include <atomic>
#include <condition_variable>
#include <deque>
@@ -13,8 +19,9 @@ namespace ks
inline constexpr UINT TrayMessage = WM_APP + 1, ErrorMessage = WM_APP + 2, HistoryMessage = WM_APP + 3,
SnippetMessage = WM_APP + 4;
inline constexpr int Toggle = 1, Case = 2, Translit = 3, Undo = 4, ShowHistory = 5, AutoCorrect = 6,
Direct = 100;
Screenshot = 7, Direct = 100;
inline constexpr UINT AutoCorrectMessage = WM_APP + 6;
inline constexpr UINT ReplayTypingMessage = WM_APP + 30;
inline constexpr UINT CaptureHotkeyMessage = WM_APP + 5;
struct LastTransform
{
@@ -30,62 +37,186 @@ class App
{
public:
HWND window{}, historyWindow{}, settingsWindow{}, snippetsWindow{};
HWND historyService{}; // Non-null only in the short-lived history UI process.
bool isolateHistory{};
unsigned historyEntryWindows{};
std::wstring historySelection, historyTop, historyQuery;
bool historyPinnedOnly{};
std::vector<std::string> historyComputers;
HINSTANCE instance{};
std::filesystem::path data;
std::vector<Layout> layouts;
Settings settings;
std::unique_ptr<History> history;
AutoCorrector autoCorrector;
AutoCorrector autoPredictor; // Preloaded, hook-thread-only lookup; no file access while typing.
std::atomic<bool> busy{false}, suspended{false};
std::atomic<DWORD> ownSequence{0};
std::atomic<DWORD> ownSequence{0}, lanSequence{0};
std::atomic<uint64_t> inputVersion{0};
std::atomic<ULONGLONG> pauseUntil{0};
HWND pasteTarget{};
HWND hotkeyCapture{};
DWORD hotkeyCaptureProcess{};
HWND logWindow{};
std::mutex stateMutex;
Run run;
std::optional<LastTransform> last;
std::wstring registrationWarning;
bool smoke{};
int startCommand{};
App(HINSTANCE, bool = false, const std::filesystem::path &dataRoot = {});
std::wstring updateStartupEvent;
App(HINSTANCE, bool = false, const std::filesystem::path &dataRoot = {}, HWND historyService = nullptr);
~App();
int execute();
void dispatch(int);
void transform(int, int snippet = -1);
void transform(int, int snippet = -1, size_t snippetLength = 0, HWND snippetEditor = nullptr);
void pasteEntry(std::wstring, bool plain);
void recognizeEntry(std::wstring);
void captureHistory();
void publishClipboard(Clip, const std::wstring &);
void receiveClipboard();
void enqueue(std::function<void()>);
void enqueueHistorySearch(std::function<void()>);
void report(const std::wstring &);
void trayMenu();
void addTray();
bool registerHotkeys(const Settings &, bool rollback = false);
void applySettings(const Settings &);
void reloadLayouts();
void clearHistory();
void openHistoryProcess();
void refreshHistoryProcess();
void remoteHistory(const char *action, const std::wstring &id = {}, bool plain = false);
LRESULT historyRequest(HWND, const COPYDATASTRUCT &);
void saveHistoryView(const std::wstring &, const std::wstring &, const std::wstring &, bool);
void clearLog(bool previous);
void setHotkeyCapture(HWND);
bool isHotkey(DWORD, UINT);
void keyboard(const KBDLLHOOKSTRUCT &);
void keyboard(const KBDLLHOOKSTRUCT &, UINT modifiers);
bool handleKeyboard(const KBDLLHOOKSTRUCT &, bool down, UINT modifiers);
void resetInput();
void resetLanInput();
void checkUpdate(bool interactive, bool install = false, const std::wstring &version = {});
std::wstring syncCommand(int action = 0, const std::vector<SyncChoice> &choices = {});
Bytes syncConflictData();
Bytes lanCommand(const Bytes &);
void pollSync();
static App *current;
private:
friend struct InputRegressionAccess;
void releaseIdleMemory(ULONGLONG now);
void applySettings(const Settings &, bool validateOcrInstallation);
void checkAutostartPath();
bool checkingAutostartPath{};
HHOOK keyboardHook{}, mouseHook{};
struct MousePress
{
HWND target{};
POINT point{};
ULONGLONG started{};
MouseAction action;
bool held{}, waitForSecond{};
};
std::array<MousePress, std::size(MouseButtonNames)> mousePresses{};
std::array<MousePress, std::size(MouseButtonNames)> pendingMouseClicks{};
std::deque<MousePress> mouseActions;
HWND mouseWheelTarget{};
int mouseWheelSignal{-1}, mouseWheelRemainder{};
void queueMouseAction(MousePress);
void flushMouseClicks();
void beginMouseVolume(int button);
void repeatMouseVolume(int button);
bool volumeButtonHeld() const;
void dispatchMouseActions();
std::unique_ptr<LanService> lanService;
HANDLE historyProcess{};
std::mutex jobsMutex;
std::condition_variable jobsReady;
std::deque<std::function<void()>> jobs;
std::thread worker;
HANDLE notificationRegistration{};
std::thread historySearchWorker;
std::condition_variable historySearchReady;
std::function<void()> pendingHistorySearch;
std::thread historyOcrWorker;
std::condition_variable historyOcrReady;
bool historyOcrRequested{};
Settings historyOcrSettings;
void scheduleHistoryOcr(const Settings &);
std::thread updateWorker;
std::thread syncWorker;
std::atomic<bool> syncDone{false};
bool syncRunning{};
ULONGLONG nextSync{}, syncRetryAfter{};
SyncResult syncResult;
SyncConfig syncConnection;
SyncConfig conflictConnection;
std::vector<SyncConflict> pendingSyncConflicts;
Bytes syncSnapshot;
std::wstring syncStatus = L"Not connected.";
bool updateRunning{}, installingUpdate{};
std::wstring availableUpdate, notifiedUpdate;
bool (*updateNotifier)(const std::wstring &){};
bool stop{};
UINT taskbarCreated{};
uint64_t pendingAutoVersion{};
bool trayRetryPending{};
uint64_t transformAttempt{}, previousAttemptVersion{};
size_t incomingLanFileCount{};
bool incomingFileBalloon{};
std::atomic<uint64_t> activeTransformAttempt{};
ULONGLONG previousAttemptAt{};
HWND previousAttemptWindow{};
int previousAttemptCommand{}, repeatAttempts{};
std::vector<std::pair<int, Hotkey>> hookBindings;
Hotkey registeredFilesHotkey{};
int filesHotkeyId{};
HotkeyHook hotkeyHook;
ChatSwitchHook chatSwitchHook;
HWND chatWindow{};
std::wstring activeChat, activeChatTitle;
void refreshChatContext();
void switchChatLayout(bool reverse);
HWND typingTarget{};
bool autoPending{};
int snippetPending = -1;
size_t snippetPendingLength{};
HWND snippetPendingEditor{};
bool snippetTyping{};
HWND snippetPopup{}, snippetTarget{}, snippetFocus{};
uint64_t snippetPopupVersion{};
size_t snippetPrefixLength{}, snippetSelection{};
bool snippetNavigated{};
int snippetRowHeight{};
UINT snippetDpi{96};
RECT snippetPopupAnchor{};
bool snippetCaretPending{};
std::optional<RECT> snippetCaret;
std::vector<size_t> snippetMatches;
std::array<bool, 256> snippetKeys{};
void matchSnippets(HWND, bool expandExact);
bool queueSnippet(HWND, size_t index, size_t length, HWND editor = nullptr);
void showSnippetPopup();
void placeSnippetPopup(RECT);
void hideSnippetPopup();
bool snippetPopupCurrent() const;
bool handleSnippetKey(DWORD, UINT);
void selectSnippet(size_t);
static LRESULT CALLBACK snippetPopupProc(HWND, UINT, WPARAM, LPARAM);
std::vector<INPUT> deferredTyping;
bool beginTypingDeferral(HWND, uint64_t, bool snippet = false);
void replayTyping();
static LRESULT CALLBACK windowProc(HWND, UINT, WPARAM, LPARAM);
static LRESULT CALLBACK keyProc(int, WPARAM, LPARAM);
static LRESULT keyboardAfterHooks(int, WPARAM, LPARAM, LRESULT);
static LRESULT CALLBACK mouseProc(int, WPARAM, LPARAM);
};
void showHistory(App &);
void showHistory(App &, bool visible = true);
void launchScreenshot(App &);
void launchScreenshotEditor(App &, const Clip &);
void refreshHistory(App &);
void showSettings(App &);
void showSettings(App &, bool visible = true);
void showLogs(App &);
void showSnippets(App &);
void showShortcuts(App &);
void showEntry(App &, const std::wstring &);
+100
View File
@@ -0,0 +1,100 @@
// History UI has no clipboard listener or hooks. The resident remains the only writer.
namespace {
constexpr ULONG_PTR HistoryRequest = 0x4b530200;
constexpr UINT HistoryHostShow = WM_APP + 71, HistoryHostRefresh = WM_APP + 72;
constexpr wchar_t HistoryHostClass[] = L"KSwitcher.HistoryHost";
using HistoryJson = nlohmann::json;
HWND historyController(HANDLE process)
{
if (!process || WaitForSingleObject(process, 0) == WAIT_OBJECT_0) return nullptr;
auto window = FindWindowW(HistoryHostClass, nullptr);
DWORD pid{}; GetWindowThreadProcessId(window, &pid);
return pid == GetProcessId(process) ? window : nullptr;
}
}
void App::openHistoryProcess()
{
auto foreground = GetForegroundWindow();
if (foreground != window && foreground != historyWindow) pasteTarget = foreground;
if (historyProcess && WaitForSingleObject(historyProcess, 0) == WAIT_OBJECT_0) {
CloseHandle(historyProcess); historyProcess = nullptr; historyWindow = nullptr;
}
if (historyProcess) {
if (auto host = historyController(historyProcess)) {
AllowSetForegroundWindow(GetProcessId(historyProcess));
PostMessageW(host, HistoryHostShow, 0, 0);
}
return;
}
auto path = executable();
auto command = L"\"" + path + L"\" --history-host=" + std::to_wstring(reinterpret_cast<uintptr_t>(window));
STARTUPINFOW startup{sizeof(startup)};
startup.dwFlags = STARTF_USESHOWWINDOW; startup.wShowWindow = SW_HIDE;
PROCESS_INFORMATION child{};
if (!CreateProcessW(path.c_str(), command.data(), nullptr, nullptr, FALSE, CREATE_NO_WINDOW,
nullptr, std::filesystem::path(path).parent_path().c_str(), &startup, &child))
throw std::runtime_error("Cannot start clipboard history window");
CloseHandle(child.hThread);
historyProcess = child.hProcess;
AllowSetForegroundWindow(child.dwProcessId);
}
void App::refreshHistoryProcess()
{
if (auto host = historyController(historyProcess)) PostMessageW(host, HistoryHostRefresh, 0, 0);
}
void App::remoteHistory(const char *action, const std::wstring &id, bool plain)
{
if (!historyService || !configurationPeer(historyService, L"KSwitcher.exe"))
throw std::runtime_error("KSwitcher has stopped. Reopen clipboard history.");
auto text = HistoryJson{{"action", action}, {"id", utf8(id)}, {"plain", plain}}.dump();
if (std::string_view(action) == "paste") {
DWORD residentPid{}; GetWindowThreadProcessId(historyService, &residentPid);
AllowSetForegroundWindow(residentPid);
}
COPYDATASTRUCT message{HistoryRequest, DWORD(text.size()), text.data()};
DWORD_PTR result{};
if (!SendMessageTimeoutW(historyService, WM_COPYDATA, reinterpret_cast<WPARAM>(window),
reinterpret_cast<LPARAM>(&message), SMTO_ABORTIFHUNG | SMTO_ERRORONEXIT,
15000, &result) || result != 1)
throw std::runtime_error("KSwitcher could not complete the history request. Reopen history and retry.");
}
LRESULT App::historyRequest(HWND sender, const COPYDATASTRUCT &message)
{
DWORD pid{}; GetWindowThreadProcessId(sender, &pid);
if (!historyProcess || pid != GetProcessId(historyProcess) ||
!configurationPeer(sender, L"KSwitcher.exe") || message.cbData > 4096 ||
!message.cbData || !message.lpData || installingUpdate) return FALSE;
try {
const auto *first = static_cast<const char *>(message.lpData);
auto job = HistoryJson::parse(first, first + message.cbData);
auto action = job.at("action").get<std::string>();
auto id = wide(job.at("id").get<std::string>());
if (id.size() > 128) return FALSE;
if (action == "ready") {
auto ui = reinterpret_cast<HWND>(uintptr_t(std::stoull(id)));
DWORD uiPid{}; GetWindowThreadProcessId(ui, &uiPid);
wchar_t name[64]{}; GetClassNameW(ui, name, 64);
if (uiPid != pid || std::wstring(name) != L"KSwitcher.History") return FALSE;
historyWindow = ui;
} else if (action == "paste") {
if (busy || !history) return FALSE;
pasteEntry(id, job.at("plain").get<bool>());
} else if (action == "ocr") {
if (busy || !history) return FALSE;
recognizeEntry(id);
} else if (action == "clear") clearHistory();
else if (action == "pin" || action == "remove") {
if (busy || !history) return FALSE;
if (action == "pin") history->pin(id); else history->remove(id);
} else return FALSE;
return TRUE;
} catch (...) { return FALSE; }
}
void App::saveHistoryView(const std::wstring &selection, const std::wstring &top, const std::wstring &query, bool pinnedOnly)
{
if (!historyService) return;
try {
auto text = HistoryJson{{"selection", utf8(selection)}, {"top", utf8(top)}, {"query", utf8(query)}, {"pinnedOnly", pinnedOnly}, {"computers", historyComputers}}.dump();
atomicWrite(data / L"history-view.bin", protect(Bytes(text.begin(), text.end())));
} catch (...) {} // Closing the UI must still release its process if state cannot be saved.
}
+116
View File
@@ -0,0 +1,116 @@
// Included by app.cpp; all App mutations happen on its window thread.
Bytes App::syncConflictData()
{
if (loadSyncConfig(data) != conflictConnection) return encodeSyncConflicts({});
return encodeSyncConflicts(pendingSyncConflicts);
}
std::wstring App::syncCommand(int action, const std::vector<SyncChoice> &choices)
{
if (action == 0) return syncStatus;
if (syncRunning) return L"Synchronization is already running.";
auto c = loadSyncConfig(data);
if (!c.enabled) return syncStatus = L"Synchronization is disabled.";
validateSyncConfig(c);
if (action == 4 && (c != conflictConnection || choices.empty()))
throw std::runtime_error("Connection changed or no choices selected. Sync again.");
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.
if (syncWorker.joinable()) syncWorker.join();
syncConnection = c;
syncSnapshot = encodeSettings(settings);
auto snapshot = settings;
auto availableLayouts = layouts;
syncRunning = true;
syncDone = false;
syncStatus = action == 2 ? L"Downloading remote settings from OpenBao..." :
action == 3 ? L"Uploading local settings to OpenBao..." : L"Connecting to OpenBao...";
nextSync = GetTickCount64() + SyncIntervalMs;
try
{
syncWorker = std::thread([this, c, snapshot, availableLayouts, action, choices] {
SyncResult result;
try
{
auto provider = makeSyncProvider(c);
// An explicit choice also repairs a missing/corrupt local baseline.
auto base = action == 1 || action == 4 ? loadSyncBase(data, c) : Bytes{};
result = synchronize(*provider, base, sharedSyncSettings(snapshot),
action == 2 ? SyncAction::Download : action == 3 ? SyncAction::Upload : SyncAction::Automatic,
[&](const Bytes &b) { restoreSyncSettings(b, snapshot, availableLayouts, c.keepLocalLayouts); }, choices,
c.keepLocalLayouts);
if (result.success && c.keepLocalLayouts)
result.status += L" Partial sync: this PC's layouts and their shortcuts were kept; server layouts were preserved.";
}
catch (const nlohmann::json::exception &) { result.status = L"Invalid sync data or server response."; }
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);
syncDone.store(true);
});
}
catch (...) { syncRunning = false; Bytes().swap(syncSnapshot); syncResult = {}; throw; }
return syncStatus;
}
void App::pollSync()
{
try
{
if (syncRunning && syncDone.load())
{
if (busy) return;
syncWorker.join();
// Own completed payloads locally so every return/exception releases them.
auto result = std::exchange(syncResult, SyncResult{});
auto snapshot = std::exchange(syncSnapshot, Bytes{});
syncRunning = false;
syncDone = false;
syncStatus = result.status;
if (result.success)
{
if (loadSyncConfig(data) != syncConnection || encodeSettings(settings) != snapshot)
{
syncStatus = L"Local settings or connection changed during sync. Checking again shortly.";
nextSync = GetTickCount64() + 2000;
return;
}
auto next = restoreSyncSettings(result.settings, settings, layouts, syncConnection.keepLocalLayouts);
if (encodeSettings(next) != snapshot)
{
atomicWrite(data / L"settings-before-sync.bin", protect(snapshot));
// A restored OCR configuration can precede the optional local installation.
applySettings(next, false);
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);
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);
conflictConnection = syncConnection;
SYSTEMTIME time{}; GetLocalTime(&time);
wchar_t stamp[32]{};
swprintf(stamp, std::size(stamp), L" (%02u:%02u:%02u)", time.wHour, time.wMinute, time.wSecond);
syncStatus += stamp;
}
// Back off after errors (including bad credentials) instead of repeatedly
// hitting userpass lockout. The explicit Sync button remains available.
syncRetryAfter = result.success ? 0 : GetTickCount64() + 15 * 60000;
nextSync = result.success ? GetTickCount64() + SyncIntervalMs : syncRetryAfter;
}
if (!syncRunning && GetTickCount64() >= nextSync)
{
nextSync = GetTickCount64() + SyncIntervalMs;
if (loadSyncConfig(data).enabled) syncCommand(1);
}
}
catch (const nlohmann::json::exception &) { syncStatus = L"Invalid local sync configuration or cached profile."; nextSync = GetTickCount64() + SyncIntervalMs; }
catch (const std::exception &e) { syncStatus = wide(e.what()); nextSync = GetTickCount64() + SyncIntervalMs; }
}
+215 -43
View File
@@ -42,73 +42,160 @@ bool safeAutoWord(const std::wstring &word, int minimumLength)
auto small = lower(word);
return word == small || (word.substr(1) == small.substr(1) && word != upper(word));
}
std::string_view AutoCorrector::Dictionary::word(size_t index) const
namespace {
constexpr size_t DictionaryBlockWords = 32;
}
std::string_view AutoCorrector::Dictionary::word(size_t block) const
{
auto start = offsets[index];
auto end = bytes.find('\t', start);
return std::string_view(bytes).substr(start, end - start);
size_t start = offsets[block];
// Every block starts with a complete word (zero shared prefix).
size_t length = static_cast<unsigned char>(bytes[start++]) & 15;
if (length == 15) length = static_cast<unsigned char>(bytes[start++]);
return std::string_view(bytes).substr(start, length);
}
AutoCorrector::Dictionary::Dictionary(const std::filesystem::path &path)
{
auto raw = readFile(path, 2 * 1024 * 1024);
bytes.assign(raw.begin(), raw.end());
for (size_t start = 0; start < bytes.size();)
const auto raw = readFile(path, 2 * 1024 * 1024);
const std::string_view source(reinterpret_cast<const char *>(raw.data()), raw.size());
std::vector<uint32_t> lines;
lines.reserve(std::count(source.begin(), source.end(), '\n') + 1);
for (size_t start = 0; start < source.size();)
{
auto end = bytes.find('\n', start);
if (end == bytes.npos)
end = bytes.size();
auto tab = bytes.find('\t', start);
if (tab == bytes.npos || tab >= end || tab == start || tab - start > 160)
auto end = source.find('\n', start);
if (end == source.npos) end = source.size();
auto tab = source.find('\t', start);
if (tab == source.npos || tab >= end || tab == start || tab - start > 160)
throw std::runtime_error("Invalid auto-correction dictionary");
int rank{};
auto rankEnd = end > tab + 1 && bytes[end - 1] == '\r' ? end - 1 : end;
auto parse = std::from_chars(bytes.data() + tab + 1, bytes.data() + rankEnd, rank);
if (parse.ec != std::errc() || parse.ptr != bytes.data() + rankEnd || rank <= 0)
auto rankEnd = end > tab + 1 && source[end - 1] == '\r' ? end - 1 : end;
auto parse = std::from_chars(source.data() + tab + 1, source.data() + rankEnd, rank);
if (parse.ec != std::errc() || parse.ptr != source.data() + rankEnd || rank <= 0)
throw std::runtime_error("Invalid dictionary rank");
offsets.push_back((uint32_t)start);
lines.push_back(static_cast<uint32_t>(start));
start = end + 1;
}
if (offsets.empty())
throw std::runtime_error("Empty auto-correction dictionary");
for (size_t i = 1; i < offsets.size(); ++i)
if (word(i - 1) >= word(i))
throw std::runtime_error("Dictionary must be uniquely sorted");
offsets.shrink_to_fit();
bytes.shrink_to_fit();
if (lines.empty()) throw std::runtime_error("Empty auto-correction dictionary");
auto entry = [&](uint32_t offset) { return source.substr(offset, source.find('\t', offset) - offset); };
auto frequency = [&](uint32_t offset) {
int rank{};
auto start = offset + entry(offset).size() + 1;
std::from_chars(source.data() + start, source.data() + source.size(), rank);
return rank;
};
std::sort(lines.begin(), lines.end(), [&](uint32_t a, uint32_t b) {
return entry(a) == entry(b) ? frequency(a) < frequency(b) : entry(a) < entry(b);
});
lines.erase(std::unique(lines.begin(), lines.end(), [&](uint32_t a, uint32_t b) {
return entry(a) == entry(b);
}), lines.end());
offsets.reserve((lines.size() + DictionaryBlockWords - 1) / DictionaryBlockWords);
std::string_view previous;
// Allocate the packed payload exactly once. Geometric growth and a final
// shrink otherwise leave discarded buffers in the resident process heap.
size_t packedSize = 0;
for (size_t i = 0; i < lines.size(); ++i)
{
auto current = entry(lines[i]);
size_t prefix = 0;
if (i % DictionaryBlockWords != 0)
while (prefix < std::min(previous.size(), current.size()) && previous[prefix] == current[prefix]) ++prefix;
auto suffix = current.size() - prefix;
packedSize += 1 + (prefix >= 15) + (suffix >= 15) + suffix + 2;
auto rank = static_cast<uint32_t>(frequency(lines[i]));
if (rank >= 65535)
do { ++packedSize; rank >>= 7; } while (rank);
previous = current;
}
bytes.reserve(packedSize);
previous = {};
for (size_t i = 0; i < lines.size(); ++i)
{
auto current = entry(lines[i]);
size_t prefix = 0;
if (i % DictionaryBlockWords == 0)
offsets.push_back(static_cast<uint32_t>(bytes.size()));
else
while (prefix < std::min(previous.size(), current.size()) && previous[prefix] == current[prefix]) ++prefix;
size_t suffix = current.size() - prefix;
bytes.push_back(static_cast<char>((std::min(prefix, size_t(15)) << 4) | std::min(suffix, size_t(15))));
if (prefix >= 15) bytes.push_back(static_cast<char>(prefix));
if (suffix >= 15) bytes.push_back(static_cast<char>(suffix));
bytes.append(current.substr(prefix));
auto rank = static_cast<uint32_t>(frequency(lines[i]));
auto shortRank = std::min(rank, uint32_t(65535));
bytes.push_back(static_cast<char>(shortRank & 255));
bytes.push_back(static_cast<char>(shortRank >> 8));
if (rank >= 65535)
do {
auto next = rank & 127;
rank >>= 7;
bytes.push_back(static_cast<char>(next | (rank ? 128 : 0)));
} while (rank);
previous = current;
}
}
int AutoCorrector::Dictionary::rank(const std::wstring &text) const
{
auto key = utf8(lower(text));
size_t begin = 0, end = offsets.size();
// Find the last block whose first word is <= the search key.
while (begin < end)
{
auto mid = begin + (end - begin) / 2;
if (word(mid) < key)
begin = mid + 1;
else
end = mid;
if (word(mid) <= key) begin = mid + 1;
else end = mid;
}
if (begin == offsets.size() || word(begin) != key)
return 0;
auto start = offsets[begin] + word(begin).size() + 1;
int rank{};
std::from_chars(bytes.data() + start, bytes.data() + bytes.size(), rank);
return rank;
if (!begin) return 0;
size_t block = begin - 1;
size_t at = offsets[block];
const size_t limit = begin < offsets.size() ? offsets[begin] : bytes.size();
char decoded[160]; // The loader validates lengths; lookup never allocates a decoded word.
while (at < limit)
{
auto lengths = static_cast<unsigned char>(bytes[at++]);
size_t prefix = lengths >> 4, suffix = lengths & 15;
if (prefix == 15) prefix = static_cast<unsigned char>(bytes[at++]);
if (suffix == 15) suffix = static_cast<unsigned char>(bytes[at++]);
memcpy(decoded + prefix, bytes.data() + at, suffix);
at += suffix;
uint32_t rank = static_cast<unsigned char>(bytes[at]) |
(uint32_t(static_cast<unsigned char>(bytes[at + 1])) << 8);
at += 2;
if (rank == 65535) {
rank = 0;
uint32_t shift = 0;
unsigned char part;
do {
part = static_cast<unsigned char>(bytes[at++]);
rank |= uint32_t(part & 127) << shift;
shift += 7;
} while (part & 128);
}
auto compared = std::string_view(decoded, size_t(prefix) + suffix).compare(key);
if (compared == 0) return static_cast<int>(rank);
if (compared > 0) return 0;
}
return 0;
}
void AutoCorrector::configure(const std::filesystem::path &folder, const std::vector<std::wstring> &wanted)
{
if (languages == wanted)
return;
std::map<std::wstring, Dictionary> next;
std::map<std::wstring, std::shared_ptr<const Dictionary>> next;
for (auto &lang : wanted)
{
if (lang != L"en" && lang != L"uk" && lang != L"ru")
throw std::runtime_error("Unsupported dictionary language");
next.emplace(lang, Dictionary(folder / (lang + L".txt")));
next.emplace(lang, std::make_shared<const Dictionary>(folder / (lang + L".txt")));
}
dictionaries = std::move(next);
languages = wanted;
}
void AutoCorrector::shareDictionaries(const AutoCorrector &source)
{
dictionaries = source.dictionaries;
languages = source.languages;
}
void AutoCorrector::reject(std::wstring word, const Settings &settings)
{
while (!word.empty() && openingDelimiter(word.front(), settings))
@@ -123,7 +210,7 @@ size_t AutoCorrector::memoryBytes() const
{
size_t bytes = 0;
for (auto &[language, d] : dictionaries)
bytes += d.bytes.capacity() + d.offsets.capacity() * sizeof(uint32_t);
bytes += d->bytes.capacity() + d->offsets.capacity() * sizeof(uint32_t);
return bytes;
}
std::optional<AutoSuggestion> AutoCorrector::suggest(const std::wstring &typed, int source,
@@ -143,7 +230,11 @@ std::optional<AutoSuggestion> AutoCorrector::suggest(const std::wstring &typed,
suffix.insert(suffix.begin(), word.back());
word.pop_back();
}
if (!safeAutoWord(word, settings.autoMinLength) || suffix.size() > 6 || rejected.contains(lower(word)))
// Punctuation keys can be letters in the target layout (for example ',' -> 'б').
// Validate letters and capitalization after conversion, not before it.
if (settings.autoMinLength < 1 || settings.autoMinLength > 32 ||
word.size() < (size_t)settings.autoMinLength || word.size() > 32 ||
suffix.size() > 6 || rejected.contains(lower(word)))
return {};
for (auto &never : settings.autoNever)
if (lower(never) == lower(word))
@@ -151,12 +242,44 @@ std::optional<AutoSuggestion> AutoCorrector::suggest(const std::wstring &typed,
auto sourceLanguage = layoutLanguage(layouts[source]);
if (!dictionaries.contains(sourceLanguage))
return {};
const std::wstring alphabet = sourceLanguage == L"en" ? L"abcdefghijklmnopqrstuvwxyz" :
sourceLanguage == L"uk" ? L"абвгґдеєжзиіїйклмнопрстуфхцчшщьюя" : L"абвгдеёжзийклмнопрстуфхцчшщъыьэюя";
auto small = lower(word);
bool inCycle = std::find(settings.cycle.begin(), settings.cycle.end(), layouts[source].id) != settings.cycle.end();
bool sameLanguage = safeAutoWord(word, settings.autoMinLength) &&
small.find_first_not_of(alphabet) == small.npos && inCycle;
auto sameLayout = [&](std::wstring text)
{
if (word != small) text[0] = upper(text.substr(0, 1))[0];
return AutoSuggestion{source, source, prefix + text + suffix};
};
if (settings.autoReplace && inCycle)
{
auto replacement = settings.autoReplacements.find(small);
if (replacement != settings.autoReplacements.end() &&
(word == small || (word.substr(1) == small.substr(1) && word != upper(word))) &&
validAutoReplacementSource(small, alphabet) &&
validAutoCorrection(replacement->second, alphabet))
return sameLayout(replacement->second);
}
auto personal = [&](const std::wstring &language, const std::wstring &candidate)
{
auto found = settings.autoWords.find(language);
return found != settings.autoWords.end() &&
std::any_of(found->second.begin(), found->second.end(), [&](const std::wstring &known)
{ return lower(known) == lower(candidate); });
};
// Valid words from either selected language are never changed.
for (auto &[language, d] : dictionaries)
if (d.rank(word))
if (d->rank(word) || personal(language, word))
return {};
std::optional<AutoSuggestion> candidate;
int matches = 0;
std::optional<std::wstring> matchedText;
auto priority = [&](int layout)
{
auto language = layoutLanguage(layouts[layout]);
return std::find(settings.autoLanguages.begin(), settings.autoLanguages.end(), language) - settings.autoLanguages.begin();
};
for (int i = 0; i < (int)layouts.size(); ++i)
{
if (i == source || !layouts[i].convertible ||
@@ -168,15 +291,64 @@ std::optional<AutoSuggestion> AutoCorrector::suggest(const std::wstring &typed,
auto converted = convert(word, layouts[source], layouts[i], true);
if (converted == word || !safeAutoWord(converted, settings.autoMinLength))
continue;
int rank = dictionary->second.rank(converted);
int rank = personal(dictionary->first, converted) ? 1 : dictionary->second->rank(converted);
if (!rank)
continue;
if (++matches > 1)
if (matchedText && *matchedText != converted)
return {};
if (settings.autoMaxRank == 0 || rank <= settings.autoMaxRank)
matchedText = converted;
if ((settings.autoMaxRank == 0 || rank <= settings.autoMaxRank) &&
(!candidate || priority(i) < priority(candidate->target)))
candidate = AutoSuggestion{source, i, prefix + converted + suffix};
}
return candidate;
if (candidate || matchedText || !settings.autoTypos || !sameLanguage || word.size() < 4)
return candidate;
// Generate only single edits. Stop at a second distinct dictionary hit,
// including rare hits: frequency limits must never hide ambiguity.
std::wstring found;
int foundRank = 0;
std::set<std::wstring> personalTargets;
if (auto it = settings.autoWords.find(sourceLanguage); it != settings.autoWords.end())
for (const auto &text : it->second) personalTargets.insert(lower(text));
auto consider = [&](const std::wstring &text)
{
if (text == small || text == found || text.size() < 4 || text.size() > 32) return true;
int rank = personalTargets.contains(text) ? 1 : dictionaries.at(sourceLanguage)->rank(text);
if (!rank) return true;
if (!found.empty()) return false;
found = text;
foundRank = rank;
return true;
};
for (size_t i = 0; i < small.size(); ++i)
{
auto text = small;
text.erase(i, 1);
if (!consider(text)) return {};
if (i + 1 < small.size())
{
text = small;
std::swap(text[i], text[i + 1]);
if (!consider(text)) return {};
}
for (wchar_t c : alphabet)
{
text = small;
text[i] = c;
if (!consider(text)) return {};
}
}
if (small.size() < 32)
for (size_t i = 0; i <= small.size(); ++i)
for (wchar_t c : alphabet)
{
auto text = small;
text.insert(i, 1, c);
if (!consider(text)) return {};
}
if (!found.empty() && (settings.autoMaxRank == 0 || foundRank <= settings.autoMaxRank))
return sameLayout(found);
return {};
}
int matchAutoInput(const Run &run, const std::wstring &text, const std::vector<Layout> &layouts,
const Layout &physical, const Settings &settings)
Loaded 100 of 215 files, more files were not shown because too many files have changed in this diff. Show more