Compare commits

...
26 changed files with 4226 additions and 33 deletions

No files matched your search

+6
View File
@@ -33,3 +33,9 @@ Wiki: https://gitea.hrynco.com/hrynco/hrynco-kswitcher/wiki
Updating only the release notes or changing only the Wiki version number does
not satisfy this rule; the current functionality must also be reviewed.
## 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.
+18 -2
View File
@@ -1,10 +1,26 @@
cmake_minimum_required(VERSION 3.24)
project(KSwitcher VERSION 0.6.76 LANGUAGES CXX RC)
project(KSwitcher VERSION 0.6.77 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_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)
@@ -173,7 +189,7 @@ 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 KSwitcherConfig KSwitcherScreenshot 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")
+56
View File
@@ -0,0 +1,56 @@
# Оформлення KSwitcher Files
Far Manager є зразком для оформлення файлового менеджера. Основне вікно використовує системний заголовок Windows. Панелі, меню й діалоги операцій використовують моноширинний шрифт менеджера та оформлення в стилі Far.
## Панелі
- Після F5 або F6 успішно оброблені елементи коротко підсвічуються в панелі, що показує папку призначення. Підсвітка тексту відрізняється від курсора й не переносить фокус.
- У широкому режимі ширина під вирівняні розширення залежить від найдовшого розширення в поточній панелі. За вузької панелі залишай місце для імені та колонки Size.
- Бірюзовий курсор поточного рядка видно лише в активній панелі. Кожна панель зберігає свою позицію курсора.
- Поточний шлях відображається по центру верхньої подвійної лінії панелі. Лінію видно з обох боків напису. Бірюзове тло активної панелі охоплює лише напис шляху з невеликими відступами. Довгий шлях скорочується в межах панелі.
## Спільні елементи діалогів
- Enter активує кнопку у фокусі. Для кнопок із власним малюванням використовуй спільний обробник `dialogButtonEnter`. У підтвердженні скасування початковий фокус стоїть на No.
- При перенесенні вікна між моніторами з різним DPI оновлюй шрифт, висоту рядків панелей і компонування відкритого прогресу копіювання. Перевір перенесення під час активного копіювання.
- Для власних діалогів використовуй подвійну рамку з одним заголовком у верхній лінії. Звичайний діалог має сіре тло, попередження — темно-червоне тло та білий текст.
- У ресурсі чи виклику задавай назву кнопки й клавішу доступу. Спільна функція `paintDialogButton` центрує текст, підсвічує фокус і додає фігурні дужки для дії за замовчуванням та квадратні для решти кнопок.
- Нижні секції з кнопками у Copy, Warning і вікні прогресу мають однакову висоту, прив’язану до висоти рядка шрифту. Використовуй `layoutDialogButtons` для такої секції та `centerDialogRow` для інших рядків між роздільниками. Межі секції визначають положення рядка.
- Поля вводу й випадаючі списки використовують бірюзове тло палітри менеджера. Текст і стрілку вирівнюй вертикально по центру. Прапорці в діалогах операцій використовують позначки `[ ]` та `[x]` і видимий клавіатурний фокус.
- Повторно використовуй спільне оформлення. Текст, дії та стан операції передавай із коду конкретної операції. Наприклад, Warning повертає відповідь і стан прапорця, а копіювання вирішує, чи застосувати відповідь до наступних конфліктів.
## Діалог Copy
- Вирівнюй підпис Existing files по вертикальному центру списку. Ширше поле списку починається ближче до підпису, а його правий край збігається з правим краєм поля імені.
- Зберігай однакові проміжки від верхнього роздільника до поля імені й до списку. Підпис, поля та прапорець мають читатися повністю.
- Відступ після прапорця до нижнього роздільника дорівнює відступу від верхнього роздільника до списку.
- Кнопки Copy та Cancel стоять по центру компактної нижньої секції. Висота кнопок має вміщати весь текст поточного шрифту.
## Вікно прогресу копіювання
- F6 використовує спільне оформлення діалогів Copy і прогресу. Заголовок підтвердження — Rename/Move, кнопка за замовчуванням — Rename, заголовок прогресу — Move.
- Поле призначення F6 початково містить шлях іншої панелі. Для одного елемента просте ім’я задає перейменування в поточній папці. Для кількох елементів діалог показує їх кількість і приймає один шлях до папки.
- Секції поточного файла й загального прогресу мають однаковий вертикальний відступ від вмісту до верхньої та нижньої ліній. Cancel стоїть по центру спільної нижньої секції.
- Обидві шкали займають усю ширину вмісту. Відсотки показуються по центру шкали, а її висота враховує поточний шрифт.
## Попередження про конфлікт
- Після розрахунку остаточної висоти центр Warning збігається з центром вікна прогресу.
- Remember choice стоїть по вертикальному центру власної секції. Кнопки Overwrite, Skip і Cancel стоять по центру нижньої секції.
- Tab проходить Remember choice, Overwrite, Skip і Cancel. Стрілки вліво та вправо циклічно перемикають три кнопки в порядку їх розташування.
- Стрілки вгору й вниз переходять між сусідніми секціями з інтерактивними елементами. У Warning стрілка вгору з ряду кнопок переводить фокус на Remember choice, а вниз із прапорця — на Overwrite.
## Видалення
- F8 показує сіре підтвердження з дією Move. Shift+Delete і підтвердження непорожньої папки мають темно-червоне тло. Вікно прогресу видалення має сіре тло, як у Copy.
- F8 переміщує підтверджені елементи в кошик Windows. Shift+Delete видаляє підтверджене дерево від вкладених елементів до батьківської папки. Прогрес і підсумок окремо показують успішні, пропущені й невдалі дії.
- Cancel і Esc у вікні прогресу відкривають спільне червоне підтвердження скасування. Yes зупиняє операцію, а No або Esc у підтвердженні повертає до прогресу. Початковий фокус стоїть на No.
## Перевірка змін
Відкрий Copy і Warning в одній збірці та порівняй висоту нижніх секцій, центрування, повноту підписів і видимість фокуса. Перевір Tab, стрілки й Esc. Після узгодження нового правила онови відповідний розділ цього документа.
Binary file not shown.
+12
View File
@@ -1,5 +1,17 @@
# Latest Windows release
## v0.6.77 — 2026-11-13
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.
**Українською.** Редактор скріншотів зберігає типове згладжування олівця на
цьому ПК та відновлює його після перезапуску. Змініть повзунок без виділеного
штриха олівця або натисніть **Apply to all**, щоб запам’ятати значення для
нових штрихів. Зміна виділеного штриха впливає лише на нього.
## v0.6.76 — 2026-11-13
Incoming LAN files suggest their original filename when saved. KSwitcher shows
+1 -1
View File
@@ -1 +1 @@
1f3b641d5b96f289d2d19792876545920f33bd702735d02912abd0218c5c9365 KSwitcher-win-x64.zip
51d95639527590b36453a5ef485532a7d2135950222fa197eb431a5309f1a45c KSwitcher-win-x64.zip
+21 -21
View File
@@ -1,86 +1,86 @@
{
"schema": 1,
"version": "0.6.77",
"files": [
{
"size": 2249216,
"path": "KSwitcher.exe",
"sha256": "a32c5b1b0d86b44d3e4a0b2951f47f04f25a959fdf1f30f0eedd30422a77b0b6"
"size": 2249216,
"sha256": "f1c79d337d746efcda03946cc833b7b5588ef4e739f1dff7d3afa15d0465469f"
},
{
"size": 2251776,
"path": "KSwitcher.Config.exe",
"sha256": "175939293f867aa38085578dcad6a7a264f801368ec3c63b71faf92c2e5e1731"
"size": 2251776,
"sha256": "016edc389007e0d15ee365fff7115fc91a820b46080cb39a3fb362f3b463bae7"
},
{
"size": 3246592,
"path": "KSwitcher.Screenshot.exe",
"sha256": "b40a1e4c7553a34d83c5db22303f1815224870dfcf106d679be085c82e807dd3"
"size": 3247616,
"sha256": "c8ef070ad01acc410802c62e0df6a96cb299fe036fe8e99f0cf6a398b7324ec6"
},
{
"size": 4401,
"path": "LICENSE",
"size": 4401,
"sha256": "d3c81140cce01c26bf6cbf52aa40e0825e1fa35b9a91d1387170a65d2597082c"
},
{
"size": 1075,
"path": "json-LICENSE.txt",
"size": 1075,
"sha256": "c0d068392ea65358b798b8c165103560f06e9e3b38c4ab4e2d8810a7b931af86"
},
{
"size": 93885,
"path": "README.md",
"size": 93885,
"sha256": "df6d40d0eb66ad7da8db1cffe6bd211de70dfee474cc228e041c821b3940718b"
},
{
"size": 11418,
"path": "LAN-PREVIEW.md",
"size": 11418,
"sha256": "2c13d3ed4761e679c352fb1ade24a1ec3060ad2862376ede801cdee5fcc3bada"
},
{
"size": 682867,
"path": "dictionaries/en.txt",
"size": 682867,
"sha256": "5f267c141c3d6837870e1c098f86b89a113ba300343119432644ead88f720c21"
},
{
"size": 20138,
"path": "dictionaries/LICENSE-CC-BY-SA-4.0.txt",
"size": 20138,
"sha256": "28a9529c7d0bb4dc51f4bf5c116a3d16ef247a052f7591466768ddf563fd1cf5"
},
{
"size": 2230,
"path": "dictionaries/README.md",
"size": 2230,
"sha256": "2df17fe4e17d527c67ab57269b07dce59a49c1fb128c429a1408decbb47109ec"
},
{
"size": 1060364,
"path": "dictionaries/ru.txt",
"size": 1060364,
"sha256": "9ef922c7699569304a428f6aba0fd13a21341c0d0cb9561f37ef83c41577a28b"
},
{
"size": 1279,
"path": "dictionaries/sources.json",
"size": 1279,
"sha256": "d6e11a3aaba22a8a7eaba19f778d8a5bd66653ff306810e85a1b0e7fff3a6b86"
},
{
"size": 898248,
"path": "dictionaries/uk.txt",
"size": 898248,
"sha256": "caeb097749d4afc84b68ad96b3d7dfb6a6569561cd32ccce0bee94a13b43ba86"
},
{
"size": 993,
"path": "tools/README.txt",
"size": 993,
"sha256": "a34b5161a8315af037e1ec71d816bfc688f7c364141d4ee2347cf0515ef0c82a"
},
{
"size": 134656,
"path": "tools/StayHere.exe",
"size": 134656,
"sha256": "6ef5cf64708d62b7fc63c3c793cc99188a513433ff4774cae01dc014f6664376"
},
{
"size": 2796,
"path": "tools/StayHere-README.md",
"size": 2796,
"sha256": "ba214e2aca7405e698b3ab21ceb1f41ce088edd996b3e30e4a9d58ff496f9413"
}
],
"version": "0.6.76"
"schema": 1
}
+8
View File
@@ -46,6 +46,14 @@ try {
& $compiler -mwindows -municode -static -s '-Wl,--gc-sections' build/lan_channel.o build/lan_input.o build/lan.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 -luser32 -lgdi32 -lcomctl32 -lshell32 -lcomdlg32 -ladvapi32 -lole32 -luuid -o $filesPath
if ($LASTEXITCODE) { throw 'File manager build failed' }
Copy-Item -LiteralPath $filesPath -Destination build/release/KSwitcher.Files.exe -Force
if ($Test) {
& $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' }
+4 -1
View File
@@ -6,6 +6,9 @@ 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
@@ -16,7 +19,7 @@ Copy-Item -LiteralPath (Join-Path $root 'resources\portable-tools-README.txt') -
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','KSwitcher.Config.exe','KSwitcher.Screenshot.exe','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 $_ }
$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()}
+3 -3
View File
@@ -9,8 +9,8 @@ IDC_SCREENSHOT_PENCIL CURSOR "icons/lucide/pencil.cur"
1 RT_MANIFEST "app.manifest"
201 RCDATA "update.ps1"
1 VERSIONINFO
FILEVERSION 0,6,76,0
PRODUCTVERSION 0,6,76,0
FILEVERSION 0,6,77,0
PRODUCTVERSION 0,6,77,0
FILEOS VOS_NT_WINDOWS32
FILETYPE VFT_APP
BEGIN
@@ -20,7 +20,7 @@ BEGIN
BEGIN
VALUE "FileDescription", "KSwitcher — native keyboard and clipboard utility"
VALUE "ProductName", "KSwitcher"
VALUE "FileVersion", "0.6.76"
VALUE "FileVersion", "0.6.77"
END
END
BLOCK "VarFileInfo"
+107
View File
@@ -0,0 +1,107 @@
#include <windows.h>
1 ICON "kswitcher-files.ico"
1 RT_MANIFEST "app.manifest"
101 DIALOGEX 0, 0, 256, 141
STYLE DS_SETFONT | DS_MODALFRAME | DS_CENTER | WS_POPUP | WS_CAPTION | WS_SYSMENU
CAPTION "Wide panel mode"
FONT 9, "Segoe UI"
BEGIN
GROUPBOX "Panel columns — Name, Size", -1, 8, 6, 240, 59
LTEXT "Size column &width (6-16 characters)", -1, 17, 22, 166, 10
EDITTEXT 201, 196, 18, 41, 14, ES_NUMBER | ES_AUTOHSCROLL
AUTOCHECKBOX "&Align file extensions", 202, 17, 42, 104, 12
AUTOCHECKBOX "Align &folder extensions", 204, 130, 42, 108, 12
GROUPBOX "Status line columns", -1, 8, 72, 240, 39
AUTOCHECKBOX "Show &details", 203, 17, 83, 94, 11
AUTOCHECKBOX "Name", 205, 17, 96, 47, 11
AUTOCHECKBOX "Size", 206, 72, 96, 42, 11
AUTOCHECKBOX "Date", 207, 128, 96, 45, 11
AUTOCHECKBOX "Time", 208, 185, 96, 45, 11
DEFPUSHBUTTON "OK", IDOK, 139, 119, 50, 14
PUSHBUTTON "Cancel", IDCANCEL, 196, 119, 50, 14
END
102 DIALOGEX 0, 0, 350, 114
STYLE DS_SETFONT | DS_CENTER | WS_POPUP
CAPTION "Copy"
FONT 10, "Consolas"
BEGIN
LTEXT "Copy to", 210, 14, 15, 322, 12, SS_PATHELLIPSIS
CONTROL "", 211, "Edit", ES_MULTILINE | ES_AUTOHSCROLL | WS_TABSTOP, 14, 29, 322, 18
LTEXT "Existing files:", 212, 14, 61, 122, 14, SS_CENTERIMAGE
COMBOBOX 213, 140, 61, 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, 81, 322, 14
CONTROL "&Copy", IDOK, "Button", BS_OWNERDRAW | WS_TABSTOP, 87, 96, 86, 18
CONTROL "&Cancel", IDCANCEL, "Button", BS_OWNERDRAW | WS_TABSTOP, 177, 96, 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, 106
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 "", 234, 12, 81, 1, 1
CONTROL "Remember choice", 232, "Button", BS_OWNERDRAW | WS_TABSTOP, 12, 57, 336, 18
CONTROL "&Overwrite", IDYES, "Button", BS_OWNERDRAW | WS_TABSTOP, 42, 89, 110, 18
CONTROL "&Skip", IDNO, "Button", BS_OWNERDRAW | WS_TABSTOP, 158, 89, 68, 18
CONTROL "&Cancel", IDCANCEL, "Button", BS_OWNERDRAW | WS_TABSTOP, 232, 89, 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
Binary file not shown.

After

Width:  |  Height:  |  Size: 34 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();
+15
View File
@@ -2769,6 +2769,7 @@ void App::trayMenu()
AppendMenuW(menu, MF_STRING, 10, L"Clipboard &history");
AppendMenuW(menu, MF_STRING, 24, L"&Capture screen region…");
AppendMenuW(menu, MF_STRING, 11, L"&Settings…");
AppendMenuW(menu, MF_STRING, 25, L"&Files…");
SetMenuDefaultItem(menu, 11, FALSE);
AppendMenuW(menu, MF_STRING, 12, L"S&nippets…");
AppendMenuW(menu, MF_STRING, 13, L"&Keyboard shortcuts…");
@@ -2821,6 +2822,20 @@ void App::trayMenu()
}
switch (cmd)
{
case 25:
{
auto path = std::filesystem::path(executable()).parent_path() / L"KSwitcher.Files.exe";
auto command = L"\"" + path.wstring() + L"\"";
STARTUPINFOW startup{sizeof(startup)};
PROCESS_INFORMATION process{};
if (!CreateProcessW(path.c_str(), command.data(), nullptr, nullptr, FALSE, 0, nullptr,
path.parent_path().c_str(), &startup, &process))
throw std::runtime_error(utf8(lastError(L"Cannot start KSwitcher Files")));
AllowSetForegroundWindow(process.dwProcessId);
CloseHandle(process.hThread);
CloseHandle(process.hProcess);
break;
}
case 24:
dispatch(Screenshot);
break;
+3250
View File
File diff suppressed because it is too large. Load diff
+110
View File
@@ -0,0 +1,110 @@
#pragma once
#include <mutex>
#include <string>
#include <thread>
#include <vector>
// The reader keeps draining the output pipe while ClosePseudoConsole shuts down.
class FilesTerminal
{
public:
~FilesTerminal() { close(); }
bool start(const wchar_t *directory, COORD size)
{
if(alive()) return true;
close();
pending.clear();
HANDLE inputRead{}, outputWrite{};
if(!CreatePipe(&inputRead,&input,nullptr,0)) return false;
if(!CreatePipe(&output,&outputWrite,nullptr,0))
{
CloseHandle(inputRead);
close();
return false;
}
const auto created=CreatePseudoConsole(size,inputRead,outputWrite,0,&console);
CloseHandle(inputRead);
CloseHandle(outputWrite);
if(FAILED(created)) { close(); return false; }
reader=std::thread([this]
{
char buffer[4096];
DWORD count{};
while(ReadFile(output,buffer,sizeof(buffer),&count,nullptr) && count)
{
std::lock_guard lock(mutex);
pending.append(buffer,count);
if(pending.size()>1024*1024) pending.erase(0,pending.size()-1024*1024);
}
});
SIZE_T bytes{};
InitializeProcThreadAttributeList(nullptr,1,0,&bytes);
std::vector<unsigned char> storage(bytes);
auto attributes=reinterpret_cast<LPPROC_THREAD_ATTRIBUTE_LIST>(storage.data());
if(!InitializeProcThreadAttributeList(attributes,1,0,&bytes)) { close(); return false; }
STARTUPINFOEXW startup{};
startup.StartupInfo.cb=sizeof(startup);
startup.lpAttributeList=attributes;
PROCESS_INFORMATION info{};
wchar_t command[]=L"powershell.exe -NoLogo -NoProfile -NoExit -Command \"Remove-Module PSReadLine -ErrorAction SilentlyContinue; function global:prompt { ([string][char]27) + '[?25l' }; Clear-Host\"";
wchar_t system[MAX_PATH]{};
const auto length=GetSystemDirectoryW(system,MAX_PATH);
const auto shell=std::wstring(system)+L"\\WindowsPowerShell\\v1.0\\powershell.exe";
const bool started=length && length<MAX_PATH &&
UpdateProcThreadAttribute(attributes,0,PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE,
console,sizeof(console),nullptr,nullptr) &&
CreateProcessW(shell.c_str(),command,nullptr,nullptr,FALSE,EXTENDED_STARTUPINFO_PRESENT,
nullptr,directory,&startup.StartupInfo,&info);
DeleteProcThreadAttributeList(attributes);
if(!started) { close(); return false; }
process=info.hProcess;
CloseHandle(info.hThread);
return true;
}
bool alive() const { return process && WaitForSingleObject(process,0)==WAIT_TIMEOUT; }
bool write(const std::wstring &value)
{
if(!alive()) return false;
const int length=WideCharToMultiByte(CP_UTF8,0,value.data(),static_cast<int>(value.size()),
nullptr,0,nullptr,nullptr);
if(!length) return false;
std::string utf8(length,'\0');
WideCharToMultiByte(CP_UTF8,0,value.data(),static_cast<int>(value.size()),
utf8.data(),length,nullptr,nullptr);
DWORD written{};
return WriteFile(input,utf8.data(),length,&written,nullptr) && written==utf8.size();
}
void resize(COORD size) { if(console) ResizePseudoConsole(console,size); }
std::string takeOutput()
{
std::lock_guard lock(mutex);
std::string result;
result.swap(pending);
return result;
}
void close()
{
if(input) CloseHandle(input);
if(console) ClosePseudoConsole(console);
if(reader.joinable()) reader.join();
if(output) CloseHandle(output);
if(process) CloseHandle(process);
input=output=process=nullptr;
console=nullptr;
}
private:
HPCON console{};
HANDLE input{},output{},process{};
std::thread reader;
std::mutex mutex;
std::string pending;
};
+186
View File
@@ -0,0 +1,186 @@
#pragma once
#include <WebView2.h>
#ifdef __MINGW32__
__CRT_UUID_DECL(ICoreWebView2_3, 0xA0D6DF20, 0x3B92, 0x416D, 0xAA, 0x0C, 0x43, 0x7A, 0x9C, 0x72, 0x78, 0x57)
__CRT_UUID_DECL(ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler, 0x4e8a3389, 0xc9d8, 0x4bd2, 0xb6, 0xb5, 0x12, 0x4f, 0xee, 0x6c, 0xc1, 0x4d)
__CRT_UUID_DECL(ICoreWebView2CreateCoreWebView2ControllerCompletedHandler, 0x6c4819f3, 0xc9b7, 0x4260, 0x81, 0x27, 0xc9, 0xf5, 0xbd, 0xe7, 0xf6, 0x8c)
__CRT_UUID_DECL(ICoreWebView2WebMessageReceivedEventHandler, 0x57213f19, 0x00e6, 0x49fa, 0x8e, 0x07, 0x89, 0x8e, 0xa0, 0x1e, 0xcb, 0xd2)
__CRT_UUID_DECL(ICoreWebView2NewWindowRequestedEventHandler, 0xd4c185fe, 0xc81c, 0x4989, 0x97, 0xaf, 0x2d, 0x3f, 0xa7, 0xab, 0x56, 0x51)
__CRT_UUID_DECL(ICoreWebView2NavigationStartingEventHandler, 0x9adbe429, 0xf36d, 0x432b, 0x9d, 0xdc, 0xf8, 0x88, 0x1f, 0xbd, 0x76, 0xe3)
#endif
#include <wrl/client.h>
#include <functional>
#include <memory>
#include <utility>
template<class Interface, class... Args>
class TerminalCallback final : public Interface
{
ULONG refs=1;
std::function<HRESULT(Args...)> invoke;
public:
explicit TerminalCallback(std::function<HRESULT(Args...)> fn):invoke(std::move(fn)) {}
HRESULT STDMETHODCALLTYPE QueryInterface(REFIID iid,void **value) override
{
if(iid!=IID_IUnknown && iid!=__uuidof(Interface)) { *value=nullptr; return E_NOINTERFACE; }
*value=static_cast<Interface*>(this);
AddRef();
return S_OK;
}
ULONG STDMETHODCALLTYPE AddRef() override { return ++refs; }
ULONG STDMETHODCALLTYPE Release() override
{
const auto remaining=--refs;
if(!remaining) delete this;
return remaining;
}
HRESULT STDMETHODCALLTYPE Invoke(Args... args) override { return invoke(args...); }
};
template<class Interface,class... Args,class Fn>
auto terminalCallback(Fn fn)
{
Microsoft::WRL::ComPtr<Interface> result;
result.Attach(new TerminalCallback<Interface,Args...>(std::move(fn)));
return result;
}
class FilesTerminalView
{
Microsoft::WRL::ComPtr<ICoreWebView2Controller> controller;
Microsoft::WRL::ComPtr<ICoreWebView2> web;
HMODULE loader{};
bool ready{};
bool visible{};
int viewportHeight{};
public:
bool initialized() const { return ready; }
void enableTrace() { if(ready) web->PostWebMessageAsString(L"trace:on"); }
void setFont(const LOGFONTW &font,int points)
{
if(!ready) return;
const auto message=L"font:"+std::to_wstring(points)+L":"+
std::to_wstring(font.lfWeight?font.lfWeight:FW_NORMAL)+L":"+
std::to_wstring(font.lfItalic!=0)+L":"+font.lfFaceName;
web->PostWebMessageAsString(message.c_str());
}
void layout(HWND host,bool show,int fullHeight)
{
visible=show;
viewportHeight=fullHeight;
if(!controller) return;
RECT bounds{};
GetClientRect(host,&bounds);
bounds.top=bounds.bottom-viewportHeight;
RECT current{};
if(FAILED(controller->get_Bounds(&current)) || !EqualRect(&current,&bounds))
controller->put_Bounds(bounds);
BOOL currentVisibility{};
if(FAILED(controller->get_IsVisible(&currentVisibility)) || !!currentVisibility!=show)
controller->put_IsVisible(show);
}
void close()
{
if(controller) controller->Close();
web.Reset();
controller.Reset();
ready=false;
}
bool send(const std::string &bytes)
{
if(!ready) return false;
constexpr wchar_t hex[]=L"0123456789abcdef";
std::wstring message;
message.reserve(bytes.size()*2);
for(unsigned char c:bytes)
{
message+=hex[c>>4];
message+=hex[c&15];
}
return SUCCEEDED(web->PostWebMessageAsString(message.c_str()));
}
bool create(HWND host,std::function<void(const std::wstring&)> receive,std::function<void()> failed)
{
wchar_t exe[MAX_PATH]{};
if(!GetModuleFileNameW(nullptr,exe,MAX_PATH)) return false;
const auto folder=std::filesystem::path(exe).parent_path();
loader=LoadLibraryExW((folder/L"WebView2Loader.dll").c_str(),nullptr,LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR|LOAD_LIBRARY_SEARCH_SYSTEM32);
if(!loader) return false;
using Create=HRESULT (STDAPICALLTYPE*)(PCWSTR,PCWSTR,ICoreWebView2EnvironmentOptions*,
ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler*);
const auto createEnvironment=reinterpret_cast<Create>(
reinterpret_cast<void*>(GetProcAddress(loader,"CreateCoreWebView2EnvironmentWithOptions")));
if(!createEnvironment) return false;
wchar_t local[MAX_PATH]{};
const auto length=GetEnvironmentVariableW(L"LOCALAPPDATA",local,MAX_PATH);
if(!length || length>=MAX_PATH) return false;
const auto profile=std::filesystem::path(local)/L"KSwitcher"/L"TerminalWebView";
const auto assets=folder/L"terminal";
auto environment=terminalCallback<ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler,
HRESULT,ICoreWebView2Environment*>([=,this](HRESULT hr,ICoreWebView2Environment *env)
{
if(!IsWindow(host)) return S_OK;
if(FAILED(hr) || !env) { failed(); return S_OK; }
auto completed=terminalCallback<ICoreWebView2CreateCoreWebView2ControllerCompletedHandler,
HRESULT,ICoreWebView2Controller*>([=,this](HRESULT result,ICoreWebView2Controller *view)
{
if(!IsWindow(host)) return S_OK;
if(FAILED(result) || !view) { failed(); return S_OK; }
controller=view;
view->get_CoreWebView2(&web);
Microsoft::WRL::ComPtr<ICoreWebView2_3> mapped;
if(!web || FAILED(web.As(&mapped)) || FAILED(mapped->SetVirtualHostNameToFolderMapping(
L"kswitcher-terminal.invalid",assets.c_str(),COREWEBVIEW2_HOST_RESOURCE_ACCESS_KIND_DENY)))
{ failed(); return S_OK; }
Microsoft::WRL::ComPtr<ICoreWebView2Settings> settings;
if(SUCCEEDED(web->get_Settings(&settings)))
{
settings->put_AreDevToolsEnabled(FALSE);
settings->put_AreDefaultContextMenusEnabled(FALSE);
settings->put_AreHostObjectsAllowed(FALSE);
}
EventRegistrationToken token{};
auto navigation=terminalCallback<ICoreWebView2NavigationStartingEventHandler,
ICoreWebView2*,ICoreWebView2NavigationStartingEventArgs*>([](auto*,auto *args)
{
PWSTR uri{};
if(SUCCEEDED(args->get_Uri(&uri)))
{
args->put_Cancel(wcscmp(uri,L"https://kswitcher-terminal.invalid/index.html")!=0);
CoTaskMemFree(uri);
}
return S_OK;
});
auto newWindow=terminalCallback<ICoreWebView2NewWindowRequestedEventHandler,
ICoreWebView2*,ICoreWebView2NewWindowRequestedEventArgs*>([](auto*,auto *args)
{ args->put_Handled(TRUE); return S_OK; });
auto messages=terminalCallback<ICoreWebView2WebMessageReceivedEventHandler,
ICoreWebView2*,ICoreWebView2WebMessageReceivedEventArgs*>([=,this](auto*,auto *args)
{
PWSTR source{}, value{};
if(SUCCEEDED(args->get_Source(&source)) &&
wcscmp(source,L"https://kswitcher-terminal.invalid/index.html")==0 &&
SUCCEEDED(args->TryGetWebMessageAsString(&value)))
{
if(wcscmp(value,L"ready")==0) ready=true;
if(ready) receive(value);
}
CoTaskMemFree(value);
CoTaskMemFree(source);
return S_OK;
});
if(FAILED(web->add_NavigationStarting(navigation.Get(),&token)) ||
FAILED(web->add_NewWindowRequested(newWindow.Get(),&token)) ||
FAILED(web->add_WebMessageReceived(messages.Get(),&token)) ||
FAILED(web->Navigate(L"https://kswitcher-terminal.invalid/index.html")))
failed();
layout(host,visible,viewportHeight);
return S_OK;
});
if(FAILED(env->CreateCoreWebView2Controller(host,completed.Get()))) failed();
return S_OK;
});
return SUCCEEDED(createEnvironment(nullptr,profile.c_str(),nullptr,environment.Get()));
}
};
+31 -4
View File
@@ -180,6 +180,7 @@ struct Capture
LineStyle lineStyle=LineStyle::Solid;
int smoothing=25;
bool smoothingDrag=false;
bool smoothingPreferenceDirty=false;
LineEnds arrowEnds=LineEnds::End, freehandEnds=LineEnds::None;
COLORREF currentColor=RGB(225,42,50);
COLORREF customColors[16]{};
@@ -221,8 +222,24 @@ struct Capture
MarkPart hoveredPart=MarkPart::None;
std::wstring statusText;
std::filesystem::path savedPath;
explicit Capture(ScreenshotHost &a) : app(a), autoOpenEditor(a.autoOpenEditor) {}
explicit Capture(ScreenshotHost &a) : app(a), autoOpenEditor(a.autoOpenEditor)
{
if(a.smoothingPreference.empty()) return;
try {
auto stored=readFile(a.smoothingPreference,1);
if(stored.size()==1 && stored.front()<=100) smoothing=stored.front();
} catch(const std::exception &) {} // Missing or unreadable preference uses the default.
}
void saveSmoothingPreference()
{
if(!smoothingPreferenceDirty || app.smoothingPreference.empty()) return;
try {
atomicWrite(app.smoothingPreference,Bytes{static_cast<unsigned char>(smoothing)});
smoothingPreferenceDirty=false;
} catch(const std::exception &) {} // A failed preference write must not interrupt editing.
}
~Capture() {
saveSmoothingPreference();
if(pencilCursor) DestroyCursor(pencilCursor);
for(auto cursor:drawingCursors) if(cursor) DestroyCursor(cursor);
if (font) DeleteObject(font);
@@ -1170,8 +1187,14 @@ LRESULT CALLBACK captureProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
setScreenshotSmoothing(c->marks[c->selected],amount);
resolveScreenshotLinks(c->marks);
c->smoothingDrag=LOWORD(wp)==TB_THUMBTRACK;
} else if(!pencil) c->smoothing=amount;
if(LOWORD(wp)!=TB_THUMBTRACK) c->smoothingDrag=false;
} else if(!pencil && c->smoothing!=amount) {
c->smoothing=amount;
c->smoothingPreferenceDirty=true;
}
if(LOWORD(wp)!=TB_THUMBTRACK) {
c->smoothingDrag=false;
c->saveSmoothingPreference();
}
syncLineOptions(w,*c); InvalidateRect(w,nullptr,FALSE);
return 0;
}
@@ -1185,7 +1208,11 @@ LRESULT CALLBACK captureProc(HWND w, UINT msg, WPARAM wp, LPARAM lp)
if (c->dragging) return 0;
if(LOWORD(wp)==44) {
int amount=int(SendDlgItemMessageW(w,41,TBM_GETPOS,0,0));
c->smoothing=amount;
if(c->smoothing!=amount) {
c->smoothing=amount;
c->smoothingPreferenceDirty=true;
}
c->saveSmoothingPreference();
if(std::any_of(c->marks.begin(),c->marks.end(),[&](const Mark &m){return m.freehand && m.smoothing!=amount;})) {
c->remember(c->marks);
for(auto &mark:c->marks) if(mark.freehand) setScreenshotSmoothing(mark,amount);
+1
View File
@@ -9,6 +9,7 @@ struct ScreenshotHost {
unsigned windows{};
bool (*notify)(const Clip &){};
bool autoOpenEditor = false;
std::filesystem::path smoothingPreference;
};
inline constexpr UINT ScreenshotRequest=WM_APP+71, ScreenshotIdle=WM_APP+72;
void showScreenshot(ScreenshotHost &);
+2
View File
@@ -110,6 +110,8 @@ int WINAPI wWinMain(HINSTANCE instance,HINSTANCE,PWSTR,int)
InitCommonControlsEx(&controls);
ks::ScreenshotHost host{instance};
host.notify=ks::notifyScreenshot;
try { host.smoothingPreference=ks::dataDirectory()/L"screenshot-smoothing"; }
catch(const std::exception &) {} // The editor can still run without local preferences.
WNDCLASSW cls{}; cls.hInstance=instance; cls.lpfnWndProc=controller; cls.lpszClassName=ControllerClass;
RegisterClassW(&cls);
host.window=CreateWindowW(ControllerClass,L"KSwitcher Screenshot",0,0,0,0,0,nullptr,nullptr,instance,&host);
+1 -1
View File
@@ -2,7 +2,7 @@
#include "platform.h"
namespace ks
{
inline constexpr wchar_t AppVersion[] = L"0.6.76";
inline constexpr wchar_t AppVersion[] = L"0.6.77";
inline constexpr UINT UpdateMessage = WM_APP + 7;
inline constexpr UINT UpdateCheckIntervalMs = 6 * 60 * 60 * 1000;
inline constexpr UINT UpdateNotificationRetryMs = 5 * 60 * 1000;
+78
View File
@@ -0,0 +1,78 @@
param([string]$Executable = "$PSScriptRoot/../build/files-dev/KSwitcher.Files.exe")
$ErrorActionPreference = 'Stop'
Add-Type -AssemblyName System.Drawing
Add-Type @'
using System;
using System.Runtime.InteropServices;
public static class PanelFrameWindow {
[StructLayout(LayoutKind.Sequential)]
public struct Rect { public int Left, Top, Right, Bottom; }
[DllImport("user32.dll")] public static extern bool GetWindowRect(IntPtr window, out Rect rect);
[DllImport("user32.dll")] public static extern IntPtr GetDlgItem(IntPtr window, int id);
[DllImport("user32.dll")] public static extern bool IsWindowVisible(IntPtr window);
[DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow();
[DllImport("user32.dll")] public static extern IntPtr SetThreadDpiAwarenessContext(IntPtr context);
}
'@
function Get-Bounds($Window) {
$bounds = [PanelFrameWindow+Rect]::new()
if (![PanelFrameWindow]::GetWindowRect($Window, [ref]$bounds)) { throw 'Missing panel control.' }
$bounds
}
function Read-Strip($X, $Y, $Width, $Height) {
$bitmap = [Drawing.Bitmap]::new($Width, $Height)
try {
$graphics = [Drawing.Graphics]::FromImage($bitmap)
try { $graphics.CopyFromScreen($X, $Y, 0, 0, $bitmap.Size) }
finally { $graphics.Dispose() }
for ($row = 0; $row -lt $Height; $row++) {
for ($column = 0; $column -lt $Width; $column++) {
$bitmap.GetPixel($column, $row).ToArgb()
}
}
} finally { $bitmap.Dispose() }
}
$process = Start-Process -FilePath (Resolve-Path $Executable) -PassThru
$previousDpi = [PanelFrameWindow]::SetThreadDpiAwarenessContext([IntPtr](-4))
try {
$keyboard = New-Object -ComObject WScript.Shell
for ($attempt = 0; $attempt -lt 50; $attempt++) {
$process.Refresh()
$window = $process.MainWindowHandle
if ($window -and $keyboard.AppActivate($process.Id)) { break }
Start-Sleep -Milliseconds 100
}
Start-Sleep -Milliseconds 300
if (!$window -or [PanelFrameWindow]::GetForegroundWindow() -ne $window) {
throw 'The test window must be visible and in the foreground.'
}
foreach ($panel in 0, 1) {
$heading = [PanelFrameWindow]::GetDlgItem($window, 140 + $panel)
if (![PanelFrameWindow]::IsWindowVisible($heading)) {
throw 'Both panels must use Wide mode for the frame test.'
}
$header = Get-Bounds $heading
$list = Get-Bounds ([PanelFrameWindow]::GetDlgItem($window, 102 + $panel))
$path = Get-Bounds ([PanelFrameWindow]::GetDlgItem($window, 142 + $panel))
$middle = [int](($header.Top + $header.Bottom) / 2)
$side = @(Read-Strip ($list.Left - 4) ($header.Top - 2) 4 1)
$atHeading = @(Read-Strip ($list.Left - 4) $middle 4 1)
if ($side[0] -ne $side[3] -or $side[1] -ne $side[2] -or $side[0] -eq $side[1] -or
($atHeading -join ',') -ne ($side -join ',')) {
throw "Panel $panel has a broken double frame beside the column header."
}
$x = $list.Left + 16
$top = @(Read-Strip $x ($path.Top + [Math]::Floor(($path.Bottom - $path.Top) / 2) - 2) 1 4)
if (($top -join ',') -ne ($side -join ',')) { throw "Panel $panel has an inconsistent top frame." }
$bottom = @(Read-Strip $x ($list.Bottom + 4) 1 ($header.Bottom - $header.Top + 6))
$bottom = $bottom[($bottom.Count - 4)..($bottom.Count - 1)]
if (($bottom -join ',') -ne ($side -join ',')) { throw "Panel $panel has an inconsistent bottom frame." }
Write-Output "Panel $panel double frame verified on screen."
}
} finally {
if ($previousDpi) { [void][PanelFrameWindow]::SetThreadDpiAwarenessContext($previousDpi) }
if (!$process.HasExited) { [void]$process.CloseMainWindow() }
}
+174
View File
@@ -0,0 +1,174 @@
param([string]$Executable = "$PSScriptRoot/../build/files-dev/KSwitcher.Files.exe", [switch]$GeometryOnly)
$ErrorActionPreference = 'Stop'
if (!$GeometryOnly) {
Add-Type -AssemblyName System.Drawing
Add-Type -ReferencedAssemblies @([Drawing.Bitmap].Assembly.Location, [Drawing.Color].Assembly.Location,
[Drawing.Graphics].Assembly.Location, [Drawing.Point].Assembly.Location, "System.Collections", "System.Runtime", "System.Threading.Thread", "System.Runtime.InteropServices", "System.Diagnostics.TraceSource", "System.Private.Windows.GdiPlus", "System.Private.Windows.Core") -TypeDefinition @'
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Threading;
public static class TerminalFrames
{
[StructLayout(LayoutKind.Sequential)]
public struct Rect { public int Left, Top, Right, Bottom; }
[DllImport("user32.dll")]
public static extern bool GetWindowRect(IntPtr window, out Rect rect);
[DllImport("user32.dll")]
private static extern void keybd_event(byte key, byte scan, uint flags, UIntPtr extra);
[DllImport("user32.dll")]
private static extern bool SetCursorPos(int x, int y);
[DllImport("user32.dll")]
private static extern void mouse_event(uint flags, uint x, uint y, uint data, UIntPtr extra);
[DllImport("user32.dll", CharSet=CharSet.Unicode)]
private static extern IntPtr FindWindowEx(IntPtr parent, IntPtr after, string kind, string title);
[DllImport("user32.dll", CharSet=CharSet.Unicode)]
private static extern IntPtr SendMessage(IntPtr window, int message, IntPtr length, System.Text.StringBuilder text);
public static void ClickOutput(IntPtr window)
{
Rect bounds;
if (!GetWindowRect(window, out bounds)) throw new InvalidOperationException("Missing test window.");
SetCursorPos((bounds.Left + bounds.Right) / 2, (bounds.Top + bounds.Bottom) / 2);
mouse_event(2, 0, 0, 0, UIntPtr.Zero);
mouse_event(4, 0, 0, 0, UIntPtr.Zero);
}
public static string ReadCommand(IntPtr window)
{
var input = FindWindowEx(window, IntPtr.Zero, "Edit", null);
var text = new System.Text.StringBuilder(256);
SendMessage(input, 13, new IntPtr(text.Capacity), text);
return text.ToString();
}
public static int[] Capture(IntPtr window, byte direction, int repeats)
{
Rect bounds;
if (!GetWindowRect(window, out bounds)) throw new InvalidOperationException("Missing test window.");
var positions = new List<int>();
var clock = Stopwatch.StartNew();
using (var bitmap = new Bitmap(150, 320))
using (var graphics = Graphics.FromImage(bitmap))
{
var trigger = new Thread(() => {
Thread.Sleep(100);
keybd_event(17, 0, 0, UIntPtr.Zero);
for (int i = 0; i < repeats; i++)
{
keybd_event(direction, 0, 1, UIntPtr.Zero);
if (i + 1 < repeats) Thread.Sleep(30);
}
keybd_event(direction, 0, 3, UIntPtr.Zero);
keybd_event(17, 0, 2, UIntPtr.Zero);
});
trigger.Start();
while (clock.ElapsedMilliseconds < 650)
{
graphics.CopyFromScreen(bounds.Left + 4, bounds.Bottom - 340, 0, 0, bitmap.Size);
int marker = -1;
for (int y = 0; y < bitmap.Height && marker < 0; y++)
for (int x = 0; x < bitmap.Width; x++)
{
var color = bitmap.GetPixel(x, y);
if (color.R > 140 && color.B > 140 && color.R - color.G > 35 && color.B - color.G > 35) { marker = y; break; }
}
positions.Add(marker);
Thread.Sleep(4);
}
trigger.Join();
}
return positions.ToArray();
}
}
'@
}
function Wait-ForTrace([string]$Pattern, [int]$After = 0) {
for ($attempt = 0; $attempt -lt 100; $attempt++) {
$lines = @(Get-Content -LiteralPath $trace -ErrorAction SilentlyContinue)
$match = $lines | Select-Object -Skip $After | Where-Object { $_ -match $Pattern } | Select-Object -First 1
if ($match) { return $match }
Start-Sleep -Milliseconds 100
}
throw "Missing terminal event: $Pattern"
}
$process = Start-Process -FilePath (Resolve-Path $Executable) -ArgumentList '--terminal-trace' -PassThru
$trace = Join-Path ([IO.Path]::GetTempPath()) "KSwitcher.Files.terminal-trace-$($process.Id).log"
try {
$keyboard = New-Object -ComObject WScript.Shell
for ($attempt = 0; $attempt -lt 50 -and !$keyboard.AppActivate($process.Id); $attempt++) {
Start-Sleep -Milliseconds 100
}
if (!$keyboard.AppActivate($process.Id)) { throw 'Could not focus the test window.' }
Wait-ForTrace ' layout ' | Out-Null
for ($attempt = 0; $attempt -lt 20; $attempt++) {
$lines = @(Get-Content -LiteralPath $trace)
if ($lines -match ' layout \d+ \d+ \d+ 1 1$') { break }
if ($keyboard.AppActivate($process.Id)) { $keyboard.SendKeys('^o') }
Start-Sleep -Milliseconds 250
}
Wait-ForTrace ' web-anchor ' | Out-Null
if (!$GeometryOnly) {
[TerminalFrames]::ClickOutput($process.MainWindowHandle)
Start-Sleep -Milliseconds 200
$keyboard.SendKeys('focus-check')
Start-Sleep -Milliseconds 200
if ([TerminalFrames]::ReadCommand($process.MainWindowHandle) -ne 'focus-check') {
throw 'Typing after clicking terminal output did not reach the command line.'
}
$keyboard.SendKeys('^a{BACKSPACE}')
$keyboard.SendKeys('alpha beta gamma^{BACKSPACE}^{BACKSPACE}')
Start-Sleep -Milliseconds 150
if ([TerminalFrames]::ReadCommand($process.MainWindowHandle) -ne 'alpha ') {
throw 'Ctrl+Backspace did not delete one word per press.'
}
$keyboard.SendKeys('^a{BACKSPACE}')
Write-Output 'Typing after clicking terminal output reached the command line.'
}
$keyboard.SendKeys('[Console]::WriteLine{(}{[}char{]}27 {+} "{[}95mMMMMMMMM" {+} {[}char{]}27 {+} "{[}0m"{)}{ENTER}')
Start-Sleep -Milliseconds 500
$keyboard.SendKeys('^o')
for ($row = 0; $row -lt 5; $row++) { $keyboard.SendKeys('^{UP}') }
Start-Sleep -Milliseconds 500
foreach ($scenario in @(@{Key='UP'; Count=1}, @{Key='DOWN'; Count=1}, @{Key='UP'; Count=10})) {
$direction = $scenario.Key
$repeats = $scenario.Count
$after = @(Get-Content -LiteralPath $trace).Count
$event = if ($direction -eq 'UP') { 'grow' } else { 'shrink' }
$frames = $null
if ($GeometryOnly) {
$keyboard.SendKeys("^{$direction $repeats}")
} else {
$key = if ($direction -eq 'UP') { 38 } else { 40 }
$frames = [TerminalFrames]::Capture($process.MainWindowHandle, $key, $repeats)
}
Wait-ForTrace " key-$event " $after | Out-Null
Wait-ForTrace ' layout ' $after | Out-Null
Start-Sleep -Milliseconds 250
$events = @(Get-Content -LiteralPath $trace | Select-Object -Skip $after)
if (@($events -match " key-$event ").Count -lt $repeats) {
throw "Did not deliver all $repeats Ctrl+$direction events."
}
if ($events -match ' web-after-fit ') { throw "Terminal grid resized after Ctrl+$direction." }
if (!$GeometryOnly) {
$baseline = $frames[0]
if ($baseline -lt 0 -or $frames[-1] -lt 0) {
throw "Terminal marker was not visible at rest for Ctrl+$direction."
}
$shifted = @($frames | Where-Object { $_ -lt 0 -or [Math]::Abs($_ - $baseline) -gt 2 })
if ($shifted.Count) {
throw "Ctrl+$direction shifted or hid the marker in $($shifted.Count) of $($frames.Count) frames (rest Y=$baseline, sampled Y=$($frames | Sort-Object -Unique))."
}
}
Write-Output "Ctrl+$direction x$repeats kept the terminal grid stable."
}
} finally {
if (!$process.HasExited) { $null = $process.CloseMainWindow() }
}
+11
View File
@@ -14,7 +14,18 @@ int main(int argc, char **argv)
std::filesystem::create_directories(root);
struct Cleanup {std::filesystem::path root; ~Cleanup(){std::error_code ec; std::filesystem::remove_all(root,ec);}} cleanup{root};
ks::ScreenshotHost app{GetModuleHandleW(nullptr)};
app.smoothingPreference=root/L"screenshot-smoothing";
ks::Capture c(app);
require(c.smoothing==25,"missing preference uses default smoothing");
c.smoothing=61;
c.smoothingPreferenceDirty=true;
c.saveSmoothingPreference();
ks::Capture reopened(app);
require(reopened.smoothing==61,"new editor restores saved smoothing");
ks::atomicWrite(app.smoothingPreference,ks::Bytes{101});
ks::Capture invalidPreference(app);
require(invalidPreference.smoothing==25,"invalid preference uses default smoothing");
std::filesystem::remove(app.smoothingPreference);
c.image=std::make_unique<ks::Surface>(320,240);
PatBlt(c.image->dc,0,0,320,240,WHITENESS);
c.marks={ks::Mark{{40,40},{180,140},0}};
+28
View File
@@ -0,0 +1,28 @@
param([Parameter(Mandatory)][string]$Destination)
$ErrorActionPreference = 'Stop'
$root = Split-Path $PSScriptRoot
$deps = New-Item -ItemType Directory -Force (Join-Path $root 'build/terminal-deps')
$version = '1.0.4314.35'
if (!(Test-Path "$deps/sdk/build/native/include/WebView2.h")) {
Invoke-WebRequest "https://api.nuget.org/v3-flatcontainer/microsoft.web.webview2/$version/microsoft.web.webview2.$version.nupkg" -OutFile "$deps/sdk.zip"
Expand-Archive "$deps/sdk.zip" "$deps/sdk" -Force
}
$assets = New-Item -ItemType Directory -Force (Join-Path $Destination 'terminal')
foreach ($package in @(
@{ Name='xterm'; Spec='@xterm/[email protected]'; Archive='xterm-xterm-6.0.0.tgz'; Files=@{'lib/xterm.js'='xterm.js'; 'css/xterm.css'='xterm.css'; 'LICENSE'='xterm-LICENSE.txt'} },
@{ Name='fit'; Spec='@xterm/[email protected]'; Archive='xterm-addon-fit-0.11.0.tgz'; Files=@{'lib/addon-fit.js'='addon-fit.js'; 'LICENSE'='addon-fit-LICENSE.txt'} }
)) {
if (!(Test-Path "$deps/$($package.Archive)")) {
& npm.cmd pack $package.Spec --pack-destination $deps.FullName --silent | Out-Host
if ($LASTEXITCODE) { throw "Could not download $($package.Spec)" }
}
$unpacked = New-Item -ItemType Directory -Force "$deps/$($package.Name)"
& tar -xf "$deps/$($package.Archive)" -C $unpacked.FullName
if ($LASTEXITCODE) { throw "Could not unpack $($package.Spec)" }
foreach ($file in $package.Files.Keys) {
Copy-Item "$unpacked/package/$file" "$assets/$($package.Files[$file])" -Force
}
}
Copy-Item "$root/resources/terminal/*" $assets -Force
Copy-Item "$deps/sdk/build/native/x64/WebView2Loader.dll" $Destination -Force
"$deps/sdk/build/native/include"